简介:这是一套基于MFC ActiveX技术开发的工控图形控件集,包含曲线、折线、柱状图三种绘制控件,面向Windows桌面应用开发者,主要解决工业监控系统中传感器数据与历史数据的实时可视化、报表展示等问题。资源包共158个文件,以C++头文件与源文件、BMP/ICO/CUR界面资源、编译好的DLL/OCX组件及VS工程文件为主体,同时附带PDF/DOCX说明文档和演示项目,整体仅2.32MB,便于下载、集成和二次开发。目前已有85人学习下载。控件集针对工控数据高实时性、高准确性的特点做了性能与安全性设计,开发者可通过源码研究实现原理,也可结合示例快速上手,并按实际需求调整图表颜色、线条样式、坐标轴格式等。对于需要快速提升数据可视化能力、缩短自绘控件开发周期的MFC程序员而言,是一套可直接落地的实用工具。
1. 工控上位机的实时曲线为什么都绕不开 MFC ActiveX 控件
一块触摸式工控一体机,跑着 MFC 对话框写的上位机软件,温度、压力、流量每隔 200ms 刷新一次,还要带历史回放和报表导出。第一次用 GDI 自绘曲线的工程师多半会踩同一个坑:单独画一条折线很容易,但一旦要滚动窗口、缩放、右键标尺、定制曲线颜色,重绘逻辑立刻膨胀,UI 线程开始卡顿。相比之下,把"画曲线"封装成一个 ActiveX 控件,注册后像标准组件一样拖进对话框,数据通过 OLE 接口下发,宿主程序完全不关心坐标变换——这是工控行业沉淀了很多年的成熟做法。这套基于 MFC ActiveX 的曲线、折线、柱状图绘制控件,源码目录叫 ST_Curve-master,曲线用于温度压力趋势,折线用于阈值变化过程,柱状图用于产量电量统计。它适合准备在 MFC/VB 上位机里做实时监控的工程师,也适合要二次开发、把图表改成自家品牌风格的团队。下面从解压后的文件开始,讲注册、接口、集成和排错。
2. 解压后的资源文件与 ActiveX 控件注册机制
2.1 压缩包里那串 .bmp 位图不是装饰品
解压后看到的 Textora.bmp、Textmauv.bmp、menuimages_hc.bmp、06.bmp、46.bmp、101.bmp 很容易被当成无关杂项扔掉,其实它们是控件自带工具栏和菜单的图标资源。Textora / Textmauv 从命名看是文本横向、竖向排列的切换图标,在曲线控件里对应 X 轴文字方向、标签旋转这类功能;menuimages_hc.bmp 是高彩色菜单图标条,多个 16×16 或 24×24 图标横向排在一张位图里,运行时用 CImageList 按索引切片加载;06.bmp、46.bmp、101.bmp 这类数字命名的位图通常是 16 色老式图标,重复出现的那一份是给禁用态或高亮态用的。它们的分工可以整理成下表。
| 文件 | 推断用途 | 二次开发注意点 |
|---|---|---|
| Textora.bmp / Textmauv.bmp | 文字横排/竖排切换图标 | 修改时保持 .rc 里的文件名引用一致 |
| menuimages_hc.bmp | 工具栏/菜单高彩图标条 | 切片顺序由 CImageList 的 Add 顺序决定 |
| 06.bmp / 46.bmp / 101.bmp | 16 色功能图标,正常/禁用两套 | 换图标只替换像素,别改尺寸 |
处理外部位图的常见做法是:解压后不要乱改名,保持与 .rc 资源脚本里的引用一致。如果二次开发时发现控件工具栏空白或图标变灰,先检查这些 bmp 是否在可执行文件同目录,或者有没有被 .rc 用BITMAP IDB_xxx "res\\xxx.bmp"写成编译期嵌入。我一般会先用 Visual Studio 的图像编辑器直接改位图像素,确认效果后再回 MFC 源码里调整 CImageList 的 Add 顺序——顺序一旦错位,所有按钮图标会整体串位,排查起来非常费时间。
2.2 regsvr32 注册控件:32 位与 64 位的坑
ActiveX 控件本质是一个 COM 进程内服务 DLL,注册的动作是让它把 CLSID、ProgID、类型库写进注册表,宿主程序才能按名字(ProgID,例如STCurve.STCurveCtrl)找到它。手工注册用 regsvr32,最常用的两条命令:
rem 注册(/s 静默,不加 /s 会弹成功对话框) regsvr32 /s "D:\work\ST_Curve\Release\STCurve.ocx" rem 反注册 regsvr32 /u /s "D:\work\ST_Curve\Release\STCurve.ocx"注意:在 64 位 Windows 上,C:\Windows\System32\regsvr32.exe是 64 位版本,C:\Windows\SysWOW64\regsvr32.exe才是 32 位版本。绝大多数工控 ActiveX 控件是 32 位编译的,必须用 SysWOW64 下的 regsvr32 注册,否则会报"模块已加载但找不到入口点 DllRegisterServer"。
注册完成后用下面命令验证 ProgID 是否写入了注册表:
reg query HKCR /f "STCurve" /s /kHKCR是 HKEY_CLASSES_ROOT 的缩写,/f指定要查找的字符串,/s递归搜索子键,/k表示只匹配键名。能查到STCurve.STCurveCtrl和CLSID\{...}这样的键就说明注册成功。网上常见的call to DllRegisterServer for xxx.ocx returns 0x80040200报错,本质是控件内部注册函数执行失败。按经验,工控机上多半是三种原因:机器没装对应版本的 VC++ 运行库、ocx 依赖的其他 DLL 缺失、注册时没有管理员权限。分别处理:装 vcredist_x86(注意是 x86 不是 x64)、用 Dependency Walker 或 Process Explorer 看加载失败的模块、用管理员 CMD 重新注册。
2.3 不写代码先验证:ActiveX Control Test Container
控件注册成功后,不建议立刻往项目里塞,先花两分钟用测试容器做冒烟。老版本 Visual Studio 的"工具"菜单里有 ActiveX Control Test Container(程序名 tstcon32.exe),打开后 Edit → Insert New Control,在列表里挑 ST Curve Control,控件会出现在一个窗口里。此时可以做三件事:右键调出 Invoke Methods 窗口手动执行 AppendPoint 往曲线里塞点;打开属性页改 BackColor、AxisColor 看是否即时重绘;拖动窗口边缘看控件是否按比例缩放。这三项过了再进对话框集成,否则排查范围会被放大到宿主工程里,很难定位是控件问题还是容器问题。新版 VS 不再带 tstcon32,替代方案是快速建一个空的 MFC 对话框工程,把控件拖上去跑一下,成本差不多,效果一样。
3. 曲线、折线、柱状图的绘制模型与数据绑定接口
3.1 三种图表在底层是三种不同的数据模型
很多工程师以为三种图只是画法不同、改个参数就行,实际上它们的输入数据单位都不一样,接口设计必须分开。折线图的数据单位是离散点,绘制时 MoveToEx / LineTo 逐个连接,适合等间隔采样但不需要平滑的场景;曲线图在折线基础上增加平滑插值,常见做法是只在缩放时按像素步长用 Catmull-Rom 样条重采样,保证放大后曲线不出现明显折角。柱状图的数据单位不是点,是类别桶,每个桶对应一个矩形,矩形高度就是数值。
| 类型 | 数据单位 | 工控典型场景 | 底层绘制 |
|---|---|---|---|
| 曲线 | 连续点 (x, y) | 温度、压力、液位趋势 | 折线 + 样条插值 |
| 折线 | 连续点 (x, y) | 阈值变化、启停记录 | LineTo 直接连线 |
| 柱状图 | 类别值 | 班产量、电量、故障次数 | FillRect 矩形填充 |
改造源码时不要把柱状图数据硬塞进曲线数组里,否则坐标轴、图例、Tooltip 全要跟着改。实时曲线和折线可以共用一套点数组接口,柱状图单独用按类别赋值的接口,这是这套控件源码里最值得保留的设计。
3.2 属性、方法、事件:ActiveX 控件对外暴露的三张脸
MFC ActiveX 控件对外暴露三样东西:属性(Property)、方法(Method)、事件(Event),在 .odl 或 .idl 文件里对应 dispinterface 和 event interface。以 ST_Curve 改造后的接口为例,属性一般有 BackColor、AxisColor、LineWidth、XMin/XMax、YMin/YMax、ShowLegend;方法有 AppendPoint、SetSeriesData、ClearSeries、SetXRange;事件有 OnPointClick(鼠标点中数据点后通知宿主,参数是系列号、索引和坐标值)和 OnRangeChanged(用户滚轮缩放后通知宿主同步其他控件)。
在 MFC 宿主里,ClassWizard 会生成一个包装类,本质是 IDispatch 的封装。追加一个点的包装函数长这样:
void CSTCurveCtrl::AppendPoint(short nSeries, double x, double y) { static BYTE parms[] = VTS_I2 VTS_R8 VTS_R8; // 参数类型声明 InvokeHelper(0x11, // DISPID,来自 .odl DISPATCH_METHOD, VT_EMPTY, NULL, parms, nSeries, x, y); }InvokeHelper是 MFC 调用自动化接口的标准入口:第一个参数是调度 ID,由 .odl 里id(0x11)决定;parms数组声明后面三个实参的类型,VTS_I2 是 16 位整数,VTS_R8 是 64 位双精度浮点,顺序必须与控件侧的类型声明完全一致。常见坑是宿主工程是 32 位而控件侧参数声明为 long(32 位),两边字节数对不上,数据一多就乱点。事件到达宿主代码通常表现为对话框 ClassWizard 里多出 OnPointClick 这样的成员函数。鼠标点在柱状图上时,宿主弹出 Tooltip 显示"当班产量 86.3 件",比每次去读整个数组再计算高效得多。
3.3 数据下发两条路:逐点追加与整段覆盖
实时监控和报表回放对接口的要求完全不同。实时场景下(比如 250ms 一帧),数据是流水账,用逐点追加:宿主每帧调一次 AppendPoint,控件内部维护环形缓冲区,旧点被自动覆盖。报表或历史回放场景下,数据是整块查出来的,几百上千行,逐点跨进程调用 InvokeHelper 要几千次,性能不可接受,必须整段覆盖。整段覆盖的常见做法是传 SAFEARRAY:
COleSafeArray sa; DWORD dims[2] = { (DWORD)nCount, 2 }; // nCount 行、2 列:x 和 y sa.Create(VT_R8, 2, dims); // 双精度浮点二维数组 long idx[2]; for (int i = 0; i < nCount; i++) { idx[0] = i; idx[1] = 0; sa.PutElement(idx, &xs[i]); // 第 i 行第 0 列 idx[1] = 1; sa.PutElement(idx, &ys[i]); // 第 i 行第 1 列 } m_curve.SetSeriesData(0, sa); // 0 号系列整段替换dims里的行数列数顺序、PutElement的行索引和列索引,都要和控件侧COleSafeArray::AccessData的解析顺序一致。这个模型与 .NET Chart 控件里 Series 数据绑定是同一个思想:数据源整体替换,控件只重算布局,不在调用层一层层拷贝。区别是 ActiveX 走 COM 的变体参数,跨语言都能调,代价是调试时问题藏得深。SetSeriesData 之后通常还要补一个InvalidateControl(FALSE),有些实现里重绘是惰性的,不主动刷界面会一直显示旧数据。
4. 在 MFC 对话框工程中集成 ActiveX 控件:从拖拽到实时刷新
4.1 插入控件与包装类的生成
前提是第 2 章的注册已经通过测试容器冒烟。打开 MFC 对话框工程的 .rc,右键对话框空白处,选"插入 ActiveX 控件",在列表里挑 ST Curve Control(列表读的是注册表里的控件条目,没注册就看不到),拖到对话框上得到 IDC_CURVE_CTRL。右键控件选"添加变量",VS 会生成 CSTCurveCtrl 包装类并把它挂到对话框成员上,DoDataExchange 里多一行:
void CMonitorDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_CURVE_CTRL, m_curve); // 控件与成员变量绑定 }DDX_Control在对话框创建时把资源里的 ActiveX 实例包装成 CSTCurveCtrl 对象,之后 m_curve 的任何方法调用都走 IDispatch。设计期属性(BackColor、AxisColor 等)存在对话框资源里,换机器不丢;运行期改属性只影响当前实例。要改默认外观,直接在属性页里调,不要每次 OnInitDialog 都手动 SetBackColor,除非确实需要根据工况动态切换主题。
4.2 定时器驱动曲线滚动刷新
实时趋势的核心是"新数据进来、旧数据滑出去"。常见做法是在 OnInitDialog 里SetTimer(IDT_DATA, 250, NULL),配合 ON_WM_TIMER 消息映射,由定时器驱动采集和刷新:
void CMonitorDlg::OnTimer(UINT_PTR nID) { if (nID == IDT_DATA) { double t = m_tick * 0.25; // 时间轴,单位秒 double v = ReadPlcFloat(m_iChannel); // 读一个通道实时值 m_curve.AppendPoint(0, t, v); // 追加到 0 号系列 // 滚动窗口:只保留最近 600 个点的可见区间 if (m_tick > 600) { m_curve.SetXMin((m_tick - 600) * 0.25); m_curve.SetXMax(t); } m_tick++; } CDialogEx::OnTimer(nID); }ReadPlcFloat 是示意函数,实际工控环境里对应 Modbus 读保持寄存器、OPC DA 异步读或者厂商 SDK 的一次取数。注意 250ms 的定时器跑在 UI 线程上,如果一次读数超过几十毫秒(比如走网口且对端响应慢),画面就会卡。我一般会改成工作线程负责采集,PostMessage(WM_DATA_READY, wParam, lParam)通知 UI 线程只做 AppendPoint——控件的所有接口都必须在创建它的线程里调用,跨线程调 InvokeHelper 会偶发崩溃。滚动窗口用 SetXMin/SetXMax 比整体重置数据数组便宜得多,控件只需重新计算坐标映射,点数组不动。如果客户要的是风机特性曲线这类"横轴流量、纵轴压头"的静态曲线,就不需要滚动窗口,把标定点整段 SetSeriesData 一次性灌进去,再打开数据点标记属性即可。
4.3 柱状图的批量推送与点击联动
柱状图对应的典型场景是"24 小时产量"这类横轴为类别的统计。整段数据一次性推入,比逐根柱子赋值快一个量级:
double values[24]; for (int i = 0; i < 24; i++) { values[i] = m_hourOutput[i]; // 每小时产量 } COleSafeArray sa; sa.CreateOneDim(VT_R8, 24, values); // 一维数组,按元素序对应类别 m_bar.SetBarData(sa); if (values[23] > 1000) { m_bar.SetBarColor(23, RGB(220, 60, 60)); // 超限柱子标红 }CreateOneDim 第一个参数仍是 VT_R8 双精度,第二个是元素个数,第三个是数据源指针,MFC 会拷贝一份进 SAFEARRAY,调用后原数组可以立即释放。类别的标签("00:00"、"01:00"…)一般在初始化时逐个设置,标签字体颜色跟随 AxisColor。超限标红用的 SetBarColor 要确认颜色参数约定是 OLE_COLOR 还是普通 long,搞反了会出现"明明设了红色却显示成蓝色"的玄学问题,排查时先看 .odl 里属性类型。事件侧,OnPointClick 传回柱子索引,宿主拿它联动另一块文本框显示"该时段故障 N 次",比轮询取值省事,也是组态类软件常见的交互方式。
5. 高分辨率自适应、双缓冲与加载失败排查
5.1 自适应屏幕分辨率与双缓冲两个改动
工控机从 1024×768 换到 1920×1080 触屏,控件最容易出的问题是"只画了左上角一块",原因几乎都是 OnDraw 里用了写死的像素坐标。MFC ActiveX 控件的绘制入口是 OnDraw,rcBounds 是控件当前实际边界,正确做法是所有元素基于 rcBounds 计算相对位置:
void CSTCurveCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { CRect rc = rcBounds; // 当前实际大小,不能用固定值 m_rcPlot = CRect(rc.left + rc.Width() / 12, rc.top, rc.right - rc.Width() / 12, rc.bottom - rc.Height() / 10); // 画轴、画线全部基于 m_rcPlot 的 Width()/Height() 计算像素位置 }再配合 COleControl 的 GetControlSize 在初始化时读一次设计尺寸,把 X、Y 两个方向的缩放因子存成成员变量,数据点转像素的函数里统一乘上因子,宿主窗口变大变小时控件就能自动跟随缩放。曲线点数超过 1000 或 OnDraw 里同时画网格、轴、图例和三条曲线时,直接画必然闪烁,标准做法是先画到兼容内存位图再用 BitBlt 整体贴回屏幕,配合宿主端 SetRedraw(FALSE) 批量更新、最后 RedrawWindow 合并重绘。
5.2 加载失败与编译报错的排查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 注册报 0x80040200,DllRegisterServer 失败 | 缺 VC++ 运行库、依赖 DLL 缺失、无管理员权限 | 装 vcredist_x86;查依赖;管理员 CMD 重注册 |
| 64 位系统拖不进控件或对话框崩溃 | 用 64 位 regsvr32 注册了 32 位 ocx | 改用C:\Windows\SysWOW64\regsvr32.exe注册 |
| 插入后黑屏只有边框 | OnDraw 用固定坐标绘制 | 按 5.1 改成 rcBounds 相对布局 |
| 编译源码报"此项目需要 MFC 库" | 工程没链接 MFC 或本机没装 MFC 组件 | VS Installer 安装 MFC 组件;工程属性→MFC 使用共享 DLL |
| 控件图标空白或工具条串位 | 外部 bmp 未随包分发或 CImageList 顺序错 | 保持解压目录完整;检查 .rc 的 BITMAP 引用路径 |
最后是一个验证技巧:控件改完属性或数据结构后,先开测试容器或临时对话框工程,手动执行一次 AppendPoint 加 SetXRange 冒烟,确认数据点与轴线对齐,再回到正式工程跑定时器。这样能把数据下发、坐标换算、重绘三层问题拆开定位,不会一上来就怀疑是控件本身坏了。
本文还有配套的精品资源,点击获取