中望CAD二次开发:C++与Qt组合的插件开发实践指南

📅 发布时间:2026/10/13 3:29:09
中望CAD二次开发:C++与Qt组合的插件开发实践指南
简介这是一份面向CAD二次开发入门者的中望CADZWCAD扩展开发示例工程基于C与QT 5.12构建在Visual Studio 2015环境中编译运行并附带了完整的VS与QT配置工具链说明。示例通过一个简单的QT对话框集成到CAD中重点演示了如何读取扩展字典帮助理解ZRX接口与QT界面协同工作的核心流程该方案也容易平移到AutoCAD平台。压缩包共273个文件包含cpp/h源码、dll库、lib/exp链接文件、ui界面描述、vcxproj工程文件、props配置以及调试生成的pdb/ilk等包体约206MB既有完整源码也有编译产物可直接参照。已有2817人学习下载适合希望快速上手“CADQT”混合界面二次开发、并需要可运行样例对照源码的开发者。1. 中望CAD二次开发C和Qt的组合为什么值得先试这条路线某个工程软件团队接到一个迁移任务把原来在 AutoCAD 上批量改图框、刷属性、导图号的工具整体挪到中望CAD 上。网上一搜中望CAD 二次开发的资料又少又散团队里有人建议用 LISP 重写有人建议转 C#。最后真正落地的是一个 C 加 Qt 的方案CAD 侧用 ZRX 接口注册命令、遍历实体、改数据库界面侧用 Qt 做参数设置对话框。两周后这个工具上了生产线后续改需求也基本都是一两天的事。这个组合能跑通核心原因在于中望CAD 的 C 接口 ZRX 和 ObjectARX 在命令注册、实体操作、块表遍历上几乎一一对应官方 SDK 的结构对 ARX 开发者来说非常眼熟而 Qt 则补上了 CAD 自带 MFC 界面开发效率低这块短板。如果你有 C 基础或者在 AutoCAD 上写过 ObjectARX再从 LISP、.NET 这些路线里选C Qt 是平衡“能不能用”和“好不好维护”最好的答案。接下来几章我会按工程配置、最小命令、批量改图、Qt 嵌接、常见崩溃问题的顺序拆开讲。前面几章可以直接照着抄第五章的坑基本是每个项目都会踩一遍的。2. 先搭对工程ZRX插件结构、编译环境和最小的命令入口2.1 ZRX与ObjectARX的分工中望CAD到底是怎么加载你的代码的中望CAD的C插件机制叫 ZRX编出来的产物不是普通 DLL而是扩展名为 .zrx 的动态库。CAD 启动后通过 APPLOAD 之类的加载命令把 .zrx 文件读进进程然后调用里面的入口函数 zcrxEntryPoint。这个入口函数按消息类型分发模块加载时收到 kInitAppMsg卸载时收到 kUnloadAppMsg开发者在这两个分支里做命令注册和资源清理。ZRX 的类命名习惯和 ARX 很接近但前缀不同ZcDbDatabase 对应 AcDbDatabaseZcDbEntity 对应 AcDbEntityZcDbCircle 对应 AcDbCircle。acedSSGet、acedRegCmds 这些全局函数也基本按原样保留。如果你之前写过 ObjectARX刚转到中望CAD 时最需要适应的是头文件路径变了以及不少类名的前缀从 Ac 换成了 Zc。如果从零开始也不用慌主线就一条入口函数收到 kInitAppMsg 时用 addCommand 注册命令用户在命令行输命令触发你的静态回调函数回调里取当前工作数据库用块表和迭代器去改图。整个插件不维护自己的窗口消息循环CAD 的主循环已经跑在进程里你的代码只是挂进去的一批回调。2.2 建工程时要改的7项配置头文件目录、lib、字符集和输出后缀用 Visual Studio 建一个空白的 Win32 DLL 工程然后把中望CAD 的 SDK 引进来。如果你拿到的是官方向导生成的工程模板可以直接用但手动改配置能让你更清楚每一项目的是什么。配置项推荐值/做法说明头文件目录添加 SDK 的 inc 和 inc64 目录32 位与 64 位分支不要混用库文件目录添加 SDK 的 lib 目录与头文件位数保持一致字符集使用 Unicode 字符集CAD 内部是宽字符代码里字符串建议写 L输出文件名扩展名改为 .zrx否则 CAD 加载模块时找不到入口模块定义文件可不填ZRX 入口用 extern C 导出一般不需要 .defC 标准C14 或以上用 Qt6 则要 C17用 Qt5 则 C14 足够运行库多线程 DLL/MD与 CAD 主进程的 CRT 尽量一致减少崩溃概率这几个配置里最容易被忽略的是字符集。中望CAD 内部字符串全是宽字符如果工程误设成多字节acedSSGet 传入的过滤器字符串就会乱码命令名注册不上去加载后输命令毫无反应。下面是一个能在任意新建图纸里跑通的最小命令功能是“提示用户框选对象然后打印选择集数量”。这个代码量不大但涵盖了入口函数、命令注册、选择集调用和结果输出四个基本动作。2.3 10行代码跑通第一个命令注册、加载、调试三步走#include zrx/zrx.h #include zrx/zced.h #include zrx/zcdb.h static void cmdCountSelection() { ads_name ssName; // 五个参数选择方式、提示文字、关键字表、过滤器、结果集 int rc acedSSGet(nullptr, nullptr, nullptr, nullptr, ssName); if (rc ! RTNORM) { ads_printf(L取消选择。\n); return; } long count 0; acedSSLength(ssName, count); ads_printf(L选中的对象数量: %ld\n, count); acedSSFree(ssName); } extern C ZcRxApp::RetCode zcrxEntryPoint(ZcRxAppMsg msg, void* pkt) { if (msg ZcRxAppMsg::kInitAppMsg) { zcRegCmds-addCommand( LMY_TOOLS, LMYCOUNT, LMYCOUNT, ZCRX_CMD_MODAL, cmdCountSelection); } return ZcRxApp::RetCode::kRetOK; }acedSSGet 的五个参数分别控制选择方式点选、窗口、全选等、点选时的提示文字、关键字表、实体过滤器、最终选择集结果。这里全部传 nullptr代表让用户用默认框选方式。命令注册的五个参数里MY_TOOLS 是命令组名卸载时按组移除MYCOUNT 是命令显示名用户在命令行输入 MYCOUNT 即可触发。ZCRX_CMD_MODAL 表示命令执行期间不允许其他命令介入如果你的命令会弹对话框这里不要随意改成 ZCRX_CMD_TRANSPARENT。调试时直接把中望CAD 的 exe 路径填到“调试→启动程序”里比先开 CAD 再附加进程更省事。编译前再确认一下平台位数64 位 CAD 只能加载 64 位插件32 位 CAD 只能加载 32 位插件。我之前在 x64 环境编译完拷到 32 位系统上加载报“无法找到模块”查了半天才发现是 SDK 的 lib 目录选错了。3. 从命令到批量改图实体选择、遍历和属性修改的可执行方案3.1 用过滤器实现“选中指定图层上的圆”第一轮的 MYCOUNT 命令只能统计数量实际工具里更常用的是按条件筛选。ZRX 的过滤器是一个 resbuf 链表restype 表示 DXF 组码resval 存具体值。比如只选择图层 MY_CIRCLE 上的圆过滤器这样构造static void cmdCountFilteredCircle() { ads_name ssName; resbuf* filter new resbuf(); filter-restype 0; // 0 组码表示实体类型 filter-resval.rstring _tcsdup(LCIRCLE); filter-rbnext new resbuf(); filter-rbnext-restype 8; // 8 组码表示图层名 filter-rbnext-resval.rstring _tcsdup(LMY_CIRCLE); int rc acedSSGet(L_X, nullptr, nullptr, filter, ssName); if (rc RTNORM) { long count 0; acedSSLength(ssName, count); ads_printf(L图层 MY_CIRCLE 上的圆数量: %ld\n, count); } acedSSFree(ssName); // 释放过滤器链表 resbuf* p filter; while (p) { resbuf* next p-rbnext; free(p-resval.rstring); delete p; p next; } }这里的关键点是 _X 选择方式它表示“在整个图形空间中按过滤器搜索”不需要用户交互批量脚本里非常常用。过滤器链表的释放要自己处理每个 rstring 是 _tcsdup 出来的堆内存不能只 delete resbuf。如果你用 Qt也可以把过滤器里的字符串直接用 fromLocal8Bit 转成 char*但要保证源字符串是 UTF-8否则中文图层名会出问题。3.2 遍历模型空间把符合条件的圆改到指定图层比选择集更底层的做法是直接遍历模型空间块表记录。它的好处是不依赖用户交互适合“整图扫描 条件修改”的场景。下面这段代码会遍历模型空间里所有实体找到半径小于等于指定值的圆把它们挪到目标图层。static void moveCircleToLayer(const wchar_t* layerName, double maxRadius) { ZcDbDatabase* db zcHostAppServices()-workingDatabase(); if (!db) return; ZcDbObjectId layerId ZcDbObjectId::kNull; if (!findOrCreateLayer(db, layerName, layerId)) return; ZcDbBlockTable* bt nullptr; db-getBlockTable(bt, ZcDb::kForRead); if (!bt) return; ZcDbBlockTableRecord* btr nullptr; bt-getAt(L*Model_Space, btr, ZcDb::kForRead); bt-close(); ZcDbBlockTableRecordIterator* iter nullptr; btr-newIterator(iter); long changed 0; for (; !iter-done(); iter-step()) { ZcDbObjectId eid iter-objectId(); ZcDbEntity* ent nullptr; if (eid.openStatus(ent, ZcDb::kForWrite) ! eOk) continue; if (ent-isKindOf(ZcDbCircle::desc())) { ZcDbCircle* circle ZcDbCircle::cast(ent); if (circle-radius() maxRadius) { ent-setLayerId(layerId); changed; } } ent-close(); } delete iter; btr-close(); ads_printf(L已处理 %ld 个圆。\n, changed); }这段代码的运行逻辑是先取当前工作数据库拿到块表再取模型空间块表记录用迭代器遍历其中每个实体。每个实体打开时用 kForWrite因为要改图层属性判断类型后走下转型半径符合条件就 setLayerId。最后逐个 close迭代器也要 delete。很多新手漏掉 close 或 delete短时间跑没问题图纸一大就会内存暴涨。参数方面layerName 是目标图层名maxRadius 是半径阈值。如果你想让用户交互输入这两个值可以用 ads_getstring 和 ads_getreal 在命令行收集后面接 Qt 界面时这套函数也可以保留。3.3 封装的三个关键操作找图层、打开实体、关闭资源上面代码里调用的 findOrCreateLayer 是自封装函数。目标图层如果不存在setLayerId 会失败所以在批量修改前要先保证图层存在。这个函数可以统一处理“查询图层 新建图层”两个分支static bool findOrCreateLayer(ZcDbDatabase* db, const wchar_t* layerName, ZcDbObjectId outId) { ZcDbLayerTable* lt nullptr; db-getSymbolTable(lt, ZcDb::kForRead); if (lt-has(layerName)) { lt-getAt(layerName, outId); lt-close(); return true; } lt-close(); // 图层不存在以写模式重新打开图层表 db-getSymbolTable(lt, ZcDb::kForWrite); ZcDbLayerTableRecord* rec new ZcDbLayerTableRecord(); rec-setName(layerName); lt-add(rec); outId rec-objectId(); rec-close(); lt-close(); return true; }这个封装的要点在于查询用 kForRead创建用 kForWrite先读后写必须把读状态关闭再重开。lt-add(rec) 之后rec 的所有权移交给图层表不能再手动 delete。图层名重复时getAt 返回的 outId 可以直接用于后续 setLayerId。选择集方式和遍历方式的分工也要说清楚如果只处理用户选中的几十个对象用 3.1 的过滤器加 acedSSGet 更快如果要对整张图的某类对象做全量扫描就用 3.2 的块表遍历。实际项目里我一般两个都写UI 上让用户勾选“处理范围”。4. Qt界面嵌入中望CAD两种做法我只推荐先做第一种4.1 模态对话框是“简单但有效”的根基QDialog::exec()怎么做中望CAD 主进程是 MFC 程序默认没有 QApplication。直接用 Qt 的 QDialog 会崩溃第一步要先在插件里创建一份 QApplication。常见做法是写一个 ensure 函数用静态指针保证只创建一次。#include QApplication #include QDialog QApplication* ensureZcadQtApp() { static QApplication* app nullptr; if (!app) { int argc 0; app new QApplication(argc, nullptr); } return app; }这里 argc 传 0 而不是 main 函数里的参数是为了避免 Qt 去解析中望CAD 的命令行参数。创建完 QApplication 之后命令回调里就可以正常弹出对话框了static void cmdUiMoveCircle() { ensureZcadQtApp(); MyCircleDialog dlg; dlg.setWindowTitle(L移动小圆到目标图层); if (dlg.exec() ! QDialog::Accepted) return; moveCircleToLayer(dlg.layerName(), dlg.radius()); }dlg.exec() 是模态事件循环它会阻塞当前命令回调但 CAD 主窗口仍然响应重绘。对话框关闭后exec 返回代码继续执行 moveCircleToLayer。这个方式之所以稳是因为 Qt 的事件循环运行在自己的栈上不会破坏 CAD 主循环。但要注意对话框打开期间CAD 命令行处于等待状态所以不要让用户在对话框里二次触发 CAD 命令。4.2 停靠面板和浮动工具栏HWND嵌入的方案与代价有些工具需要常驻面板比如“实时显示选中图元坐标”或“图框目录树”。这种场景很多人第一反应是把 Qt 的 QWidget 停靠到 CAD 主窗口里。思路是把 Qt 窗口的 HWND 取出来用 SetParent 挂到 CAD 主窗口下再手动处理位置和大小。HWND hCad (HWND)zcHostAppServices()-mainWindow(); // 仅示意 SetParent((HWND)dlg.winId(), hCad); dlg.show();这段代码跑起来不难但后续问题非常多CAD 不跑 Qt 的事件循环Qt 窗口里的回车、Tab 切换、下拉框滚轮有时会失灵切换文档标签时CAD 会重排子窗口你的 Qt 面板位置要自己监听消息去纠正中望CAD 某些版本对子窗口的 Z 序有自己的管理逻辑面板偶尔会被其他窗口盖住。方案开发量稳定性适用场景模态 QDialog小高批量参数设置、向导式操作停靠 QWidget大中常驻工具面板、实时属性查看实际项目中我建议项目前期一律用模态对话框。只有当你验证过“常驻面板确实是业务刚需”之后再投入人力做 HWND 嵌入。先出一个能用的版本比一开始就追求“高级感”重要得多。4.3 UI只收集参数不要让UI直接改图很多二次开发新手第一次在 Qt 里点按钮调 acedCommandS 发命令结果 CAD 直接卡死或命令不执行。原因是 Qt 的槽函数运行在自己的事件循环里此时 CAD 的命令上下文未必就绪。更稳的写法是UI 只负责收集参数真正改图的是 3.2 里那个独立函数。class MyCircleDialog : public QDialog { Q_OBJECT public: QString layerName() const; double radius() const; private slots: void onApply(); private: QLineEdit* layerEdit; QDoubleSpinBox* radiusSpin; }; void MyCircleDialog::onApply() { if (layerEdit-text().trimmed().isEmpty()) { QMessageBox::warning(this, L提示, L图层名不能为空); return; } accept(); }对话框关闭后cmdUiMoveCircle 里再取 layerName() 和 radius()把参数传给 moveCircleToLayer。这样 UI 和 CAD 数据操作完全分离Qt 侧只依赖两个普通函数。如果你需要“点击应用后不关闭对话框实时预览效果”可以把 moveCircleToLayer 改成先收集选择集再执行并且用文档锁保护但不要让 UI 层直接拼命令字符串。5. 中望CAD二次开发避坑现象、原因、解决5.1 第二次加载时命令重复注册现象开发阶段反复 APPLOAD 和卸载插件某次加载后命令行提示“MYCOUNT 命令重复”或者同一个命令被执行两次。原因上一轮卸载时没有移除命令组。常见的情况是 zcrxEntryPoint 的 kUnloadAppMsg 分支没有写 removeGroup或者调试时进程异常退出导致卸载消息根本没跑。解决注册命令前先移除同组命令把这一步放在 kInitAppMsg 的最前面即使上次卸载失败也能兜底。if (msg ZcRxAppMsg::kInitAppMsg) { zcRegCmds-removeGroup(LMY_TOOLS); zcRegCmds-addCommand(/* 注册参数 */); }5.2 Qt的exec()弹出后CAD和对话框一起冻结现象对话框能弹出来但 CAD 主窗口完全不能拖动对话框内部的输入框也不刷新。原因QApplication 没有先创建或者对话框里触发了依赖 CAD 命令行的操作。更隐蔽的原因是进程里已经存在另一个 QApplication 实例比如其他 Qt 插件先创建了你再 new 一个直接崩。解决统一用 ensureZcadQtApp() 获取单例 QApplication对话框内不要用 acedCommand 系列函数exec 之前可以调用一次 processEvents() 让 Qt 的事件系统预热。5.3 改完实体属性屏幕不刷新导出却正常现象程序里读实体属性已经是新值但 CAD 图形区显示的还是原来的颜色或图层保存关闭重开后又正常。原因实体打开状态是 kForRead修改被静默忽略或者修改成功后没有通知视图刷新。解决实体打开时用 kForWrite批量修改完成后调用 zcRebuildAll() 或者发送一个图形已修改的协议消息。只改几个实体时可以逐实体调用 recordGraphicsModified()这个习惯也能避免后面做标注更新时翻车。5.4 卸载插件时崩溃特别是打开过Qt窗口以后现象用 APPLOAD 卸载 .zrxCAD 直接异常退出没有任何提示。原因Qt 窗口对象在卸载后仍被 Qt 事件系统引用或者 QApplication 被插件卸载动作提前销毁。中望CAD 主进程是 MFC不会正常接管 Qt 的资源清理。解决在 kUnloadAppMsg 分支里先删除所有顶层 Qt 窗口再 delete ensureZcadQtApp() 返回的指针最后返回 kRetOK。不要在静态析构函数里做 Qt 清理那一步的执行时机不受你控制。else if (msg ZcRxAppMsg::kUnloadAppMsg) { // 先回收所有已打开对话框和面板资源 QApplication* app ensureZcadQtApp(); delete app; }5.5 多文档切换时数据串场现象同时打开两张图在 A 图执行批量改图层B 图的实体也被改了。原因在 zcrxEntryPoint 初始化阶段缓存了 workingDatabase 指针之后所有命令都用这个缓存。中望CAD 是多文档每个文档有自己的数据库切换标签后缓存失效。解决每次命令回调里重新调用 zcHostAppServices()-workingDatabase()不要把数据库指针存到 static 全局变量。Qt 对话框里如果持有数据库引用也要在 exec 之前重新获取。6. 调试技巧和一条值得长期坚持的开发习惯6.1 用OutputDebugString解决“黑匣子”式排错CAD 插件调试最难受的是断点命中后整个进程就挂住了尤其涉及 Qt 事件循环时F10 单步反而会让问题更难复现。我一般会在关键的入口函数、实体遍历循环、对话框 accept 三个位置加日志输出用 OutputDebugString 写到系统调试通道然后开一个 DbgView 工具实时查看。#ifdef _DEBUG #define ZRX_LOG(msg) OutputDebugStringA(msg) #else #define ZRX_LOG(msg) ((void)0) #endif static void cmdUiMoveCircle() { ZRX_LOG([zrx] enter cmdUiMoveCircle\n); ensureZcadQtApp(); MyCircleDialog dlg; if (dlg.exec() ! QDialog::Accepted) { ZRX_LOG([zrx] dialog canceled\n); return; } ZRX_LOG([zrx] dialog accepted, apply move\n); moveCircleToLayer(dlg.layerName(), dlg.radius()); }Release 版里 ZRX_LOG 自动不编译不用手动删。碰到“对话框关闭后 CAD 崩溃”这种问题日志能帮你定位是 exec 返回后崩还是 moveCircleToLayer 内部崩比看调用栈快得多。6.2 核心算法与界面分离同一个命令既能命令行跑也能UI跑moveCircleToLayer 这个函数自始至终没有依赖 Qt 和 MFC它只接收宽字符串和 double 参数。基于这个封装同一个逻辑可以注册两个命令入口一个命令行版 MYMOVE用户用键盘输参数一个 UI 版 MYMOVEUI用 Qt 对话框收集参数。static void cmdMoveCircleFromCmdLine() { ZcGe::GeDouble radius; ads_getreal(L输入半径阈值, radius); moveCircleToLayer(LMY_CIRCLE, radius); }命令行版的价值不只是“多一个入口”它还是一个天然的回归测试工具。每次改完逻辑先命令行跑一遍再套 Qt 界面跑一遍能快速区分“算法崩了”和“界面崩了”。我交付前都会把这两个入口都测一遍再把命令行版留在文档里后续维护的人不需要打开 Qt 就能验证核心逻辑。这个习惯救过我很多次尤其是遇到那种“UI 一开就崩、命令行一切正常”的疑难杂症。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取