自定义控件

当内置控件无法满足需求时,我们就需要开发自定义控件。Winform中自定义控件主要有两种路线:组合控件(UserControl,把现有控件组合成一个可复用单元)和自绘控件(继承Control,自己用GDI+绘制全部外观)。这篇笔记我们分别介绍这两种方式。

UserControl 组合控件

UserControl是最常用的自定义控件方式,它本质上是一个可以复用的小窗体,我们把一组控件摆上去、写好交互逻辑,之后就可以像普通控件一样反复将其拖放到各个窗体上使用。

创建组合控件

在工程上点击右键,选择添加->用户控件,即可创建一个UserControl。例如我们做一个带清除按钮的搜索框控件,把一个TextBox和一个Button拖上去。

暴露属性

控件内部控件的属性不应该让使用者直接访问,标准做法是在UserControl上定义公开属性做转发。

namespace Gacfox.Demo;

public partial class SearchBox : UserControl
{
    public SearchBox()
    {
        InitializeComponent();
    }

    public string SearchText
    {
        get => textBox.Text;
        set => textBox.Text = value;
    }

    private void clearButton_Click(object sender, EventArgs e)
    {
        textBox.Clear();
    }
}

暴露的属性如果想在设计器的属性面板中出现并支持序列化,还可以加上特性标注。

[Category("自定义")]
[Description("搜索框中的文本")]
[DefaultValue("")]
public string SearchText
{
    get => textBox1.Text;
    set => textBox1.Text = value;
}

Category控制属性在属性面板中的分组,Description是属性面板底部的说明文字,DefaultValue告诉设计器默认值是什么。

暴露事件

同理,内部控件的事件也需要包装后再暴露给使用者。

[Category("自定义")]
[Description("搜索文本发生变化时触发")]
public event EventHandler SearchTextChanged;

private void textBox1_TextChanged(object sender, EventArgs e)
{
    SearchTextChanged?.Invoke(this, EventArgs.Empty);
}

编译工程后,自定义的UserControl会自动出现在工具箱顶部的工程分组中,它可以拖放到任何窗体上,其公开属性和事件在设计器中都能正常使用。

自绘控件

如果控件的外观完全无法用现有控件拼出来,例如仪表盘、进度环、游戏地图等,此时就需要继承Control自己绘制。

using System.ComponentModel;
using System.Drawing.Drawing2D;

namespace Gacfox.Demo;

public partial class CircleProgress : UserControl
{
    private int _value;

    [Category("自定义")]
    [Description("当前进度(0-100)")]
    [DefaultValue(0)]
    public int Value
    {
        get => _value;
        set
        {
            _value = Math.Max(0, Math.Min(100, value));
            // 属性变化后请求重绘
            Invalidate();
        }
    }

    public CircleProgress()
    {
        // 开启双缓冲三件套,防止闪烁
        SetStyle(ControlStyles.OptimizedDoubleBuffer
               | ControlStyles.AllPaintingInWmPaint
               | ControlStyles.UserPaint
               | ControlStyles.ResizeRedraw, true);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        Graphics graphics = e.Graphics;
        graphics.SmoothingMode = SmoothingMode.AntiAlias;

        Rectangle rect = new Rectangle(10, 10, Width - 20, Height - 20);
        using (Pen penBack = new Pen(Color.LightGray, 8))
        using (Pen penValue = new Pen(Color.DodgerBlue, 8))
        {
            graphics.DrawEllipse(penBack, rect);
            graphics.DrawArc(penValue, rect, -90, _value * 3.6f);
        }
    }
}

编译工程后,自定义的自绘控件也会出现在工具箱中,且支持预览。

自绘控件的要点:

  1. 绘制逻辑统一写在OnPaint()重写中,使用e.Graphics绘制,自定义属性变化时调用Invalidate()请求重绘,这和GDI+章节的最佳实践完全一致。
  2. 构造函数中应使用SetStyle()开启双缓冲和自绘样式。
  3. 需要响应交互时应重写OnMouseDown()OnMouseMove()OnKeyDown()等方法,而非订阅自己的事件。
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。