В идеальном мире, по версии Microsoft, дизайнеры творят интерфейс в Expression Blend, а программисты пишут бэкенд на C#. В реальности же дизайнеры творят в Photoshop, а программисты ломают голову как вот эту красивую кнопку сконвертировать в XAML, да так чтобы без большого количества костылей. Многие добиваются в этом выдающихся результатов. Стоит только слегка поискать и в сети найдётся куча вариантов, как можно из элементов WPF собрать довольно сложные дизайнерские решения. К сожалению, пару раз так повозившись и оценив затраты по времени понимаешь, что оно того не стоит, проще взять исходный элемент из нарезки как есть, т.е. в виде изображения. И такой вариант, как по мне, является наилучшим с точки зрения продуктивности, однако есть одно НО.
Картинкам часто нельзя изменить пропорции. Края изображения сильно сжимаются с одной стороны и растягиваются с другой. К примеру, дизайнер прислал нам изображения кнопки:
Вставив её в проект и слегка изменив пропорции получим следующую непотребщину:
Встаёт вопрос, как при ресайзе изображения сохранить края в таком виде, как предполагал дизайнер?
Посмотрим на структуру изображения, его можно условно разделить на 9 регионов: 4 угла, 4 края и центр.
При изменении пропорций нам надо, чтобы углы в этой сетке не изменяли размер совсем, края растягивались только в ширь, а центр мог менять размер свободно.
Этого добьёмся разрезав исходное изображение на регионы разместив в сетке элемента Grid.
<UserControl
x:Class="UniformImageNamespace.UniformImage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<UserControl.Template>
<ControlTemplate>
<Grid SnapsToDevicePixels="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image
Grid.Row="0"
Grid.Column="0"
Stretch="None"
Source="{Binding TopLeftImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
<Border Grid.Row="0" Grid.Column="1">
<Border.Background>
<ImageBrush ImageSource="{Binding TopImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Image
Grid.Row="0"
Grid.Column="2"
Stretch="None"
Source="{Binding TopRightImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
<Border Grid.Row="1" Grid.Column="0">
<Border.Background>
<ImageBrush ImageSource="{Binding LeftImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Border Grid.Row="1" Grid.Column="1">
<Border.Background>
<ImageBrush ImageSource="{Binding CenterImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Border Grid.Row="1" Grid.Column="2">
<Border.Background>
<ImageBrush ImageSource="{Binding RightImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Image
Grid.Row="2"
Grid.Column="0"
Stretch="None"
Source="{Binding BottomLeftImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
<Border Grid.Row="2" Grid.Column="1">
<Border.Background>
<ImageBrush ImageSource="{Binding BottomImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Image
Grid.Row="2"
Grid.Column="2"
Stretch="None"
Source="{Binding BottomRightImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
Я создал пользовательский элемент управления с названием UniformImage.
В ячейки элемента Grid помещены части исходного изображения, нарезать которое будем в code behind. Также для нейтрализации проблем при состыковке регионов, задано свойство SnapsToDevicePixels. Заметьте, что крайние регионы изображения пришлось вставить в виде фона элемента Border. На это пришлось пойти, так как иначе элемент Image некорректно отображал свои пропорции.
Переходя к коду C# добавим два свойства: одно типа ImageSource для задания исходного изображения и второе StaticArea типа Thikness для получения толщины границы изображения, которая должна быть статична при ресайзе.
public static readonly DependencyProperty StaticAreaProperty = DependencyProperty.Register(
"StaticArea",
typeof(Thickness),
typeof(UniformImage),
new PropertyMetadata(InvalidateImage)
);
public Thickness StaticArea
{
get { return (Thickness)GetValue(StaticAreaProperty); }
set { SetValue(StaticAreaProperty, value); }
}
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
"Source",
typeof(ImageSource),
typeof(UniformImage),
new PropertyMetadata(InvalidateImage)
);
public ImageSource Source
{
get { return (ImageSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
Метод InvalidateImage() в обоих свойствах будет отвечать за обновление регионов изображения при изменении этих свойств.
private static void InvalidateImage(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uniformImage = (UniformImage)d;
uniformImage.TopLeftImage = GetCroppedPart(AlignmentX.Left, AlignmentY.Top, uniformImage);
uniformImage.TopImage = GetCroppedPart(AlignmentX.Center, AlignmentY.Top, uniformImage);
uniformImage.TopRightImage = GetCroppedPart(AlignmentX.Right, AlignmentY.Top, uniformImage);
uniformImage.LeftImage = GetCroppedPart(AlignmentX.Left, AlignmentY.Center, uniformImage);
uniformImage.CenterImage = GetCroppedPart(AlignmentX.Center, AlignmentY.Center, uniformImage);
uniformImage.RightImage = GetCroppedPart(AlignmentX.Right, AlignmentY.Center, uniformImage);
uniformImage.BottomLeftImage = GetCroppedPart(AlignmentX.Left, AlignmentY.Bottom, uniformImage);
uniformImage.BottomImage = GetCroppedPart(AlignmentX.Center, AlignmentY.Bottom, uniformImage);
uniformImage.BottomRightImage = GetCroppedPart(AlignmentX.Right, AlignmentY.Bottom, uniformImage);
}
Недоступные извне свойства для хранения частей изображения:
protected static readonly DependencyProperty TopLeftImageProperty = DependencyProperty.Register(
"TopLeftImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource TopLeftImage
{
get { return (ImageSource)GetValue(TopLeftImageProperty); }
set { SetValue(TopLeftImageProperty, value); }
}
protected static readonly DependencyProperty TopImageProperty = DependencyProperty.Register(
"TopImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource TopImage
{
get { return (ImageSource)GetValue(TopImageProperty); }
set { SetValue(TopImageProperty, value); }
}
protected static readonly DependencyProperty TopRightImageProperty = DependencyProperty.Register(
"TopRightImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource TopRightImage
{
get { return (ImageSource)GetValue(TopRightImageProperty); }
set { SetValue(TopRightImageProperty, value); }
}
protected static readonly DependencyProperty RightImageProperty = DependencyProperty.Register(
"RightImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource RightImage
{
get { return (ImageSource)GetValue(RightImageProperty); }
set { SetValue(RightImageProperty, value); }
}
protected static readonly DependencyProperty BottomRightImageProperty = DependencyProperty.Register(
"BottomRightImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource BottomRightImage
{
get { return (ImageSource)GetValue(BottomRightImageProperty); }
set { SetValue(BottomRightImageProperty, value); }
}
protected static readonly DependencyProperty BottomImageProperty = DependencyProperty.Register(
"BottomImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource BottomImage
{
get { return (ImageSource)GetValue(BottomImageProperty); }
set { SetValue(BottomImageProperty, value); }
}
protected static readonly DependencyProperty BottomLeftImageProperty = DependencyProperty.Register(
"BottomLeftImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource BottomLeftImage
{
get { return (ImageSource)GetValue(BottomLeftImageProperty); }
set { SetValue(BottomLeftImageProperty, value); }
}
protected static readonly DependencyProperty LeftImageProperty = DependencyProperty.Register(
"LeftImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource LeftImage
{
get { return (ImageSource)GetValue(LeftImageProperty); }
set { SetValue(LeftImageProperty, value); }
}
protected static readonly DependencyProperty CenterImageProperty = DependencyProperty.Register(
"CenterImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource CenterImage
{
get { return (ImageSource)GetValue(CenterImageProperty); }
set { SetValue(CenterImageProperty, value); }
}
Метод GetCroppedPart() будет рассчитывать и возвращать нужный кусок изображения. За непосредственно нарезку будет отвечать встроенный в .NET класс CroppedBitmap, на вход конструктора которого подаются координаты региона изображения и его размеры.
private static ImageSource GetCroppedPart(AlignmentX alignmentX, AlignmentY alignmentY, UniformImage uniformImage)
{
ImageSource imageSource = uniformImage.Source;
if (imageSource == null) return null;
double totalWidth = imageSource.Width;
double totalHeight = imageSource.Height;
Thickness staticArea = uniformImage.StaticArea;
if (totalWidth <= 0 || totalHeight <= 0) return null;
// Предопределенные размеры
double topHeight = staticArea.Top;
double centerHeight = totalHeight - (staticArea.Top + staticArea.Bottom);
double bottomHeight = staticArea.Bottom;
double leftWidth = staticArea.Left;
double centerWidth = totalWidth - (staticArea.Left + staticArea.Right);
double rightWidth = staticArea.Right;
// Предопределенные отступы
double centerTopMargin = staticArea.Top;
double bottomTopMargin = totalHeight - staticArea.Bottom;
double centerLeftMargin = staticArea.Left;
double rightLeftMargin = totalWidth - staticArea.Right;
// Расчет позиции и размера региона в зависимости от его выравнивания
double topMargin = 0;
double leftMargin = 0;
double width = 0;
double height = 0;
switch (alignmentX)
{
case AlignmentX.Left:
width = leftWidth;
break;
case AlignmentX.Center:
leftMargin = centerLeftMargin;
width = centerWidth;
break;
case AlignmentX.Right:
leftMargin = rightLeftMargin;
width = rightWidth;
break;
}
switch (alignmentY)
{
case AlignmentY.Top:
height = topHeight;
break;
case AlignmentY.Center:
topMargin = centerTopMargin;
height = centerHeight;
break;
case AlignmentY.Bottom:
topMargin = bottomTopMargin;
height = bottomHeight;
break;
}
if (height <= 0 || width <= 0) return null;
if (topMargin < 0) topMargin = 0;
if (leftMargin < 0) leftMargin = 0;
if (height + topMargin > totalHeight) height = totalHeight - topMargin;
if (width + leftMargin > totalWidth) width = totalWidth - leftMargin;
return new CroppedBitmap(
(BitmapSource)imageSource,
new Int32Rect((int)leftMargin, (int)topMargin, (int)width, (int)height));
}
На этом всё! Пример использования:
<Window
x:Class="UniformImageNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UniformImageNamespace"
>
<local:UniformImage Source="button.png" StaticArea="30" />
</Window>
Теперь можем менять пропорции изображения не переживания за его вид. 🙂
Исходный код
<UserControl
x:Class="UniformImageNamespace.UniformImage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<UserControl.Template>
<ControlTemplate>
<Grid SnapsToDevicePixels="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image
Grid.Row="0"
Grid.Column="0"
Stretch="None"
Source="{Binding TopLeftImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
<Border Grid.Row="0" Grid.Column="1">
<Border.Background>
<ImageBrush ImageSource="{Binding TopImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Image
Grid.Row="0"
Grid.Column="2"
Stretch="None"
Source="{Binding TopRightImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
<Border Grid.Row="1" Grid.Column="0">
<Border.Background>
<ImageBrush ImageSource="{Binding LeftImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Border Grid.Row="1" Grid.Column="1">
<Border.Background>
<ImageBrush ImageSource="{Binding CenterImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Border Grid.Row="1" Grid.Column="2">
<Border.Background>
<ImageBrush ImageSource="{Binding RightImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Image
Grid.Row="2"
Grid.Column="0"
Stretch="None"
Source="{Binding BottomLeftImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
<Border Grid.Row="2" Grid.Column="1">
<Border.Background>
<ImageBrush ImageSource="{Binding BottomImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Border.Background>
</Border>
<Image
Grid.Row="2"
Grid.Column="2"
Stretch="None"
Source="{Binding BottomRightImage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
/>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace UniformImageNamespace
{
public partial class UniformImage : UserControl
{
public static readonly DependencyProperty StaticAreaProperty = DependencyProperty.Register(
"StaticArea",
typeof(Thickness),
typeof(UniformImage),
new PropertyMetadata(InvalidateImage)
);
public Thickness StaticArea
{
get { return (Thickness)GetValue(StaticAreaProperty); }
set { SetValue(StaticAreaProperty, value); }
}
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
"Source",
typeof(ImageSource),
typeof(UniformImage),
new PropertyMetadata(InvalidateImage)
);
public ImageSource Source
{
get { return (ImageSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public UniformImage()
{
InitializeComponent();
}
protected static readonly DependencyProperty TopLeftImageProperty = DependencyProperty.Register(
"TopLeftImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource TopLeftImage
{
get { return (ImageSource)GetValue(TopLeftImageProperty); }
set { SetValue(TopLeftImageProperty, value); }
}
protected static readonly DependencyProperty TopImageProperty = DependencyProperty.Register(
"TopImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource TopImage
{
get { return (ImageSource)GetValue(TopImageProperty); }
set { SetValue(TopImageProperty, value); }
}
protected static readonly DependencyProperty TopRightImageProperty = DependencyProperty.Register(
"TopRightImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource TopRightImage
{
get { return (ImageSource)GetValue(TopRightImageProperty); }
set { SetValue(TopRightImageProperty, value); }
}
protected static readonly DependencyProperty RightImageProperty = DependencyProperty.Register(
"RightImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource RightImage
{
get { return (ImageSource)GetValue(RightImageProperty); }
set { SetValue(RightImageProperty, value); }
}
protected static readonly DependencyProperty BottomRightImageProperty = DependencyProperty.Register(
"BottomRightImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource BottomRightImage
{
get { return (ImageSource)GetValue(BottomRightImageProperty); }
set { SetValue(BottomRightImageProperty, value); }
}
protected static readonly DependencyProperty BottomImageProperty = DependencyProperty.Register(
"BottomImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource BottomImage
{
get { return (ImageSource)GetValue(BottomImageProperty); }
set { SetValue(BottomImageProperty, value); }
}
protected static readonly DependencyProperty BottomLeftImageProperty = DependencyProperty.Register(
"BottomLeftImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource BottomLeftImage
{
get { return (ImageSource)GetValue(BottomLeftImageProperty); }
set { SetValue(BottomLeftImageProperty, value); }
}
protected static readonly DependencyProperty LeftImageProperty = DependencyProperty.Register(
"LeftImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource LeftImage
{
get { return (ImageSource)GetValue(LeftImageProperty); }
set { SetValue(LeftImageProperty, value); }
}
protected static readonly DependencyProperty CenterImageProperty = DependencyProperty.Register(
"CenterImage",
typeof(ImageSource),
typeof(UniformImage)
);
protected ImageSource CenterImage
{
get { return (ImageSource)GetValue(CenterImageProperty); }
set { SetValue(CenterImageProperty, value); }
}
private static void InvalidateImage(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uniformImage = (UniformImage)d;
uniformImage.TopLeftImage = GetCroppedPart(AlignmentX.Left, AlignmentY.Top, uniformImage);
uniformImage.TopImage = GetCroppedPart(AlignmentX.Center, AlignmentY.Top, uniformImage);
uniformImage.TopRightImage = GetCroppedPart(AlignmentX.Right, AlignmentY.Top, uniformImage);
uniformImage.LeftImage = GetCroppedPart(AlignmentX.Left, AlignmentY.Center, uniformImage);
uniformImage.CenterImage = GetCroppedPart(AlignmentX.Center, AlignmentY.Center, uniformImage);
uniformImage.RightImage = GetCroppedPart(AlignmentX.Right, AlignmentY.Center, uniformImage);
uniformImage.BottomLeftImage = GetCroppedPart(AlignmentX.Left, AlignmentY.Bottom, uniformImage);
uniformImage.BottomImage = GetCroppedPart(AlignmentX.Center, AlignmentY.Bottom, uniformImage);
uniformImage.BottomRightImage = GetCroppedPart(AlignmentX.Right, AlignmentY.Bottom, uniformImage);
}
private static ImageSource GetCroppedPart(AlignmentX alignmentX, AlignmentY alignmentY, UniformImage uniformImage)
{
ImageSource imageSource = uniformImage.Source;
if (imageSource == null) return null;
double totalWidth = imageSource.Width;
double totalHeight = imageSource.Height;
Thickness staticArea = uniformImage.StaticArea;
if (totalWidth <= 0 || totalHeight <= 0) return null;
// Предопределенные размеры
double topHeight = staticArea.Top;
double centerHeight = totalHeight - (staticArea.Top + staticArea.Bottom);
double bottomHeight = staticArea.Bottom;
double leftWidth = staticArea.Left;
double centerWidth = totalWidth - (staticArea.Left + staticArea.Right);
double rightWidth = staticArea.Right;
// Предопределенные отступы
double centerTopMargin = staticArea.Top;
double bottomTopMargin = totalHeight - staticArea.Bottom;
double centerLeftMargin = staticArea.Left;
double rightLeftMargin = totalWidth - staticArea.Right;
// Расчет позиции и размера региона в зависимости от его выравнивания
double topMargin = 0;
double leftMargin = 0;
double width = 0;
double height = 0;
switch (alignmentX)
{
case AlignmentX.Left:
width = leftWidth;
break;
case AlignmentX.Center:
leftMargin = centerLeftMargin;
width = centerWidth;
break;
case AlignmentX.Right:
leftMargin = rightLeftMargin;
width = rightWidth;
break;
}
switch (alignmentY)
{
case AlignmentY.Top:
height = topHeight;
break;
case AlignmentY.Center:
topMargin = centerTopMargin;
height = centerHeight;
break;
case AlignmentY.Bottom:
topMargin = bottomTopMargin;
height = bottomHeight;
break;
}
if (height <= 0 || width <= 0) return null;
if (topMargin < 0) topMargin = 0;
if (leftMargin < 0) leftMargin = 0;
if (height + topMargin > totalHeight) height = totalHeight - topMargin;
if (width + leftMargin > totalWidth) width = totalWidth - leftMargin;
return new CroppedBitmap(
(BitmapSource)imageSource,
new Int32Rect((int)leftMargin, (int)topMargin, (int)width, (int)height));
}
}
}
Исходники на GitHub.

