转:WPF 自定义控件教程:从入门到精通
in C# with 0 comment

看到一篇不错的文章 转载一下 WPF 自定义控件教程:从入门到精通

前言

WPF (Windows Presentation Foundation) 提供了强大的 UI 框架,允许开发者创建美观且功能丰富的应用程序。WPF 的核心优势之一在于其高度的 可定制性。除了使用内置控件外,WPF 还允许我们创建完全自定义的控件,以满足特定的 UI 需求。

本教程将带你深入了解 WPF 自定义控件的开发,涵盖从基础概念到高级技巧,并提供详细的代码示例和解释,助你掌握自定义控件的精髓。

目录

  1. 为什么要自定义控件?
  2. 自定义控件的基础概念

    • 2.1. 依赖属性 (Dependency Properties)
    • 2.2. 路由事件 (Routed Events)
    • 2.3. 控件模板 (Control Templates)
    • 2.4. 样式和主题 (Styles and Themes)
    • 2.5. 内容呈现 (Content Presentation)
  3. 自定义控件的类型

    • 3.1. 用户控件 (User Controls)
    • 3.2. 模板化控件 (Templated Controls / Custom Controls)
    • 3.3. 元素控件 (Element Controls / FrameworkElement 直接派生)
  4. 实战教程:创建一个自定义评分控件 (Templated Control)

    • 4.1. 创建自定义控件库项目
    • 4.2. 定义依赖属性 (评分值、星星数量)
    • 4.3. 定义路由事件 (评分值改变事件)
    • 4.4. 设计控件模板 (XAML 结构和样式)
    • 4.5. 实现控件逻辑 (C# 代码,属性更改处理,事件触发)
    • 4.6. 应用样式和主题
    • 4.7. 在应用程序中使用自定义控件
  5. 高级自定义控件技术

    • 5.1. 命令 (Commands)
    • 5.2. 自定义控件中的数据绑定
    • 5.3. 自定义布局 (MeasureOverride, ArrangeOverride)
    • 5.4. 控件的可访问性 (Accessibility)
    • 5.5. 性能优化
  6. 总结

1. 为什么要自定义控件?

WPF 提供了丰富的内置控件,但在以下情况下,你可能需要创建自定义控件:

2. 自定义控件的基础概念

在开始创建自定义控件之前,我们需要理解 WPF 中与控件开发密切相关的几个核心概念。

2.1. 依赖属性 (Dependency Properties)

依赖属性是 WPF 属性系统中的核心概念,它增强了 CLR 属性的功能,并为 WPF 控件提供了强大的特性,例如:

如何定义依赖属性:

使用 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 范围内
}

代码解释:

2.2. 路由事件 (Routed Events)

路由事件是 WPF 事件系统中的核心概念,它允许事件在元素树中 "路由",即事件可以从触发元素沿着元素树向上 (冒泡路由) 或向下 (隧道路由) 传播,或者直接在触发元素上处理 (直接路由)。

路由事件的主要优势在于:

路由事件的类型:

如何定义路由事件:

使用 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); // 触发路由事件
}

代码解释:

2.3. 控件模板 (Control Templates)

控件模板 (ControlTemplate) 是 WPF 中用于 定义控件视觉结构 的核心机制。它决定了控件在屏幕上如何呈现,包括控件由哪些元素组成,以及这些元素如何布局和样式化。

如何定义控件模板:

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>

代码解释:

2.4. 样式和主题 (Styles and Themes)

样式 (Styles) 和主题 (Themes) 是 WPF 中用于 控制控件外观 的重要机制。

样式和主题的关系:

如何定义样式:

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>

代码解释:

如何应用样式:

2.5. 内容呈现 (Content Presentation)

WPF 控件通常可以显示内容,例如文本、图像、其他控件等。WPF 提供了几种机制来处理控件的内容呈现:

3. 自定义控件的类型

WPF 中创建自定义控件主要有以下三种类型,复杂度递增:

3.1. 用户控件 (User Controls)

创建用户控件的步骤:

  1. 在项目中添加 "用户控件 (WPF)" 项。
  2. 在用户控件的 XAML 文件中,使用现有 WPF 控件组合构建用户界面。
  3. 在用户控件的代码文件中,编写 C# 代码实现逻辑,例如处理事件、访问控件属性等。
  4. 在其他 WPF 窗口或控件中使用用户控件,就像使用普通控件一样。

示例:创建一个简单的计数器用户控件  

  1. 创建用户控件项目: 在 WPF 项目中,右键点击项目 -> "添加" -> "新建文件夹",命名为 "CustomControls"。 然后在 "CustomControls" 文件夹上右键点击 -> "添加" -> "用户控件 (WPF)...",命名为 "CounterControl.xaml"。
  2. 设计用户界面 (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>
  1. 编写代码逻辑 (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();
           }
       }
   }
}
  1. 在窗口中使用用户控件 (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>
  1. 在窗口代码中访问用户控件属性 (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)

创建模板化控件的步骤:

  1. 创建 WPF 自定义控件库项目 (推荐): 为了更好地组织和重用自定义控件,通常建议创建一个独立的 WPF 自定义控件库项目。
  2. 在自定义控件库项目中添加 "自定义控件 (WPF)" 项。
  3. 定义依赖属性和路由事件 (在控件的代码文件中)。
  4. 在 "Themes" 文件夹下的 "Generic.xaml" 文件中,定义控件的默认样式和 ControlTemplate
  5. 在应用程序中使用自定义控件库中的控件。

实战教程:创建一个自定义评分控件 (Templated Control) (将在下一节详细展开)

3.3. 元素控件 (Element Controls / FrameworkElement 直接派生)

创建元素控件的步骤:

  1. 创建 WPF 项目或自定义控件库项目。
  2. 创建一个 C# 类,从 FrameworkElement 或其子类派生。
  3. 重写 MeasureOverride()ArrangeOverride() 方法,实现自定义布局逻辑。
  4. 重写 OnRender() 方法,实现自定义渲染逻辑 (使用 DrawingContext 进行绘制)。
  5. 处理用户输入事件 (例如 MouseDown, MouseMove, KeyDown 等)。
  6. 定义依赖属性和路由事件 (可选,但通常需要)。  
  7. 在 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. 创建自定义控件库项目

  1. 打开 Visual Studio,创建新的 WPF 项目。 选择 "WPF 自定义控件库 (.NET Framework)" 模板,命名为 "RatingControlLibrary"。
  2. 项目结构: 创建完成后,项目结构应该包含:

    • Themes 文件夹: 用于存放控件的默认样式和模板 (Generic.xaml)。
    • CustomControl1.cs (或你自定义的控件类名).
  3. 重命名控件类和 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
    }
}

代码解释:

4.3. 定义路由事件 (评分值改变事件)

在上面的 RatingControl.cs 代码中,我们已经定义了路由事件 ValueChangedRoutedEvent 和 CLR 事件 ValueChanged

4.4. 设计控件模板 (XAML 结构和样式)

打开 "Themes/Generic.xaml" 文件,修改 ControlTemplateStyle 的内容如下:

<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>

代码解释:

4.5. 实现控件逻辑 (C# 代码,属性更改处理,事件触发)

RatingControl.cs 文件中,我们已经实现了大部分控件逻辑,包括依赖属性的定义、属性更改回调、强制值和验证、路由事件触发等。

关键代码回顾:

4.6. 应用样式和主题

模板化控件的样式和主题化非常灵活。

4.7. 在应用程序中使用自定义控件

  1. 添加对自定义控件库项目的引用: 在你的 WPF 应用程序项目中,右键点击 "引用" -> "添加引用" -> "项目",选择 "RatingControlLibrary" 项目,点击 "确定"。
  2. 在 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>
  1. 处理路由事件 (可选): 如果需要响应 RatingControl 的路由事件 ValueChangedRoutedEvent,可以在窗口或父控件的代码文件中添加事件处理程序。
private void RatingControl_ValueChangedRouted(object sender, RoutedPropertyChangedEventArgs<int> e)
{
   MessageBox.Show($"评分值已更改: 旧值 = {e.OldValue}, 新值 = {e.NewValue}");
}

5. 高级自定义控件技术

5.1. 命令 (Commands)

命令 (Commands) 是一种用于 解耦 UI 交互和业务逻辑 的机制。在自定义控件中,可以使用命令来处理用户操作 (例如按钮点击、菜单选择等),并将操作委托给命令执行器 (通常是 ViewModel) 处理。

5.2. 自定义控件中的数据绑定

自定义控件可以充分利用 WPF 的数据绑定机制,实现 UI 与数据的双向同步。

5.3. 自定义布局 (MeasureOverride, ArrangeOverride)

对于需要 完全自定义布局行为 的复杂控件 (例如自定义布局面板、图表控件等),可能需要重写 FrameworkElementMeasureOverride()ArrangeOverride() 方法。

5.4. 控件的可访问性 (Accessibility)

创建自定义控件时,需要考虑控件的可访问性,确保残障人士也能正常使用你的应用程序。

5.5. 性能优化

自定义控件的性能优化非常重要,特别是对于复杂控件或大量使用的控件。

6. 总结

本教程深入介绍了 WPF 自定义控件的开发,涵盖了从基础概念到高级技巧的各个方面。通过学习本教程,你应该能够:

自定义控件开发是 WPF 开发中的重要技能,掌握它可以让你创建出更加强大、灵活和美观的 WPF 应用程序。希望本教程能够帮助你入门并精通 WPF 自定义控件开发!

后续学习方向:

祝你在 WPF 自定义控件开发的道路上取得成功!