看到一篇不错的文章 转载一下 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() 方法在控件类中注册依赖属性。
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() 方法在控件类中注册路由事件。
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 对象。
<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 属性指定样式应用的控件类型。
<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):
<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):
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):
<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):
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 中使用自定义元素控件。
元素控件示例 (简单的自定义圆形控件):
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 中使用元素控件:
<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 文件,添加以下代码来定义依赖属性:
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 的内容如下:
<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>标签来使用自定义控件。
<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,可以在窗口或父控件的代码文件中添加事件处理程序。
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 自定义控件开发的道路上取得成功!
本文由 jxxxy 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。