Всех приветствую, решил выложить свой первый пост на Хабре, не судите строго - вдруг кому-нибудь да пригодится =)
Исходная ситуация: в рамках проекта по разработке декстопного приложения под винду заказчиком было выражено фи по поводу деталей интерфейса, в частности кнопок. Возникла необходимость сделать свой контрол а-ля навигационные кнопки в браузерах.
Задача: сделать контрол кнопки (WPF): круглая, с возможностью использования в качестве иконки объекта Path, с возможностью использовать свойство IsChecked, и сменой цветовых схем при наведении/нажатии.
В итоге кнопка будет иметь следующий внешний вид (иконки само-собой произвольные):
Переходим к реализации. Назовем наш контрол VectorRoundButton, наследуя его от UserControl. XAML разметка нашего контрола предельно проста: масштабируемый Grid; объект Ellipse, символизирующий столь желанную круглую кнопку и объект Path с выбранной иконкой.
<UserControl x:Class="UserControls.VectorRoundButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:UserControls"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="50" Loaded="UserControl_Loaded" MouseEnter="UserControl_MouseEnter" MouseLeave="UserControl_MouseLeave" MouseLeftButtonDown="UserControl_MouseLeftButtonDown" MouseLeftButtonUp="UserControl_MouseLeftButtonUp">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20*"/>
<RowDefinition Height="60*"/>
<RowDefinition Height="20*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20*"/>
<ColumnDefinition Width="60*"/>
<ColumnDefinition Width="20*"/>
</Grid.ColumnDefinitions>
<Ellipse x:Name="ButtonEllipse" Grid.ColumnSpan="3" Grid.RowSpan="3"/>
<Path x:Name="ButtonImage" Stretch="Uniform" Grid.Row="1" Grid.Column="1" />
</Grid>
</UserControl>
Для контроля внешнего вида и состояния кнопки будем использовать следующие свойства:
IsCheckable - возможность отображения в режиме чек-бокса
IsChecked - в речиме чек-бокса - включено/выключено (кнопка обводится кружком)
ActiveButtonColor - цвет активной кнопки (при наведенном курсоре)
InactiveButtonColor - цвет кнопки в нормальном состоянии
ButtonIcon - иконка кнопки
public partial class VectorRoundButton : UserControl
{
public bool IsCheckable
{
get { return (bool)GetValue(IsCheckableProperty); }
set { SetValue(IsCheckableProperty, value); }
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public Brush ActiveButtonColor
{
get { return (Brush)GetValue(ActiveButtonColorProperty); }
set { SetValue(ActiveButtonColorProperty, value); }
}
public Brush InactiveButtonColor
{
get { return (Brush)GetValue(InactiveButtonColorProperty); }
set { SetValue(InactiveButtonColorProperty, value); }
}
public Path ButtonIcon
{
get { return (Path)GetValue(ButtonIconProperty); }
set { SetValue(ButtonIconProperty, value); }
}
}
Для корректной работы контрола в процессе визуальной верстки нашего приложения необходимо реализовать привязку данных через соответствующие DependencyProperty:
public static readonly DependencyProperty IsCheckableProperty = DependencyProperty.Register(
"IsCheckable",
typeof(bool),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, IsCheckablePropertChanged));
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(
"IsChecked",
typeof(bool),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, IsCkeckedPropertChanged));
public static readonly DependencyProperty InactiveButtonColorProperty = DependencyProperty.Register(
"InactiveButtonColor",
typeof(Brush),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(System.Windows.SystemColors.ControlBrush, FrameworkPropertyMetadataOptions.AffectsRender, InactiveButtonColorPropertyChanged));
public static readonly DependencyProperty ActiveButtonColorProperty = DependencyProperty.Register(
"ActiveButtonColor",
typeof(Brush),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(System.Windows.SystemColors.ControlDarkBrush, FrameworkPropertyMetadataOptions.AffectsRender, ActiveButtonColorPropertyChanged));
public static readonly DependencyProperty ButtonIconProperty = DependencyProperty.Register(
"ButtonIcon",
typeof(Path),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, ButtonIconPropertyChanged));
В описанных присоединенных свойствах указаны обработчики событий на их изменение, связанные с выбором иконки, цвета, нажатием на кнопку и т.д. Ниже приводятся методы, реализующие данные обработчики.
Обработчик изменения иконки нашей кнопки:
private static void ButtonIconPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.ButtonIcon.Data = (e.NewValue as Path)?.Data;
control.ButtonIcon.Fill = (e.NewValue as Path)?.Fill;
control.ButtonImage.Data = control.ButtonIcon.Data;
control.ButtonImage.Fill = control.ButtonIcon.Fill;
}
}
Обработчики изменения цветов кнопки:
private static void ActiveButtonColorPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.ActiveButtonColor = (Brush)e.NewValue;
}
}
private static void InactiveButtonColorPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.InactiveButtonColor = (Brush)e.NewValue;
control.ButtonEllipse.Fill = (Brush)e.NewValue;
}
}
Обработчики изменения состояния включено/выключено для кнопки в режиме чек-бокс, а также включения/выключения данного режима:
private static void IsCkeckedPropertChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
if (control.IsCheckable)
{
control.IsChecked = (bool)e.NewValue;
if (control.IsChecked)
{
control.ButtonEllipse.Stroke = System.Windows.SystemColors.ControlDarkBrush;
control.ButtonEllipse.StrokeThickness = 2;
}
else
{
control.ButtonEllipse.Stroke = null;
control.ButtonEllipse.StrokeThickness = 1;
}
}
}
}
private static void IsCheckablePropertChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.IsCheckable = (bool)e.NewValue;
}
}
Осталось совсем немного - реализовать реакцию кнопки на перемещение мышки через данный контрол, а также событие нажатия левой кнопки мыши:
private void UserControl_MouseEnter(object sender, MouseEventArgs e)
{
ButtonEllipse.Fill = ActiveButtonColor;
}
private void UserControl_MouseLeave(object sender, MouseEventArgs e)
{
ButtonEllipse.Fill = InactiveButtonColor;
if (!IsChecked)
ButtonEllipse.Stroke = null;
}
private void UserControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ButtonEllipse.Stroke = System.Windows.SystemColors.ActiveCaptionBrush;
}
private void UserControl_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ButtonEllipse.Fill = ActiveButtonColor;
ButtonEllipse.Stroke = null;
if (IsCheckable)
{
IsChecked = !IsChecked;
}
}
В итоге, имеем следующий код пользовательского контрола:
public partial class VectorRoundButton : UserControl
{
public bool IsCheckable
{
get { return (bool)GetValue(IsCheckableProperty); }
set { SetValue(IsCheckableProperty, value); }
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public Brush ActiveButtonColor
{
get { return (Brush)GetValue(ActiveButtonColorProperty); }
set { SetValue(ActiveButtonColorProperty, value); }
}
public Brush InactiveButtonColor
{
get { return (Brush)GetValue(InactiveButtonColorProperty); }
set { SetValue(InactiveButtonColorProperty, value); }
}
public Path ButtonIcon
{
get { return (Path)GetValue(ButtonIconProperty); }
set { SetValue(ButtonIconProperty, value); }
}
public static readonly DependencyProperty IsCheckableProperty = DependencyProperty.Register(
"IsCheckable",
typeof(bool),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, IsCheckablePropertChanged));
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(
"IsChecked",
typeof(bool),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, IsCkeckedPropertChanged));
public static readonly DependencyProperty InactiveButtonColorProperty = DependencyProperty.Register(
"InactiveButtonColor",
typeof(Brush),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(System.Windows.SystemColors.ControlBrush, FrameworkPropertyMetadataOptions.AffectsRender, InactiveButtonColorPropertyChanged));
public static readonly DependencyProperty ActiveButtonColorProperty = DependencyProperty.Register(
"ActiveButtonColor",
typeof(Brush),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(System.Windows.SystemColors.ControlDarkBrush, FrameworkPropertyMetadataOptions.AffectsRender, ActiveButtonColorPropertyChanged));
public static readonly DependencyProperty ButtonIconProperty = DependencyProperty.Register(
"ButtonIcon",
typeof(Path),
typeof(VectorRoundButton),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, ButtonIconPropertyChanged));
private static void ButtonIconPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.ButtonIcon.Data = (e.NewValue as Path)?.Data;
control.ButtonIcon.Fill = (e.NewValue as Path)?.Fill;
control.ButtonImage.Data = control.ButtonIcon.Data;
control.ButtonImage.Fill = control.ButtonIcon.Fill;
}
}
private static void ActiveButtonColorPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.ActiveButtonColor = (Brush)e.NewValue;
}
}
private static void InactiveButtonColorPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.InactiveButtonColor = (Brush)e.NewValue;
control.ButtonEllipse.Fill = (Brush)e.NewValue;
}
}
private static void IsCkeckedPropertChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
if (control.IsCheckable)
{
control.IsChecked = (bool)e.NewValue;
if (control.IsChecked)
{
control.ButtonEllipse.Stroke = System.Windows.SystemColors.ControlDarkBrush;
control.ButtonEllipse.StrokeThickness = 2;
}
else
{
control.ButtonEllipse.Stroke = null;
control.ButtonEllipse.StrokeThickness = 1;
}
}
}
}
private static void IsCheckablePropertChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source is VectorRoundButton)
{
VectorRoundButton control = source as VectorRoundButton;
control.IsCheckable = (bool)e.NewValue;
}
}
public VectorRoundButton()
{
InitializeComponent();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
ButtonImage.Fill = ButtonIcon?.Fill;
ButtonImage.Data = ButtonIcon?.Data;
ButtonEllipse.Fill = InactiveButtonColor;
}
private void UserControl_MouseEnter(object sender, MouseEventArgs e)
{
ButtonEllipse.Fill = ActiveButtonColor;
}
private void UserControl_MouseLeave(object sender, MouseEventArgs e)
{
ButtonEllipse.Fill = InactiveButtonColor;
if (!IsChecked)
ButtonEllipse.Stroke = null;
}
private void UserControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ButtonEllipse.Stroke = System.Windows.SystemColors.ActiveCaptionBrush;
}
private void UserControl_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ButtonEllipse.Fill = ActiveButtonColor;
ButtonEllipse.Stroke = null;
if (IsCheckable)
{
IsChecked = !IsChecked;
}
}
}
Во собственно и все =) Понимаю, что все написанное до банального просто и вряд ли представляет интерес для серьезных разработчиков, тем более что реализация довольно кривая, но в рамках каких-либо учебных проектов может кому и сгодится.
Камнями не кидайтесь, за сим хочу раскланяться =)
Автор: Роман Хижняк