看到一篇不错的文章 转载一下 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 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。