ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ObjectARX自定义实体完全指南:从AcDbEntity派生到夹点交互

ObjectARX自定义实体完全指南:从AcDbEntity派生到夹点交互

简介:面向AutoCAD二次开发初学者,这套ObjectARX自定义实体实例围绕一条自定义直线实体,完整演示了ARX项目框架搭建、自定义类继承AcDbLine并重写dwgInFields/dwgOutFields、dxfInFields/dxfOutFields、worldDraw等关键函数,以及实体注册、命令实现、编译加载到AutoCAD的整套流程。资源包含TestCmd与TestEntity两个工程目录,命令入口与实体定义分离,源码注释清晰,可对照学习DWG/DXF序列化、屏幕绘制、数据完整性与调试等底层机制,省去从零搭建环境的时间。压缩包共70个文件,以C++头文件、源文件和obj/ilk等编译中间文件为主,附带dsp/dsw工程配置、arx/dbx插件产物,整体仅5.08MB,便于快速下载与查阅。目前已有1162人浏览学习,作为小而完整的入门示例,对后续开发复杂自定义实体有直接铺垫作用,适合理解ObjectARX与AutoCAD内部图形系统的交互方式,也是进一步扩展算法、属性与事件响应能力的基础。

1. 自定义实体为什么是 ARX 的「分水岭」:先看清它解决什么问题

拿到一张带参数化符号的成套图纸,图块一多,改一个尺寸就要炸开重画、重新对齐,卡顿到怀疑人生。这时候如果底层用的是 ARX 自定义实体,双击、拖夹点、属性面板实时联动,图纸体量再大也不会散架。ObjectARX 是 C++ 开发者做 VC++ 环境下的 AutoCAD 二次开发最直接的手段,而自定义实体(从 AcDbEntity 派生自定义类)是 ARX 能力的天花板——它让你创建的图形不再是「死图元」,而是自带行为逻辑的活对象。这篇笔记就用一个最简三角形实体把整个链路走一遍:从类设计、绘制、保存到踩坑修复,新手能照着搭,熟手也能对一对边界处理。

2. 自定义实体的体系结构:从 AcDbEntity 派生到五个必须重载的方法

2.1 先分清「普通图元」和「自定义实体」的本质差别

ARX 里画一根线、一个圆,用的是 AcDbLine、AcDbCircle,这些是框架写死的类。你往图面上一扔,它就是一个静态对象:位置定了,数据就定死了。想改,只能用命令去改它的属性,稍微复杂一点的参数联动——比如三角形的底边拉长、高度跟着夹点动——靠普通图元做,要么写一堆外部逻辑在命令里改坐标,要么炸开重组,完全失去「对象感」。

自定义实体从根本上换了一条路:你从 AcDbEntity 派生一个新类,把图形的几何逻辑封装在类内部。图形画出来之后,它知道自己的基点和高度,你拖夹点,它自己重算另外两个顶点;你改属性面板里的参数,它自己触发重生成。这就是「数据与行为内聚」,也是 ARX 自定义实体和普通图元的本质界限。

2.2 五个核心方法的分工:从绘制到持久化

对象在 CAD 里的生命周期大致是:创建 → 显示 → 保存 → 再打开 → 交互修改。每个环节都对应一个可以重载的虚函数。最常用的五个如下表:

方法名作用必须重载的原因
worldDraw / subWorldDraw把实体画到屏幕上框架不知道你的图形长什么样
dwgOutFields把成员数据写进 dwg 文件保存后重开,数据不能丢
dwgInFields从 dwg 文件读回数据与 dwgOutFields 严格对称
subGetGripPoints返回夹点坐标数组鼠标悬停时要有可抓的点
subMoveGripPointsAt夹点拖动的回调拖动后要自己更新几何

这里最容易迷糊的是 worldDraw 和 subWorldDraw。前者是传统入口,后者是 2007 之后推荐的子类化入口。简单说:你直接用 worldDraw 会破坏基类的某些缓存逻辑,subWorldDraw 是用更细粒度的回调替代它。两者重写一个即可,我建议自定义实体一律走 subWorldDraw,省掉一堆缓存相关的疑难杂症。

2.3 引用计数与 AcDbObjectId:新手最容易漏的「所有权」概念

ARX 里所有数据库对象(AcDbObject 派生类)都用引用计数管理内存。你用 new 创建了一个自定义实体,它还不是数据库的一部分,引用计数的初始值是 0。把它 addAcDbEntity 到块表记录里,引用计数变成 1,这时候你把本地指针置空就行——千万不能 delete,否则 CAD 会直接崩溃。

对应的还有 AcDbObjectId,它只是数据库对象的一个「索引号」,不是指针。平时跨函数传递实体身份,传 id 不传指针,拿 id 用 acdbOpenObject 打开实体、用完立刻 close 再置空。这条规则是 ARX 开发的红线,后面第五章的崩溃坑大半都来自这里。

3. 环境准备与工程骨架:VS 配置、ARX 向导与模块加载

3.1 环境匹配第一关:VS 版本与 SDK 版本的配对

ObjectARX 的 SDK 和编译器版本是强绑定的。用错了组合,编译出来的 arx 文件加载就报「不兼容的模块」或直接让 CAD 崩溃。常见做法是:先确定你手里的 CAD 主版本,再去 ObjectARX SDK 文档里找到它对应的 VS 版本范围。比如较新的 CAD 主版本要求 VS 2022 配合对应的 SDK,老一点的 CAD 只能配 VS 2017 或 2019。

我一般会在装 SDK 之前先把 VS 装好,再装 SDK,然后立刻用一个最简单向导工程编译测试,确认 arx 能加载再往下写。顺序反了,SDK 装完检测不到编译器,又要折腾环境变量。

3.2 工程骨架:从向导工程开始,别手搓

SDK 自带的 ObjectARX 向导(Wizard)会生成一个完整的最小工程。它替你搞定了大部分编译配置:include 路径、库文件、预处理器定义、导出函数入口。正规步骤是「向导生成 → 编译通过 → 改代码」,不是「新建空项目 → 手动加配置」。

向导生成的工程里最关键的三个文件:

  • rxinit.cpp— 入口点 acrxEntryPoint 所在,也是 acrxRegisterApp 注册程序名的地方
  • acedCmd.cpp— 定义具体命令的 ARX 命令函数,用 acedRegCmds 注册
  • StdArx.h— 预编译头,包含几乎全部 ObjectARX 头文件

3.3 注册命令与卸载逻辑:加载成功后你能看到什么

命令注册集中在入口点的 kInitAppMsg 分支,卸载在 kUnloadAppMsg。一个合格的入口函数长这样:

extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* appId) { switch (msg) { case AcRx::kInitAppMsg: acrxDynamicLinker->unlockApplication(appId); acrxRegisterApp(ACRX_APP_NAME); acedRegCmds->addCommand(_T("OBJECTARX_SAMPLE"), _T("MAKETRI"), _T("MAKETRI"), ACRX_CMD_MODAL, makeTriCommand); break; case AcRx::kUnloadAppMsg: acedRegCmds->removeGroup(_T("OBJECTARX_SAMPLE")); break; default: break; } return AcRx::kRetOK; }

代码逻辑有三层:加载时先解锁应用(unlockApplication 是为了允许在 AutoCAD 运行时替换 arx 文件,调试时很有用),再注册全局程序名,最后往命令栈里塞一个命令。卸载时按组名移除命令组即可。注意第一个和第三个参数都是_T("MAKETRI")——第一个是内部命令名,第三个是用户输入的命令名,可以不一样,但通常保持一致,免得命令明明存在却找不到。

调试时要注意的细节:acedRegCmds->addCommand必须在 kInitAppMsg 里调用,不能放在命令函数里;卸载时removeGroup的参数要和addCommand第一个参数完全一致,否则命令组残留,下次加载会报重复定义。

4. 把三角形实体落地:绘制、参数化与模型空间注册

4.1 类定义与成员设计:基点和高度是两个可变参数

自定义实体的几何模型越简单越能看清套路。我们的三角形实体有两个本地成员:一个基点坐标 m_basePt,一个高度 m_height。三个顶点全部由这两个参数派生出来——这样设计的好处是,夹点拖动时只需要改这两个值,不用去处理三个顶点的联动。

class TriEntity : public AcDbEntity { public: ACRX_DECLARE_MEMBERS(TriEntity); TriEntity(); TriEntity(const AcGePoint3d& basePt, double height); virtual ~TriEntity(); // 绘制 virtual Adesk::Boolean subWorldDraw(AcGiWorldDraw* mode); // 序列化 virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* filer) const; virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler* filer); // 夹点 virtual Acad::ErrorStatus subGetGripPoints(AcGePoint3dArray& gripPoints, AcDbIntArray& apps, AcDbIntArray& modes) const; virtual Acad::ErrorStatus subMoveGripPointsAt(const AcDbIntArray& indices, const AcGeVector3d& offset); // 访问器 AcGePoint3d basePoint() const { return m_basePt; } double height() const { return m_height; } void setBasePoint(const AcGePoint3d& pt); void setHeight(double h); private: AcGePoint3d m_basePt; double m_height; };

ACRX_DECLARE_MEMBERS 这个宏会在类里生成静态的 RxClassDesc 描述符,ObjectARX 靠它识别这个类名、归档、运行时类型转换。少了它,整个类在数据库里注册不了。成员变量两名足够:基点决定三角形左下角位置,高度决定顶点的 y 方向偏移——第三个顶点和右下角顶点全部由这两个值计算,这样夹点拖动的重绘逻辑会非常干净。

4.2 worldDraw 实现:用 AcGiWorldDraw 画三角形

Adesk::Boolean TriEntity::subWorldDraw(AcGiWorldDraw* mode) { // 根据基点和高度派生三个顶点 AcGePoint3d p0 = m_basePt; AcGePoint3d p1(m_basePt.x + m_height, m_basePt.y, 0.0); AcGePoint3d p2(m_basePt.x + m_height * 0.5, m_basePt.y + m_height, 0.0); // 画闭合的三角形轮廓 AcGePoint3dArray pts; pts.append(p0); pts.append(p1); pts.append(p2); mode->geometry().polyline(3, pts.asArrayPtr(), NULL, Adesk::kTrue); return Adesk::kTrue; }

注意几个细节:polyline 的第一个参数是点数,pts.asArrayPtr()把 AcGePoint3dArray 转成原始指针数组,最后一个参数 kTrue 表示闭合,也就是从最后一个点自动连回第一个点。返回值 kTrue 表示「绘制完成,不需要默认绘制逻辑继续干活」;如果返回 kFalse,基类的边框、代理图形等逻辑还会再跑一遍,通常没必要。

实际项目中如果实体会被频繁重生成,建议把三顶点坐标存成成员变量,只在参数变化时更新一次,而不是每次绘制都现算。这一步对大型图纸的性能感知比较明显,三角形无所谓,但几百个虚线多边形叠加时,差异就在帧率上了。

4.3 序列化实现:dwgOutFields 与 dwgInFields 的严格对称

保存和读取是两个必须「长相一样」的函数。ObjectARX 没有自动序列化,你写几个成员,就必须在读取时按同样的顺序拿几个成员。顺序错了,读出来的数据全是乱的,而且是静默错乱——CAD 不报错,只是图形位置漂移。

Acad::ErrorStatus TriEntity::dwgOutFields(AcDbDwgFiler* filer) const { assertWriteEnabled(); Acad::ErrorStatus es = AcDbEntity::dwgOutFields(filer); if (es != Acad::eOk) return es; // 按成员变量逐一写出,顺序必须与读取一致 filer->writePoint3d(m_basePt); filer->writeDouble(m_height); return filer->filerStatus(); } Acad::ErrorStatus TriEntity::dwgInFields(AcDbDwgFiler* filer) { assertReadEnabled(); Acad::ErrorStatus es = AcDbEntity::dwgInFields(filer); if (es != Acad::eOk) return es; // 与 dwgOutFields 顺序严格一致:先点,后高度 m_basePt = filer->readPoint3d(); m_height = filer->readDouble(); return filer->filerStatus(); }

assertWriteEnabled和assertReadEnabled是 ARX 的「后悔药」机制的核心——在修改或读取数据前,框架会检查当前对象是否处于正确的打开状态。如果是只读打开却要写数据,直接触发断言,帮你提前发现问题,而不是拖到保存时才炸。序列化里最容易犯的错就是在 dwgInFields 里没有先调用基类的读取逻辑,导致基类数据(比如图层、线型)全部丢失,图形变成「裸对象」。

写完后一定要做一次「保存-关闭-重开」的往返测试。文件能存、能取、能重开,这三个都过了,序列化才算及格。

4.4 注册命令:把对象真正放进模型空间

有了类,还差一个把它塞进数据库的命令函数。这是 ARX 新手最常翻车的地方——new 出来的对象不知道加到哪里,或者加了之后忘了关闭引用。下面的代码是标准动作:

void makeTriCommand() { // 拿到当前数据库的块表 AcDbDatabase* db = acdbHostApplicationServices()->workingDatabase(); AcDbBlockTable* pBlkTbl = NULL; db->getBlockTable(pBlkTbl, AcDb::kForRead); // 定位模型空间块表记录 AcDbObjectId msId; pBlkTbl->getAt(ACDB_MODEL_SPACE, msId); pBlkTbl->close(); AcDbBlockTableRecord* pMsRec = NULL; acdbOpenObject(pMsRec, msId, AcDb::kForWrite); // 创建自定义实体,加到模型空间 TriEntity* pTri = new TriEntity(AcGePoint3d(0, 0, 0), 10.0); AcDbObjectId triId; pMsRec->appendAcDbEntity(triId, pTri); // 关闭记录和实体——append 之后所有权归数据库 pMsRec->close(); pTri->close(); acdbHostApplicationServices()->workingDatabase()->regenerate(); }

过程的每一步都有对应的资源管理责任:块表以只读打开,用完立刻 close;模型空间记录以写打开,负责接收新实体;实体 new 出来先 append 进记录,再 close——注意这里不是 delete,close 会触发引用计数减一,但 append 的时候加一,所以对象依然存活但由数据库托管。关闭顺序必须是「先关记录,再关实体」,反过来会让实体在写入时找不到所属记录。

regenerate()是最后一步,强制全图重生成。如果不调,命令执行后图形可能不显示,必须手动 regen 才出来。

5. 实测踩坑与排查:数据丢失、白屏、崩溃的常见问题

5.1 现象:命令执行后屏幕上什么都没有

这是第一个绕不开的坎。命令跑了,加了个实体,但图面上看不到任何东西。

原因大概率在两个地方。第一,实体坐标超出了当前视图范围——基点设成 (0,0,0) 但当前视图 zoom 到别的区域去了。第二,subWorldDraw 里没有正确绘制,或者返回值写成了 kFalse,导致框架认为你不想画。

解决办法分两步:先用zoom extents看整个图面,排除视图范围问题;然后在 subWorldDraw 里加一个临时acutPrintf(_T("draw called"))输出,确认回调有没有被触发。如果回调压根没触发,问题在实体的类注册和命令注册上;如果触发了但看不到,检查 polyline 的坐标是否确实在合理数值范围内,有些坐标差极小(比如 1e-6 量级)会导致图形肉眼不可见。

5.2 现象:保存后重开,实体消失了

dwg 文件里明明存了东西,重开就是没有。这种情况十有八九是序列化函数写漏了成员,或者读写顺序不一致。

我遇到过最典型的一次:dwgOutFields 里先写了高度再写基点,dwgInFields 里却先读基点再读高度。数值全错位,但偏偏图形「看起来」正常——因为基点和高度都是 double 类型,错位之后只是数值变了,没有类型不匹配的报错,所以排查难度极大。

解决办法是强制让两个函数并排写,逐行对照。我自己的习惯是先写 dwgOutFields,再拷贝一份改成 dwgInFields,把 write 改成 read,这样顺序天然一致。

5.3 现象:夹点能显示,但拖动后图形错乱

夹点出现了,说明 subGetGripPoints 返回的坐标数组是有效的。但拖动时图形变形,通常是 subMoveGripPointsAt 里更新几何的逻辑不完整。

比如我最初只在拖动时更新了基点坐标,忘了重新计算三条边,结果图形拉成一个扭曲的四边形。还有一种更隐蔽的情况:夹点数组里有两个点都映射到同一个顶点,拖动其中一个,另一个没跟着动,图形就撕裂了。

解决办法是,定义夹点坐标时明确「哪个夹点对应哪个参数」,而不是把派生顶点直接塞进去。我的做法是:index 0 对应基点,index 1 对应高度——subMoveGripPointsAt 拿到 offset 偏移量后,只 update 对应的参数,其余几何全部由参数计算推导。这样逻辑永远是一致的。

5.4 现象:双击编辑或属性修改时 CAD 直接崩溃

崩溃问题里很大的比例来自对象引用计数。典型场景:代码里用了acdbOpenObject打开实体处理完,忘了 close;或者对同一个指针 close 了两次,第二次是一个悬空指针操作,崩溃无提示。

ARX 崩溃大部分时候不弹任何对话框,直接程序消失。原因是 ObjectARX 框架会在结束事务时统一检查引用计数,发现不匹配就 SegFault 退出。

排查这类问题没有捷径,只能靠「代码审查 + 调试器」。我自己的习惯是写一个辅助日志函数,在每次 open/close 后记录指针地址和操作,崩溃后看最后几行日志,基本一眼定位。另外要注意:同一个实体如果在同一次事务里被 open 两次,第二次的 close 才是真正释放引用计数的,第一次如果也 close,就会把引用多减一次,同样炸。

5.5 现象:arx 卸载时提示「仍然有实体引用本模块」

开发调试时反复加载卸载 arx,某次卸载直接报错,提示还有实体正在使用。原因就是图纸里已有的自定义实体还在引用你的类,而你要把包含这个类的模块卸掉。

解决办法不是硬卸载,而是先清理图面上的图纸实体:删除图中所有 TriEntity(可以遍历模型空间拿 AcDbEntity 判断 isKindOf),再执行卸载。另外入口点的 kUnloadAppMsg 里也要记得把命令组移除干净,否则命令残留比实体引用更隐蔽。

如果你实在不想删图纸内容,就把卸载操作交给 CAD 自己去判断:用appload里「卸载」时,它其实也会先检查,报错就说明有实体依赖。配合class命令查看有多少驻留实例,可以辅助判断是否还有存活对象。

6. 进阶用法:属性面板联动与夹点自定义的收尾技巧

自定义实体只做到「画出来」只是及格线。真正让人愿意用,还得打通属性面板和夹点交互这两条体验链路。

属性面板显示自定义数据,靠的是重载subGetClassID和subGetAttributes这一组回调。ObjectARX 的机制是:属性面板在渲染某个实体的属性页时,会向这个实体询问「你有什么自定义字段」,你再把这些字段通过描述符机制暴露出去。典型的字段就是我们的 m_height,把高度暴露成可编辑项,用户在属性面板改数值,内部触发setHeight,自动重生成图形——这比手动拖夹点精确得多,尤其适合输入精确尺寸的场景。

我的实际做法会在 setHeight 里加一句:

void TriEntity::setHeight(double h) { assertWriteEnabled(); m_height = h; // 通知数据库本对象已修改,触发重生成 recordGraphicsModified(); }

recordGraphicsModified是 ARX 提供的「标记图形脏」接口,它会告知 AutoCAD 该实体的显示缓存已失效,需要重画。不加这句,你改了属性面板的数值,图形却纹丝不动,用户会以为数据没生效。

夹点自定义方面,之前第五章的三角形只有两个夹点——基点和一个高度控制点。实际项目中往往需要更多:中点夹点、旋转夹点、对齐夹点。subGetGripPoints返回点数组时,还可以通过modes数组给每个夹点指定类型。常见的类型包括kGripPoint(普通移动)、kGripRotate(旋转)、kGripStretch(拉伸),不同类型的夹点光标样式不同,交互语义也不同。给三角形加一个「高度中点夹点」的话,只需要在 subGetGripPoints 里多 append 一个点,在 subMoveGripPointsAt 里判断 indices 里含有 2 时,把偏移量换算成高度的增量即可。

最后提醒一点:整条链路走下来,最容易让人崩溃的永远不是逻辑复杂度,而是你以为写完的东西其实没被调用——类没注册、命令没注册、回调没触发,三者任何一个断裂,代码就像黑匣子一样静默失败。我从那以后每完成一个自定义实体,都会强制走一遍「新建 → 绘制 → 保存 → 重开 → 夹点拖拽 → 属性修改」这六步验证清单,全部通过才敢说「这个实体做完了」。希望这篇笔记能帮你少走几趟这些弯路,直接把精力花在真正值得做的几何逻辑上。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进