Построение пользовательского элемента

34

Хороший способ начать разработку пользовательских элементов управления — попробовать создать самый простой элемент. В этом разделе мы начнем с создания базового указателя цвета. Позднее будет показано, как усовершенствовать этот элемент до более развитого элемента на основе шаблонов.

Создание указателя цвета достаточно просто. В Интернете доступно несколько примеров такого инструмента, в том числе один в комплекте .NET Framework SDK. Тем не менее, создание собственного инструмента для выбора цвета остается полезным упражнением. Оно не только позволяет продемонстрировать широкое разнообразие важных концепций построения элементов управления, но также предоставляет практичный кусок функциональности.

Для начала можно создать специализированное диалоговое окно для выбора цвета, подобное тому, что включено в Windows Forms. Но если необходим указатель цвета, который можно было бы интегрировать в различные окна, то пользовательский элемент управления — намного лучший вариант. Наиболее простой тип специализированного элемента — пользовательский элемент управления, который позволяет собрать комбинацию элементов подобно тому, как это делается при проектировании окна или страницы. Поскольку указатель цвета должен выглядеть несколько сложнее, чем примитивная группа существующих элементов управления с дополнительной функциональностью, он представляется наилучшим выбором.

Обычный указатель цвета позволяет пользователю выбирать цвет щелчком где-то на поле цветового градиента либо указанием индивидуальных составляющих красного, зеленого и синего цветов. На рисунке показан базовый указатель цвета, который будет создан в этом разделе (в верхней части окна). Он состоит из трех элементов управления Slider для настройки цветовых составляющих, а также элементом Rectangle для предварительного отображения выбранного цвета:

Указатель цвета

Подход на основе пользовательских элементов управления обладает одним существенным недостатком — он ограничивает возможности настройки внешнего вида указателя цвета с целью адаптации к разным окнам, приложениям и пользователям. К счастью, ненамного сложнее сделать следующий шаг к элементу управления на основе шаблона.

Определение свойств зависимости

Первый шаг в создании указателя цвета — добавление пользовательского элемента управления в проект библиотеки элементов управления. Когда это делается, Visual Studio создает файл разметки XAML и соответствующий специальный класс, чтобы определить в них инициализацию и код обработки событий. Это то же самое, что приходится делать при создании нового окна или страницы. Единственное отличие в том, что контейнером верхнего уровня выступает класс UserControl:

public partial class ColorPicker : System.Windows.Controls.UserControl
{ ... }

Далее легче всего начать с проектирования общедоступного интерфейса, который пользовательский элемент управления предоставит внешнему миру. Другими словами, нужно создать свойства, методы и события, которые будут поступать в этот элемент управления, и на которые будет опираться приложение, использующее его для взаимодействия с указателем цвета.

Наиболее фундаментальной деталью является свойство Color. В конце концов, указатель цвета — не что иное, как специализированный инструмент для отображения и выбора цветового значения. Чтобы поддержать такие средства WPF, как привязка данных, стили и анимация, доступные для записи свойства элемента управления почти всегда должны быть свойствами зависимости.

Как известно, первый шаг в создании свойства зависимости — это определение статического поля для него с добавленным словом Property в конце его имени:

public static DependencyProperty ColorProperty;

Свойство Color позволит коду, использующему этот элемент управления, программно устанавливать или извлекать значение цвета. Однако ползунки в указателе цвета также позволят пользователю модифицировать по одному аспекту текущего цвета. Для реализации такого проектного решения можно применить обработчики событий, реагирующие на изменение положений ползунка и соответствующим образом обновляющие значение свойства Color. Но проще будет присоединить ползунки к этому свойству с помощью привязки данных. Чтобы сделать это, придется определить каждую составляющую цвета в виде отдельного свойства зависимости:

public static DependencyProperty RedProperty;
public static DependencyProperty GreenProperty;
public static DependencyProperty BlueProperty;

Хотя свойство Color будет хранить объект System.Windows.Media.Color, свойства Red, Green и Blue будут хранить индивидуальные байтовые значения, представляющие каждый из трех компонентов цвета. (Можно также добавить ползунок и свойство для установки альфа-значения, что позволит создавать частично прозрачные цвета, но в данном примере это не делается.)

Определение статических полей для свойств — лишь первый шаг. Также понадобится статический конструктор элемента управления, который зарегистрирует свойства, указывая имя свойства, тип данных и класс элемента управления, владеющий данным свойством. Как было показано, это позволит воспользоваться некоторыми специфическими средствами свойств (вроде наследования значений) за счет передачи объекта FrameworkPropertyMetadata с правильно установленными флагами. Также в этот момент можно присоединить обратные вызовы для проверки достоверности, коррекции значений и уведомлений об изменении значения.

В указателе цвета нужно сделать только одно — добавить обратные вызовы, которые будут реагировать на изменение различных свойств. Это объясняется тем, что свойства Red, Green и Blue — на самом деле просто другое представление свойства Color, и при изменении любого из этих трех следует обеспечить синхронизацию последнего.

Ниже приведен код статического конструктора, регистрирующего четыре свойства зависимости для указателя цвета:

static ColorPicker()
        {
            // Регистрация свойств зависимости
            ColorProperty = DependencyProperty.Register("Color", typeof(Color), typeof(ColorPicker), 
                new FrameworkPropertyMetadata(Colors.Black, new PropertyChangedCallback(OnColorChanged)));
            RedProperty = DependencyProperty.Register("Red", typeof(byte), typeof(ColorPicker),
                new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));
            GreenProperty = DependencyProperty.Register("Green", typeof(byte), typeof(ColorPicker),
                new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged))); 
            BlueProperty = DependencyProperty.Register("Blue", typeof(byte), typeof(ColorPicker),
                 new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));
         }

Теперь, определив свойства зависимости, можно добавить стандартные оболочки для свойств, которые облегчают доступ к ним и обеспечивают возможность обращения из XAML-разметки:

public Color Color
{
      get { return (Color)GetValue(ColorProperty); }
      set { SetValue(ColorProperty, value); }
}
public byte Red
{
      get { return (byte)GetValue(RedProperty); }
      set { SetValue(RedProperty, value); }
}
public byte Green
{
      get { return (byte)GetValue(GreenProperty); }
      set { SetValue(GreenProperty, value); }
}
public byte Blue
{
      get { return (byte)GetValue(BlueProperty); }
      set { SetValue(BlueProperty, value); }
}

Вспомните, что оболочки свойств не должны содержать никакой логики, поскольку свойства могут устанавливаться и извлекаться непосредственно с помощью методов SetValue() и GetValue() базового класса DependencyObject. Например, логика синхронизации свойств в данном примере реализуется с использованием обратных вызовов, которые инициируются при изменении свойств через их оболочки, либо при прямом вызове SetValue().

Обратные вызовы изменения свойств отвечают за сохранение соответствия свойства Color текущим значениям Red, Green и Blue. Всякий раз, когда изменяется свойство Red, Green или Blue, свойство Color тоже соответствующим образом модифицируется:

private static void OnColorRGBChanged(DependencyObject sender, 
            DependencyPropertyChangedEventArgs e)
        {
            ColorPicker colorPicker = (ColorPicker)sender;
            Color color = colorPicker.Color;
            if (e.Property == RedProperty)
                color.R = (byte)e.NewValue;
            else if (e.Property == GreenProperty)
                color.G = (byte)e.NewValue;
            else if (e.Property == BlueProperty)
                color.B = (byte)e.NewValue;

            colorPicker.Color = color;
        }

В случае установки свойства Color свойства Red, Green и Blue также обновляются:

private static void OnColorChanged(DependencyObject sender,
      DependencyPropertyChangedEventArgs e)
{
      Color newColor = (Color)e.NewValue;
      ColorPicker colorpicker = (ColorPicker)sender;
      colorpicker.Red = newColor.R;
      colorpicker.Green = newColor.G;
      colorpicker.Blue = newColor.B;
}

Хотя на первый взгляд может показаться, что такой код инициирует бесконечную последовательность вызовов, когда каждое свойство будет изменять другое, на самом деле подобного не происходит. Это объясняется тем, что WPF не допускает повторного вхождения при обратных вызовах изменения свойств. Например, при изменении свойства Color инициируется метод OnColorChanged(). Он модифицирует свойства Red, Green и Blue, генерируя три раза обратный вызов OnColorRGBChanged() (по одному для каждого из свойств). Однако OnColorRGBChanged() не вызовет еще раз OnColorChanged().

Может случиться так, что для обработки свойств цвета будут применены принудительные обратные вызовы. Однако такой подход нецелесообразен. Принудительные обратные вызовы свойств предназначены для взаимосвязанных свойств, которые могут переопределять или влиять друг на друга. Они не имеют смысла для свойств, представляющих одни и те же данные разными способами. Если вы примените принудительные свойства в данном примере, то станет возможно устанавливать разные значения свойств Red, Green и Blue, тем самым переопределяя цветовую информацию свойства Color. Поведение, которое в действительности нужно, заключается в установке свойств Red, Green и Blue и применении этой информации для постоянного изменения значения свойства Color.

Определение маршрутизируемых событий

Также может понадобиться добавить маршрутизируемые события, которые позволяют уведомлять потребителя элемента управления о том, что что-то произошло. В примере с указателем цвета удобно иметь событие, возбуждаемое при изменении цвета. Хотя это событие может быть определено как обычное событие .NET, применение маршрутизируемого события позволит организовать пузырьковое распространение и туннелирование, так что события смогут обрабатывать более высокоуровневые родители вроде содержащего элемент окна.

Как и в случае свойств зависимости, первый шаг в определении маршрутизируемого события — это создание статического свойства для него, со словом Event, добавленным в конец имени:

public static readonly RoutedEvent ColorChangedEvent;

Затем можно зарегистрировать это событие в статическом конструкторе. При этом указывается имя события, стратегия маршрутизации, сигнатура и класс-владелец:

// Регистрация маршрутизируемого события
ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged",RoutingStrategy.Bubble,
    typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPicker));

Вместо того, чтобы разрабатывать новый делегат для сигнатуры события, иногда можно воспользоваться существующими делегатами.

Два полезных делегата, которые могут пригодиться в этом случае — это RoutedEventHandler (для маршрутизируемых событий, не передающих никакой дополнительной информации) и RoutedPropertyChangedEventHandler (для маршрутизируемых событий, передающих старое и новое значения после изменения свойства). RoutedPropertyChangedEventHandler, используемый в предыдущем примере — это обобщенный делегат, параметризованный типом. В результате его можно применять с любыми данными свойств, не нарушая безопасности типов.

Определив и зарегистрировав событие, понадобится создать стандартную оболочку для события .NET, которая примет событие. Эта оболочка события может быть использована для присоединения (и удаления) слушателей события:

public event RoutedPropertyChangedEventHandler<Color> ColorChanged
{
     add { AddHandler(ColorChangedEvent, value); }
     remove { RemoveHandler(ColorChangedEvent, value); }
}

Вспомните, что обратный вызов OnColorChanged() инициируется при любой модификации свойства Color — будь то непосредственно или же при изменении компонентов цвета Red, Green и Blue.

Лучший чат для C# программистов