☰
ObjectARX自定义实体开发实战:从AcDbEntity派生到夹点拖拽与DWG存盘
2026/10/15 20:12:04 网站建设 项目流程

简介:这份资源面向具备C++与VC++基础、希望入门AutoCAD二次开发的工程师,围绕ObjectARX自定义实体这一核心概念,通过一个可运行的自定义直线实例,讲解从项目搭建到实体注册、命令实现与调试的完整流程。包内共70个文件,以h头文件与cpp源文件为主体,辅以obj、pdb、lib、exp等编译中间与库文件,以及dsp、dsw、rc、def等工程与资源文件,压缩包约5.08MB,工程结构完整,可直接在Visual Studio中打开参考。实例覆盖继承AcDbLine、重写dwgInFields/dwgOutFields、dxfInFields/dxfOutFields与worldDraw等关键方法,并演示数据库记录实现、acedRegApp注册及命令回调创建实体的思路,便于读者对照代码理解自定义实体的数据读写与显示机制。目前已有1159人学习下载,适合作为ObjectARX自定义实体开发的入门参照与排错起点。

1. 从一条“画完就后悔”的线说起:ObjectARX 自定义实体到底解决什么问题

如果你在 AutoCAD 二次开发里画过带业务属性的构件,大概率遇到过这种尴尬:用普通图元拼出来的“墙”“梁”“设备”,一旦被拉伸、复制、镜像,附加的数据就跟丢了;想改个参数,得写一堆选择集遍历去反查。ObjectARX 自定义实体(Custom Entity)就是冲着这个痛点来的——它让你把几何形状和业务数据绑成一个原生数据库对象,AutoCAD 的夹点、捕捉、打印、保存全按你的规则走。这个简单实例要做的,就是定义一个最小可用的自定义实体:一个带两个夹点的矩形,能拖拽改尺寸,能随图纸存盘。适合已经会写 ARX 命令、但还没碰过 AcDbEntity 派生的人,也适合被“数据跟着图形跑”折磨过的老手回头补课。

2. 自定义实体的骨架:从 AcDbEntity 派生要补哪几个虚函数

2.1 为什么必须重写 worldDraw 和 dwgOut/dwgIn

AcDbEntity 是个抽象基类,AutoCAD 在重生成、存盘、读盘时会回调一组虚函数。最核心的三个:worldDraw负责把几何画到屏幕和打印设备上,dwgOut把对象序列化进 DWG,dwgIn从 DWG 反序列化。少写任何一个,要么图形不显示,要么存盘后打开变成空对象。常见做法是让自定义实体只存“参数”(比如宽、高、原点),几何在worldDraw里实时算,这样文件小、改参数快。

class CSimpleRectEnt : public AcDbEntity { public: ACRX_DECLARE_MEMBERS(CSimpleRectEnt); CSimpleRectEnt(); virtual ~CSimpleRectEnt(); // 核心重写 virtual Adesk::Boolean worldDraw(AcGiWorldDraw* mode) override; virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* filer) const override; virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler* filer) override; virtual Acad::ErrorStatus getGripPoints(AcGePoint3dArray& pts, AcDbIntArray& indices, AcDbIntArray& snap) override; virtual Acad::ErrorStatus moveGripPointsAt(const AcDbIntArray& indices, const AcGeVector3d& offset) override; private: AcGePoint3d m_origin; // 左下角 double m_width; // 宽 double m_height; // 高 };

这段声明里,ACRX_DECLARE_MEMBERS是 ARX 对象体系的运行时类型信息宏,配套的ACRX_DEFINE_MEMBERS要放到 cpp 里。getGripPoints和moveGripPointsAt是夹点编辑的入口,不重写的话对象选中后没有可拖的点。参数说明:m_origin用世界坐标,m_width/m_height用正数,负值会让矩形翻转,后面夹点逻辑要处理符号。

2.2 注册到 ARX 运行时:类声明与 rxInit 的配对

光有类不够,AutoCAD 得知道这个类叫什么、怎么 new 出来。每个自定义实体都要在模块初始化时注册,否则dwgIn读盘时找不到类,直接报“无法识别的对象”。

// 在 cpp 顶部 ACRX_DEFINE_MEMBERS(CSimpleRectEnt, AcDbEntity, AcDbEntity); // 模块初始化函数里 extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: acrxUnlockApplication(pkt); acrxRegisterAppMDIAware(pkt); CSimpleRectEnt::rxInit(); // 注册类 acdbRegApp(_T("SIMPLE_RECT")); // 注册扩展数据用的 AppName break; case AcRx::kUnloadAppMsg: CSimpleRectEnt::rxUnload(); // 反注册 break; } return AcRx::kRetOK; }

rxInit内部会调用desc()->makeAutoDelete()之类的设置,让 ARX 管理对象生命周期。acdbRegApp注册的字符串是给扩展数据(XData)或字典用的,这里先占个位。注意acrxUnlockApplication和acrxRegisterAppMDIAware在多文档环境下必须调,否则切换图纸时对象可能失效。参数上,pkt在kInitAppMsg时是模块句柄,别丢。

2.3 最小可运行命令:把实体加进模型空间

注册完类,还得有个命令能创建它。下面这个命令演示了从 new 到 close 的标准流程,漏掉close()会导致对象被锁死,后续编辑全失败。

void CreateSimpleRect() { CSimpleRectEnt* pEnt = new CSimpleRectEnt(); pEnt->setOrigin(AcGePoint3d(0, 0, 0)); pEnt->setSize(100.0, 50.0); AcDbBlockTable* pBlockTable = nullptr; acdbHostApplicationServices()->workingDatabase() ->getBlockTable(pBlockTable, AcDb::kForRead); AcDbBlockTableRecord* pModelSpace = nullptr; pBlockTable->getAt(ACDB_MODEL_SPACE, pModelSpace, AcDb::kForWrite); AcDbObjectId entId; pModelSpace->appendAcDbEntity(entId, pEnt); // 加入数据库 pModelSpace->close(); pBlockTable->close(); pEnt->close(); // 必须关,否则对象一直处于打开状态 }

appendAcDbEntity会把对象的所有权交给块表记录,之后pEnt的引用计数由数据库管。setOrigin/setSize是自定义的 setter,内部要调assertWriteEnabled()再改成员,否则撤销(undo)记录不进去。这个命令注册成SIMPLE_RECT_CREATE就能在命令行敲出来。

3. 让图形“活”起来:worldDraw 画几何与夹点拖拽的实现

3.1 worldDraw 里用 AcGiGeometry 画四条边

worldDraw是每帧都可能被调的地方,别在里面做数据库查询或 new 大对象。画矩形用AcGiGeometry::polyline或worldLine都行,前者一次提交四条边,效率更好。

Adesk::Boolean CSimpleRectEnt::worldDraw(AcGiWorldDraw* mode) { AcGePoint3d pts[5]; pts[0] = m_origin; pts[1] = AcGePoint3d(m_origin.x + m_width, m_origin.y, 0); pts[2] = AcGePoint3d(m_origin.x + m_width, m_origin.y + m_height, 0); pts[3] = AcGePoint3d(m_origin.x, m_origin.y + m_height, 0); pts[4] = pts[0]; // 闭合 mode->geometry().polyline(5, pts); // 画闭合折线 return Adesk::kTrue; // 返回 kTrue 表示已处理 }

polyline的第二个参数是点数,这里 5 个点首尾重合形成闭合。返回kTrue告诉 AutoCAD 这个实体的显示已经画完,不用再调基类。如果返回kFalse,系统会尝试用默认方式画,通常啥也看不到。注意AcGiWorldDraw在打印和屏幕显示时都有效,别在里面用acedCommand之类的交互函数。

3.2 夹点定义:两个角点怎么映射到索引

夹点编辑靠getGripPoints返回点数组,moveGripPointsAt根据索引改参数。索引是自己定的,只要两个函数对得上就行。常见做法是 0 号夹点管左下角,1 号管右上角。

Acad::ErrorStatus CSimpleRectEnt::getGripPoints( AcGePoint3dArray& pts, AcDbIntArray& indices, AcDbIntArray& snap) { pts.append(m_origin); // 索引 0 pts.append(AcGePoint3d(m_origin.x + m_width, m_origin.y + m_height, 0)); // 索引 1 indices.append(0); indices.append(1); return Acad::eOk; } Acad::ErrorStatus CSimpleRectEnt::moveGripPointsAt( const AcDbIntArray& indices, const AcGeVector3d& offset) { assertWriteEnabled(); // 开启写权限,记录 undo for (int i = 0; i < indices.length(); i++) { if (indices[i] == 0) { m_origin += offset; m_width -= offset.x; // 左下角动,宽高反向补偿 m_height -= offset.y; } else if (indices[i] == 1) { m_width += offset.x; // 右上角动,直接加 m_height += offset.y; } } return Acad::eOk; }

assertWriteEnabled()是 ARX 的写保护检查,在修改成员前必须调,否则撤销栈里没有这一步。offset是拖拽的增量向量,单位跟图形单位一致。左下角移动时宽高要减掉偏移,否则矩形会整体平移而不是缩放。如果宽高被拖成负数,可以在 setter 里取绝对值或限制最小值,看业务需求。

3.3 存盘与读盘:dwgOutFields 和 dwgInFields 的字段顺序

DWG 序列化靠 filer 的读写对,顺序必须严格一致,否则读盘时数据错位。常见坑是只写不读,或者读的时候类型不匹配。

Acad::ErrorStatus CSimpleRectEnt::dwgOutFields(AcDbDwgFiler* filer) const { assertReadEnabled(); filer->writePoint3d(m_origin); filer->writeDouble(m_width); filer->writeDouble(m_height); return Acad::eOk; } Acad::ErrorStatus CSimpleRectEnt::dwgInFields(AcDbDwgFiler* filer) { assertWriteEnabled(); filer->readPoint3d(&m_origin); filer->readDouble(&m_width); filer->readDouble(&m_height); return Acad::eOk; }

writePoint3d和readPoint3d是成对的,writeDouble对应readDouble。顺序不能换,比如先写宽再写高,读的时候也得先读宽。如果以后加字段,只能往末尾追加,不能插在中间,否则旧图纸读新代码会错位。assertReadEnabled和assertWriteEnabled在调试版会检查对象打开状态,发布版可去掉,但建议保留。

4. 避坑与排查:自定义实体最容易翻车的五个地方

4.1 现象:对象能画出来,存盘再打开就没了

原因:dwgOutFields或dwgInFields没重写,或者rxInit没在初始化时调用。AutoCAD 存盘时找不到类的序列化入口,直接跳过。解决:确认ACRX_DEFINE_MEMBERS和rxInit配对,并在dwgInFields里加日志或断点,看读盘时是否被调用。

4.2 现象:夹点拖拽后图形不更新,或者撤销后错乱

原因:moveGripPointsAt里改了成员但没调assertWriteEnabled(),或者worldDraw里用了缓存没重算。解决:每次修改成员前调assertWriteEnabled(),并确保worldDraw完全根据当前成员实时计算几何,不存中间结果。

4.3 现象:复制、镜像后新对象参数不对

原因:自定义实体默认的clone和transformBy没重写,基类实现只拷贝了 AcDbEntity 的公共部分,自定义成员没跟过去。解决:重写clone返回新对象并拷贝参数,重写transformBy对m_origin应用矩阵,宽高按缩放因子调整。

4.4 现象:多文档切换时崩溃

原因:acrxUnlockApplication或acrxRegisterAppMDIAware没调,或者静态成员跨文档共享了数据库对象 ID。解决:在kInitAppMsg里补上这两个调用,避免用全局变量存 AcDbObjectId,改用 AcDbObjectId 的持久引用或每次从数据库查。

4.5 现象:打印时线条粗细不对或填充丢失

原因:worldDraw里只画了几何,没设置属性(颜色、线型、线宽)。解决:在worldDraw开头用mode->subEntityTraits().setColor()等设置当前属性,或者让实体继承图层、颜色等公共属性,别在几何里硬编码。

5. 进阶技巧:用 AcDbEntity 的 subGetClassID 和协议扩展做数据校验

自定义实体稳定跑起来后,下一步通常是加业务规则。一个实用技巧是重写subGetClassID配合AcDbObject的协议扩展,在对象被修改时触发校验。比如矩形宽高不能超过某个值,可以在moveGripPointsAt里直接判断,但更优雅的是用AcDbEntity::subMoveGripPointsAt的返回值控制。另一个技巧是给实体加AcDbAssocAction关联,让参数变化自动驱动其他对象,但这属于另一个话题了。

验证方法很简单:写个命令遍历模型空间,对每个CSimpleRectEnt调dwgOut到内存 filer 再dwgIn回来,对比参数是否一致。这个“自读自写”测试能提前发现序列化顺序错误。

void TestRoundTrip() { AcDbBlockTable* pBT = nullptr; acdbHostApplicationServices()->workingDatabase() ->getBlockTable(pBT, AcDb::kForRead); AcDbBlockTableRecord* pMS = nullptr; pBT->getAt(ACDB_MODEL_SPACE, pMS, AcDb::kForRead); AcDbBlockTableRecordIterator* pIter = nullptr; pMS->newIterator(pIter); for (; !pIter->done(); pIter->step()) { AcDbEntity* pEnt = nullptr; pIter->getEntity(pEnt, AcDb::kForRead); CSimpleRectEnt* pRect = CSimpleRectEnt::cast(pEnt); if (pRect) { AcDbMemFiler filer; // 内存 filer pRect->dwgOut(&filer); filer.seek(0); // 回到开头 CSimpleRectEnt* pCopy = new CSimpleRectEnt(); pCopy->dwgIn(&filer); // 对比 pRect 和 pCopy 的参数 pCopy->close(); } pEnt->close(); } delete pIter; pMS->close(); pBT->close(); }

AcDbMemFiler是内存流,适合做单元测试。seek(0)把读指针复位,否则dwgIn从末尾读,啥也读不到。cast是 ARX 的安全向下转型,比dynamic_cast快且不依赖 RTTI。这个测试跑通,基本能排除序列化层面的低级错误。

我自己的习惯是每加一个成员变量,先写dwgOut/dwgIn,再写worldDraw,最后补夹点。顺序反了容易漏字段,等存盘丢数据再回头查,那才叫后悔药没处买。希望帮到你。

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

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

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

立即咨询