UniformImage: Изменение пропорций картинки без деформации содержимого по краям

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *