WPF 自定义控件继承体系全解析(附圆形进度条示例)

C# · 05-12 · 422 人浏览
WPF 自定义控件继承体系全解析(附圆形进度条示例)

这篇文章呢,整理了 WPF 中用于自定义控件的核心继承类,按层级结构和用途从底层到底层进行了分类,适合控件开发者快速查阅和选择合适的继承基类。

🧩 控件继承类汇总表

继承类继承自用途/定位是否参与布局可否包含子元素典型用途示例
VisualDependencyObject最底层绘图元素,无布局、无交互图形渲染、构建低级绘图控件
DrawingVisualVisual可绘制内容的轻量级元素,适合自绘型 UI图形编辑器、缩略图、图层渲染控件
UIElementVisual增加了布局、输入、焦点、事件支持自定义输入处理控件,如手势控件
FrameworkElementUIElement提供布局、样式、数据绑定等完整基础自定义容器、自绘控件、数据绑定控件
DecoratorFrameworkElement只能包含一个子元素,用于包裹和增强子元素功能✅(单个)Border、自定义阴影或边框控件
PanelFrameworkElement容器控件,可布局多个子元素✅(多个)GridStackPanel、自定义布局容器
ControlFrameworkElement基础控件类,支持样式和模板✅(可定义)创建样式化可重用控件,如开关、进度条等
ContentControlControl支持一个内容的控件,内容可为任意对象✅(一个)自定义按钮、卡片、标签等
ItemsControlControl支持多个内容项的控件,通常用于数据列表✅(多个)列表、标签组、自定义列表控件
ShapeFrameworkElement用于矢量图形绘制(线、圆、矩形等)自定义图形控件(如频谱图、路径动画等)
AdornerFrameworkElement用于视觉装饰其他控件的叠加层拖拽提示框、元素边框装饰、拖放指示等
UserControlContentControl组合控件的容器,适合快速搭建封装控件(不是最底层)快速开发组合控件,如登录框、自定义弹窗等

🎯 继承建议速查

目标推荐继承类理由
需要完全自绘和极致性能DrawingVisual / UIElement控制精细,性能高,适合复杂绘图控件(但功能需全手动实现)
自定义布局容器Panel完全控制子元素的布局规则
做一个轻量但样式可变的控件Control支持样式、模板、绑定,是自定义控件的标准方式
做带有子内容的容器控件ContentControl / ItemsControl继承 Control,支持模板化和内容绑定
做快速组合控件(例如封装控件树)UserControl简洁方便,但不适合深度定制的可重用控件
做装饰或增强其他控件Decorator / Adorner无需重新绘制,可复用现有控件外观

🎯 案例:自绘圆环进度控件 RingProgressBar

🛠 控件实现(C# + WPF)

🎉 你想打造什么样的控件?
  • 如果你不打算使用 TextBlockBorder 等现成控件,只想自己“画”控件,我们可以从 FrameworkElement 或更底层的 UIElement 继承,重写 OnRender,使用 DrawingContext 手动绘制内容。
  • 如果希望控件具有样式扩展性、模板支持、状态逻辑复用等功能,我们可以从 Control 继承并创建默认 ControlTemplate来实现控件的视觉结构。
  • 当然,如果你想掌握或深入了解 DrawingVisual 的用法,可进一步深入 DrawingContextVisual 的使用,这种新式在性能优化型控件中比较常见。

    💡 这个案例我们不继承Control ,而是直接从 FrameworkElement 继承,手动去重写渲染逻辑,创建一个简洁、可绑定进度值的圆形进度条控件。🥳
    // 首先呢,我们得引入命名空间吧😂
    using System;
    using System.Windows;
    using System.Windows.Media;
    using System.Windows.Controls;
    
    /// <summary>
    /// 自定义圆环进度条控件,继承自 FrameworkElement,重写 OnRender 方法进行绘制。
    /// </summary>
    public class RingProgressBar : FrameworkElement
    {
      /// <summary>
      /// 声明并注册一个依赖属性 Progress,用于控制进度条显示的百分比(0 ~ 100)。
      /// </summary>
      public static readonly DependencyProperty ProgressProperty =
          DependencyProperty.Register(
              nameof(Progress),                         // 属性名称
              typeof(double),                           // 属性类型
              typeof(RingProgressBar),                  // 所属类型
              new FrameworkPropertyMetadata(
                  0.0,                                  // 默认值
                  FrameworkPropertyMetadataOptions.AffectsRender // 当值变化时自动触发重绘
              ));
    
      /// <summary>
      /// Progress 属性包装器,提供进度值的获取和设置。
      /// 设置时自动裁剪到 0~100 范围。
      /// </summary>
      public double Progress
      {
          get => (double)GetValue(ProgressProperty);
          set => SetValue(ProgressProperty, Math.Max(0, Math.Min(100, value)));
      }
    
      /// <summary>
      /// 重写 OnRender 方法,使用 DrawingContext 绘制控件外观。
      /// </summary>
      protected override void OnRender(DrawingContext dc)
      {
          double width = ActualWidth;
          double height = ActualHeight;
    
          // 根据控件尺寸计算圆环半径,保留 5 像素边距
          double radius = Math.Min(width, height) / 2 - 5;
    
          // 圆心位置
          Point center = new(width / 2, height / 2);
    
          // 1. 绘制背景圆环(灰色圆环)
          dc.DrawEllipse(
              null,                                       // 无填充
              new Pen(Brushes.LightGray, 10),             // 使用浅灰色画笔,宽度10
              center,                                     // 圆心
              radius, radius                              // 水平半径 & 垂直半径
          );
    
          // 2. 计算进度角度(转换为 0~360)
          double angle = Progress / 100 * 360;
    
          // 3. 将角度转换为弧度,-90 是为了从正上方开始绘制
          double radians = (angle - 90) * Math.PI / 180;
    
          // 4. 圆环起点:圆顶部
          Point startPoint = new(center.X, center.Y - radius);
    
          // 5. 计算终点:根据弧度计算 X/Y
          Point endPoint = new(
              center.X + radius * Math.Cos(radians),
              center.Y + radius * Math.Sin(radians)
          );
    
          // 6. 判断是否需要绘制大角度弧(超过180度)
          bool isLargeArc = angle > 180;
    
          // 7. 创建一个弧线段,从 startPoint 到 endPoint,构成圆环的一部分
          PathFigure figure = new(
              startPoint,                                // 弧线起点
              new[]
              {
                  new ArcSegment(
                      endPoint,                          // 弧线终点
                      new Size(radius, radius),          // 弧线的X/Y半径
                      0,                                 // 旋转角度
                      isLargeArc,                        // 是否为大角度弧线
                      SweepDirection.Clockwise,          // 顺时针方向
                      true                               // 弧线是否可见(用于裁剪,一般设为 true)
                  )
              },
              false                                       // 图形是否闭合(环形不闭合)
          );
    
          // 8. 创建 PathGeometry 对象用于绘制
          PathGeometry geometry = new();
          geometry.Figures.Add(figure);
    
          // 9. 绘制进度圆环(蓝色)
          dc.DrawGeometry(
              null,                                      // 无填充
              new Pen(Brushes.SteelBlue, 10),            // 钢蓝色画笔,宽度10
              geometry                                   // 绘制路径
          );
      }
    
      /// <summary>
      /// Measure 阶段:建议大小为 100x100。
      /// </summary>
      protected override Size MeasureOverride(Size availableSize) => new Size(100, 100);
    
      /// <summary>
      /// Arrange 阶段:直接接受布局系统给予的最终大小。
      /// </summary>
      protected override Size ArrangeOverride(Size finalSize) => finalSize;
    }
    

    🧪 使用方式(XAML 中使用)

    接着我们需要在 XAML 文件中使用该控件,首先要在窗体或页面里引入命名空间,然后像这样使用:

<Window
    x:Class="Custom.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ct="clr-namespace:Custom.Controls.Controls;assembly=Custom.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:Custom"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="RingProgressBar 自定义控件预览效果"
    Width="800"
    Height="450"
    WindowStartupLocation="CenterScreen"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
        </Grid.RowDefinitions>
        <!--  // 自定义控件 - 预览主体部分 //  -->
        <ct:RingProgressBar
            x:Name="ringProgress"
            Width="200"
            Height="200"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Progress="0" />
        <TextBlock
            x:Name="ShowProgressValue"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="24"
            Foreground="#d3d3d3" />
        <Button
            Width="100"
            Height="30"
            Margin="0,50"
            HorizontalAlignment="Center"
            VerticalAlignment="Bottom"
            Click="Button_Click"
            Content="🚀 执行" />
    </Grid>
</Window>

🧩 效果预览

这个控件会在一个 200x200 区域内绘制一个灰色圆环,然后根据 Progress 属性(0 到 100)绘制一段彩色弧线作为进度条,没有使用 Border、TextBlock 或其它任何控件元素,完全基于 OnRender 自绘。
在这里插入图片描述


🚀 写在最后:自定义控件,其实没你想的那么玄

我也是刚开始学习这个自定义控件方面,没开始之前感觉自定义控件比较难,但是只有自己上手之后,才发现自定义控件并不神秘,只是你少了一把钥匙。现在,门已经打开了,剩下的,就看你如何发散设计思维、去动手实现那些你想要的功能、交互和体验。每一个小控件,都是你对技术世界的一次“雕刻”。

希望这篇文章能为你点亮一盏灯。如果对你有启发,欢迎点个赞、留个言、顺手收藏一下——这是对我继续创作最好的支持。
写给愿意在细节里打磨作品的你,咱们下篇文章见 。🥳