ObjectARX自定义实体开发:从绘制、夹点到对象捕捉的完整实现

发布时间:2026/10/19 4:13:30
ObjectARX自定义实体开发:从绘制、夹点到对象捕捉的完整实现
简介ObjectARX自定义实体简单实例是一套面向AutoCAD二次开发者的C示例工程聚焦ObjectARX自定义实体的核心开发流程。其中以一条自定义直线实体为主线完整演示了实体类继承、DXF/DWG数据读写、worldDraw屏幕绘制、数据库注册以及扩展命令调用等关键环节适合具备一定C基础、希望深入ARX对象机制的CAD开发者参考。压缩包共70个文件包含h/cpp源码、dsp/dsw/rc等Visual C工程文件、arx/dbx编译输出及obj/pdb中间数据整体约5.08MB工程结构清晰。内容预览显示资源内含TestCmd与TestEntity两个工程便于对照学习普通命令扩展与自定义实体实现。编译后可在AutoCAD中加载调用直观验证自定义实体的创建、保存与显示效果。已有1162人学习下载是一份可运行、可调试的自定义实体入门范本。1. 自定义实体不是玄学对象捕捉、夹点、数据一起写进 CADObjectARX 自定义实体听着像深水区核心思路其实就一句话在 VC 里用 C 从 AcDbEntity 派生一个类重写几个虚函数。拿 LISP 画圆很容易但要在图纸里放一个“能记住业务数据、能被对象捕捉、能拖着夹点改”的图形脚本就使不上劲了。这个实例拿自定义圆当靶子从环境配置一路拆到 worldDraw、夹点、对象捕捉的实现最后落到怎么加自己的业务字段。适合正在啃 ARX 的 C 开发者也适合用脚本画图到瓶颈、想转入底层二次开发的熟手。2. 环境搭建与工程骨架VC 配置决定你今晚能不能跑起来ARX 开发里最耗时间的阶段往往不是写实体代码而是第一次把工程配明白。身边不少人在这个阶段卡过代码没毛病结果在“加载失败”上折腾一下午。这一章先把工程骨架拆开把容易踩的坑提前标出来。2.1 版本匹配SDK、Visual Studio 和 CAD 是三位一体ObjectARX 的 SDK 包跟着 CAD 主版本走主版本不匹配接口签名和导入库就对不上。SDK 文档里会写明它要求的 VC 工具集版本所以装环境第一件事不是写代码而是确认三件事CAD 的版本号、SDK 的版本号、VS 的版本。三者必须同属一个代系位数也必须一致——现在的 CAD 主流是 64 位你的 VC 工程就选 x64。除了版本附加包含目录要指向 SDK 的 inc 文件夹附加库目录要指向 lib 文件夹链接输入的导入库要和目标 CAD 版本配套。这些配置在工程属性里逐个核对别指望默认值能用。链接阶段常见的“找不到 acdbXX.lib”类错误基本都是这里路径指错了。提示字符集建议用 Unicode运行库选择 /MD多线程 DLL这两项和 CAD 进程保持一致能省掉后续一大半运行时问题。2.2 最小的 acrxEntryPoint加载和卸载的生命周期ARX 模块不像普通 DLL 那样导出一堆业务函数它只需要一个入口acrxEntryPoint。CAD 加载模块时按消息调用它kInitAppMsg 是初始化kUnloadAppMsg 是卸载。下面是一个最小骨架// MyCircleApp.cpp #include MyCircleEntity.h #include acedCmdNm.h static void createMyCircle(); extern C AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: // 解锁应用并声明支持多文档CAD 要求这两个调用必须在加载期完成 acrxDynamicLinker-unlockApplication(pkt); acrxRegisterAppMDIAware(pkt); // 注册实体类打开 DWG 时 CAD 才能按类名实例化 MyCircleEntity::rxInit(); // 注册命令组命令行输入 MYCIRCLE 即可触发 createMyCircle acedRegCmds-addCommand(LMY_CIRCLE_COMMANDS, LMYCIRCLE, LMYCIRCLE, ACRX_CMD_MODAL, createMyCircle); break; case AcRx::kUnloadAppMsg: // 卸载命令组和类否则重新加载时会提示重复定义 acedRegCmds-removeGroup(LMY_CIRCLE_COMMANDS); deleteAcRxClass(MyCircleEntity::desc()); break; } return AcRx::kRetOK; }acrxEntryPoint 里最需要注意的是 kInitAppMsg 分支中的两个固定调用unlockApplication 解锁模块acrxRegisterAppMDIAware 注册多文档支持。少调一个某些版本里就会出现卸载后模块句柄残留、二次加载失败的怪现象。addCommand 的三个字符串参数分别是命令组名、全局命令名、本地命令名第三个支持本地化但为了命令行输入方便通常建议和第二个保持一致。实体类的 rxInit 在这个阶段调用是为了让 CAD 的运行时类系统认识 MyCircleEntity。卸载时 deleteAcRxClass 则负责把这个类从运行库里摘掉忘了它下一次 APPLOAD 大概率会报“类已存在”之类的错误。2.3 命令函数、块表与模型空间第一行“用户可见”的代码有了入口还得有实际创建实体的命令函数。自定义实体和普通实体一样要放进块表记录里才算真正进入图纸static void createMyCircle() { AcDbBlockTable* pBlockTable nullptr; // 拿到当前工作数据库的块表只读打开 acdbHostApplicationServices()-workingDatabase() -getBlockTable(pBlockTable, AcDb::kForRead); AcDbBlockTableRecord* pRecord nullptr; // 从块表里取出模型空间这次要写权限 if (pBlockTable-getAt(ACDB_MODEL_SPACE, pRecord, AcDb::kForWrite) ! Acad::eOk) { pBlockTable-close(); return; } pBlockTable-close(); // 创建自定义实体并初始化数据 MyCircleEntity* pEnt new MyCircleEntity(); pEnt-setCenter(AcGePoint3d(0, 0, 0)); pEnt-setRadius(100.0); pEnt-setColorIndex(1); AcDbObjectId entId; pRecord-appendAcDbEntity(entId, pEnt); pRecord-close(); pEnt-close(); // 刷新当前视图让实体立刻显示 acedUpdateDisplay(); }这段代码的打开顺序是固定的先按读锁打开块表再从块表里按写锁取出模型空间记录然后关闭块表。CAD 的对象模型里块表是“橱柜”块表记录才是“抽屉”实体只能住进抽屉里。getAt 不加 kForWriteappendAcDbEntity 会失败append 之后pEnt 的所有权交给数据库不能再 delete 它只能 close。工程里文件分工大致如下文件职责MyCircleEntity.h / cpp自定义实体的类声明与实现MyCircleApp.cppacrxEntryPoint 入口与命令函数MyCircleApp.def导出 acrxEntryPoint 符号MyCircleApp.vcxprojVC 工程配置def 文件虽然内容很少但少了它链接器不会导出 acrxEntryPointCAD 加载时直接报“入口点不存在”。很多人在这一步翻车检查顺序一般是def 导出 → 导入库 → 位数。完成这些一个能被 CAD 接受的 ARX 模块就有了骨架。命令框里 APPLOAD 加载 arx输入 MYCIRCLE模型空间就会出现一个红色圆。但这个圆目前还只是“看起来像圆”的白板下一步要让 CAD 真正认识它的数据结构。3. 类设计与注册机制让 AutoCAD 认识你的新实体实体类设计是整个 ARX 二次开发里最核心的一步也是“接口多、不知道重写哪些”的重灾区。先看类骨架再把持久化机制讲透。3.1 从 AcDbEntity 派生把这几个虚函数当成工作清单自定义实体必须有且只有一个父类AcDbEntity。派生之后CAD 才会把对象捕捉、夹点、打印这些机制自动路由给你。但机制归机制具体行为要靠自己实现。// MyCircleEntity.h #pragma once class MyCircleEntity : public AcDbEntity { public: ACRX_DECLARE_MEMBERS(MyCircleEntity); MyCircleEntity(); virtual ~MyCircleEntity(); // 业务数据接口 void setCenter(const AcGePoint3d pt); AcGePoint3d center() const; void setRadius(double r); double radius() const; // 绘制 virtual Acad::ErrorStatus worldDraw(AcGiWorldDraw* pWd) override; // 数据存取 virtual Acad::ErrorStatus dxfOutFields(AcDbDxfFiler* pFiler) const override; virtual Acad::ErrorStatus dxfInFields(AcDbDxfFiler* pFiler) override; // 交互夹点 virtual Acad::ErrorStatus subGetGripPoints(AcDbGripDataArray gripDataArray, double currentViewScale, int gripSize, AcDbIntArray curGripAppData, AcDbIntArray curGripSubObj) const override; virtual Acad::ErrorStatus subMoveGripPointsAt(const AcDbIntArray gripArray, const AcGeVector3d offset, const AcDbGripDataArray gripDataArray) override; // 交互对象捕捉 virtual Acad::ErrorStatus subGetOsnapPoints(AcDbOsnapType osnapType, double gsSelectionMark, const AcGePoint3d pickPoint, const AcGePoint3d lastPoint, const AcGeMatrix3d viewXform, AcGePoint3dArray points, AcDbIntArray geomIds) const override; private: AcGePoint3d m_center; // 圆心 double m_radius; // 半径 };这套虚函数按职责分三组。形状绘制组负责在屏幕上生成图元数据存取组负责把 m_center、m_radius 写进 DWG 的 DXF 流交互组负责夹点、对象捕捉这些“CAD 命令要和你对话”的入口。ACRX_DECLARE_MEMBERS 在类里声明静态 desc() 和运行时类相关方法cpp 里再用 ACRX_DEFINE_MEMBERS 展开这套宏是 CAD 运行时类系统识别实体的关键。3.2 DXF 组码设计数据存进 DWG 的唯一通道自定义实体和普通图元最大的区别在于它要带着自己的私有数据存盘。DWG 内部按 DXF 组码记录对象实体保存时调用 dxfOutFields打开时调用 dxfInFields。这两个函数决定了你的数据能不能原样进出图纸Acad::ErrorStatus MyCircleEntity::dxfOutFields(AcDbDxfFiler* pFiler) const { // 先让父类把公共字段句柄、图层等写出去 Acad::ErrorStatus es AcDbEntity::dxfOutFields(pFiler); if (es ! Acad::eOk) return es; // 100 组码是子类标记CAD 靠它识别这是哪个类 pFiler-writeItem(AcDb::kDxfXdSubclass, LMyCircleEntity); // 10 组码写圆心40 组码写半径 pFiler-writeItem(AcDb::kDxfXCoordinate, m_center); pFiler-writeItem(AcDb::kDxfReal, m_radius); return Acad::eOk; }dxfInFields 是逆过程先读子类标记确认是自己再读回圆心和半径。Acad::ErrorStatus MyCircleEntity::dxfInFields(AcDbDxfFiler* pFiler) { Acad::ErrorStatus es AcDbEntity::dxfInFields(pFiler); if (es ! Acad::eOk) return es; // 检查子类标记是否为 MyCircleEntity是才开始读数据字段 if (!pFiler-atSubclassData(LMyCircleEntity)) return Acad::eMissingRecord; while (pFiler-nextItem() ! AcDb::kDxfEnd) { switch (pFiler-item().type()) { case AcDb::kDxfXCoordinate: m_center pFiler-item().getPoint3d(); break; case AcDb::kDxfReal: m_radius pFiler-item().getDouble(); break; default: break; } } return Acad::eOk; }组码选择有讲究10 在 DXF 协议里约定为一组点坐标40 约定为实数或距离这是所有 CAD 图元共用的语义如果给自定义实体乱编组码别的软件读不了CAD 自己也可能在后续版本里不兼容。写子类标记时字符串必须和 ACRX_DEFINE_MEMBERS 里的名称一致——哪怕差一个字母打开图纸时 CAD 都无法实例化只能退化成代理实体。3.3 类工厂rxInit 与 desc() 到底做了什么自定义实体能被实例化靠的是运行时类系统而不是普通构造函数。cpp 文件里 ACRX_DEFINE_MEMBERS 展开后会生成 desc()、cast() 等静态方法rxInit 会把“类名到创建函数”的映射注册进 CAD 的运行时字典。当 CAD 打开 DWG 遇到 100 组码里的 MyCircleEntity它就去这个字典里查创建函数查到就 new 一个出来查不到就显示为代理对象属性数据全部丢失。这也就是为什么第 2 章在 kInitAppMsg 里调用 rxInit、在 kUnloadAppMsg 里调用 deleteAcRxClass。加载顺序错了或者卸载没删干净轻则文件打不开重则当前会话直接崩溃。类工厂是实体的“身份证”不是可有可无的形式。到这一步CAD 已经知道怎么创建 MyCircleEntity、怎么存取它的数据但屏幕上看起来它还只是一块空白因为绘制和交互部分还没写。4. 核心实现拆解自定义圆从绘制到交互的完整链路实体的“生命感”来自三个环节绘制、夹点编辑、对象捕捉。这一章按一条命令在 CAD 内部跑过的顺序逐个拆开这三个实现。4.1 worldDraw真正把图形画到屏幕上的函数视图刷新、缩放、移动时CAD 会对每个可见实体调用 worldDraw。入参是 AcGiWorldDraw 指针所有屏幕上的线、弧、填充都要经由它的 geometry() 画出来。Acad::ErrorStatus MyCircleEntity::worldDraw(AcGiWorldDraw* pWd) { if (pWd nullptr) return Acad::eInvalidInput; // 设置子实体颜色1 对应 CAD 调色板中的红色 pWd-subEntityTraits().setColor(1); // 用底层几何接口绘制整圆圆心、半径、起始角、终止角 // 角度单位是弧度0 到 2π 正好一圈 AcGePoint3d ptCenter m_center; double r m_radius; pWd-geometry().circArc(ptCenter, r, 0.0, 6.283185307179586); return Acad::eOk; }circArc 是 AcGiGeometry 提供的圆弧绘制函数参数依次是圆心、半径、起始弧度、终止弧度。把它当画圆工具是因为它足够底层能演示“任何曲线都能用几何接口拼出来”的思路。要画带缺口的标注圆只需要改终止角度。注意 worldDraw 返回 eOk 表示绘制完毕如果返回 eNotImplementedCAD 会尝试调用默认绘制路径容易造成重复绘制一般不建议偷懒。4.2 夹点编辑从 subGetGripPoints 到 subMoveGripPointsAt夹点是选中实体后出现的小方块。自定义实体默认没有夹点需要自己提供subGetGripPoints 返回夹点位置subMoveGripPointsAt 在用户拖动夹点时修改实体数据。Acad::ErrorStatus MyCircleEntity::subGetGripPoints(AcDbGripDataArray gripDataArray, double currentViewScale, int gripSize, AcDbIntArray curGripAppData, AcDbIntArray curGripSubObj) const { // 清空默认夹点换成我们自己的 gripDataArray.setLogicalLength(0); // 0 号夹点圆心 AcDbGripData* pGrip new AcDbGripData(); pGrip-setGripPoint(m_center); gripDataArray.append(pGrip); // 1~4 号夹点圆的四个象限点 AcGePoint3dArray pts; pts.append(AcGePoint3d(m_center.x m_radius, m_center.y, 0.0)); pts.append(AcGePoint3d(m_center.x - m_radius, m_center.y, 0.0)); pts.append(AcGePoint3d(m_center.x, m_center.y m_radius, 0.0)); pts.append(AcGePoint3d(m_center.x, m_center.y - m_radius, 0.0)); for (int i 0; i pts.length(); i) { AcDbGripData* pQuad new AcDbGripData(); pQuad-setGripPoint(pts[i]); gripDataArray.append(pQuad); } return Acad::eOk; }这段代码的核心是“夹点也是数据”。0 号夹点绑定圆心1~4 号夹点绑定四个象限点。选中实体时CAD 读这些点来画夹点方块真正改数据发生在拖拽之后。AcDbGripData 对象由 CAD 负责释放所以 new 出来直接 append 即可不要手动 delete。拖拽由 subMoveGripPointsAt 接手Acad::ErrorStatus MyCircleEntity::subMoveGripPointsAt(const AcDbIntArray gripArray, const AcGeVector3d offset, const AcDbGripDataArray gripDataArray) { for (int i 0; i gripArray.length(); i) { int idx gripArray[i]; // 从夹点数组里拿到拖动前的点加上偏移得到新点 AcGePoint3d newPt gripDataArray[idx]-gripPoint() offset; if (idx 0) { // 圆心被拖动整体平移 m_center newPt; } else if (idx 1 idx 4) { // 象限点被拖动通过距离重新计算半径 m_radius (newPt - m_center).length(); } } return Acad::eOk; }gripArray 是用户这次实际拖动的夹点索引可能同时拖多个offset 是本次鼠标移动向量。这里把圆心夹点和象限夹点分开处理圆心被拖只改 m_center象限点被拖用新点到圆心的距离作半径。这种“按索引约定行为”的方式最直接适合做原型正式项目里更稳的做法是通过 AcDbGripData 的 appData 给每个夹点附加身份标记思路一样但不会因索引变化而误判。4.3 对象捕捉让捕捉命令认识自定义实体对象捕捉是 CAD 里最高频的交互之一。画线时端点捕捉、画圆时圆心捕捉和象限捕捉这些对自定义实体全部失效因为 CAD 不知道你的几何定义。需要重写 subGetOsnapPointsAcad::ErrorStatus MyCircleEntity::subGetOsnapPoints(AcDbOsnapType osnapType, double gsSelectionMark, const AcGePoint3d pickPoint, const AcGePoint3d lastPoint, const AcGeMatrix3d viewXform, AcGePoint3dArray points, AcDbIntArray geomIds) const { // 圆心捕捉 if (osnapType AcDb::kOsnapCenter) { points.append(m_center); return Acad::eOk; } // 象限捕捉 if (osnapType AcDb::kOsnapQuadrant) { points.append(AcGePoint3d(m_center.x m_radius, m_center.y, 0.0)); points.append(AcGePoint3d(m_center.x - m_radius, m_center.y, 0.0)); points.append(AcGePoint3d(m_center.x, m_center.y m_radius, 0.0)); points.append(AcGePoint3d(m_center.x, m_center.y - m_radius, 0.0)); return Acad::eOk; } // 其余捕捉类型交给父类默认处理 return AcDbEntity::subGetOsnapPoints(osnapType, gsSelectionMark, pickPoint, lastPoint, viewXform, points, geomIds); }osnapType 枚举覆盖了端点、中点、圆心、象限、交点、垂足等所有捕捉类型。这里的实现只处理 kOsnapCenter 和 kOsnapQuadrant捕捉时把对应点塞进 points 数组。其余类型交给 AcDbEntity 父类处理它内部会检查你有没有用 AcGi 绘制线段必要时返回零个点——这也是“没实现就是不支持”的合理语义。几个参数里pickPoint 是鼠标当前位置lastPoint 是上一个点viewXform 是视图变换矩阵做复杂几何判断时会用到简单实体平时可以忽略。到这里自定义圆完整“活”了能画出来、能拖动、能被捕捉。接下来聊最容易翻车的地方。5. 常见问题与避坑排查编译、链接、加载三个黑匣子ARX 的报错有两个特点一是很多错误发生在加载阶段而不是编译阶段二是现象和根因经常隔得很远。把踩过的坑整理成五条。5.1 编译通过但链接时报“无法解析的外部符号 acrxEntryPoint”现象工程整体编译成功链接阶段报 LNK2019提示找不到 acrxEntryPoint。原因最常见的是 .def 文件没有把 acrxEntryPoint 导出或者 .def 根本没加进工程另一个常见原因是入口函数被写成了 C 修饰名没有用 extern C 修饰。解决确认 .def 文件里 EXPORTS 下有 acrxEntryPoint且工程设置里 .def 已指定为模块定义文件同时把 acrxEntryPoint 定义处加上 extern C。编译后用 dumpbin 查看导出符号如果看到的是 acrxEntryPoint 而不是一串修饰名问题就解决了。5.2 APPLOAD 加载即失败提示模块版本不匹配或进程崩溃现象arx 文件放到 CAD 里加载弹出版本不符严重时直接让 CAD 进程退出。原因ARX 是进程内 DLLCAD 对模块的 PE 格式、位数、运行时和 SDK 版本极其敏感。最常见的组合是“CAD 64 位 工程 x86”“SDK 版本大于 CAD 版本”“项目使用 /MT 静态运行时”。解决工程平台一律和 CAD 一致当前 CAD 主流是 x64SDK 版本严格匹配 CAD 主版本号运行时库选 /MD字符集选 Unicode。改完别忘了全量重新编译因为有时改配置后增量编译不会重新链接整个模块。5.3 实体画出来了但保存后重新打开变成了“代理对象”现象运行 MYCIRCLE 创建实体当前会话一切正常保存后重开 DWG图形还在但属性丢失显示为代理实体夹点捕捉全部失效。原因DXF 存取链路断了。要么 dxfOutFields 没写 100 子类标记要么写的类名和 ACRX_DEFINE_MEMBERS 注册的类名不一致导致打开时 CAD 找不到运行时类要么加载时忘了调 rxInit。解决先核对 dxfInFields 里 atSubclassData 的字符串与 dxfOutFields 写的字符串完全一致再检查 acrxEntryPoint 的 kInitAppMsg 分支是否调用 MyCircleEntity::rxInit()。删掉现有实体重新加载 ARX 后新建实体再保存验证。5.4 重写了 subGetOsnapPoints对象捕捉却毫无反应现象圆心捕捉和象限捕捉模式下鼠标移到自定义圆附近没有出现捕捉标记。原因最常见的不是逻辑写错而是函数签名漏了 const。ARX 对虚函数匹配要求严格少一个 const 会静默匹配到父类的同名函数你的实现根本没被调用。第二个常见原因是 points 数组追加了错误类型的点比如忘了保证坐标位于 WCS。解决检查 subGetOsnapPoints 声明末尾是否带 const和头文件保持一致捕捉点在追加前确认几何坐标没有经过错误的视图变换。用调试器在函数第一行下断点能最快确认它到底有没有被调用。5.5 夹点能显示但拖起来数值乱跳现象选中实体能看到夹点拖动象限点时圆的位置和半径不停闪动最终结果和鼠标位置对不上。原因这是 subMoveGripPointsAt 里身份判断的锅。按“第几个夹点”的固定索引识别身份而用户在拖动过程中同时触发了夹点重新生成索引顺序会变。另一个常见原因是把 offset 当成“目标位置”直接用忘记 offset 是相对位移。解决先用最简单的一致性验证——首次拖拽时打印 gripArray 和 gripDataArray 的内容确认索引稳定后再实现正式逻辑。实现时牢记 offset 是相对位移目标位置等于原夹点位置加 offset而不是原位置加绝对坐标。真正常用的做法是给夹点写入 appData 身份标记后续版本里再升级。这五条坑覆盖了从链接到运行再到存档的完整链路。遇到问题先定位阶段再按对应原因排查比在代码里瞎试快得多。6. 二次开发改造把简单实例升级成业务实体如果要把这个实例用到真实项目里比如做一个带编号的设备符号有两个改造点可以先落地成本很低但收益明显。6.1 给实体加业务字段与扩展组码在类里增加一个 CString m_deviceNodxfOutFields 里追加一句 writeItem(AcDb::kDxfXdText, m_deviceNo)dxfInFields 里补对应 case 读取worldDraw 里用 AcGiWorldDraw 的 text 接口把编号画到圆下方一个“带数据的圆”就成型了。图纸里的编号会随实体一起保存、随夹点一起移动后续做统计、出清单都能直接读它。6.2 三次验证闭环第一属性和捕捉改动后要走一遍固定流程新建实体、保存关闭、重新打开、对象捕捉、夹点拖动五步全过才算通过。第二换一台没装开发环境的机器只装 CAD 和 arx 文件验证类注册不依赖 SDK。第三批量生成几百个实体做保存压测DXF 写入的崩溃大概率在批量场景下才冒出来。批量创建不要裸调 appendAcDbEntity常见做法是包一层事务startTransaction 打开循环 append最后一次 commit中间任何一步失败就 abort数据库不会残留半成品。从那以后我每次给实体加自定义字段无论多小都强制走一遍保存、关闭、重开的闭环哪怕只是加一个 double 也不跳过。ARX 的很多“玄学”问题根子都在加载顺序和组码不一致这两处这个习惯能拦住大半。希望帮到你。本文还有配套的精品资源点击获取