WPF 自定义控件教程:从入门到精通
前言
WPF (Windows Presentation Foundation) 提供了强大的 UI 框架,允许开发者创建美观且功能丰富的应用程序。WPF 的核心优势之一在于其高度的 可定制性。除了使用内置控件外,WPF 还允许我们创建完全自定义的控件,以满足特定的 UI 需求。
本教程将带你深入了解 WPF 自定义控件的开发,涵盖从基础概念到高级技巧,并提供详细的代码示例和解释,助你掌握自定义控件的精髓。
目录
为什么要自定义控件?
自定义控件的基础概念
2.1. 依赖属性 (Dependency Properties)
2.2. 路由事件 (Routed Events)
2.3. 控件模板 (Control Templates)
2.4. 样式和主题 (Styles and Themes)
2.5. 内容呈现 (Content Presentation)
自定义控件的类型
3.1. 用户控件 (User Controls)
3.2. 模板化控件 (Templated Controls / Custom Controls)
3.3. 元素控件 (Element Controls / FrameworkElement 直接派生)
实战教程:创建一个自定义评分控件 (Templated Control)
4.1. 创建自定义控件库项目
4.2. 定义依赖属性 (评分值、星星数量)
4.3. 定义路由事件 (评分值改变事件)
4.4. 设计控件模板 (XAML 结构和样式)
4.5. 实现控件逻辑 (C# 代码,属性更改处理,事件触发)
4.6. 应用样式和主题
4.7. 在应用程序中使用自定义控件
高级自定义控件技术
5.1. 命令 (Commands)
5.2. 自定义控件中的数据绑定
5.3. 自定义布局 (MeasureOverride, ArrangeOverride)
5.4. 控件的可访问性 (Accessibility)
5.5. 性能优化
总结
1. 为什么要自定义控件?
WPF 提供了丰富的内置控件,但在以下情况下,你可能需要创建自定义控件:
满足特定 UI 需求: 内置控件无法完全满足你的应用程序的独特界面设计或交互逻辑。
封装可重用 UI 组件: 你需要创建可以在应用程序中多次使用的、具有特定功能和外观的 UI 组件。
提升代码可维护性: 将复杂的 UI 逻辑封装在自定义控件中,可以提高代码的可读性和可维护性。
创建独特的品牌风格: 自定义控件可以帮助你打造与众不同的应用程序界面,体现独特的品牌风格。
2. 自定义控件的基础概念
在开始创建自定义控件之前,我们需要理解 WPF 中与控件开发密切相关的几个核心概念。
2.1. 依赖属性 (Dependency Properties)
依赖属性是 WPF 属性系统中的核心概念,它增强了 CLR 属性的功能,并为 WPF 控件提供了强大的特性,例如:
样式设置 (Styling): 允许通过样式 (Styles) 和模板 (Templates) 设置属性值。
数据绑定 (Data Binding): 支持将属性值绑定到数据源,实现 UI 与数据的同步。
属性值继承 (Property Value Inheritance): 允许子元素继承父元素的属性值。
动画 (Animation): 支持对属性值进行动画操作。
属性值更改通知 (Property Change Notification): 提供属性值更改时的回调机制。
验证 (Validation): 可以在属性值设置时进行验证。
强制值 (Coercion): 可以强制属性值在一定范围内。
默认值 (Default Value): 可以为属性设置默认值。
如何定义依赖属性:
使用 DependencyProperty.Register()
方法在控件类中注册依赖属性。
C#
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", // 属性名称
typeof(int), // 属性类型
typeof(MyCustomControl), // 属性所属的控件类型
new FrameworkPropertyMetadata(
0, // 默认值
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, // 元数据选项
new PropertyChangedCallback(OnValueChanged) // 属性更改回调
),
new ValidateValueCallback(ValidateValue) // 属性值验证回调 (可选)
);
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyCustomControl control = (MyCustomControl)d;
int newValue = (int)e.NewValue;
int oldValue = (int)e.OldValue;
// 在这里处理属性值更改的逻辑
control.OnValueChanged(oldValue, newValue);
}
private static bool ValidateValue(object value)
{
int v = (int)value;
return v >= 0 && v <= 100; // 验证值是否在 0-100 范围内
}
代码解释:
DependencyProperty.Register(...)
: 静态方法,用于注册依赖属性。"Value"
: 依赖属性的名称,通常与 CLR 属性名相同。typeof(int)
: 依赖属性的类型。typeof(MyCustomControl)
: 拥有该依赖属性的控件类型。FrameworkPropertyMetadata
: 属性元数据,用于设置属性的行为特性。0
: 属性的默认值。FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender
: 指定属性更改会影响控件的布局 (Measure) 和渲染 (Render)。new PropertyChangedCallback(OnValueChanged)
: 指定属性值更改时的回调方法OnValueChanged
。
new ValidateValueCallback(ValidateValue)
: (可选) 指定属性值验证回调方法ValidateValue
。
public int Value { get; set; }
: CLR 属性包装器,用于简化对依赖属性的访问。在 get 和 set 访问器中,分别调用GetValue()
和SetValue()
方法来操作依赖属性。OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
: 属性值更改回调方法,当ValueProperty
的值发生变化时,该方法会被调用。ValidateValue(object value)
: 属性值验证回调方法,在设置ValueProperty
的值之前,该方法会被调用进行值验证。
2.2. 路由事件 (Routed Events)
路由事件是 WPF 事件系统中的核心概念,它允许事件在元素树中 "路由",即事件可以从触发元素沿着元素树向上 (冒泡路由) 或向下 (隧道路由) 传播,或者直接在触发元素上处理 (直接路由)。
路由事件的主要优势在于:
事件共享: 多个元素可以监听同一个路由事件。
事件处理集中化: 可以在元素树的较高层级处理来自子元素的事件。
控件组合: 路由事件使得组合和定制控件的行为更加灵活。
路由事件的类型:
冒泡路由 (Bubbling Routing): 事件从触发元素开始,沿着元素树向上冒泡,依次触发父元素、祖父元素...直到根元素的事件处理程序。这是最常见的路由类型。
隧道路由 (Tunneling Routing): 事件从元素树的根元素开始,沿着元素树向下隧道,依次触发祖父元素、父元素...直到触发元素的事件处理程序。隧道路由事件通常以 "Preview" 开头,例如
PreviewMouseDown
。直接路由 (Direct Routing): 事件只在触发元素自身上触发,不进行路由传播。CLR 事件通常是直接路由事件。
如何定义路由事件:
使用 EventManager.RegisterRoutedEvent()
方法在控件类中注册路由事件。
C#
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
"ValueChanged", // 事件名称
RoutingStrategy.Bubble, // 路由策略 (冒泡)
typeof(RoutedPropertyChangedEventHandler<int>), // 事件处理程序类型
typeof(MyCustomControl) // 事件所属的控件类型
);
public event RoutedPropertyChangedEventHandler<int> ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
private void OnValueChanged(int oldValue, int newValue)
{
RoutedPropertyChangedEventArgs<int> args = new RoutedPropertyChangedEventArgs<int>(oldValue, newValue);
args.RoutedEvent = ValueChangedEvent;
RaiseEvent(args); // 触发路由事件
}
代码解释:
EventManager.RegisterRoutedEvent(...)
: 静态方法,用于注册路由事件。"ValueChanged"
: 路由事件的名称,通常与 CLR 事件名相同。RoutingStrategy.Bubble
: 路由策略,这里使用冒泡路由。typeof(RoutedPropertyChangedEventHandler<int>)
: 路由事件处理程序的委托类型,RoutedPropertyChangedEventHandler<int>
是 WPF 预定义的委托,用于处理属性值更改的路由事件,<int>
指定了事件参数的类型。typeof(MyCustomControl)
: 拥有该路由事件的控件类型。
public event RoutedPropertyChangedEventHandler<int> ValueChanged { add; remove; }
: CLR 事件包装器,用于简化路由事件的添加和移除处理程序。OnValueChanged(int oldValue, int newValue)
: 触发路由事件的方法。RoutedPropertyChangedEventArgs<int> args = new RoutedPropertyChangedEventArgs<int>(oldValue, newValue);
: 创建路由事件参数对象,包含旧值和新值。args.RoutedEvent = ValueChangedEvent;
: 设置事件参数的路由事件类型。RaiseEvent(args);
: 触发路由事件,开始路由传播。
2.3. 控件模板 (Control Templates)
控件模板 (ControlTemplate
) 是 WPF 中用于 定义控件视觉结构 的核心机制。它决定了控件在屏幕上如何呈现,包括控件由哪些元素组成,以及这些元素如何布局和样式化。
完全自定义外观:
ControlTemplate
允许你 完全替换控件默认的视觉树 (Visual Tree)。你可以使用 XAML 自由地组合各种 WPF 元素 (例如Border
,TextBlock
,Path
,Shape
, 以及其他控件),构建出完全自定义的控件外观。模板绑定 (TemplateBinding):
ControlTemplate
中可以使用TemplateBinding
表达式,将模板内部元素的属性 绑定到控件自身的依赖属性。这使得模板内部的元素可以响应控件属性的变化,例如Background
,Foreground
,BorderBrush
,IsEnabled
,IsMouseOver
等。触发器 (Triggers):
ControlTemplate
可以包含触发器 (Trigger
,DataTrigger
,EventTrigger
等),用于根据控件的状态 (例如IsMouseOver
,IsPressed
,IsFocused
,IsExpanded
等) 或数据变化,动态地改变模板内部元素的属性,实现动态的视觉效果。
如何定义控件模板:
在 Style
或控件资源中,设置 Template
属性的值为 ControlTemplate
对象。
XML
<Style TargetType="{x:Type MyCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MyCustomControl}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBlock Text="{TemplateBinding Text}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
代码解释:
<Style TargetType="{x:Type MyCustomControl}">
: 定义针对MyCustomControl
类型的样式。<Setter Property="Template">
: 设置Template
属性,指定控件模板。<Setter.Value>
:Template
属性的值是一个ControlTemplate
对象。<ControlTemplate TargetType="{x:Type MyCustomControl}">
: 定义MyCustomControl
的控件模板。<Border...>
: 模板的根元素,使用Border
作为容器。Background="{TemplateBinding Background}"
,BorderBrush="{TemplateBinding BorderBrush}"
,BorderThickness="{TemplateBinding BorderThickness}"
: 使用TemplateBinding
将Border
的属性绑定到MyCustomControl
自身的同名属性,实现样式联动。<TextBlock Text="{TemplateBinding Text}" />
: 在Border
内部添加一个TextBlock
,并将其Text
属性绑定到MyCustomControl
的Text
属性。
2.4. 样式和主题 (Styles and Themes)
样式 (Styles) 和主题 (Themes) 是 WPF 中用于 控制控件外观 的重要机制。
样式 (Styles): 用于 封装一组属性值,并将其应用于一个或多个控件。样式可以定义在资源字典 (
ResourceDictionary
) 中,并在控件或其父元素上引用。样式可以极大地简化控件外观的统一管理和修改。主题 (Themes): 是一组样式的集合,用于 定义应用程序的整体视觉风格。WPF 提供了默认的主题 (例如 Classic, Luna, Royale),你也可以创建自定义主题。主题允许应用程序在不同视觉风格之间切换。
样式和主题的关系:
主题是由多个样式组成的。
样式可以定义在主题中,也可以独立存在。
样式可以覆盖主题中的样式设置。
如何定义样式:
在 ResourceDictionary
中定义 Style
对象,并设置 TargetType
属性指定样式应用的控件类型。
XML
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="LightBlue"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontFamily" Value="微软雅黑"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="5,3"/>
</Style>
<Style TargetType="{x:Type MyCustomControl}">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MyCustomControl}">
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
代码解释:
<ResourceDictionary>
: 资源字典的根元素。<Style TargetType="{x:Type Button}">
: 定义针对Button
类型的样式。<Setter Property="Background" Value="LightBlue"/>
,...: 使用Setter
元素设置Button
的各种属性值。
<Style TargetType="{x:Type MyCustomControl}">
: 定义针对MyCustomControl
类型的样式,并设置了ControlTemplate
。
如何应用样式:
隐式样式 (Implicit Style): 当样式没有
x:Key
属性时,它会成为隐式样式。隐式样式会 自动应用于 资源字典作用域内 所有指定类型的控件。显式样式 (Explicit Style): 当样式定义了
x:Key
属性时,它会成为显式样式。显式样式需要 通过Style="{StaticResource 样式Key}"
在控件上显式引用 才能应用。
2.5. 内容呈现 (Content Presentation)
WPF 控件通常可以显示内容,例如文本、图像、其他控件等。WPF 提供了几种机制来处理控件的内容呈现:
ContentPropertyAttribute
: 用于指定控件的 默认内容属性。例如,Button
的默认内容属性是Content
,Label
的默认内容属性也是Content
。ContentPresenter
: 是一个控件,用于 在控件模板中呈现控件的Content
属性。ContentPresenter
会根据控件的ContentTemplate
和ContentStringFormat
属性来呈现内容。ContentControl
: 是一个基类,许多 WPF 控件 (例如Button
,Label
,GroupBox
,ScrollViewer
等) 都派生自ContentControl
。ContentControl
提供了Content
,ContentTemplate
,ContentStringFormat
等属性,用于管理和呈现内容。ItemsControl
: 是一个基类,用于显示 集合数据。ItemsControl
提供了ItemsSource
,ItemTemplate
,ItemsPanel
等属性,用于管理和呈现集合数据。ListBox
,ComboBox
,TreeView
,DataGrid
等控件都派生自ItemsControl
。
3. 自定义控件的类型
WPF 中创建自定义控件主要有以下三种类型,复杂度递增:
3.1. 用户控件 (User Controls)
最简单: 用户控件是最简单的自定义控件类型。它本质上是一个 由现有 WPF 控件组合而成的复合控件。
可视化设计: 用户控件通常使用 可视化设计器 (例如 Visual Studio 的设计视图) 创建,通过拖拽和组合现有控件来构建用户界面。
代码分离: 用户控件的代码 (C# 代码) 和界面 (XAML 代码) 通常 分离在不同的文件中,易于维护和管理。
适用场景: 适用于 封装简单的 UI 组合,例如,一个包含标签和文本框的输入框组,一个自定义工具栏等。
创建用户控件的步骤:
在项目中添加 "用户控件 (WPF)" 项。
在用户控件的 XAML 文件中,使用现有 WPF 控件组合构建用户界面。
在用户控件的代码文件中,编写 C# 代码实现逻辑,例如处理事件、访问控件属性等。
在其他 WPF 窗口或控件中使用用户控件,就像使用普通控件一样。
示例:创建一个简单的计数器用户控件
创建用户控件项目: 在 WPF 项目中,右键点击项目 -> "添加" -> "新建文件夹",命名为 "CustomControls"。 然后在 "CustomControls" 文件夹上右键点击 -> "添加" -> "用户控件 (WPF)...",命名为 "CounterControl.xaml"。
设计用户界面 (CounterControl.xaml):
XML
<UserControl x:Class="WpfApp.CustomControls.CounterControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Button x:Name="btnDecrease" Content="-" Click="btnDecrease_Click"/> <TextBlock Grid.Column="1" x:Name="txtCount" Text="0" TextAlignment="Center" VerticalAlignment="Center"/> <Button Grid.Column="2" x:Name="btnIncrease" Content="+" Click="btnIncrease_Click"/> </Grid> </UserControl>
编写代码逻辑 (CounterControl.xaml.cs):
C#
using System.Windows; using System.Windows.Controls; namespace WpfApp.CustomControls { public partial class CounterControl: UserControl { private int _count = 0; public CounterControl() { InitializeComponent(); UpdateCountText(); } private void btnIncrease_Click(object sender, RoutedEventArgs e) { _count++; UpdateCountText(); } private void btnDecrease_Click(object sender, RoutedEventArgs e) { if (_count > 0) { _count--; UpdateCountText(); } } private void UpdateCountText() { txtCount.Text = _count.ToString(); } public int Count { get { return _count; } set { _count = value; UpdateCountText(); } } } }
在窗口中使用用户控件 (MainWindow.xaml):
XML
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp" xmlns:customControls="clr-namespace:WpfApp.CustomControls" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <customControls:CounterControl HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="myCounter"/> <Button Content="获取计数器值" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,50" Click="Button_Click"/> </Grid> </Window>
在窗口代码中访问用户控件属性 (MainWindow.xaml.cs):
C#
using System.Windows; namespace WpfApp { public partial class MainWindow: Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show($"计数器值: {myCounter.Count}"); } } }
3.2. 模板化控件 (Templated Controls / Custom Controls)
更灵活: 模板化控件 (也常被称为 Custom Controls) 提供了比用户控件 更大的灵活性和定制性。
完全自定义外观: 通过 自定义
ControlTemplate
,你可以完全重新定义控件的视觉结构,使其外观与内置控件完全不同。逻辑与外观分离: 模板化控件 严格遵循逻辑与外观分离的原则。控件的逻辑代码 (C# 代码) 专注于处理控件的行为和数据,而控件的外观则完全由
ControlTemplate
(XAML 代码) 定义。这使得控件的样式和主题化更加容易。适用场景: 适用于 创建需要高度定制外观和行为的复杂控件,例如,自定义进度条、滑块、日历控件、图表控件等。
创建模板化控件的步骤:
创建 WPF 自定义控件库项目 (推荐): 为了更好地组织和重用自定义控件,通常建议创建一个独立的 WPF 自定义控件库项目。
在自定义控件库项目中添加 "自定义控件 (WPF)" 项。
定义依赖属性和路由事件 (在控件的代码文件中)。
在 "Themes" 文件夹下的 "Generic.xaml" 文件中,定义控件的默认样式和
ControlTemplate
。在应用程序中使用自定义控件库中的控件。
实战教程:创建一个自定义评分控件 (Templated Control) (将在下一节详细展开)
3.3. 元素控件 (Element Controls / FrameworkElement 直接派生)
最底层,最复杂: 元素控件是 最底层的自定义控件类型。它直接从
FrameworkElement
或其子类 (例如Panel
,Shape
) 派生。完全控制渲染: 元素控件需要 完全自定义控件的渲染逻辑,包括如何绘制自身、如何处理布局、如何响应用户输入等。
性能优化: 对于需要 极致性能和高度定制渲染 的场景,例如游戏 UI、高性能图表控件等,元素控件可能是最佳选择。
复杂性高: 元素控件开发难度较高,需要深入理解 WPF 的渲染管道和布局系统。
创建元素控件的步骤:
创建 WPF 项目或自定义控件库项目。
创建一个 C# 类,从
FrameworkElement
或其子类派生。重写
MeasureOverride()
和ArrangeOverride()
方法,实现自定义布局逻辑。重写
OnRender()
方法,实现自定义渲染逻辑 (使用DrawingContext
进行绘制)。处理用户输入事件 (例如
MouseDown
,MouseMove
,KeyDown
等)。定义依赖属性和路由事件 (可选,但通常需要)。
在 XAML 中使用自定义元素控件。
元素控件示例 (简单的自定义圆形控件):
C#
using System.Windows;
using System.Windows.Media;
namespace WpfApp.CustomControls
{
public class CircleControl: FrameworkElement
{
#region 依赖属性
public static readonly DependencyProperty FillProperty =
DependencyProperty.Register("Fill", typeof(Brush), typeof(CircleControl),
new FrameworkPropertyMetadata(Brushes.Red, FrameworkPropertyMetadataOptions.AffectsRender));
public Brush Fill
{
get { return (Brush)GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
// 返回控件的期望大小,这里简单地返回 50x50
return new Size(50, 50);
}
protected override Size ArrangeOverride(Size finalSize)
{
// 返回控件的最终大小,这里直接使用 finalSize
return finalSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
// 获取控件的渲染区域
Rect rect = new Rect(0, 0, ActualWidth, ActualHeight);
// 计算圆心和半径
Point center = rect.GetCenter();
double radius = System.Math.Min(rect.Width, rect.Height) / 2;
// 绘制圆形
drawingContext.DrawEllipse(Fill, null, center, radius, radius);
}
}
}
在 XAML 中使用元素控件:
XML
<Window...
xmlns:customControls="clr-namespace:WpfApp.CustomControls"...>
<Grid>
<customControls:CircleControl Fill="Blue" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Window>
4. 实战教程:创建一个自定义评分控件 (Templated Control)
本节将通过一个详细的步骤,创建一个自定义的星级评分控件 (RatingControl),演示如何开发模板化控件。
4.1. 创建自定义控件库项目
打开 Visual Studio,创建新的 WPF 项目。 选择 "WPF 自定义控件库 (.NET Framework)" 模板,命名为 "RatingControlLibrary"。
项目结构: 创建完成后,项目结构应该包含:
Themes 文件夹: 用于存放控件的默认样式和模板 (Generic.xaml)。
CustomControl1.cs (或你自定义的控件类名).
重命名控件类和 Generic.xaml 文件: 将 "CustomControl1.cs" 重命名为 "RatingControl.cs",并将 "Generic.xaml" 中的
<Style TargetType="{x:Type local:CustomControl1}">
修改为<Style TargetType="{x:Type local:RatingControl}">
(将local:CustomControl1
替换为你的控件命名空间和类名)。
4.2. 定义依赖属性 (评分值、星星数量)
打开 RatingControl.cs
文件,添加以下代码来定义依赖属性:
C#
using System.Windows;
using System.Windows.Controls;
namespace RatingControlLibrary
{
public class RatingControl: Control
{
static RatingControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(RatingControl), new FrameworkPropertyMetadata(typeof(RatingControl)));
}
#region 依赖属性
// 评分值
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(int), typeof(RatingControl),
new FrameworkPropertyMetadata(
0, // 默认值
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnValueChanged), // 属性更改回调
new CoerceValueCallback(CoerceValue) // 强制值回调
),
new ValidateValueCallback(ValidateValue) // 属性值验证回调
);
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RatingControl control = (RatingControl)d;
int newValue = (int)e.NewValue;
control.UpdateVisualState(); // 更新视觉状态
control.OnValueChanged(newValue); // 触发 CLR 事件
}
private static object CoerceValue(DependencyObject d, object baseValue)
{
RatingControl control = (RatingControl)d;
int value = (int)baseValue;
if (value < 0) return 0;
if (value > control.StarCount) return control.StarCount;
return value;
}
private static bool ValidateValue(object value)
{
return (int)value >= 0;
}
// 星星数量
public static readonly DependencyProperty StarCountProperty =
DependencyProperty.Register(
"StarCount", typeof(int), typeof(RatingControl),
new FrameworkPropertyMetadata(5, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, null, new CoerceValueCallback(CoerceStarCount)), new ValidateValueCallback(ValidateStarCount));
public int StarCount
{
get { return (int)GetValue(StarCountProperty); }
set { SetValue(StarCountProperty, value); }
}
private static object CoerceStarCount(DependencyObject d, object baseValue)
{
return System.Math.Max(1, (int)baseValue); // 星星数量最小为 1
}
private static bool ValidateStarCount(object value)
{
return (int)value > 0; // 星星数量必须大于 0
}
#endregion
#region 路由事件
public static readonly RoutedEvent ValueChangedRoutedEvent =
EventManager.RegisterRoutedEvent(
"ValueChangedRouted", RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<int>), typeof(RatingControl));
public event RoutedPropertyChangedEventHandler<int> ValueChangedRouted
{
add { AddHandler(ValueChangedRoutedEvent, value); }
remove { RemoveHandler(ValueChangedRoutedEvent, value); }
}
protected virtual void OnValueChanged(int newValue)
{
RoutedPropertyChangedEventArgs<int> args = new RoutedPropertyChangedEventArgs<int>(Value, newValue, ValueChangedRoutedEvent);
RaiseEvent(args); // 触发路由事件
}
#endregion
#region 视觉状态
private void UpdateVisualState()
{
VisualStateManager.GoToState(this, "Value" + Value, true); // 根据 Value 属性切换视觉状态
}
#endregion
}
}
代码解释:
static RatingControl() {... }
: 静态构造函数,用于设置控件的默认样式键 (DefaultStyleKeyProperty
),这是模板化控件的必要步骤。ValueProperty
: 评分值依赖属性。CoerceValueCallback(CoerceValue)
: 强制值回调,确保Value
值在 0 到StarCount
之间。ValidateValueCallback(ValidateValue)
: 验证值回调,确保Value
值非负。PropertyChangedCallback(OnValueChanged)
: 属性更改回调,当Value
值改变时,调用UpdateVisualState()
更新视觉状态,并触发 CLR 事件ValueChanged
。
StarCountProperty
: 星星数量依赖属性。CoerceValueCallback(CoerceStarCount)
: 强制值回调,确保StarCount
值最小为 1。ValidateValueCallback(ValidateStarCount)
: 验证值回调,确保StarCount
值大于 0。
ValueChangedRoutedEvent
: 评分值更改路由事件,当Value
值改变时触发。OnValueChanged(int newValue)
: 触发ValueChangedRoutedEvent
路由事件的方法。UpdateVisualState()
: 根据Value
属性值,使用VisualStateManager.GoToState()
切换控件的视觉状态 (将在控件模板中定义视觉状态)。
4.3. 定义路由事件 (评分值改变事件)
在上面的 RatingControl.cs
代码中,我们已经定义了路由事件 ValueChangedRoutedEvent
和 CLR 事件 ValueChanged
。
4.4. 设计控件模板 (XAML 结构和样式)
打开 "Themes/Generic.xaml" 文件,修改 ControlTemplate
和 Style
的内容如下:
XML
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RatingControlLibrary">
<Style TargetType="{x:Type local:RatingControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RatingControl}">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<VisualState Name="Value0">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star1" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star2" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star3" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star4" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star5" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="Value1">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star1" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarFilledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star2" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star3" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star4" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star5" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="Value5">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star1" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarFilledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star2" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarFilledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star3" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarFilledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star4" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarFilledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star5" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarFilledBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel Orientation="Horizontal">
<Path Name="Star1" Style="{StaticResource StarStyle}"/>
<Path Name="Star2" Style="{StaticResource StarStyle}"/>
<Path Name="Star3" Style="{StaticResource StarStyle}"/>
<Path Name="Star4" Style="{StaticResource StarStyle}"/>
<Path Name="Star5" Style="{StaticResource StarStyle}"/>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="StarStyle" TargetType="{x:Type Path}">
<Setter Property="Data" Value="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
<Setter Property="Fill" Value="{StaticResource StarEmptyBrush}"/>
<Setter Property="Stroke" Value="Black"/>
<Setter Property="StrokeThickness" Value="0"/>
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Margin" Value="2"/>
</Style>
<SolidColorBrush x:Key="StarFilledBrush" Color="Gold"/>
<SolidColorBrush x:Key="StarEmptyBrush" Color="LightGray"/>
</ResourceDictionary>
代码解释:
<Style TargetType="{x:Type local:RatingControl}">
: 定义RatingControl
的默认样式。<ControlTemplate TargetType="{x:Type local:RatingControl}">
: 定义RatingControl
的控件模板。<Grid>
: 模板的根元素,使用Grid
作为容器。<VisualStateManager.VisualStateGroups>
: 定义视觉状态组ValueStates
,用于管理不同评分值下的视觉状态。<VisualState Name="Value0">
,<VisualState Name="Value1">
,...,<VisualState Name="Value5">
: 定义了评分值从 0 到 5 的视觉状态。<Storyboard>
: 每个视觉状态都包含一个Storyboard
,用于定义状态切换时的动画效果 (这里使用ObjectAnimationUsingKeyFrames
快速切换星星的填充颜色)。<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Star1" Storyboard.TargetProperty="Fill">
: 针对名为 "Star1" 的Path
元素,设置其Fill
属性的动画。<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource StarEmptyBrush}"/>
: 在 0 时刻,将 "Star1" 的Fill
属性设置为StarEmptyBrush
(空星画刷)。根据不同的视觉状态 (Value0, Value1,..., Value5),设置不同数量星星的填充颜色为
StarFilledBrush
(填充星画刷) 或StarEmptyBrush
。
<StackPanel Orientation="Horizontal">
: 使用StackPanel
水平排列星星。<Path Name="Star1" Style="{StaticResource StarStyle}"/>
,...,<Path Name="Star5" Style="{StaticResource StarStyle}"/>
: 创建 5 个Path
元素作为星星,并应用StarStyle
样式。
<Style x:Key="StarStyle" TargetType="{x:Type Path}">
: 定义星星的样式StarStyle
。<Setter Property="Data" Value="...">
: 设置星星的形状数据 (五角星 PathGeometry)。<Setter Property="Fill" Value="{StaticResource StarEmptyBrush}"/>
: 设置星星的默认填充颜色为空星画刷。<Setter Property="Stroke" Value="Black"/>
,...: 设置星星的其他样式属性 (边框、大小、间距等)。
<SolidColorBrush x:Key="StarFilledBrush" Color="Gold"/>
,<SolidColorBrush x:Key="StarEmptyBrush" Color="LightGray"/>
: 定义填充星和空星的画刷资源。
4.5. 实现控件逻辑 (C# 代码,属性更改处理,事件触发)
在 RatingControl.cs
文件中,我们已经实现了大部分控件逻辑,包括依赖属性的定义、属性更改回调、强制值和验证、路由事件触发等。
关键代码回顾:
OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
: 在ValueProperty
的属性更改回调中,调用control.UpdateVisualState()
更新视觉状态,并调用control.OnValueChanged(newValue)
触发 CLR 事件ValueChanged
。UpdateVisualState()
: 根据Value
属性值,使用VisualStateManager.GoToState(this, "Value" + Value, true)
切换控件的视觉状态,从而改变星星的填充颜色,实现评分的视觉效果。
4.6. 应用样式和主题
模板化控件的样式和主题化非常灵活。
默认样式: 在 "Themes/Generic.xaml" 中定义的
Style
是控件的 默认样式。当控件没有显式设置样式时,会应用默认样式。自定义样式: 可以在应用程序的资源字典 (
App.xaml
, 窗口资源, 控件资源) 中 重写或扩展控件的样式。可以修改默认样式中的Setter
值,或者完全替换ControlTemplate
,实现自定义的外观。主题切换: 通过 替换应用程序的资源字典,可以实现主题切换。例如,可以创建不同的资源字典文件 (例如 "Themes/LightTheme.xaml", "Themes/DarkTheme.xaml"),分别定义不同主题的样式,然后在应用程序启动时或运行时动态加载不同的资源字典,实现主题切换效果。
4.7. 在应用程序中使用自定义控件
添加对自定义控件库项目的引用: 在你的 WPF 应用程序项目中,右键点击 "引用" -> "添加引用" -> "项目",选择 "RatingControlLibrary" 项目,点击 "确定"。
在 XAML 中使用自定义控件: 在需要使用
RatingControl
的 XAML 文件中,添加命名空间声明,并使用<local:RatingControl>
标签来使用自定义控件。XML
<Window... xmlns:local="clr-namespace:RatingControlLibrary;assembly=RatingControlLibrary"...> <Grid> <local:RatingControl Value="3" StarCount="7" HorizontalAlignment="Center" VerticalAlignment="Center" ValueChangedRouted="RatingControl_ValueChangedRouted"/> </Grid> </Window>
处理路由事件 (可选): 如果需要响应
RatingControl
的路由事件ValueChangedRoutedEvent
,可以在窗口或父控件的代码文件中添加事件处理程序。C#
private void RatingControl_ValueChangedRouted(object sender, RoutedPropertyChangedEventArgs<int> e) { MessageBox.Show($"评分值已更改: 旧值 = {e.OldValue}, 新值 = {e.NewValue}"); }
5. 高级自定义控件技术
5.1. 命令 (Commands)
命令 (Commands) 是一种用于 解耦 UI 交互和业务逻辑 的机制。在自定义控件中,可以使用命令来处理用户操作 (例如按钮点击、菜单选择等),并将操作委托给命令执行器 (通常是 ViewModel) 处理。
ICommand
接口: WPF 命令系统基于ICommand
接口,该接口定义了Execute()
(执行命令) 和CanExecute()
(判断命令是否可执行) 方法。RoutedCommand
和DelegateCommand
: WPF 提供了RoutedCommand
和DelegateCommand
等命令实现。RoutedCommand
是路由命令,可以沿着元素树传播;DelegateCommand
是委托命令,可以将命令执行逻辑委托给委托方法。CommandBinding
: 用于将命令与控件的特定操作 (例如按钮点击) 关联起来。CommandParameter
: 用于传递命令参数。
5.2. 自定义控件中的数据绑定
自定义控件可以充分利用 WPF 的数据绑定机制,实现 UI 与数据的双向同步。
绑定到依赖属性: 自定义控件的依赖属性可以作为数据绑定的目标属性。
Binding
表达式: 可以使用Binding
表达式在 XAML 中将控件属性绑定到数据源 (例如 ViewModel 的属性)。INotifyPropertyChanged
接口: 如果数据源对象实现了INotifyPropertyChanged
接口,当数据源属性值发生变化时,会自动通知 UI 更新。
5.3. 自定义布局 (MeasureOverride, ArrangeOverride)
对于需要 完全自定义布局行为 的复杂控件 (例如自定义布局面板、图表控件等),可能需要重写 FrameworkElement
的 MeasureOverride()
和 ArrangeOverride()
方法。
MeasureOverride(Size availableSize)
: 用于测量控件的期望大小。控件应该根据availableSize
(可用空间) 计算并返回自身的期望大小 (DesiredSize
)。ArrangeOverride(Size finalSize)
: 用于排列控件的子元素。控件应该根据finalSize
(最终分配的大小) 和子元素的DesiredSize
,确定子元素的位置和大小,并返回自身的最终大小 (ArrangeBounds
)。
5.4. 控件的可访问性 (Accessibility)
创建自定义控件时,需要考虑控件的可访问性,确保残障人士也能正常使用你的应用程序。
UI 自动化 (UI Automation): WPF 提供了 UI 自动化框架,用于辅助技术 (例如屏幕阅读器) 访问和操作 UI 元素。
AutomationProperties
类: 可以使用AutomationProperties
类的附加属性 (例如AutomationProperties.Name
,AutomationProperties.HelpText
,AutomationProperties.ControlType
等) 为自定义控件提供可访问性信息。键盘导航 (Keyboard Navigation): 确保自定义控件支持键盘导航,用户可以使用 Tab 键、方向键等进行操作。
高对比度主题 (High Contrast Themes): 测试自定义控件在高对比度主题下的显示效果,确保控件在不同主题下都具有良好的可读性。
5.5. 性能优化
自定义控件的性能优化非常重要,特别是对于复杂控件或大量使用的控件。
减少视觉复杂度: 尽量简化控件的视觉结构,减少 XAML 元素的数量。
优化渲染逻辑: 在
OnRender()
方法中,避免进行复杂的计算或资源加载操作。使用
DrawingVisual
: 对于复杂的自定义图形渲染,可以使用DrawingVisual
来提高性能。虚拟化 (Virtualization): 对于显示大量数据的列表控件 (例如自定义
ListBox
,DataGrid
),可以使用虚拟化技术 (例如VirtualizingStackPanel
) 来提高性能。延迟加载 (Deferred Loading): 对于不必要的资源 (例如大型图像、复杂模板),可以使用延迟加载技术,只在需要时才加载。
6. 总结
本教程深入介绍了 WPF 自定义控件的开发,涵盖了从基础概念到高级技巧的各个方面。通过学习本教程,你应该能够:
理解 WPF 自定义控件的必要性和优势。
掌握自定义控件开发的基础概念,例如依赖属性、路由事件、控件模板、样式和主题、内容呈现等。
了解不同类型的自定义控件 (用户控件、模板化控件、元素控件) 的特点和适用场景。
通过实战教程,学会创建模板化控件,并应用样式和主题。
了解高级自定义控件技术,例如命令、数据绑定、自定义布局、可访问性和性能优化。
自定义控件开发是 WPF 开发中的重要技能,掌握它可以让你创建出更加强大、灵活和美观的 WPF 应用程序。希望本教程能够帮助你入门并精通 WPF 自定义控件开发!
后续学习方向:
深入研究 WPF 样式和主题系统。
学习更多高级控件模板技巧,例如使用
ItemsPresenter
,ScrollViewer
,AdornerDecorator
等。探索 WPF 的布局系统,学习如何创建自定义布局面板。
研究 WPF 的渲染管道,学习如何进行高性能自定义渲染。
阅读更多关于 WPF 自定义控件开发的书籍、文章和示例代码。
祝你在 WPF 自定义控件开发的道路上取得成功!