Выполнение специального рисования

44

При создании собственных пользовательских элементов может быть отдано предпочтение переопределению метода OnRender() для рисования специального содержимого. Переопределить OnRender() можно в элементе, включающем содержимое (чаще всего это класс-наследник Decorator), так, чтобы добавить некоторое графическое украшение вокруг содержимого. Или же можно переопределить OnRender() в элементе, который не имеет никакого вложенного содержимого, чтобы нарисовать сразу все его визуальное представление.

Например, можно создать собственный элемент, рисующий мелкие графические детали, который затем использовать в другом элементе управления через композицию. Примером в WPF может служить элемент TickBar, который рисует метки в Slider. Элемент TickBar встроен в визуальное дерево Slider через свой шаблон по умолчанию (наряду с Border и Track, которые включают два RepeatButton и Thumb).

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

Например, рассмотрим класс ButtonChrome. В реализации WPF этого класса специальный код отображения принимает во внимание различные свойства, включая RenderDefaulted, RenderMouseOver и RenderPressed. Шаблон элемента управления по умолчанию для Button использует триггеры для установки этих свойств в соответствующее время. Например, это когда курсор мыши перемещается над кнопкой, класс Button использует триггер для установки свойства ButtonChrome.RenderMouseOver в true.

Всякий раз при изменении свойств RenderDefaulted, RenderMouseOver или RenderPressed класс ButtonChrome вызывает базовый метод InvalidateVisual(), указывая, что его текущий внешний вид перестал быть актуальным. Затем WPF вызывает метод ButtonChrome.OnRender() для получения нового графического представления.

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

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

Элемент, выполняющий специальное рисование

Зная, как работает метод OnRender(), и когда его следует использовать, рассмотрим пользовательский элемент управления, который продемонстрирует его в действии.

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

Элемент CustomDrawnElement не нуждается в том, чтобы заключать в себе какое-то дочернее содержимое, поэтому он унаследован непосредственно от FrameworkElement. Он позволяет устанавливать только одно свойство — цвет фона градиента. (Цвет переднего плана жестко закодирован как белый, хотя это легко изменить.)

 public class CustomDrawElement : FrameworkElement
    {
        public static DependencyProperty BackgroundColorProperty;
        static CustomDrawElement()
        {
            FrameworkPropertyMetadata metadata =
                new FrameworkPropertyMetadata(Colors.Yellow);
            metadata.AffectsRender = true;
            BackgroundColorProperty = DependencyProperty.Register("BackgroundColor", 
                typeof(Color), typeof(CustomDrawElement), metadata);
        }

        public Color BackgroundColor
        {
            get { return (Color)GetValue(BackgroundColorProperty); }
            set { SetValue(BackgroundColorProperty, value); }
        }
        ...

Свойство зависимости BackgroundColor специально помечено флагом FrameworkPropertyMetadata.AffectRender. В результате этого WPF автоматически вызывает OnRender() всякий раз при изменении цвета. Однако также понадобится обеспечить вызов метода OnRender(), когда курсор мыши перемещается в новую позицию. Это осуществляется вызовом метода InvalidateVisual() в нужные моменты:

protected override void OnMouseMove(MouseEventArgs e)
   {
            base.OnMouseMove(e);
            this.InvalidateVisual();
   }

   protected override void OnMouseLeave(MouseEventArgs e)
   {
            base.OnMouseLeave(e);
            this.InvalidateVisual();
   }

Единственное, что остается—код отображения. Он использует метод DrawingContext.DrawRectangle() для рисования фона элемента. Свойства ActualWidth и ActualHeight указывают финальные отображаемые размеры элемента. И, наконец, приватный вспомогательный метод по имени GetForegroundBrush() конструирует корректную кисть RadialGradientBrush на основе текущей позиции курсора мыши. Чтобы вычислить центральную точку, понадобится преобразовать текущую позицию курсора мыши над элементом в относительную позицию от 0 до 1, которой ожидает RadialGradientBrush.

protected override void OnRender(DrawingContext drawingContext)
   {
            base.OnRender(drawingContext);
            Rect bounds = new Rect(0, 0, base.ActualWidth, base.ActualHeight);
            drawingContext.DrawRectangle(GetForegroundBrush(), null, bounds);
   }

   private Brush GetForegroundBrush()
   {
        if (!IsMouseOver)
        {
                return new SolidColorBrush(BackgroundColor);
        }
        else
        {
                RadialGradientBrush brush = new RadialGradientBrush(Colors.White, BackgroundColor);
                Point pt = Mouse.GetPosition(this);
                Point pt1 = new Point(pt.X / base.ActualWidth, pt.Y / base.ActualHeight);
                brush.GradientOrigin = pt1;
                brush.Center = pt1;
                return brush;
         }
  }

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

<Grid>
                <Grid.RowDefinitions>
                    <RowDefinition></RowDefinition>
                    <RowDefinition Height="auto"></RowDefinition>
                </Grid.RowDefinitions>
                <lib:CustomDrawElement BackgroundColor="{Binding ElementName=grb,Path=Text}"></lib:CustomDrawElement>
                <StackPanel Grid.Row="1" Margin="10" Orientation="Horizontal">
                    <Label>Выберите цвет: </Label>
                    <ComboBox MinWidth="100" Text="Lime" Name="grb">
                        <ComboBoxItem>Green</ComboBoxItem>
                        <ComboBoxItem>LightBlue</ComboBoxItem>
                        <ComboBoxItem>Blue</ComboBoxItem>
                    </ComboBox>
                </StackPanel>
            </Grid>
Элемент, выполняющий специальное рисование
Пройди тесты
Лучший чат для C# программистов