Разработка компонентов (CustomControl) на WPF


Предисловие

Некоторое время назад я писал статьи о разработке компонентов на 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»
Жду комментариев и дополнений.

ссылка на оригинал статьи https://habrahabr.ru/post/283124/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *