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

Всем привет. Когда я только начал знакомится с 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>
Все. Кнопка готова. Она круглая и иконка есть. Проблема в том, что для каждой кнопки придется делать такой копипаст, а это и неудобно и некрасиво. Пример правильный, только нужно вынести шаблон в ресурсы и применять его к каждой кнопке.
Так и сделаю, но сначала опишу требования, которые я поставил перед своей кнопкой:
- Иконка должна быть векторная;
- Реализовать нужные привязки, что бы кнопка могла менять размеры и цвета;
- Все это должно красиво выглядеть;
Изначально, думал сразу же писать код, но желание нарисовать иконку меня перебороло. Поэтому сделаю небольшое отступление и создам иконку. Ни, или несколько…
Знаю, многие скажут, что то вроде «Не программиста это дело - иконки рисовать». Может и так, но я люблю рисовать, создавать дизайн своих программ, и уж тем более контролировать этот процесс, если кто-то мне помогает )). Может, сказалось, то, что я вырос в семье художников? Ну, или то, что я и сам на художника учился, до того как программированием не занялся :-) пропустить создание иконки
Создание иконки
Выберем графический редактор. Например, 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>
Теперь можно менять и размер, и цвет иконки. А еще получился красивый эффект при наведении.

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