В идеальном мире, по версии 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.