Круглая кнопка с векторной иконкой
Всем привет. Когда я только начал знакомится с 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>
Теперь можно менять и размер, и цвет иконки. А еще получился красивый эффект при наведении.

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