Как я делал user-control на WPF (VS2019, c#)

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

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

Задача: сделать контрол кнопки (WPF): круглая, с возможностью использования в качестве иконки объекта Path, с возможностью использовать свойство IsChecked, и сменой цветовых схем при наведении/нажатии.

В итоге кнопка будет иметь следующий внешний вид (иконки само-собой произвольные):

image-loader.svg

Переходим к реализации. Назовем наш контрол VectorRoundButton, наследуя его от UserControl. XAML разметка нашего контрола предельно проста: масштабируемый Grid; объект Ellipse, символизирующий столь желанную круглую кнопку и объект Path с выбранной иконкой.


    
        
            
            
            
        
        
            
            
            
        
        
      

      

    

Для контроля внешнего вида и состояния кнопки будем использовать следующие свойства:

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;
            }
        }
    }

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

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

© Habrahabr.ru