Общие сведения о разработке управления
Расширяемость модели элементов управления Windows Presentation Foundation (WPF) значительно уменьшает необходимость создания новых элементов управления. Однако в некоторых случаях разработчику по-прежнему может понадобиться создать пользовательский элемент управления. В этом разделе обсуждаются средства, которые уменьшают необходимость создавать пользовательские управления и различные модели разработки в Windows Presentation Foundation (WPF). Этот раздел также демонстрирует способ создания нового элемента управления.
В этом разделе содержатся следующие подразделы.
- Альтернативы написанию новых элементов управления
- Модели для создания элемента управления
- Основы разработки элементов управления
- Связанные разделы
Альтернативы написанию новых элементов управления
Ранее, когда разработчикам требовалось настроить специальное отображение существующего элемента управления, они были ограничены в изменениях стандартных свойств элемента управления — цвета фона, ширины границы и размеров шрифта. Чтобы расширить возможности внешнего вида элемента управления, используя не только предопределенные параметры, разработчикам приходилось создавать новый элемент управления путем наследования от существующего элемента и переопределения метода, ответственного за отрисовку элемента. Хотя это возможно и сейчас, WPF теперь позволяет настраивать существующие элементы управления с помощью модели расширенного содержимого, стилей, шаблонов и триггеров. В следующем списке перечислены способы использования этих средств для создания пользовательских единообразных отображений, не прибегая к созданию новых элементов управления.
Расширенное содержимое. Многие стандартные элементы управления WPF поддерживают расширенное содержимое. Например, свойство содержимого Button имеет тип Object, поэтому на Button в теории может отображаться все что угодно. Чтобы кнопка отображала изображение и текст, можно добавить изображение и TextBlock к StackPanel, а затем присвоить StackPanel к свойству Content. Так как элементы управления могут отображать визуальные элементы WPF и произвольные данные, это уменьшает потребность в создании новых элементов управления или изменения существующего элемента управления для поддержки сложной визуализации. Дополнительные сведения о модели содержимого для Button и других моделях содержимого в WPF см. в разделе Модель содержимого WPF.
Стили. Style представляет собой набор значений, представляющих свойства для элемента управления. С помощью стилей разработчик может создать повторно используемое представление нужного внешнего вида и поведения элемента управления без написания нового элемента управления. В качестве примера предположим, что все элементы управления TextBlock должны иметь красные надписи, сделанные шрифтом Arial размером 14. Стиль можно создать как ресурс, задав соответствующие параметры. Это приведет к тому, что каждый добавляемый в приложение TextBlock будет выглядеть одинаково.
Шаблоны данных DataTemplate позволяет настраивать отображение данных на элементе управления. Например, с помощью DataTemplate можно определять формат отображения данных в ListBox. Пример см. в разделе Общие сведения о шаблонах данных. В дополнение к настройке внешнего вида данных DataTemplate может включать в себя элементы пользовательского интерфейса, что дает разработчикам больше возможностей при создании специальных интерфейсов. Например, при помощи DataTemplate можно создать ComboBox, каждый элемент, в котором содержит флажок.
Шаблоны элементов управления. Многие элементы управления в WPF используют шаблон ControlTemplate для определения структуры и внешнего вида элемента управления, который отделяет внешний вид элемента управления от его функциональных возможностей. Переопределив шаблон ControlTemplate, можно существенно изменить внешний вид элемента управления. Предположим, требуется элемент управления, который представляет собой светофор. Этот элемент имеет простой пользовательский интерфейс и функциональность. Он состоит из трех кругов, при этом в каждый момент времени может быть зажжен только один из них. После некоторого размышления становится понятно, что RadioButton обеспечивает функциональность для одновременного выделения только одного из них, но внешний вид RadioButton по умолчанию мало похож на светофор. Так как RadioButton использует шаблон элемента управления для определения своего внешнего вида, проще всего переопределить ControlTemplate, подстроив его под требования элемента управления, а затем воспользоваться переключателями для построения светофора.
Примечание Хотя RadioButton может использовать DataTemplate, в этом случае DataTemplate недостаточно.DataTemplate определяет внешний вид содержимого элемента управления.В случае с RadioButton содержимым является все то, что отображается справа от круга, который указывает, выбран ли RadioButton.В примере со светофором переключатель должен быть просто кругом, который может "зажечься". Так как требования к внешнему виду светофора отличаются от внешнего вида RadioButton по умолчанию, необходимо переопределить ControlTemplate.В общем случае DataTemplate используется для определения содержимого (или данных) элемента управления, а ControlTemplate применяется для построения структуры элемента управления.
Триггеры. Trigger позволяет динамически изменять внешний вид и поведение элемента управления без необходимости создания нового элемента. Например, предположим, что в приложении существует несколько элементов управления ListBox. При этом элементы каждого ListBox должны отображаться красным цветом в жирном начертании при их выделении. Самым очевидным способом выполнения этой задачи является создание класса, наследующего от ListBox, и переопределение метода OnSelectionChanged для изменения внешнего вида выбранного элемента. Однако лучшим решением будет добавить к стилю ListBoxItem триггер, изменяющий внешний вид выделенного элемента. Триггер позволяет изменять значения свойств или совершать действия на основе значения свойства. EventTrigger позволяет выполнять действия при возникновении события.
Дополнительные сведения о стилях, шаблонах и триггерах см. в разделе Стилизация и использование шаблонов.
В общем случае, если элемент управления по функциональности соответствует существующему элементу, но должен выглядеть по-другому, вначале стоит подумать, можно ли использовать какой-либо из описанных в этом разделе методов для изменения существующего внешнего вида элементов.
Модели для создания элемента управления
Модель расширенного содержимого, стили, шаблоны и триггеры уменьшают необходимость создания новых элементов. Тем не менее, если новый элемент управления необходим, важно понимать различные модели разработки элементов управления в WPF. WPF предоставляет три общих модели для создания элементов управления, каждая из которых имеет собственный набор функций и уровень гибкости. Основными классами для этих моделей являются UserControl, Control и FrameworkElement.
Производные от UserControl
Самым простым способом создания элемента управления в WPF является создание производной от UserControl. При построении элемента управления, наследующего от UserControl, разработчик добавляет существующие компоненты к UserControl, называет их и ссылается на обработчики событий в Extensible Application Markup Language (XAML). Это позволяет впоследствии ссылаться на именованные элементы и определять обработчики событий в коде. Эта модель разработки очень схожа с моделью, используемой для разработки приложений в WPF:
Будучи построен правильно, UserControl может пользоваться преимуществами расширенного содержимого, стилями и триггерами. В то же самое время, если элемент управления наследует от UserControl, то пользователи элемента управления не смогут применить DataTemplate или ControlTemplate для настройки его внешнего вида. Для создания пользовательского элемента управления, поддерживающего шаблоны, необходимо создать производную от класса Control или от одной из его производных (отличной от UserControl).
Преимущества создания производной UserControl
Попробуйте создать производную от UserControl, если соблюдены все нижеперечисленные условия:
Необходимо построить элемент управления так же, как строится приложение.
Элемент управления состоит только из существующих компонентов.
Поддержка сложной настройки не требуется.
Производные от элемента управления
Модель, порождаемая от класса Control, используется большинством существующих элементов управления WPF. Когда создается элемент управления, наследующий от класса Control, то его внешний вид определяется при помощи шаблонов. Это позволяет отделить рабочую логику от визуального представления. Разделение пользовательского интерфейса и логики также достигается путем применения команд и привязок вместо событий, а также сокращением использования ссылок на элементы в ControlTemplate. Если пользовательский интерфейс и логика элемента управления должным образом разделены, пользователи элемента управления могут переопределить его шаблон ControlTemplate для изменения внешнего вида элемента управления. Хотя создание пользовательского элемента управления Control не является такой же простой процедурой, как построение элемента управления UserControl, пользовательский элемент управления Control предоставляет наибольшую гибкость.
Преимущества производной от элемента управления
Попробуйте создать производную от Control вместо использования класса UserControl, если выполняется одно из следующих условий:
Внешний вид элемента управления должен настраиваться через ControlTemplate.
Элемент управления должен поддерживать различные темы.
Производная от FrameworkElement
Элементы управления, производные от UserControl или от Control, основываются на сочетании существующих элементов. Для многих скриптов это решение приемлемо, так как любой объект, наследующий от FrameworkElement, может быть в ControlTemplate. В то же время бывают случаи, когда для внешнего вида элемента управления требуется функциональность, превосходящая возможности построения простого элемента. В подобных скриптах необходимо строить компоненты на основе FrameworkElement.
Существует два стандартных метода для построения компонентов, основанных на FrameworkElement: прямая отрисовка и настраиваемое построение элемента. Прямая отрисовка включает переопределение метода OnRender из FrameworkElement и предоставление операций DrawingContext, которые явно определяют графические параметры компонента. Этот метод используется Image и Border. Пользовательское построение элемента включает в себя создание экземпляров и использование объектов типа Visual для построения внешнего вида компонента. Пример см. в разделе Использование объектов DrawingVisual. Track является примером элемента управления в WPF, который использует пользовательское построение элемента. Также в одном элементе можно комбинировать прямую отрисовку и настраиваемое построение.
Преимущества производной от FrameworkElement
Попробуйте создать производную от FrameworkElement, если соблюдено одно из следующих условий:
Необходим тонкий контроль внешнего вида элемента управления, что не обеспечивается простым построением элемента.
Необходимо определить внешний вид элемента управления путем определения собственной логики отображения.
Требуется составить существующие элементы нестандартными способами, выходящими за рамки возможностей UserControl и Control.
Основы разработки элементов управления
Как указано выше, одной из наиболее мощных функций WPF является возможность выхода за пределы задания базовых свойств элемента управления для изменения его внешнего вида и поведения без необходимости создания нового пользовательского элемента управления. Системой свойств WPF и системой событий WPF предоставляются такие возможности, как стили, привязка к данным и триггеры. В следующих разделах даются некоторые практические рекомендации, которым необходимо следовать независимо от модели, используемой для создания пользовательского элемента управления, чтобы пользователи этого элемента управления могли использовать данные возможности точно так же, как в случае любого другого элемента управления, входящего в состав WPF.
Использование свойств зависимостей
Если свойство является свойством зависимостей, разработчик может выполнить следующие действия:
Задать свойство в стиле.
Привязать свойство к источнику данных.
Использовать динамический ресурс в качестве значения свойства.
Анимировать свойство.
Если свойство элемента управления должно поддерживать одну из перечисленных функций, необходимо реализовать его как свойство зависимостей. В следующем примере для определения свойства зависимостей с именем Value необходимо выполнить приведенные ниже действия.
Определите идентификатор DependencyProperty с именем ValueProperty в качестве поля public static readonly.
Зарегистрируйте имя свойства в системе свойств путем вызова DependencyProperty.Register, указывая следующие данные:
Имя свойства.
Тип свойства.
Тип владельца свойства.
Метаданные для свойства. Метаданные содержат значение свойства по умолчанию, CoerceValueCallback и PropertyChangedCallback.
Определите свойство программы-оболочки CLR с именем Value (это имя совпадает с именем, используемым для регистрации свойства зависимостей) путем реализации методов доступа get и set этого свойства. Обратите внимание, что методы доступа get и set вызывают только GetValue и SetValue соответственно. Рекомендуется, чтобы методы доступа к свойствам зависимостей не содержали дополнительной логики, так как клиенты и WPF могут пропускать методы доступа и вызывать GetValue и SetValue напрямую. Например, если свойство привязано к источнику данных, то метод доступа свойства set не вызывается. Вместо добавления дополнительной логики к методам доступа получения и установки следует использовать делегаты ValidateValueCallback, CoerceValueCallback, и PropertyChangedCallback для ответа на значения или их проверки при изменении. Дополнительные сведения об этих обратных вызовах см. в разделе Проверка и обратные вызовы свойства зависимостей.
Определите метод для CoerceValueCallback с именем CoerceValue. Метод CoerceValue гарантирует, что значение Value больше или равно MinValue и меньше или равно MaxValue.
Определите метод для PropertyChangedCallback с именем OnValueChanged. Метод OnValueChanged создает объект RoutedPropertyChangedEventArgs<T> и подготавливает создание перенаправленного события ValueChanged. Перенаправленные события рассматриваются в следующем разделе.
''' <summary>
''' Identifies the Value dependency property.
''' </summary>
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(Decimal), GetType(NumericUpDown), New FrameworkPropertyMetadata(MinValue, New PropertyChangedCallback(AddressOf OnValueChanged), New CoerceValueCallback(AddressOf CoerceValue)))
''' <summary>
''' Gets or sets the value assigned to the control.
''' </summary>
Public Property Value() As Decimal
Get
Return CDec(GetValue(ValueProperty))
End Get
Set(ByVal value As Decimal)
SetValue(ValueProperty, value)
End Set
End Property
Private Shared Overloads Function CoerceValue(ByVal element As DependencyObject, ByVal value As Object) As Object
Dim newValue As Decimal = CDec(value)
Dim control As NumericUpDown = CType(element, NumericUpDown)
newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue))
Return newValue
End Function
Private Shared Sub OnValueChanged(ByVal obj As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
Dim control As NumericUpDown = CType(obj, NumericUpDown)
Dim e As New RoutedPropertyChangedEventArgs(Of Decimal)(CDec(args.OldValue), CDec(args.NewValue), ValueChangedEvent)
control.OnValueChanged(e)
End Sub
/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(decimal), typeof(NumericUpDown),
new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
new CoerceValueCallback(CoerceValue)));
/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{
get { return (decimal)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static object CoerceValue(DependencyObject element, object value)
{
decimal newValue = (decimal)value;
NumericUpDown control = (NumericUpDown)element;
newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));
return newValue;
}
private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
NumericUpDown control = (NumericUpDown)obj;
RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
(decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
control.OnValueChanged(e);
}
Дополнительные сведения см. в разделе Пользовательские свойства зависимостей.
Использование перенаправленных событий
Аналогично свойствам зависимостей, которые расширяют свойства CLR дополнительными функциональными возможностями, перенаправленные события расширяют понятие стандартных событий CLR. При создании нового элемента управления WPF рекомендуется также реализовывать событие как перенаправленное, так как перенаправленные события поддерживают следующее поведение:
События могут обрабатываться в родительском элементе нескольких элементов управления. Если событие является пузырьковым, на него может подписаться один родительский элемент в дереве элементов. Затем разработчики приложения могут использовать один обработчик для реакции на событие нескольких элементов управления. Например, если элемент управления является частью каждого элемента в ListBox (так как он включен в DataTemplate), разработчики приложения могут определить обработчик событий для события элемента управления в ListBox. Обработчик событий вызывается при возникновении события на любом элементе управления.
Перенаправленные события можно использовать в EventSetter. Это позволяет разработчикам указывать обработчик события внутри стиля.
Перенаправленные события можно использовать в EventTrigger, что может пригодиться для анимации свойств при помощи XAML. Дополнительные сведения см. в разделе Общие сведения об эффектах анимации.
В следующем примере определяется перенаправленное событие с помощью следующих действий:
Определите идентификатор RoutedEvent с именем ValueChangedEvent в качестве поля public static readonly.
Зарегистрируйте перенаправленное событие путем вызова метода EventManager.RegisterRoutedEvent. При вызове RegisterRoutedEvent в примере указывается следующая информация:
Событие имеет имя ValueChanged.
Стратегией маршрутизации является Bubble, то есть вначале вызывается обработчик событий в источнике (объекте, вызывающем событие), а затем поочередно вызываются обработчики событий родительских элементов источника, начиная с обработчика событий ближайшего родительского элемента.
Обработчик событий имеет тип RoutedPropertyChangedEventHandler<T>, созданный с помощью типа Decimal.
Типом владельца события является NumericUpDown.
Объявите открытое событие, называемое ValueChanged и включающее объявления метода доступа к событию. В этом примере вызывается AddHandler в объявлении метода доступа add и RemoveHandler в объявлении метода доступа remove для использования служб событий WPF.
Создайте защищенный виртуальный метод с именем OnValueChanged, создающий событие ValueChanged.
''' <summary>
''' Identifies the ValueChanged routed event.
''' </summary>
Public Shared ReadOnly ValueChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, GetType(RoutedPropertyChangedEventHandler(Of Decimal)), GetType(NumericUpDown))
''' <summary>
''' Occurs when the Value property changes.
''' </summary>
Public Custom Event ValueChanged As RoutedPropertyChangedEventHandler(Of Decimal)
AddHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
MyBase.AddHandler(ValueChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
MyBase.RemoveHandler(ValueChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As System.Object, ByVal e As RoutedPropertyChangedEventArgs(Of Decimal))
End RaiseEvent
End Event
''' <summary>
''' Raises the ValueChanged event.
''' </summary>
''' <param name="args">Arguments associated with the ValueChanged event.</param>
Protected Overridable Sub OnValueChanged(ByVal args As RoutedPropertyChangedEventArgs(Of Decimal))
MyBase.RaiseEvent(args)
End Sub
/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
"ValueChanged", RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));
/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
RaiseEvent(args);
}
Дополнительные сведения см. в разделах Общие сведения о перенаправленных событиях и Практическое руководство. Создание пользовательских перенаправленных событий.
Использование привязки
Для отделения пользовательского интерфейса элемента управления от логики попробуйте использовать привязку данных. Это особенно важно, если внешний вид элемента управления определяется при помощи ControlTemplate. Использование привязки данных может избавить разработчика от необходимости ссылаться на определенные части пользовательского интерфейса из кода. Рекомендуется избегать ссылающихся элементов, находящихся в ControlTemplate, потому что когда код ссылается на элементы в ControlTemplate, а объект ControlTemplate изменен, ссылочный элемент должен быть включен в новый ControlTemplate.
Следующий пример обновляет TextBlock элемента управления NumericUpDown, присваивая ему имя и ссылаясь на текстовое поле в коде по имени.
<Border BorderThickness="1" BorderBrush="Gray" Margin="2"
Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
Private Sub UpdateTextBlock()
valueText.Text = Value.ToString()
End Sub
private void UpdateTextBlock()
{
valueText.Text = Value.ToString();
}
В следующем примере для выполнения тех же действий используется привязка.
<Border BorderThickness="1" BorderBrush="Gray" Margin="2"
Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
Дополнительные сведения о привязке данных см. в разделе Общие сведения о связывании данных.
Разработка для конструкторов
Чтобы получить поддержку пользовательских элементов управления WPF в Конструктор WPF для Visual Studio, необходимо воспользоваться этими правилами. Дополнительные сведения по разработке для сред. Конструктор WPF см. в разделе Конструктор WPF.
Свойства зависимостей
Реализуйте методы доступа CLR get and set, как описано ранее в разделе "Использование свойств зависимостей". Конструкторы могут использовать программу-оболочку для обнаружения присутствия свойств зависимости, но, подобно WPF и клиентам элемента управления, не обязаны вызывать методы доступа при получении или установке свойств.
Вложенные свойства зависимостей
Вложенные свойства зависимостей на пользовательских элементах управления следует реализовывать с помощью следующих правил:
Воспользуйтесь public static readonly DependencyProperty из формы PropertyNameProperty, которая производила создание с помощью метода RegisterAttached. Имя свойства, которое передается RegisterAttached должно соответствовать PropertyName.
Реализуйте пару public static методов CLR с именами SetPropertyName и GetPropertyName. Оба метода должны принимать производный класс от DependencyProperty в качестве первого аргумента. Метод SetPropertyName также принимает аргумент, тип которого соответствует зарегистрированному типу данных для свойства. Метод GetPropertyName должен возвращать значение того же типа. Если метод SetPropertyName отсутствует, свойство будет отмечено как свойство только для чтения.
Методы Setимя_свойства и Getимя_свойства должны перенаправляться непосредственно в методы GetValue и SetValue целевого объекта зависимостей, соответственно. Конструкторы могут обращаться к вложенному свойству зависимостей с помощью вызова через программу-оболочку метода или с помощью прямого вызова целевого объекта зависимостей.
Дополнительные сведения о вложенных свойствах зависимостей см. в разделе Общие сведения о вложенных свойствах зависимостей.
Определение и использование общих ресурсов
Элемент управления можно включить в ту же сборку, что и приложение, или можно пакетировать его в отдельной сборке, которая может использоваться в нескольких приложениях. В большинстве своем, сведения, обсуждаемые в этом разделе, применимы независимо от используемого метода. Однако одно отличие требует упоминания. При помещении элемента управления в одну сборку с приложением можно свободно добавить глобальные ресурсы в файл app.xaml. Однако сборка, содержащая только элементы управления, не имеет связанного с ней объекта Application, поэтому файл app.xaml недоступен.
Когда приложение выполняет поиск ресурса, оно использует три уровня в следующем порядке:
Уровень элемента.
Система начинает работу с элементом, ссылающимся на ресурс, и затем ищет ресурсы логического родительского элемента и так далее, пока не будет достигнут корневой элемент.
Уровень приложения.
Ресурсы, определенные объектом Application.
Уровень темы.
Словари уровня темы сохраняются во вложенной папке с именем Themes. Файлы в папке Themes соответствуют темам. Например, могут быть файлы Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml и т.д. Также может иметься файл с именем generic.xaml. Когда система ищет ресурс на уровне темы, сначала она выполняет поиск в файле отдельной темы и затем — в файле generic.xaml.
Если элемент управления находится в сборке отдельно от приложения, глобальные ресурсы необходимо поместить на уровне элемента или темы. Оба метода имеют свои преимущества.
Определение ресурсов на уровне элемента
Общие ресурсы можно определить на уровне элемента созданием пользовательского словаря ресурсов и его объединением со словарем ресурсов элемента управления. При использовании этого метода файл ресурса можно назвать любым способом, и он может находиться в той же папке, что и элементы управления. Ресурсы на уровне элемента также могут использовать простые строки как ключи. В следующем примере создается файл ресурса LinearGradientBrush с именем Dictionary1.xaml.
<ResourceDictionary
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<LinearGradientBrush
x:Key="myBrush"
StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Red" Offset="0.25" />
<GradientStop Color="Blue" Offset="0.75" />
</LinearGradientBrush>
</ResourceDictionary>
После определения ресурса его необходимо объединить со словарем ресурсов элемента управления. Это можно сделать с помощью XAML или кода.
В следующем примере выполняется объединение словаря ресурсов с помощью XAML.
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
Недостатком этого метода является то, что объект ResourceDictionary создается при каждой ссылке на него. Например, если в библиотеке имеется 10 пользовательских элементов управления и выполняется объединение словарей общих ресурсов для каждого элемента управления с использованием файла XAML, будут созданы 10 идентичных объектов ResourceDictionary. Этого можно избежать созданием статического класса, который объединяет ресурсы в коде и возвращает итоговый объект ResourceDictionary.
В следующем примере создается класс, который возвращает общий объект ResourceDictionary.
internal static class SharedDictionaryManager
{
internal static ResourceDictionary SharedDictionary
{
get
{
if (_sharedDictionary == null)
{
System.Uri resourceLocater =
new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml",
System.UriKind.Relative);
_sharedDictionary =
(ResourceDictionary)Application.LoadComponent(resourceLocater);
}
return _sharedDictionary;
}
}
private static ResourceDictionary _sharedDictionary;
}
В следующем примере выполняется объединение общего ресурса с ресурсами пользовательского элемента управления в конструкторе элемента управления до того, как он вызывает InitilizeComponent. Так как SharedDictionaryManager.SharedDictionary является статическим свойством, ResourceDictionary создается только один раз. Так как словарь ресурсов был объединен до того, как был вызван InitializeComponent, эти ресурсы доступны элементу управления в его файле XAML.
public NumericUpDown()
{
this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
InitializeComponent();
}
Определение ресурсов на уровне темы
WPF позволяет создавать ресурсы для разных тем Windows. Автор элемента управления может определить ресурс для отдельной темы, чтобы изменять внешний вид элемента управления в зависимости от используемой темы. Например, внешний вид элемента управления Button в теме Windows Classic (тема по умолчанию для Windows 2000) отличается от внешнего вида элемента управления Button в теме Windows Luna (тема по умолчанию для Windows XP), так как Button использует отдельный шаблон ControlTemplate для каждой темы.
Ресурсы, указанные для темы, хранятся в словаре ресурсов с отдельным именем файла. Эти файлы должны находиться в папке с именем Themes, которая является вложенной в папке, содержащей элемент управления. В следующей таблице перечислены файлы словарей ресурсов и схемы, с которыми они связаны в каждом файле:
Имя файла словаря ресурсов |
Тема Windows |
---|---|
Classic.xaml |
Классический внешний вид Windows 9x/2000 для Windows XP |
Luna.NormalColor.xaml |
Синяя тема по умолчанию для Windows XP |
Luna.Homestead.xaml |
Оливковая тема для Windows XP |
Luna.Metallic.xaml |
Серебристая тема для Windows XP |
Royale.NormalColor.xaml |
Тема по умолчанию для Windows XP Media Center Edition |
Aero.NormalColor.xaml |
Тема по умолчанию для Windows Vista |
Не требуется определять ресурс для каждой темы. Если ресурс не определен для конкретной темы, элемент управления выполняет поиск ресурса в файле Classic.xaml. Если ресурс не определен ни в файле, соответствующем конкретной теме, ни в файле Classic.xaml, элемент управления использует универсальный ресурс из файла словаря ресурсов generic.xaml. Файл generic.xaml расположен в одной папке с файлами словаря ресурсов для конкретных тем. Хотя файл generic.xaml не соответствует отдельной теме Windows, он является словарем уровня схемы.
В примере пользовательского элемента управления NumericUpDown с темой и поддержкой модели автоматизации пользовательского интерфейса содержатся два словаря ресурсов для элемента управления NumericUpDown: generic.xaml и Luna.NormalColor.xaml. Можно запустить приложение и переключаться между серебристой темой в Windows XP и другой темой, чтобы увидеть разницу между двумя шаблонами элемента управления. (При работе в Windows Vista можно переименовать Luna.NormalColor.xaml в Aero.NormalColor.xaml и переключаться между двумя темами, например классической Windows и темой по умолчанию для Windows Vista.)
При помещении ControlTemplate в другие файлы словарей ресурсов отдельных тем необходимо создать статический конструктор для элемента управления и вызвать метод OverrideMetadata(Type, PropertyMetadata) в DefaultStyleKey, как показано в следующем примере.
Shared Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
static NumericUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Определяющие и ссылочные ключи для ресурсов тем
При определении ресурса на уровне элемента можно назначить строку в качестве его ключа и обращаться к ресурсу через эту строку. При определении ресурса на уровне темы необходимо в качестве ключа использовать ComponentResourceKey. В следующем примере определяется ресурс в файле generic.xaml.
<LinearGradientBrush
x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:Painter},
ResourceId=MyEllipseBrush}"
StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Blue" Offset="0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>
В следующем примере выполняется ссылка на ресурс определением в качестве ключа ComponentResourceKey.
<RepeatButton
Grid.Column="1" Grid.Row="0"
Background="{StaticResource {ComponentResourceKey
TypeInTargetAssembly={x:Type local:NumericUpDown},
ResourceId=ButtonBrush}}">
Up
</RepeatButton>
<RepeatButton
Grid.Column="1" Grid.Row="1"
Background="{StaticResource {ComponentResourceKey
TypeInTargetAssembly={x:Type local:NumericUpDown},
ResourceId=ButtonBrush}}">
Down
</RepeatButton>
Определение местоположения ресурсов тем
Чтобы найти ресурсы для элемента управления, ведущее приложение должно знать, что сборка содержит ресурсы отдельного элемента управления. Для этого можно добавить ThemeInfoAttribute в сборку, которая содержит элемент управления. ThemeInfoAttribute имеет свойство GenericDictionaryLocation, которое указывает местоположение универсальных ресурсов, и свойство ThemeDictionaryLocation, указывающее местоположение ресурсов отдельной схемы.
В следующем примере для свойств GenericDictionaryLocation и ThemeDictionaryLocation задается значение SourceAssembly, чтобы указать, что универсальные и отдельные ресурсы схем находятся в одной сборке с элементом управления.
<Assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)>
[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
ResourceDictionaryLocation.SourceAssembly)]