Круглая кнопка с векторной иконкой

0

Всем привет. Когда я только начал знакомится с WPF, одним из первых моих заданий было создание нестандартной кнопки. Самая элементарная и, наверное, самая популярная сейчас  – это круглая форма. Поэтому следуя моде на Modern UI , я решил сделать такую-же кнопку.


Обычную «одноразовую» кнопку  можно создать и так:

<Button Height="100" Width="100">
    <Button.Template>
        <ControlTemplate>
            <Grid>
                <Ellipse StrokeThickness="2" Stroke="Red" />
                <Image Height="70" Width="70" Source="image.png" />
            </Grid>
        </ControlTemplate>
    </Button.Template>
</Button>

Все. Кнопка готова. Она круглая и иконка есть. Проблема в том, что для каждой кнопки придется делать такой копипаст, а это и неудобно и некрасиво.  Пример правильный, только нужно вынести шаблон в ресурсы и применять  его к каждой кнопке.
Так и сделаю, но сначала опишу требования, которые я поставил перед своей кнопкой:

  1. Иконка должна быть векторная;
  2. Реализовать нужные привязки, что бы кнопка могла менять размеры и цвета;
  3. Все это должно красиво выглядеть;

Изначально, думал сразу же писать код, но желание нарисовать иконку меня перебороло. Поэтому сделаю небольшое отступление и создам иконку. Ни, или несколько…

Знаю, многие скажут, что то вроде «Не программиста это дело - иконки рисовать». Может и так, но я люблю рисовать, создавать дизайн своих программ, и уж тем более контролировать этот процесс, если кто-то мне помогает )). Может, сказалось, то, что я вырос в семье художников? Ну, или то, что я и сам на художника учился, до того как программированием не занялся :-) пропустить создание иконки

Создание иконки

Выберем графический редактор. Например, Adobe Illustrator, Microsoft Expression Design и т.д. Я буду использовать CorelDRAW, а переведу в xaml уже в Design. Странная комбинация получилась, но никак руки не доходили познакомиться с Design как следует. 

Открываем Corel, создаем новый проект – рисуем. Можете нарисовать стандартную геометрическую фигуру, звездочку или стрелочку. Правда, если речь идет о чем то таком простом, то проще это нарисовать в самом WPF

Я нарисовал иконку. Что дальше?
Нужно сохранить ее в векторном формате. Выбираем Экспорт и сохраняем, например в emf.  Что бы перевести иконку в xaml  я использую программу Microsoft Expression Design. Она входит в пакет Microsoft Expression Studio

Открываем иконку. Находим Экспорт. В появившемся окне выбираем нужный формат.  Я выбрал XAML WPF Resource Dictionary.

Дальше, в WPF проекте создаем Resource Dictionary с именем Icons. Туда копируем генерируемую разметку иконки.  Можно и не создавать отдельный файл, а записать все в ресурсы окна или приложения, но лучше структурировать сразу.  Тогда можно держать все свои иконки в отдельной библиотеке и с легкостью переносить в другие проекты.

Можно создать в библиотеке Icons ресурс SolidColorBruch и, там где иконки будут одноцветные, задать его в Brush свойства. Потом, если нужно будет поменять цвет иконкам, достаточно будет изменить  свойство Color в этом ресурсе. Хотя, думаю, все зависит от ваших личных предпочтений. 


Библиотека Icons, пока с одной иконкой:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="IconsBrush" Color="WhiteSmoke" />

    <DrawingBrush x:Key="RockIcon" Stretch="Uniform">
        <DrawingBrush.Drawing>
            <DrawingGroup>
                <DrawingGroup.Children>
                    <GeometryDrawing Brush="{StaticResource IconsBrush}" Geometry=""/>
                </DrawingGroup.Children>
            </DrawingGroup>
        </DrawingBrush.Drawing>
    </DrawingBrush>
  
</ResourceDictionary>

Значение свойства Geometry я сознательно стер с примера – слишком много лишнего получилось бы. Скажу только, что там укороченная запись геометрических путей, из которых состоит иконка.  Детально, можете посмотреть на msdn.

Иконка создана. Можно вернуться к кнопке…

Круглая кнопка

Что я узнал, покопавшись в интернете, посмотрев исходники? 

У каждой кнопки есть свойство Content. Это очень удобное свойство, так  как влияет на внешний вид, всего, что наследуется от ContentControl (кнопка его потомок), через шаблон. Поэтому у кнопки может быть любое содержимое. Например, кнопка с обычным текстом содержит TextBlock, на которой и отображается на самом деле текст кнопки. Или опять же в Content можно поместить контейнер, в котором может быть все что угодно.

Создаем папку Themes, а в ней generic.xaml. И шаблон кнопки: 

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="BubleButton" TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Grid x:Name="grid">
                        <Ellipse StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent},Path=BorderThickness.Top}" 
                                    Stroke="{TemplateBinding Foreground}" 
                                    Fill="{TemplateBinding Background}" />                    
                    </Grid> 
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary> 

Теперь, если кнопке задать стиль BubleButton, то она примет форму обычного эллипса.

Пара моментов:

  • StrokeThickness принимает значение только одно, а у BorderThickness четыре значения,  так что нужно присвоить одно из них.
  • Если кнопке не задать Background, то при нажатии на пустое пространство внутри круга, ничего не случится (обработчик события кнопки не сработает), поэтому его придется задать .

Теперь я добавлю в Grid шаблона Rectangle, и привяжу к свойству Fill Content кнопки:

<Rectangle Fill="{TemplateBinding Content}" />

И разметка кнопки в окне:

<Grid x:Name="grid" Background="#222222" >
    <Button Style="{StaticResource BubleButton}" 
        Height="100"
        Width="100"
        BorderThickness="2"
        Background="{Binding ElementName=grid, Path=Background}"
        Foreground="WhiteSmoke"
        Content="{StaticResource RockIcon}" />
</Grid>

Результат меня как-то не впечатлил. Иконка одного размера с кнопкой (

Первое, что пришло на мысль, задать фиксированный размер для Rectangle. Но тогда и кнопка должна быть точного размера. Но, все-таки, можно сделать иконку, скажем, 80% от размера кнопки. Привязки позволяют нам это сделать с помощью конвертеров. Название говорит само за себя. 

Я создал класс ScaleConverter, который реализует интерфейс IValueConverter:

class ScaleConverter : IValueConverter {
    public double Scale { get; set; }

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        double num = (double)value;
        return (num * (Scale / 100));
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return null;
    }
}

Дополненный вариант шаблона, с применением конвертера ScaleConverter :

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:y="clr-namespace:BubleButtonDemo" >
    <Style x:Key="BubleButton" TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <ControlTemplate.Resources>
                        <y:ScaleConverter x:Key="ScaleConverter" Scale="80"  />
                    </ControlTemplate.Resources>
                    <Grid x:Name="grid">
                        <Ellipse
                                StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Top}"
                                Stroke="{TemplateBinding Foreground}"
                                Fill="{TemplateBinding Background}" />
                         
                        <Rectangle Height="{Binding ElementName=grid, Path=ActualHeight, Converter={StaticResource ScaleConverter}}" 
                                Width="{Binding ElementName=grid, Path=ActualHeight, Converter={StaticResource ScaleConverter}}"
                                Fill="{TemplateBinding Content}" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Можно еще добавить эффект, который будет при наведении на кнопку.
Например, задать для Grid Opacity=".8", а в ControlTemplate создать триггер:

<ControlTemplate.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
        <Setter TargetName="grid" Property="Opacity" Value="1" />
    </Trigger>
</ControlTemplate.Triggers>

Стиль кнопки готовый. Векторная иконка, изменение размера, эффект при наведении. Красиво?

Ну, относительно. Хотелось же, помимо размера, свободно изменять и цвет. Если для Ellipse можно спокойно менять и Stroke через Foreground и Fill через Background кнопки, то, как изменить цвет иконки? Первое о чем я сразу же подумал ContentConverter.

Перед  тем как присвоить DrawingBrush, достать из него все GeometryDrawing и поменять им свойства Brush...

Испробовав (ну конечно!) это извращение, я был немого разочарован, когда узнал что  GeometryDrawing нельзя редактировать. За это отвечает свойство IsFrozen. Если оно возвращает true, то объект заморожен и доступен только для чтения.  И действительно, я же питался изменить цвет ресурса, для конкретной кнопки. Но можно создать копию, у которой по умолчанию IsFrozen равно false, изменить уже в ней нужные свойства и вернуть в качестве результата ))). 

Это сработает, но показывать уже не буду, так как то, что я описал, показалось мне настолько корявым, что лучше отказаться от изменения цвета. Да и спихнуть все на то, что обычно в приложениях кнопки определенного цвета всегда можно :-)

Опишу еще один вариант шаблона. Мне он показался более красивым и удачным решением, чем предыдущий шаблон:

И все-таки создаем ContentConverter. Хотя цель его будет не та что описана ранее. Код конвертера:

class ContentConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        DrawingGroup draw = (value as DrawingBrush).Drawing as DrawingGroup;
        PathGeometry path = new PathGeometry();

        foreach (GeometryDrawing item in draw.Children) {
            path.AddGeometry(item.Geometry);
        }

        return path;
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return null;
    }
}

Здесь возвращается PathGeometry, который содержит всю геометрию иконки.
И полная разметка нового шаблона с ContentConverter и применением геометрии:

<Style x:Key="BubleButton" TargetType="Button">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <ControlTemplate.Resources>
                    <y:ContentConverter x:Key="ContentConverter" />
                    <y:ScaleConverter x:Key="ScaleConverter" Scale="70"  />
                </ControlTemplate.Resources>
                <Grid x:Name="grid">
                    <Path x:Name="ellipse"
                            Stretch="Uniform"
                            StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Left}"
                            Stroke="{TemplateBinding Foreground}"
                            Fill="{TemplateBinding Background}" >
                        <Path.Data>
                            <EllipseGeometry RadiusX="50" RadiusY="50" />
                        </Path.Data>
                    </Path>
                    <Path x:Name="shape" 
                            Stretch="Uniform" 
                            Height="{Binding ElementName=grid, Path=ActualHeight, Converter={StaticResource ScaleConverter}}" 
                            Width="{Binding ElementName=grid, Path=ActualWidth, Converter={StaticResource ScaleConverter}}"
                            Fill="{TemplateBinding Foreground}" 
                            Data="{TemplateBinding Content, Converter={StaticResource ContentConverter}}" >
                    </Path>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="shape" Property="Fill" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Background}" />
                        <Setter TargetName="ellipse" Property="Fill" Value="{Binding RelativeSource={RelativeSource Self}, Path=Stroke}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style> 

Теперь можно менять и размер, и цвет иконки. А еще получился красивый эффект при наведении.

Думаю, что теперь кнопка соответствует всем требованиям, что я поставил в начале. Поэкспериментируйте с исходниками сами. Буду рад услышать ваше мнение.

Prev Next