Всех приветствую, решил выложить свой первый пост на Хабре, не судите строго - вдруг кому-нибудь да пригодится =)

Исходная ситуация: в рамках проекта по разработке декстопного приложения под винду заказчиком было выражено фи по поводу деталей интерфейса, в частности кнопок. Возникла необходимость сделать свой контрол а-ля навигационные кнопки в браузерах.

Задача: сделать контрол кнопки (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;
            }
        }
    }

Во собственно и все =) Понимаю, что все написанное до банального просто и вряд ли представляет интерес для серьезных разработчиков, тем более что реализация довольно кривая, но в рамках каких-либо учебных проектов может кому и сгодится.

Камнями не кидайтесь, за сим хочу раскланяться =)

Комментарии (7)


  1. NN1
    15.01.2022 22:29
    +4

    if (source is VectorRoundButton)

    {
    VectorRoundButton control = source as VectorRoundButton;

    Почему не использовать сопоставление с образцом для упрощения кода: if(source is VextorRoundButton control) ?

    Поставьте Roslynator/ReSharper или подолное для автоматической замены.


    1. lazyknight Автор
      15.01.2022 23:19

      да, согласен, спасибо. как я и писал код не сильно оптимизирован.


  1. izobr
    15.01.2022 23:19
    +2

    Я очень давно WPF занимался, но уверен, что здесь не нужно было делать свой контрол, а нужен был всего-лишь template.


    1. AgentFire
      16.01.2022 01:03

      Но там нет IsCheckable.


  1. egorozh
    15.01.2022 23:24
    +4

    Можно было взять обычный ToggleButton и всё это реализовать в стиле в XAML.


  1. Sing
    16.01.2022 07:41
    +3

    Немножко замечаний

    1. Просто вкинуть кучу кода, причём ещё два раза один и тот же — это не совсем статья, мне кажется. Если она нацелена на тех, кто не умеет делать контролы, то они ничего не поймут.
    2. Есть контрол ToggleButton, используйте его
    3. Зачем нужен IsCheckable? Есть IsEnabled.
    4. Для изменения стиля контролов в wpf используются… стили и шаблоны. Если нужно динамически изменять их (для чего-то же вам нужна была DependencyProperty) — используйте DynamicResource. Следовательно, свойства для иконок и цветов не нужны.
    5. Если таргетите .net framework — рекомендую использовать библиотеку Bindables, сократит код для DependencyProperty до двух строк + код обработки.
    6. wpf-way — это использовать биндинги. Например, ButtonImage вы меняете в code-behind, это не нужно. В итоге сами свойства вы хоть и меняете, но вообще не используете.
    7. В IsCkeckedPropertChanged вы меняете руками стили хардкодом. А что если я захочу в каком-то месте другой стиль при нажатии? Это делается триггерами, завязанными на IsChecked — когда он true, то одни сеттеры, когда false — другие. Хочет пользователь поменять только что-то одно, унаследуется от стиля и поменяет. Захочет что-то своё, сделает свой стиль.
    8. UserControl_MouseEnter и UserControl_MouseLeave — то же самое. Делаем триггер стиля на IsMouseOver.


    1. lazyknight Автор
      16.01.2022 08:52

      спасибо, согласен со всем. учтем и переработаем =)