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

В идеальном мире, по версии Microsoft, дизайнеры творят интерфейс в Expression Blend, а программисты пишут бэкенд на C#. В реальности же дизайнеры творят в Photoshop, а программисты ломают голову как вот эту красивую кнопку сконвертировать в XAML, да так чтобы без большого количества костылей. Многие добиваются в этом выдающихся результатов. Стоит только слегка поискать и в сети найдётся куча вариантов, как можно из элементов WPF собрать довольно сложные дизайнерские решения. К сожалению, пару раз так повозившись и оценив затраты по времени понимаешь, что оно того не стоит, проще взять исходный элемент из нарезки как есть, т.е. в виде изображения. И такой вариант, как по мне, является наилучшим с точки зрения продуктивности, однако есть одно НО.

Картинкам часто нельзя изменить пропорции. Края изображения сильно сжимаются с одной стороны и растягиваются с другой. К примеру, дизайнер прислал нам изображения кнопки:
Вставив её в проект и слегка изменив пропорции получим следующую непотребщину:
Встаёт вопрос, как при ресайзе изображения сохранить края в таком виде, как предполагал дизайнер?

Посмотрим на структуру изображения, его можно условно разделить на 9 регионов: 4 угла, 4 края и центр.
При изменении пропорций нам надо, чтобы углы в этой сетке не изменяли размер совсем, края растягивались только в ширь, а центр мог менять размер свободно.
Этого добьёмся разрезав исходное изображение на регионы разместив в сетке элемента Grid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<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 для получения толщины границы изображения, которая должна быть статична при ресайзе.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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() в обоих свойствах будет отвечать за обновление регионов изображения при изменении этих свойств.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

Недоступные извне свойства для хранения частей изображения:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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, на вход конструктора которого подаются координаты региона изображения и его размеры.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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));
}

На этом всё! Пример использования:

1
2
3
4
5
6
7
8
<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>

Теперь можем менять пропорции изображения не переживания за его вид. 🙂



Исходный код

Исходники на GitHub.

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

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