Предисловие
Некоторое время назад я писал статьи о разработке компонентов на 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)
WarFollowsMe
08.05.2016 11:03У вас не wpf-стиль. В WPF заниматься отрисовкой элементов в коде нужно только в очень редких случаях.
Для наглядности я накидал свой вариант вашего проекта.
Сравните. Я конечно не утверждаю, что мой вариант эталон «как надо писать», но примерное представление там можно
увидеть.BlackEngineer
08.05.2016 11:12Можно, плиз, ссылку на проект.
WarFollowsMe
08.05.2016 11:36А есть какие-то ограничения на html-теги? Просто в help указано что при карме 0 использовать теги можно, и даже в предпросмотре ссылка была.
В любом случае, вот проект: https://gitlab.com/WarFollowsMe/wpfsampleWarFollowsMe
08.05.2016 11:37хм. а сейчас прописались. может баг хабра, при отправке на модерацию теги съедаются.
dordzhiev
Вы опять делаете это WinForms way. Переопределять OnRender и OnMouseUp в вашем случае совершенно излишне, в качестве ячеек стоит использовать Button. Для смены цвета ячеек можно использовать триггеры. Любители оверинжиниринга могут сделать свойства TrueStyle и FalseStyle (не самые удачные названия на мой взгляд), ну а совсем матерые ребята могут еще и VisualStates прикрутить.
PahanMenski
Мне тоже кажется более уместным использование композиции визуальных элементов (например, кнопок), нежели рисование в OnRender.
Кроме того, у меня возникли следующие замечания:
1) Вместо того, чтобы дергать для перерисовки метод InvalidateVisual(), достаточно в конструктор FrameworkPropertyMetadata передать флаг FrameworkPropertyMetadataOptions.AffectsRender.
2) Текущая реализация свойств зависимости, хранящих коллекции, имеет ряд проблем:
Во-первых, поскольку у вас подразумевается, что эти свойства только для чтения, необходимо регистрировать их методом RegisterReadOnly. В текущей реализации их значение можно изменить методом DependecnyObject.SetValue() в обход методов SetValuesADR и SetValuesDATA.
Во-вторых, эти свойства можно было бы сделать изменяемыми и использовать для проверки значений ValidateValueCallback или CoerceValueCallback.
В-третьих, вы используете одну и ту же изменяемую коллекцию для инициализации значения по-умолчанию. А значит изменения элементов коллекции в одном объекте затронет все остальные, включая вновь созданные. Значение по умолчанию для этих свойств следует сделать null и инициализировать свойства в нестатическом конструкторе.
BlackEngineer
По первому и второму — согласен. Упущение.
BlackEngineer
Согласен, но по поводу Button — не совсем. Мой способ ресурсоэффективнее и я его выбрал просто как пример, что можно и не используя стандартные компоненты сделать. По сути CustomBinView — это некий упрощенный многокнопочный Button)