简介:这是Iocomp Components v4.02 SP2完整源码包,面向工控领域仪表显示控件的开发与二次集成,适配Delphi 4至2009、C++Builder 5/6及RAD Studio等主流IDE。压缩包共938个文件,大小17.91MB,包含289个Pascal源文件、137个窗体定义、95个位图资源及80个资源文件,并附带各版本工程文件(dpr、dpk、bdsproj等),便于直接编译安装。另有DLL、OCX及示例程序,可直观呈现仪表控件渲染效果。已有837人学习下载。对于需要深入掌握仪表控件绘制原理、控件封装技巧或希望在老版本IDE中实现专业仪表界面的开发者,这套源码提供了完整参考,能大幅节省底层绘图与交互设计时间。
1. Iocomp Components v4.02 SP2:一套带完整源码的工业监控控件库
接手一套工控上位机要做管道监控,界面要圆表、条形表、趋势曲线、报警灯、开关和滑块,第一反应就是别自己用 GDI 一点点画。Iocomp Components v4.02 SP2 Full Source 是我最近完整拆过的一套商业级工业监控组件源码包,包含全部控件的实现,不只是发给你一个调好参数就能用的 ocx。它的价值不在省下授权费,而在让你能直接看到表盘刻度怎么算、指针怎么转、属性页怎么挂上去,甚至把某一类控件改成自己风格再编出个新控件。适合 MFC/ATL、C++ Builder 和 Delphi 背景的工控上位机开发,也适合想研究商业控件里绘图、消息映射、IDispatch 自动化这套体系的人。
2. 核心模块拆解:表盘绘制、属性管理与数据刷新链路
把源码包打开以后,你会发现这套组件不是几十个孤立控件堆在一起,而是有一条非常清晰的公共基类链。我先说整体分类,再往下拆绘制和属性这两条主线,因为二次开发和排障基本都围绕它们展开。
2.1 控件家族与常见组合
按用途可以把常用控件分成四类,对应工控界面的不同区域。
| 控件类别 | 典型控件 | 界面场景 |
|---|---|---|
| 表盘类 | Gauge、Meter、Knob | 温度、压力、转速等实时数值展示 |
| 数据展示类 | TrendChart、BarGraph、Display | 趋势曲线、柱状对比、数码管读数 |
| 操作类 | Slider、Switch、PushButton | 手动调节设定值、启停开关 |
| 状态类 | LED、Light、Alarm | 报警灯、运行状态指示 |
常见组合是左侧放两个 Gauge 做主要参数展示,中间放一张 TrendChart 拉历史趋势,右侧一排 Switch 加 LED 做开关到位状态,底部加一个 BarGraph 做批量对比。这样一套界面下来,所有控件共同依赖的只有两样东西:一样是基类里的窗口与消息处理,另一样是属性管理器。
2.2 继承结构:所有控件都挂在一条基类链上
源码里的类关系大致是:最顶层是 CwComObject,负责运行时类型识别和序列化;往下是 CwComWindow,封装窗口句柄与消息循环;再往下是 CwComControl,把 ActiveX 控件的接口、属性、事件和窗口绑定在一起。具体控件如 CwGauge、CTrendChart 全部从 CwComControl 继承,只重写布局和绘制相关的方法。
这个设计的高明之处在于,窗口、自动化、绘制三个职责是分开的。我一般会先把 CwComControl 这个头文件通读一遍,因为新增一个控件时你不需要处理消息循环,只需要覆写三五个方法,包括计算客户区、画背景、画前景、处理鼠标命中。后面做自定义控件时,这套结构能省掉大量重复工作。
2.3 表盘绘制链路:从 value 到屏幕像素
Gauge 的绘制是所有控件里最有代表性的。它的 WM_PAINT 最终会走到一个统一的绘制函数,我在源码里看到的关键路径可以精简成下面这样:
// 伪代码:CwGauge 关键路径的绘制顺序 void CwGauge::OnDrawRect(Gdiplus::Graphics &g, const CRect &rc) { // 1. 先用缓冲位图,防止控件在调整大小时闪烁 Gdiplus::Bitmap buffer(rc.Width(), rc.Height(), &g); Gdiplus::Graphics mem(&buffer); // 2. 背景、外圈和刻度区域,全部按照控件尺寸做等比缩放 DrawBezel(mem, rc); DrawScale(mem, rc); DrawTicks(mem, rc); // 3. 把数据值映射成指针角度,再绘制指针和中轴 double angle = MapValueToAngle(m_value, m_min, m_max); DrawPointer(mem, rc, angle); g.DrawImage(&buffer, 0, 0); }这段代码说明了一个关键做法:所有绘制先画到内存缓冲,最后一次性把整张位图提交到屏幕 DC。原因是 ActiveX 控件在设计器里被拖动、缩放时,如果直接在窗口 DC 上绘制,画面会闪成一片,双缓冲是连续重绘场景下的基本解法。rc 是客户区矩形,m_value 是当前实时值,m_min 和 m_max 是量程上下限,这三个参数覆盖了大部分定制需求。
再看指针角度是怎么算的。表盘不是完整的圆,而是从 -225 度起、扫过 270 度的扇区,线性映射就在这个区间里做:
double CwGauge::MapValueToAngle(double value, double min, double max) { // 表盘从 -225° 起,扫过 270°;量程外的值钳制在两端 double ratio = (value - min) / (max - min); if (ratio < 0.0) ratio = 0.0; if (ratio > 1.0) ratio = 1.0; return -225.0 + ratio * 270.0; }注意末尾的钳制逻辑,它避免数据源在启动瞬间传入一个异常值导致指针转出表盘范围。如果想让控件变成完整的圆表,把起始角改成 0 或 -180,再把扫描角改成 360 即可,这套公式可以直接复用。
2.4 属性管理器:dispid、类型库与属性页
每个控件里都有一张 dispatch map,它把容器的自动化调用映射到成员变量上。以 Gauge 为例,属性表骨架长这样:
// 属性表的一部分,把属性ID映射到成员变量与存取方法 BEGIN_DISPATCH_MAP(CwGauge, CwComControl) DISP_PROPERTY_ID(CwGauge, "Min", dispidMin, m_min, VT_R8) DISP_PROPERTY_ID(CwGauge, "Max", dispidMax, m_max, VT_R8) DISP_PROPERTY_ID(CwGauge, "Value", dispidValue, m_value, VT_R8) END_DISPATCH_MAP()这是 IDispatch 自动化的入口,组态软件、脚本、其他语言宿主改控件的属性全走这条路。每个 dispid 在源码的枚举里全局唯一,新增属性时最忌讳随便复制一个已有的 dispid,重复 ID 会导致属性互相覆盖,我见过不止一次因为这种低级问题排查到半夜。
2.5 数据刷新链路:外部赋值到屏幕重绘
外部容器设置 Value 属性时,真正执行的流程是进入 put_Value,再触发属性管理器和重绘:
STDMETHODIMP CwGauge::put_Value(double value) { // 自动属性映射最终进入这里 m_value = value; // 通知属性管理器:该属性在编辑界面上需要同步刷新 m_pPropManager->PropertyChanged(dispidValue); // 触发客户区重绘 Refresh(); return S_OK; }PropertyChanged 会让属性页上的编辑框更新当前值,同时也可以触发事件通知容器,比如值越限。Refresh 内部会调用 InvalidateRect 并立即 UpdateWindow,保证数据刷新频率上来了画面还能跟得上。按我的习惯,高频数据源最好控制在一秒二三十次以内,太频繁反而会把 GDI+ 绘制压垮。
3. 源码包工程落地:目录结构、编译顺序与运行验证
拿到完整源码包以后,第一件事不是找入口,而是把目录结构和编译顺序理清楚。这套东西不像普通应用程序只有一个主工程,它是二三十个控件工程加公共库加 Demo 的组合,顺序乱了,编译报错能让你怀疑人生。
3.1 解压后的目录结构
我一般先花十分钟把顶层文件过一遍,重点看表格里这几块。
| 目录/文件 | 作用 | 我会先看 |
|---|---|---|
| License.txt | 授权条款,确认完整源码可用范围 | 每行读完 |
| Source\Lib | 公共绘制、属性管理、序列化基础代码 | PropManager.h |
| Source\Controls | 每个控件的实现文件 | Gauge.cpp、Meter.cpp |
| Demo | 各编译环境的示例工程 | 选贴近自己环境的版本 |
| Bin | 预编译产物,不一定需要 | 不看,等自编译 |
Release 4.02 SP2 这个版本的头文件里大量使用模板和 ATL 宏,如果直接用高版本 VS 编译老工程,第一步就会撞上字符集和警告级别的问题,后面避坑章节再详细展开。
3.2 编译顺序:从公共库到控件再到 Demo
工程之间的依赖关系是分层的。公共库在最底层,被所有控件依赖;每个控件工程输出一个 ocx 或 dll;Demo 工程引用这些控件输出。命令行编译的习惯做法是:
# 按依赖顺序编译(示意,不同版本工程名有差异) devenv Iocomp.sln /Build "Release|Win32" /Project CommonLib devenv Iocomp.sln /Build "Release|Win32" /Project Gauge devenv Iocomp.sln /Build "Release|Win32" /Project Meter其实新版构建系统会自动处理工程依赖,如果只是完整编译整个解决方案,直接 Build Iocomp.sln 也行。我一般会先只编公共库,确认这一层没有报错,再编一个 Gauge 控件验证工程配置,最后才全量编译。这样能把问题隔离在某一层,而不是一口气冒出几百个错误找不到根源。
编译成功以后,每个控件工程输出目录下会生成 ocx 类型库文件。这一步如果开着杀毒软件,部分环境会把 ocx 注册动作误判为危险行为,可以先临时关闭实时防护再继续。
3.3 注册控件到系统
ActiveX 控件必须注册到注册表才能在设计器和运行时被容器加载。命令行注册是标准做法:
# 注册 Gauge 控件;/s 参数表示静默模式 regsvr32 /s "Release\IocompGauge.ocx" regsvr32 /s "Release\IocompMeter.ocx"64 位 Windows 上有个坑:如果你编出来的是 32 位控件,必须用 SysWOW64 目录下的 32 位 regsvr32,否则会提示模块加载失败。命令改成这样:
C:\Windows\SysWOW64\regsvr32.exe /s "Release\IocompGauge.ocx"每次重编译 ocx 后重新注册一次,这是个好习惯。因为 COM 类 ID 和接口 ID 在源码里是显式定义的,重新编译通常不会变,但如果改了 .odl 或 .rgs 文件,旧注册信息可能残留。
3.4 用 Demo 验证控件行为
注册完成以后打开 Demo 工程直接跑一遍。Demo 里一般会有一个综合测试窗体,上面整整齐齐排列着各种控件,还有一个测试数据源持续向 Gauge 和 TrendChart 喂数据。
我会故意做两个小操作验证控件是否正常工作:第一个是拖动窗口边缘,看控件是否平滑缩放、没有明显闪烁;第二个是在属性页里切换单位和量程,看表盘是否立刻重绘。这两个动作能覆盖双缓冲和属性通知两条核心链路。
4. 实战避坑:编译、注册、部署与二次开发的五个翻车点
这套源码已经编译通过过无数次,但还是有重复出现的问题。我把印象最深、遇到最多的五个场景记在这里,都是先写现象、再说原因、最后给解决方案。
4.1 高版本 VS 打开老工程报 C4819 或乱码
现象:用高版本 Visual Studio 打开老版工程,转换向导完成后报出几百个 C4819 警告,部分源文件里的注释变成乱码,编译输出窗口根本没法看。
原因:老工程源码按 ANSI/GBK 编码保存,而新版本 IDE 默认把源文件按系统区域设置里的代码页解析,两者不一致导致多字节字符被截断告警。我以前一度以为源码坏了,后来才发现只是编码问题。
解决:在工程属性里的“C/C++ 命令行”附加参数中加上/utf-8,让编译器统一按 UTF-8 解析源文件。如果不想动工程文件,也可以在项目里新建一个 .props 文件集中控制。我建议顺手把警告级别从 /W4 降回 /W3,老代码在 /W4 下的告警多到淹没了真问题。
4.2 设计器里弹 License 校验对话框
现象:在对话框设计界面拖入 Gauge 控件,控件直接变成灰色并弹授权失败提示,无论如何都拉不出一个能交互的表盘。
原因:ActiveX 控件区分设计期授权和运行期授权。设计器在放置控件时会检查设计期授权文件是否已注册,源码包里一般附带 .lic 文件,但它需要被正确复制到编译输出目录或系统授权目录,缺了这一步,IDE 无法确认你有权使用设计期功能。
解决:先用 4.3 的命令注册控件,再把源码包里的 .lic 文件复制到编译输出目录,重新打开设计器即可。注意不要把设计期授权文件一起打包进最终发布目录,它只服务于开发环境。
4.3 事件接不上、回调没触发
现象:在自己的 MFC 对话框里创建控件实例,控件显示正常、属性读写正常,唯独鼠标点击、值改变等事件一直不触发。
原因:容器绑定事件时通过类型库里的 event interface GUID 建立连接。如果重新编译控件时改了 .odl 文件里的事件接口 ID,或者用 #import 生成临时头文件的时机太早,老 GUID 会被缓存下来,事件路由就对不上,看起来像控件根本没发事件。
解决:删除工程目录下所有 #import 生成的 .tlh 和 .tli 临时文件,重新编译整个解决方案,让容器重新解析类型库。改过事件接口后,还要记得检查 .rgs 文件里的注册表项是否同步更新。
4.4 Release 版部署到干净机器上没有画面
现象:开发机一切正常,把编译好的 ocx 和 Demo 程序拷到没有装开发环境的机器上,控件区域空白或者红叉,事件日志里看不到任何异常。
原因:默认 Release 配置使用动态链接 MFC 和 ATL 运行库,目标机器缺了这些 DLL;再加上 GDI+ 绘图库版本太旧,控件初始化时创建 Gdiplus::Graphics 失败,整个绘制链直接断掉。
解决:在 Release 配置里把“使用 MFC”改为“在静态库中使用 MFC”,ATL 也改成静态链接,同时把 GDI+ 相关依赖随程序一并分发。我习惯再做一次验证:用虚拟机纯净系统跑一遍 Demo,确认控件能正常画出来。
4.5 修改绘制代码后窗口明显闪烁
现象:基于 Gauge 做定制,直接在绘制代码里改了一个圆形填充,结果每次拖动窗口大小、刷新数据时表盘周围闪得厉害。
原因:新写的绘制代码绕过了基类的双缓冲路径,直接在窗口 DC 上画背景和指针,系统每次擦除背景后再画前景,视觉上就是明显闪烁。
解决:改动时要沿着 OnDrawRect 这条路走,先画到内存缓冲再整体上屏;如果确有必要自绘,还要把 OnEraseBkgnd 返回 TRUE,禁止系统额外擦除背景。闪烁问题十次里有八次是这个原因,剩下两次是刷新频率太高,把刷新间隔从 10 毫秒调到 33 毫秒就解决了。
5. 二次开发:把 Gauge 改造成带报警色的温度表
动手改源码前,先确定改哪里:Gauge 的属性系统在 dispatch map 里,绘制逻辑在 OnDrawRect 里,属性页在 .odl 和属性页工程里,这三处是分开的。我照这个思路试过把 Gauge 扩展成一个温度计控件。
5.1 复制控件并改类名
把 Gauge.cpp 和 Gauge.h 复制一份,重命名成 TempGauge.cpp 和 TempGauge.h,全局替换类名 CwGauge 为 CTempGauge。继承关系不用动,继续挂在 CwComControl 下即可。替换后构造函数里 new 出的属性管理器会自动工作,dispid 枚举保持原值不冲突。
5.2 新增报警属性
在头文件里加两个成员变量,然后注册到 dispatch map:
// TempGauge.h 中新增的报警属性 double m_alarmValue; // 报警阈值,超过则指针变红 BOOL m_alarmEnabled; // 是否启用报警变色在 dispatch map 里补充对应的属性 ID 和 VT_R8 / VT_BOOL 类型映射。为了让属性页也能显示,还要在 .odl 类型库描述里同步增加这两个属性并更新库 GUID,否则容器侧只会看到旧版接口。
5.3 让表盘在越限时变色
绘制阶段,我在调用完父类的 OnDrawRect 之后叠加一个报警指针:
void CTempGauge::OnDrawRect(Gdiplus::Graphics &g, const CRect &rc) { // 先调用父类画好表盘、刻度、指针 CGauge::OnDrawRect(g, rc); // 值越过阈值时,用高亮的红色重画一次指针,起到报警提示作用 if (m_alarmEnabled && m_value >= m_alarmValue) { Gdiplus::Color alarmColor(255, 220, 30, 30); Gdiplus::Pen alarmPen(alarmColor, 5.0f); g.DrawLine(&alarmPen, m_center, m_pointerTip); } }这里的 m_center 和 m_pointerTip 是父类绘制阶段算好并保存的成员,直接用可以避免重复计算表盘圆心。报警后要验证两件事:把值降到阈值以下,红色指针应该消失;拖动窗口大小,报警线不应发生漂移。从那以后我每次拿到带源码的控件库,第一件事就是先把工程编译通、再跑一遍 Demo,确认这套绘制路径和消息循环可靠,然后才动手改类。希望帮到你。
本文还有配套的精品资源,点击获取