☰
中望CAD二次开发:C++搭配QT界面与扩展字典的落地实践
2026/10/13 7:09:16 网站建设 项目流程

简介:面向中望CAD二次开发初学者,这份示例工程采用C++语言并结合QT对话框,演示了如何在中望CAD环境中编写扩展程序并读取扩展字典。代码量不大,结构完整,侧重展示“界面交互—插件加载—字典读写”的常用链路;开发环境为VS2015、Qt 5.12.9、ZWCAD 2022中文版,示例同样易于迁移到AutoCAD平台,适合具备基础C++知识、想快速入门CAD插件开发的技术人员。包体内部共273个文件,解压后约206MB,其中包含134个动态链接库,配合lib导入库、exp导出文件与pdb调试符号,可以满足不同条件下的编译与运行;另有7个头文件和7个源文件构成可阅读的代码主体,ui文件描述QT界面布局,sln、vcxproj等组成完整VS解决方案,props、def等文件则用于配置生成选项与导出符号,目录结构与常见二次开发工程一致。目前这份示例已吸引2817人学习下载,虽然定位简单,但胜在直接可用;读者拿到后不仅能看到从零搭建QT界面到读取扩展字典的完整示例,还能借用现成的工程模板和编译配置,节省环境搭建时间,快速迁移到自己的实际项目中。

1. 中望CAD二次开发:C++配QT界面,扩展字典存数据,这套组合确实能落地

刚接手一个需在CAD里做参数化标注的小需求时,我最初也没想到最终会落在中望CAD平台上。原因很实际:某公司的正版化审查只认国产CAD,而设计院传来的图纸又多带自定义对象数据,标准DXF存不住。折腾两周后我做出来一套东西——用C++写一个加载到CAD内部的插件,用QT搭参数面板,用扩展字典把业务数据挂到图元上。整套方案在公司内部跑了三个月没崩过,也敢说“简单但有效,可用”。这套组合适合谁?适合被国产化替代逼着搞二次开发、又要快速出活的人;也适合刚接触ZRX架构、想知道QT界面怎么塞进CAD进程的开发者。下文直接按“环境搭建→最小插件→QT界面嵌入→扩展字典读写→踩坑与进阶”的顺序拆。

2. 选型与工程搭建:为什么是C++, ZRX和QT能共存于一个进程

2.1 中望CAD的二次开发接口体系:ZRX与dotnet的边界

中望CAD的C++二次开发接口叫ZRX(类似某国外CAD的ARX),本质上是一个基于MFC和C++的运行扩展机制。插件以模块(.zrx或.dll)形式被主程序加载,注册命令、监听事件、操作数据库。

我选C++而非dotnet,原因有三:第一,中望的ZRX接口在C++层暴露得最完整,类型转换和数据库操作更直接;第二,QT本身是C++框架,UI逻辑和CAD交互逻辑可以放在同一语言体系里,不用跨语言调用;第三,性能敏感的场景下C++写实体遍历和数学计算更稳。

工程结构上,我用的方案是:

  • 主模块:一个DLL工程,导出zrxEntryPoint作为入口
  • 界面模块:QT的Widget或Dialog类,封装成独立可复用的类
  • 数据模块:扩展字典的读写封装

2.2 搭建开发环境的三个关键步骤

步骤一:准备好头文件和库文件

中望CAD安装目录下有zwrx.dll、zwdatabase.dll等动态库,头文件在inc目录。QT这边我用的是5.15.2的MSVC版本。注意,QT的编译器和中望CAD插件必须一致,否则加载会挂。这里我用的是VS2019(v142)工具集。

// 头文件路径示例,需要在项目属性里做包含目录配置 #include "zrx.h" #include "zdbdict.h" #include "zdbxrecord.h" #include "zdbid.h"

中望的类命名以Z开头,看清头文件再动手:ZwDatabase对应数据库结构,ZdbObjectId对应对象ID,ZdbXrecord对应扩展记录。

步骤二:编写模块入口

// zrxEntryPoint 是整个插件被CAD加载时的第一入口 extern "C" int zrxEntryPoint() { // 初始化自定义命令注册表 zrxInitApp(); // 注册一个名为 "ZWQTDEMO" 的命令 accdRegCmds()->addCommand("ZWQTDEMO_GROUP", L"ZWQTDEMO", L"ZWQTDEMO", ACRX_CMD_MODAL, &zwQtDemoCommand); return ZRX_OK; } extern "C" int zrxExitPoint() { // 注销命令,释放资源 accdRegCmds()->removeGroup("ZWQTDEMO_GROUP"); return ZRX_OK; }

这段代码的作用是:CAD加载插件时执行zrxEntryPoint,注册一个叫ZWQTDEMO的命令,用户输命令时触发zwQtDemoCommand函数(这个函数还没定义,定义在QT界面模块里)。参数说明:ACRX_CMD_MODAL表示命令是模态的,即命令执行期间不允许其他CAD操作;若改成ACRX_CMD_TRANSPARENT,则可以在其他命令执行中嵌套调用。

步骤三:配置QT的工程文件

// QtDemo.pro 关键配置 QT += widgets CONFIG += release DEFINES += ZRX_QT_DLL INCLUDEPATH += "C:/Program Files/ZwCAD/inc" LIBS += "C:/Program Files/ZwCAD/zwrx.lib"

这里有个坑:QT的moc预处理器要求所有带Q_OBJECT宏的类必须在头文件里声明,否则生成不了元对象代码。我一般把QT界面类单独放一个QtPanel.h/QtPanel.cpp,避免和ZRX的zrxEntryPoint文件混在一起,省得moc误处理CAD的头文件。

2.3 做个最简单的“能加载”Demo验证环境

源码敲完先别急着写功能,先去CAD里敲APPLOAD命令加载这个DLL。加载成功会有提示信息,加载失败会弹对话框说明原因。验证命令是否注册成功,只要在命令行输入ZWQTDEMO,能弹出一个空的QT窗口,说明整个链路已经通了。这一步走通,剩下就是往窗口里填东西的事。

工程里常见的错误是zrxEntryPoint里的函数名导出符号被C++编译器做名称修饰,导致CAD找不到入口。解决办法在extern "C"之外,还要确保__declspec(dllexport)被正确应用,或者在.def文件里显式导出函数名。

3. 把QT界面嵌入CAD主窗口:句柄、消息循环和模态控制

3.1 用QWindow包装CAD窗口句柄实现原生嵌入

CAD本身是Windows原生程序,而QT在Windows上也有原生的窗口句柄(HWND)。把QT界面嵌入CAD主窗口有两个方案:

方案一:独立对话框(非模态),在CAD里显示为顶层窗口,不跟CAD主窗口发生父子关系。摆弄起来容易,但用户切换CAD文档或最小化时,对话框会丢焦点。

方案二:子窗口嵌入,把QT界面作为CAD主窗口的child窗口。这个方案的难点是获取CAD主窗口句柄,以及处理好窗口尺寸变化时的同步。

我采用的是方案二,关键代码长这样:

// 获取CAD主窗口句柄 HWND getCadMainWindow() { // 中望CAD提供一个全局函数获取应用主窗口 // 常见做法是通过ZwHostApp的mainWnd()接口获取 return (HWND)ZwHostApp::mainWnd(); } // 把QT窗口嵌入CAD void embedQtWidget(QWidget* panel) { HWND cadHwnd = getCadMainWindow(); if (!cadHwnd) return; // 将QT Widget的winId取出来,设置为CAD的子窗口 HWND qtHwnd = (HWND)panel->winId(); SetParent(qtHwnd, cadHwnd); SetWindowLong(qtHwnd, GWL_STYLE, WS_CHILD | WS_VISIBLE); // 调整大小 RECT rect; GetClientRect(cadHwnd, &rect); SetWindowPos(qtHwnd, NULL, 0, 0, rect.right / 3, rect.bottom, SWP_SHOWWINDOW); }

说明一下:panel->winId()会强制QT创建原生窗口,这之后panel就同时存在于QT的世界和Windows窗口系统里。SetWindowLong把窗口样式改成子窗口后,QT自身的布局系统依然有效,因为QT的事件循环还在处理这个消息。

参数表:

函数/参数作用注意事项
ZwHostApp::mainWnd()获取CAD主窗口句柄只能在CAD加载插件后调用,初始化前调用返回空
SetParent()设置父子窗口关系调用后需要在QT侧调用show()保持界面可见
GWL_STYLE修改窗口样式保留WS_CLIPSIBLINGS可避免绘制闪烁

3.2 事件循环与CAD互操作:命令必须回归ZRX线程

把窗口嵌入进去之后,重要的一件事是——QT的事件循环不能抢CAD的线程控制权。

CAD是一个单线程的消息泵程序,所有数据库操作必须在CAD的主线程里执行。QT界面上按钮的clicked信号默认是在QT的事件循环线程里执行的,这个线程是QT创建的,不是CAD的主线程,直接操作CAD数据库会导致崩溃。

处理办法是把CAD操作封装成命令,再通过CAD的命令队列调度。我实现了一个线程间跳转的工具函数:

// 将UI线程的请求转发到CAD主线程执行 void postToCadThread(std::function<void()> task) { // 使用acedPostCommand 或 ZwGe::postCommand 投递一个自定义命令 // 这里主要用acedPostCommand模拟命令行输入 QString cmdSeq = QString("ZWQT_EXEC_CB %1").arg(reinterpret_cast<qulonglong>(&task)); acedPostCommand(cmdSeq.toStdWString().c_str()); } static void zwQtExecCallback() { // 在CAD主线程中取出函数指针并执行 extern void* g_currentTaskPtr; if (g_currentTaskPtr) { std::function<void()>* task = reinterpret_cast<std::function<void()>*>(g_currentTaskPtr); (*task)(); delete task; g_currentTaskPtr = nullptr; } }

这段代码的逻辑是:QT线程往CAD投递一条自定义命令字符串,CAD主线程执行该命令时,从全局变量取出函数对象执行。需要说明两点:一是acedPostCommand是把字符串当作CAD命令行输入处理的,所以命令名只能用合法字符;二是std::function<void()>里捕获的上下文在跨线程时必须保证生命周期有效,所以这里用了new在堆上分配函数对象。

3.3 我踩过的界面卡死经历:模态对话框和CAD冲突

有一次我在QT界面里直接调QMessageBox::exec()弹提示框,结果CAD整个界面白屏。原因是exec()启动了嵌套消息循环,而这个循环和CAD内部的消息循环彼此竞争,焦点来回跳,最后界面失去响应。后来我统一改用QTimer::singleShot(0, ...)延后处理,或者把提示打到CAD的命令行里,界面就好多了。

真实场景里,能用非模态就用非模态,能用命令行输出就用命令行输出。CAD环境里,原生窗口交互越少越稳。

4. 扩展字典实战:把业务数据绑定到图元和数据库

4.1 扩展字典是什么,和XData的区别

CAD图元自带的扩展数据(XData)有16MB限制且只支持DXF注册应用名下的数据,存储结构偏平。扩展字典(Extension Dictionary)则是一个挂在图元或数据库根节点下的无名字典对象,里面可以存储任意数量的ZdbObject派生对象,包括ZdbXrecord(扩展记录)这种键值对容器。

实际差异我用一个表格说清楚:

对比项XData(扩展数据)扩展字典(Extension Dictionary)
容量限制16KB左右无硬性限制(受内存和磁盘)
数据类型只能存DXF标准类型可挂任意Zdb对象,自定义类也可
是否跟随实体是是
扩展记录兼容性老图纸通用中望、某些国外CAD支持
业务场景例如存储简单标记例如存储多组参数表、嵌套列表

需要特别注意:扩展字典是按需创建的,图元默认没有,必须先调用createExtensionDictionary()创建并获取字典ID。

4.2 用ZdbXrecord写入和读取业务数据

下面这组代码封装了扩展字典的读写,核心是操作ZdbXrecord的at()方法存储键值。

// 写入:给指定实体ID挂一组键值数据 bool writeDictData(const ZdbObjectId& entId, const QString& key, const QVariant& value) { // 1. 打开实体,获取或创建扩展字典 ZdbEntity* pEnt = nullptr; if (acdbOpenObject(pEnt, entId, Zdb::kForWrite) != ZRX_OK) return false; ZdbObjectId dictId; if (!pEnt->hasExtensionDictionary()) { dictId = pEnt->createExtensionDictionary(); pEnt->close(); } else { dictId = pEnt->extensionDictionary(); pEnt->close(); } // 2. 打开字典,查找或创建名为 WELLDATA 的扩展记录 ZdbDictionary* pDict = nullptr; if (acdbOpenObject(pDict, dictId, Zdb::kForWrite) != ZRX_OK) return false; ZdbObjectId recId; if (pDict->has(L"WELLDATA")) { recId = pDict->getAt(L"WELLDATA"); } else { recId = pDict->setAt(L"WELLDATA", new ZdbXrecord()); } pDict->close(); // 3. 打开扩展记录,写入键值 ZdbXrecord* pRec = nullptr; if (acdbOpenObject(pRec, recId, Zdb::kForWrite) != ZRX_OK) return false; // 清空原数据再写,避免残留 pRec->clear(); // 注意:ZdbXrecord的at()方法支持链式调用 pRec->at(L"KEY") = value.toString().toStdWString().c_str(); pRec->at(L"VALUE_COUNT") = value.toInt(); pRec->close(); return true; }

逻辑说明:第一步的createExtensionDictionary()只在第一次需要调用;如果实体已有字典,直接获取ID即可。第二步的setAt()相当于往字典里塞子对象,如果WELLDATA这个条目已经存在,则返回原有记录的ID,不会重复创建。第三步是关键,ZdbXrecord用at()返回一个可赋值的代理对象,重载了赋值运算符,写不同类型的数据会走不同的存储分支。

读取反过来,步骤就是“打开实体→获取字典→找到记录→遍历键值”:

// 读取:从实体扩展字典中读出某个键的值 QVariant readDictData(const ZdbObjectId& entId, const QString& key) { ZdbEntity* pEnt = nullptr; if (acdbOpenObject(pEnt, entId, Zdb::kForRead) != ZRX_OK) return QVariant(); ZdbObjectId dictId = pEnt->extensionDictionary(); pEnt->close(); if (dictId.isNull()) return QVariant(); ZdbDictionary* pDict = nullptr; if (acdbOpenObject(pDict, dictId, Zdb::kForRead) != ZRX_OK) return QVariant(); if (!pDict->has(L"WELLDATA")) { pDict->close(); return QVariant(); } ZdbObjectId recId = pDict->getAt(L"WELLDATA"); pDict->close(); ZdbXrecord* pRec = nullptr; if (acdbOpenObject(pRec, recId, Zdb::kForRead) != ZRX_OK) return QVariant(); // 遍历查找指定的键 ZdbResbuf* pBuf = pRec->data(); QVariant result; while (pBuf) { if (pBuf->restype == Zdb::kDxfText) { QString strVal = QString::fromWCharArray(pBuf->resval.wstr); if (strVal == key && pBuf->next) { // 下一个缓冲区的值就是我们要找的值 if (pBuf->next->restype == Zdb::kDxfReal) { result = pBuf->next->resval.rpoint?->x; } else if (pBuf->next->restype == Zdb::kDxfInt32) { result = pBuf->next->resval.rint; } else { result = QString::fromWCharArray(pBuf->next->resval.wstr); } break; } } pBuf = pBuf->next; } pRec->close(); return result; }

这段代码用了ZdbResbuf的链表结构来遍历扩展记录内容。restype是数据类型标记,kDxfText表示宽字符串。每次读到键匹配时,再看下一个节点的类型去取出对应的数值或字符串。这个判定链写起来略笨,但兼容性最好,ZRX的ZdbXrecord没有直接提供getAt的公开重载,链表遍历是通法。

4.3 扩展字典的跨图纸迁移和保存

扩展字典是随图元或数据库持久化的,保存DWG时自动写入。但有两个注意点:

第一,同一张图里的实体复制(Ctrl+C/V)会带上扩展字典,但跨图纸复制,如果目标图纸没有注册对应的自定义类,扩展记录可能直接丢失或变成不可读。解决办法是做一个导出/导入工具函数,把扩展记录转成普通实体属性或者文件输出。

第二,数据库根节点下也可以挂扩展字典,存储全图的业务信息。我常用它存参数模板:

void writeDbRootData(ZwDatabase* db, const QString& key, const QVariant& val) { // 数据库对象本身也有扩展字典 ZdbObjectId dictId = db->extensionDictionary(); if (dictId.isNull()) { dictId = db->createExtensionDictionary(); } // ... 后续逻辑与实体挂扩展字典完全一致 }

5. 避坑指南:中望CAD二次开发常见问题与排查方法

5.1 加载DLL后提示“无法找到入口点”

现象:APPLOAD加载时报错,提示找不到zrxEntryPoint导出函数。

原因:工程属性里的导出定义不一致。要么是代码里没有加extern "C",要么是编译器做了名称修饰;也可能是工程配置了_DLL宏但实际链接了静态CRT,符号表对不上。

解决:检查三个位置:第一,zrxEntryPoint函数名前面确保有extern "C"修饰;第二,项目链接器设置里把“导出”方式改为.def文件方式,在里面显式写EXPORTS zrxEntryPoint;第三,若用VS,确认运行时库选择的是/MD而不是/MT,因为QT和CAD的动态库都依赖动态CRT。

5.2 QT窗口嵌入后CAD菜单点击没响应

现象:窗口嵌入成功,按钮也能点,但CAD自己的菜单栏点了完全没反应。

原因:SetWindowLong修改样式后,CAD主窗口的焦点管理被破坏;多半是嵌入时没有设置WS_EX_CONTROLPARENT或把窗口置为了顶层样式。

解决:嵌入之前,把QT窗口先设为顶层窗口,再设置父窗口。关键顺序是先show()后SetParent,否则QT内部认为窗口不可见,事件处理会跳过。另外,嵌入后调用SetFocus回CAD主窗口,避免焦点停在QT控件上。

5.3 扩展字典写的值,关图纸再开变成空

现象:往实体扩展字典里写了几组数值,Save后再打开,读出来全为空。

原因:没有把扩展记录里的缓冲数据通过ZdbXrecord::data()赋值回去。at()赋值操作只在内存里改了一个包装器,需要调用setData()或setFromResbuf()把整个resbuf链条回填进记录对象。

解决:写入时,把键值组装成一个ZdbResbuf链表,这个链表记录了从kDxfText键到值数据的所有节点,然后用pRec->setData(headBuf)整体写入。不要依赖at()多次调用的累积效果。

5.4 自定义命令里的中文参数出现乱码

现象:在QT界面里输入中文存进扩展字典,重新打开读出来变成问号。

原因:中望CAD的内部字符编码是UTF-16,但有些接口函数接收char*多字节编码;如果用了QString::toStdString(),默认是UTF-8,两者不一致,写进去就是乱码。

解决:统一用QString::toStdWString().c_str()传给ZRX接口,读取时也用QString::fromWCharArray()转回来。涉及命令字符串时,用acedPostCommand传参也必须是宽字符。

5.5 QT的Q_OBJECT宏与ZRX头文件冲突

现象:编译时报告一堆与zrxlua.h相关的宏冲突错误,尤其是slot和signals被其他头文件宏替换掉。

原因:ZRX头文件里某些不得不用signals或slots作为标识符(比如类声明),而QT的Q_OBJECT会展开这些标识符,导致编译错误。

解决:把包含ZRX头文件的#include语句放到QT的#include之前,并且用#undef slots、#undef signals包围ZRX的头文件引用;或者在QT的.pro文件里添加DEFINES += Q_MOC_RUN限制moc对ZRX代码的处理。

6. 进阶技巧:把扩展记录封装成统一的KV存储工具

做到这里,基础链路已经通了。再往上走一步,我习惯把扩展字典的读写封装成一套带默认值和增量更新的工具类,这样业务方调用时不用关心底层是ZdbXrecord还是字典ID,只要传入实体、键名和值就行。

这层封装要考虑三个边界:实体是否有扩展字典、记录是否已存在、写入时是覆盖还是合并。我提供了一个带版本号的键值对协议:

struct KvEntityData { int schemaVersion = 1; // 协议版本号,便于后续升级 QMap<QString, QVariant> kvMap; };

写入时把所有数据打包进一个ZdbXrecord,在第一条记录位置放一个版本号标记。读取时先读版本,再决定解析策略。这样后续加字段、改字段类型都不会推倒重来,老图纸读出来也能按版本走兼容分支。

我另外加的一个小习惯是:每次操作完扩展记录之后,强制pRec->close(),然后调一下db->audit()(仅调试版本)。我在一次大批量写入时漏了关闭对象,导致打开句柄泄漏,CAD在点了20次保存后才开始抽风——后来每轮循环结束都关闭对象,问题就消失了。从那以后我每次写扩展字典都强制走一遍“打开→完整读写→已读关闭”,并且把调试打印留在读取路径上,数据不对立刻能看到。这套习惯一直帮我省了不少排查时间,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询