winform 4 12 winform自绘控件

报错

csharp 复制代码
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Modbus.Device;

namespace WindowsFormsApp2
{
    public class ModbusSerialHelper:IDisposable
    {
        private SerialPort _serialPort;
        public IModbusMaster ModbusMaster { get; private set; }
        public bool IsOpen => _serialPort?.IsOpen ?? false;
        public ModbusSerialHelper()
        {
            _serialPort = new SerialPort()
            {
                ReadTimeout = 1000,
                WriteTimeout = 1000,
                Encoding = Encoding.ASCII
            };
        }
        public bool Open(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits)
        {
            try
            {
                if (_serialPort.IsOpen)
                    _serialPort.Close();

                _serialPort.PortName = portName;
                _serialPort.BaudRate = baudRate;
                _serialPort.DataBits = dataBits;
                _serialPort.Parity = parity;
                _serialPort.StopBits = stopBits;
                _serialPort.Open();
                ModbusMaster = ModbusSerialMaster.CreateRtu(_serialPort);
                ModbusMaster.Transport.ReadTimeout = 1000;
                ModbusMaster.Transport.WriteTimeout = 1000;
                return true;
            }
            catch (Exception ex) {
                ModbusMaster = null;
                return false;
            }
        }
        public void Close()
        {
            if (_serialPort?.IsOpen ?? false)
            {
                _serialPort.Close();
            }
            ModbusMaster= null;
        }
        public void Dispose()
        {
           // throw new NotImplementedException();
           Close();
            _serialPort?.Dispose();
        }
    }
}

最常见的原因是:你引用了错误或不匹配的 Modbus 库版本。

有些 Modbus 库为了统一,把 CreateRtu和 CreateTcp等方法合并了,然后根据传入的参数类型(TcpClient或

SerialPort)来判断用哪种方式。但你引用的这个库,可能只实现了网络(TCP)部分,或者API设计不同。

另一个可能是,你同时引用了多个不同的 Modbus 库,编译器使用了错误的那一个。

更新版本后解决,原为3.0

WinForms 自绘控件

1.WinForms 自绘控件基础

重写 OnPaint 方法

这是自绘控件的入口,所有绘制逻辑必须在这里执行;

Graphics 对象

WinForms 绘制的核心工具,掌握其常用方法(DrawLine/FillEllipse/DrawString/FillPath 等);

抗锯齿设置

g.SmoothingMode = SmoothingMode.AntiAlias 是保证绘制效果平滑的关键,必须记住;

资源释放

所有 Brush/Pen/Font 等实现 IDisposable 的对象,必须用 using 包裹(避免内存泄漏)。

问:为什么重写 OnPaint 而不是直接在 Load 里绘制?

答:Load 仅执行一次,控件尺寸变化、属性修改、窗口遮挡后无法重新绘制;OnPaint 是控件的绘制生命周期方法,每次需要重绘时(如 Invalidate()、窗口刷新)都会触发,保证视觉始终最新。

问:代码中 SetStyle 配置了哪些参数,分别有什么用?

答:

AllPaintingInWmPaint:忽略 WM_ERASEBKGND 消息,避免背景重绘闪烁;

OptimizedDoubleBuffer:启用双缓冲,先在内存绘制再渲染到屏幕,解决闪烁;

UserPaint:控件自行绘制,不使用系统默认绘制逻辑;

ResizeRedraw:控件尺寸变化时自动重绘。

问:为什么所有 Brush/Pen 都用 using 包裹?

答:这些类继承自 IDisposable,封装了 GDI+ 非托管资源,不用 using 会导致资源泄漏,长期运行可能引发内存溢出或界面卡顿

2. 几何与三角函数(仪表盘的核心数学基础)

极坐标→直角坐标转换:仪表盘的刻度 / 指针都基于「圆心 + 角度 + 半径」计算坐标,核心公式:

plaintext

x = 中心X + cos(弧度) * 半径

y = 中心Y + sin(弧度) * 半径

角度与数值的映射:将「数值范围(MinMax)」映射到「角度范围(135°405°)」,核心是比例换算:

plaintext

角度 = 起始角度 + (当前值-最小值)/(最大值-最小值) * 总角度范围

弧度转换:C# 三角函数(Math.Cos/Math.Sin)要求参数是弧度,需通过 角度 * Math.PI / 180 转换。

核心知识点
  • Graphics 常用方法:FillEllipse(填充椭圆)、DrawLine(绘制刻度)、FillPath(填充指针路径)、DrawString(绘制文字)的参数与用法;
  • 坐标系统:WinForms 以左上角为原点的坐标规则,仪表盘中心坐标(centerX/centerY)的计算逻辑;
  • 路径绘制(GraphicsPath):指针用 AddLines 构建三角形的原因(AddPoint 不是有效方法,AddLines 批量添加顶点形成闭合路径)

问:仪表盘背景用 FillEllipse 绘制,传入的 rect 是控件整个区域,为什么能画出正圆?

答:代码中 radius = Math.Min(centerX, centerY) - 10 保证了半径取宽 / 高中的较小值,且控件默认尺寸是 200x200(正方形),因此 FillEllipse 绘制的是正圆;即使控件缩放,半径的计算逻辑也能保证表盘始终是正圆且不超出边界。

问:绘制当前值文字时,为什么要先用 MeasureString 计算尺寸?

答:为了让文字居中显示。通过 MeasureString 获取文字的宽高,再用 centerX - textSize.Width / 2 计算文字的起始 X 坐标,保证文字在表盘水平居中。

问:仪表盘的角度范围为什么是 135°~405°?换成 0°~270° 会有什么问题?

答:135°~405° 覆盖 270° 扇形,对应仪表盘 "左下方→正右→右上方" 的经典布局(符合用户视觉习惯);如果换成 0°~270°,表盘会从正右方向上绘制,不符合常规仪表的视觉逻辑,且刻度会集中在控件上半部分,交互体验差。

问:指针底部的 left/right 点是怎么计算的?为什么要减 Math.PI?

答:sideRad 是指针角度 + 90°(垂直于指针的方向),left 点是该方向上的 6 像素位置;right 点需要取反方向(减 Math.PI 即 180°),因此 sideRad - Math.PI 得到垂直指针的反方向,最终 left/right 形成中心两侧的两个点,和指针顶点组成三角形。

问:如果要把仪表盘的数值范围改成 0~100,需要修改哪些地方?

答:只需修改 MaxValue 的默认值(从 10 改为 100),核心绘制逻辑无需改动 ------ 因为刻度 / 指针的角度计算是基于 MinValue/MaxValue 的比例映射,而非固定数值,这是代码的可扩展性设计。

3. 动态更新机制

Invalidate() 方法:修改属性(如 Value/NeedleColor)后调用 Invalidate(),触发 OnPaint 重绘,实现视觉更新;

属性封装:所有可配置属性(Value/MinValue/NeedleColor 等)都封装了 set 方法,修改时自动触发重绘,保证属性变更即时生效。

核心知识点

  • 可绑定属性:Category/Description 特性的作用(在属性面板分类显示,提升可设计性);
  • 数值限幅:MaxValue/MinValue 的 set 方法中,修改后重新赋值 Value 的原因(保证当前值始终在有效范围内);
  • 异常防护:DrawNeedle 中 if (MaxValue == MinValue) return 的作用(避免除零异常

问:Value 属性的 set 方法为什么没有显式限幅(比如 _value = Math.Clamp(value, MinValue, MaxValue))?是否有问题?

答:当前代码仅在 MinValue/MaxValue 修改时重新赋值 Value 来隐式限幅,但 Value 直接赋值时(如 meter.Value = 20,而 MaxValue=10)会超出范围,存在健壮性问题;优化方案是在 Value 的 set 方法中添加 _value = Math.Clamp(value, MinValue, MaxValue),保证数值始终合法。

问:属性上的 [Category("自定义仪表")] 特性有什么实际作用?

答:在 Visual Studio 的属性面板中,会将这些属性归类到 "自定义仪表"/"外观" 分组下,方便开发者在设计时快速找到并修改,提升控件的易用性(无此特性则属性会散落在 "杂项" 分组)

4.坐标与尺寸适配

所有绘制坐标都基于控件的 Width/Height 计算,而非固定值,保证控件缩放时表盘始终居中且比例正确;

文字绘制前用 g.MeasureString 计算尺寸,保证文字居中(如当前值 + 单位)。

5.性能优化与扩展

核心知识点

  • 重复创建资源的优化:当前代码在 OnPaint 中每次都创建 Font/Pen,可缓存常用资源(如刻度字体、画笔)避免重复创建;
  • 局部重绘:Invalidate() 可传入矩形区域,只重绘指针 / 数值变化的部分,减少绘制开销;
  • 扩展场景:如支持自定义刻度数量、指针样式、渐变背景等。

问:如果这个控件频繁更新 Value(比如每秒 10 次),可能会有性能问题吗?如何优化?

答:有问题 ------ 每次 OnPaint 都会创建 Font/Pen/Brush,频繁创建销毁会产生 GC 压力;

优化方案:

缓存常用资源:将刻度字体、画笔定义为类级变量,在构造函数创建,Dispose 时释放;

局部重绘:调用 Invalidate(new Rectangle(...)) 只重绘指针和数值区域,而非整个控件;

防抖:限制重绘频率(如每秒最多 60 次),避免短时间内多次触发 Invalidate。

问:如何给仪表盘添加渐变背景?

答:将 SolidBrush 替换为 LinearGradientBrush/PathGradientBrush,示例:

csharp 复制代码
运行
using (var brush = new LinearGradientBrush(rect, Color.LightBlue, Color.Blue, 45f))
{
    g.FillEllipse(brush, rect);
}
相关推荐
CSharp精选营6 天前
都是微软亲儿子,WPF凭啥干不掉WinForm?这3个场景说明白了
c#·wpf·跨平台·winform
小贺儿开发12 天前
Unity3D 家居视频遥控效果演示
unity·udp·人机交互·网络通信·winform·远程·photon
czhc114007566313 天前
winform 330 跨线程 异步
wpf·线程·winform
light blue bird15 天前
原生控件GDI完成作业协同界面
jvm·数据库·.net·winform·gdi+界面
light blue bird1 个月前
MES/ERP大数据报表条件索引查询组件
数据库·.net·winform·t-sql·大数据报表
香煎三文鱼1 个月前
winform读取不到App.config配置文件中的配置信息
winform
Aevget1 个月前
界面控件DevExpress WinForms中文教程:Data Grid - 数据绑定(二)
.net·界面控件·winform·devexpress·ui开发
小曹要微笑1 个月前
WinForms 验证码类的实现
c#·验证码·winform·验证码类
我本梁人1 个月前
Winform实现多语言切换
winform