Предисловие


Некоторое время назад я писал статьи о разработке компонентов на WinForms, о разработке на WPF в стиле WinForms, а теперь хочу поделиться опытом разработки компонентов на WPF, но в “стиле WPF”.
Кому интересно, прошу под кат.

Постановка задачи


Задача такая же как и прежде разработать компонент для отображения слова ДПК. Слово ДПК 32-х разрядное, состоит из адреса – 8 бит и данных – 24 бита. На рисунке показано как выглядит результат.


Выбранное решение


Для решения этой задачи я выбрал “Пользовательский настраиваемый компонент” – по терминологии Visual Studio.
Создал решение с двумя проектами:
  • Debug_WpfApplication – запускаемый проект для отладки;
  • WpfCustomControlLibrary – собственно проект с компонентом.

На рисунке представлено как это выглядит в Visual Studio.


Разбор решения


Как видно, из первого рисунка компонент слова ДПК (CustomDpkView) состоит из 4-х областей:
  • «TextBlock» Адрес;
  • Значение адреса с 1 по 8 бит (Разработанный компонент CustomBinView);
  • «TextBlock» Данные;
  • Значение данных с 9 по 32 бит (Разработанный компонент CustomBinView);

Итак, что же из себя представляют эти два компонента CustomBinView и CustomDpkView?
Поля и методы (данные и поведение) описаны в соответствующих «*.cs» файлах и графический макет (стиль) описан в файле «Generic.xaml».

Компонент CustomBinView



Рассмотрим «CustomBinView.cs».
Здесь описаны DependecyPropery и их регистрация.
Пример 1
//Значение по-умолчанию кол-ва ячеек
        const int DEFAULT_CNT=8;
        static CustomBinView() 
        {
            //Переопределение стандартного стиля
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomBinView), new FrameworkPropertyMetadata(typeof(CustomBinView)));
            ArrayList temp = new ArrayList(DEFAULT_CNT);
            for (int i = 0; i < DEFAULT_CNT; i++) temp.Add(true);
            ValuesProperty = DependencyProperty.Register("Values", typeof(ArrayList), typeof(CustomBinView), new FrameworkPropertyMetadata(temp, new PropertyChangedCallback(InvalidateVisualCallback)));
            FirstNumberProperty = DependencyProperty.Register("FirstNumber", typeof(int), typeof(CustomBinView), new FrameworkPropertyMetadata(new PropertyChangedCallback(InvalidateVisualCallback)));
            ColorTrueProperty = DependencyProperty.Register("ColorTrue", typeof(Brush), typeof(CustomBinView), new FrameworkPropertyMetadata(new PropertyChangedCallback(InvalidateVisualCallback)));
            ColorFalseProperty = DependencyProperty.Register("ColorFalse", typeof(Brush), typeof(CustomBinView), new FrameworkPropertyMetadata(new PropertyChangedCallback(InvalidateVisualCallback)));
            IsVisibleTextProperty = DependencyProperty.Register("IsVisibleText", typeof(bool), typeof(CustomBinView), new FrameworkPropertyMetadata(new PropertyChangedCallback(InvalidateVisualCallback)));
        }
        //При изменении свойств, влияющих на визуальный вид - перерисовывать компонент
        private static void InvalidateVisualCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e) { ((CustomBinView)sender).InvalidateVisual(); }
        //------------------------------------------------------------------------------------------//
        #region DependencyProperty и их регистрация
            public static DependencyProperty ValuesProperty;
            public static DependencyProperty FirstNumberProperty;
            public static DependencyProperty ColorTrueProperty;
            public static DependencyProperty ColorFalseProperty;
            public static DependencyProperty IsVisibleTextProperty;      
        #endregion
        //------------------------------------------------------------------------------------------//
        #region Свойства доступа к значениям DependencyProperty
            //Ячейки со значениями
            public ArrayList Values { get { return (ArrayList)GetValue(ValuesProperty); } set { SetValue(ValuesProperty, value); } }
            //Номер, с которого будут считаться ячейки
            public int FirstNumber { get { return (int)GetValue(FirstNumberProperty); } set { SetValue(FirstNumberProperty, value); } }
            //Цвет true-значения
            public Brush ColorTrue { get { return (Brush)GetValue(ColorTrueProperty); } set { SetValue(ColorTrueProperty, value); } }
            //цвет false-значения
            public Brush ColorFalse { get { return (Brush)GetValue(ColorFalseProperty); } set { SetValue(ColorFalseProperty, value); } }
            //Признак отображения текста в ячейках
            public bool IsVisibleText { get { return (bool)GetValue(IsVisibleTextProperty); } set { SetValue(IsVisibleTextProperty, value); } } 
        #endregion


Ниже описана реализация рендеринга и алгоритма клика по ячейкам.
Пример 2
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {//при изменении размеров перерисовывать содержимое
            base.OnRenderSizeChanged(sizeInfo);
            InvalidateVisual();
        }
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
            DrawingContext dc = drawingContext;
            //Отрисовка фона и гриниц
            dc.DrawRectangle(Background, null, new Rect(new Point(0, 0), RenderSize));
            #region Расчёт размеров и проверка условий
                if ((Values == null) || (Values.Count == 0)) return;
                double FullWidthCell = RenderSize.Width / (double)(Values.Count);
                double WidthCell = FullWidthCell;
                double HeightCell = RenderSize.Height;
                if ((WidthCell > 2) && (HeightCell > 2)) { WidthCell -= 2; HeightCell -= 2; } 
            #endregion
            #region Отрисовка ячеек
                //Текущий номер ячейки, который будет рисоваться в ячейке
                int currentNumber = FirstNumber;
                //точка-указатель на текущую рисующуюся ячейку 
                Point currentPointCell = new Point(1, 1);
                //Размер длины окургления ячейки
                double roundedLenght = ((0.15 * WidthCell) > (0.15 * HeightCell)) ? (0.15 * HeightCell) : (0.15 * WidthCell);
                foreach (bool item in Values)//отрисовка всех ячеек
                {
                    //отрисовка прямоугольника ячейки с фоном
                    dc.DrawRoundedRectangle((item) ? ColorTrue : ColorFalse, new Pen(BorderBrush, 1),
                        new Rect(currentPointCell, new Size(WidthCell, HeightCell)),
                        roundedLenght, roundedLenght);
                    //отрисовка текста ячейки
                    if (IsVisibleText) 
                        Service.DrawTxt(dc, currentNumber.ToString().PadLeft(2, '0'), currentPointCell, new Size(WidthCell, HeightCell), 
                            Foreground, this.FontFamily, this.FontStyle, this.FontWeight);
                    //переход к следующей ячейке
                    currentPointCell.X += FullWidthCell;
                    currentNumber++;
                } 
            #endregion    
        }
        #region Маршрутизируемое событие "Клик по элементу"
            //Класс с аргументами для события
            public class ClickItemRoutedEventArgs : RoutedEventArgs
            {
                //Индекс элемента (нумерация с 0)
                public int Index { get; protected set; }
                //Сам элемент
                public object Item { get; protected set; }
                //Информация о клике мыши
                public MouseButtonEventArgs MouseEventArg { get; protected set; }
                public ClickItemRoutedEventArgs(RoutedEvent routedEvent, object item, int index, MouseButtonEventArgs arg)
                    : base(routedEvent) { Item = item; Index = index; MouseEventArg = arg; }
                public ClickItemRoutedEventArgs()
                    : base() { Item = null; Index = -1; MouseEventArg = null; }
            }
            //Регистрация события
            public static readonly RoutedEvent ClickItemEvent = EventManager.RegisterRoutedEvent("ClickItem", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CustomBinView));
            //Контейнер добавления-удаления подписчиков
            public event RoutedEventHandler ClickItem
            {
                add { base.AddHandler(ClickItemEvent, value); }
                remove { base.RemoveHandler(ClickItemEvent, value); }
            }
            //Функция генерации события
            void RaiseClickItem(object item, int index, MouseButtonEventArgs arg)
            {
                ClickItemRoutedEventArgs args = new ClickItemRoutedEventArgs(ClickItemEvent, item, index, arg);
                RaiseEvent(args);
            }
        #endregion
        protected override void OnMouseUp(MouseButtonEventArgs e)//Клик мышкой по контролу
        {
            base.OnMouseUp(e);
            #region Расчёт размеров и проверка условий
                if ((Values == null) || (Values.Count == 0)) return;
                double FullWidthCell = RenderSize.Width / (double)(Values.Count);
                double WidthCell = FullWidthCell;
                double HeightCell = RenderSize.Height;
                if ((WidthCell > 2) && (HeightCell > 2)) { WidthCell -= 2; HeightCell -= 2; }
                Point currentPointCell = new Point(1, 1); 
            #endregion
            for (int currentNumber = 0; currentNumber < Values.Count; currentNumber++, currentPointCell.X += FullWidthCell)
            {
                //Проверка попадания по ячейке
                if (new Rect(currentPointCell, new Size(WidthCell, HeightCell)).Contains(e.GetPosition(this)))
                {
                    Values[currentNumber] = !(bool)Values[currentNumber];//ИНверсия значения
                    this.InvalidateVisual();//Вызов рендеринга
                    RaiseClickItem(Values[currentNumber], currentNumber, e);//Генерация события клика
                }
            }
        }


Теперь рассмотрим «Generic.xaml». Здесь описан визуальный стиль (макет) компонента.
Пример 3
    <!--Стиль CustomBinView-->
    <Style TargetType="{x:Type local:CustomBinView}">
        <!--Стиль оформления для компонента CustomBinView
        Задание соответствующим полям в CustomBinView.cs значения по-умолчанию-->
        <Setter Property="FirstNumber" Value="1"/>
        <Setter Property="ColorTrue">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="WhiteSmoke" Offset="0" />
                    <GradientStop Color="Red" Offset="1" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="ColorFalse">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="WhiteSmoke" Offset="0" />
                    <GradientStop Color="Gray" Offset="1" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="BorderBrush" Value="Black" />
        <Setter Property="IsVisibleText" Value="True"/>
        <Setter Property="FontFamily" Value="Courier New"/>
        <Setter Property="FontStyle" Value="Normal"/>
        <Setter Property="FontWeight" Value="Normal"/>
        <Setter Property="MinHeight" Value="20"/>
        <Setter Property="MinWidth" Value="50"/>
    </Style>


Компонент CustomDpkView


Рассмотрим «CustomDpkView.cs». Здесь описаны DependencyProperty и их регистрация.
Пример 4
public class CustomDpkView : Control
    {
        public static DependencyProperty ValuesADRProperty; 
        public static DependencyProperty ValuesDATAProperty;
        public static DependencyProperty ColorTrueProperty;
        public static DependencyProperty ColorFalseProperty;
        public const int CNT_ADR_BIT = 8;//Кол-во бит в поле адрес ДПК
        public const int CNT_DATA_BIT = 24;//Кол-во бит в поле данные
        static CustomDpkView()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomDpkView), new FrameworkPropertyMetadata(typeof(CustomDpkView)));
            #region Регистрация и инициализация ValuesADRProperty
                ArrayList temp = new ArrayList(CNT_ADR_BIT);
                for (int i = 0; i < CNT_ADR_BIT; i++) temp.Add(true);
                ValuesADRProperty = DependencyProperty.Register("ValuesADR", typeof(ArrayList), typeof(CustomDpkView), new PropertyMetadata(temp)); 
            #endregion
            #region Регистрация и инициализация ValuesDATAProperty
                temp = new ArrayList(24);
                for (int i = 0; i < 24; i++) temp.Add(true);
                ValuesDATAProperty = DependencyProperty.Register("ValuesDATA", typeof(ArrayList), typeof(CustomDpkView), new PropertyMetadata(temp)); 
            #endregion
            ColorTrueProperty = DependencyProperty.Register("ColorTrue", typeof(Brush), typeof(CustomDpkView));
            ColorFalseProperty = DependencyProperty.Register("ColorFalse", typeof(Brush), typeof(CustomDpkView));            
        }
        //Значения бит в поле Адрес (8 бит)
        public ArrayList ValuesADR { get { return (ArrayList)GetValue(ValuesADRProperty); } protected set { SetValue(ValuesADRProperty, value); } }
        //Значения бит в поле ДАнные (24 бита)
        public ArrayList ValuesDATA { get { return (ArrayList)GetValue(ValuesDATAProperty); } protected set { SetValue(ValuesDATAProperty, value); } }
        //Цвет true - значения
        public Brush ColorTrue { get { return (Brush)GetValue(ColorTrueProperty); } set { SetValue(ColorTrueProperty, value); } }
        //Цвет false - значения
        public Brush ColorFalse { get { return (Brush)GetValue(ColorFalseProperty); } set { SetValue(ColorFalseProperty, value); } }
        #region Программное задание адреса и данных
            public bool SetValuesADR(ArrayList array)
            {
                if (array.Count != CNT_ADR_BIT) return false;
                ValuesADR = array;
                return true;
            }
            public bool SetValuesDATA(ArrayList array)
            {
                if (array.Count != CNT_DATA_BIT) return false;
                ValuesDATA = array;
                return true;
            }
        #endregion
    }


Теперь рассмотрим «Generic.xaml». Здесь описан визуальный стиль (макет) компонента, его строение.
Пример 5
<!--Стиль CustomDpkView-->
    <Style TargetType="TextBlock" x:Key="labelStyle"><!--Стиль оформления для текстовых полей адрес и данные-->
        <Setter Property="TextAlignment" Value="Center"/>
        <Setter Property="FontFamily" Value="Comic Sans MS"/>
        <Setter Property="FontWeight" Value="Bold"/>
        <Setter Property="FontSize" Value="20"/>
    </Style>     
    <Style TargetType="{x:Type local:CustomDpkView}"><!--Стиль оформления для компонента CustomDpkView-->
        <Setter Property="Template">
            <Setter.Value><!--Структура компонента-->
                <ControlTemplate TargetType="{x:Type local:CustomDpkView}">
                    <Grid>
                        <Grid.RowDefinitions><!--Разделение на 4 области (со звёздочкой - плавающее, без - постоянное)-->
                            <RowDefinition Height="30" />
                            <RowDefinition Height="30*" MinHeight="30" />
                            <RowDefinition Height="30" />
                            <RowDefinition Height="30*" MinHeight="30"/>
                        </Grid.RowDefinitions>
                        <!--Текстовое поле Адрес-->
                        <TextBlock Style="{StaticResource labelStyle}" Grid.Row="0" Text="Адрес:"/>
                        <!--Двоичное значение поля Адрес-->
                        <!--Связка полей с помощью TemplateBinding
                        в CustomDpkView.cs с полями компонента CustomBinView-->
                        <local:CustomBinView FirstNumber="1" Grid.Row="1" 
                                             ColorTrue="{TemplateBinding ColorTrue}" 
                                             ColorFalse="{TemplateBinding ColorFalse}"  
                                             Values="{TemplateBinding ValuesADR}"/>
                        <!--Текстовое поле Данные-->
                        <TextBlock Style="{StaticResource labelStyle}" Grid.Row="2" Text="Данные:"/>
                        <!--Двоичное значение поля Данные-->
                        <!--Связка полей с помощью TemplateBinding
                        в CustomDpkView.cs с полями компонента CustomBinView-->
                        <local:CustomBinView FirstNumber="9" Grid.Row="3" 
                                             ColorTrue="{TemplateBinding ColorTrue}"
                                             ColorFalse="{TemplateBinding ColorFalse}" 
                                             Values="{TemplateBinding ValuesDATA}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <!--Значение по-умолчанию для поля в CustomDpkView.cs-->
        <Setter Property="ColorTrue">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="WhiteSmoke" Offset="0" />
                    <GradientStop Color="Red" Offset="1" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <!--Значение по-умолчанию для поля в CustomDpkView.cs-->
        <Setter Property="ColorFalse">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="WhiteSmoke" Offset="0" />
                    <GradientStop Color="Gray" Offset="1" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
    </Style>


Использование этих компонентов в Debug_WpfApplication


Рассмотрим «Main.xaml».

Пример 6
<Window.Resources>
<!-- Стиль для customDpkView1 -->
        <Style TargetType="{x:Type ccl:CustomDpkView}" x:Key="dpkViewStyle_1">
            <Setter Property="ColorTrue">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="WhiteSmoke" Offset="0" />
                        <GradientStop Color="Yellow" Offset="1" />
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="ColorFalse">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="Gray" Offset="0" />
                        <GradientStop Color="White" Offset="1" />
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="114" />
            <RowDefinition MinHeight="50" Height="114*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>
        <ccl:CustomBinView  Name="customBinView1" Margin="12,12,12,20" />
<!-- CustomDpkView с пользовательским стилем -->
        <ccl:CustomDpkView Style="{StaticResource dpkViewStyle_1}" Name="customDpkView1" Grid.Row="1" Height="174" VerticalAlignment="Top" Margin="12,0" />
        <TextBlock Name="textBlock1" Text="TextBlock" Grid.Row="2" TextAlignment="Center" FontStretch="Normal" FontFamily="Comic Sans MS" FontWeight="Bold" FontSize="20" Margin="0,0,429,0" />
        <Button Content="Button" Grid.Row="2" Name="button1" HorizontalAlignment="Right" Width="75" Click="button1_Click" />
<!-- CustomDpkView со стилем по-умолчанию -->
        <ccl:CustomDpkView Grid.Row="1" Margin="12,180,12,0" Name="customDpkView2" VerticalAlignment="Top" Height="133" />
    </Grid>


P.S.


Ссылка на проект: Скачать проект
Примечание: разработка велась в MS Visual Studio 2010 под «.Net Framework 4»
Жду комментариев и дополнений.

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


  1. dordzhiev
    08.05.2016 01:45
    +4

    Вы опять делаете это WinForms way. Переопределять OnRender и OnMouseUp в вашем случае совершенно излишне, в качестве ячеек стоит использовать Button. Для смены цвета ячеек можно использовать триггеры. Любители оверинжиниринга могут сделать свойства TrueStyle и FalseStyle (не самые удачные названия на мой взгляд), ну а совсем матерые ребята могут еще и VisualStates прикрутить.


    1. PahanMenski
      08.05.2016 11:03
      +1

      Мне тоже кажется более уместным использование композиции визуальных элементов (например, кнопок), нежели рисование в OnRender.
      Кроме того, у меня возникли следующие замечания:
      1) Вместо того, чтобы дергать для перерисовки метод InvalidateVisual(), достаточно в конструктор FrameworkPropertyMetadata передать флаг FrameworkPropertyMetadataOptions.AffectsRender.
      2) Текущая реализация свойств зависимости, хранящих коллекции, имеет ряд проблем:
      Во-первых, поскольку у вас подразумевается, что эти свойства только для чтения, необходимо регистрировать их методом RegisterReadOnly. В текущей реализации их значение можно изменить методом DependecnyObject.SetValue() в обход методов SetValuesADR и SetValuesDATA.
      Во-вторых, эти свойства можно было бы сделать изменяемыми и использовать для проверки значений ValidateValueCallback или CoerceValueCallback.
      В-третьих, вы используете одну и ту же изменяемую коллекцию для инициализации значения по-умолчанию. А значит изменения элементов коллекции в одном объекте затронет все остальные, включая вновь созданные. Значение по умолчанию для этих свойств следует сделать null и инициализировать свойства в нестатическом конструкторе.


      1. BlackEngineer
        08.05.2016 11:11

        По первому и второму — согласен. Упущение.


    1. BlackEngineer
      08.05.2016 11:10

      Согласен, но по поводу Button — не совсем. Мой способ ресурсоэффективнее и я его выбрал просто как пример, что можно и не используя стандартные компоненты сделать. По сути CustomBinView — это некий упрощенный многокнопочный Button)


  1. WarFollowsMe
    08.05.2016 11:03

    У вас не wpf-стиль. В WPF заниматься отрисовкой элементов в коде нужно только в очень редких случаях.
    Для наглядности я накидал свой вариант вашего проекта.
    Сравните. Я конечно не утверждаю, что мой вариант эталон «как надо писать», но примерное представление там можно
    увидеть.


    1. BlackEngineer
      08.05.2016 11:12

      Можно, плиз, ссылку на проект.


      1. WarFollowsMe
        08.05.2016 11:36

        А есть какие-то ограничения на html-теги? Просто в help указано что при карме 0 использовать теги можно, и даже в предпросмотре ссылка была.
        В любом случае, вот проект: https://gitlab.com/WarFollowsMe/wpfsample


        1. WarFollowsMe
          08.05.2016 11:37

          хм. а сейчас прописались. может баг хабра, при отправке на модерацию теги съедаются.


  1. dymanoid
    08.05.2016 12:04
    +1

    Пожалуйста, не используйте шрифт Comic Sans. Точка.