简介:本资源面向AutoCAD二次开发初学者与C++工程师,聚焦ObjectARX自定义实体这一核心概念,通过一个可运行的自定义直线实例,帮助读者理解如何创建能被AutoCAD识别并与其他标准实体交互的新型几何对象。压缩包共70个文件,约5.08MB,以h头文件与cpp源文件为主体,辅以obj、pdb、lib、arx、dbx等编译产物与工程配置,完整保留了Visual Studio下的ARX工程结构。实例覆盖从项目框架搭建、继承AcDbLine定义MyCustomLine类,到重写dwgInFields、dxfInFields、worldDraw等关键方法,再到实体注册、命令实现、编译加载与调试测试的完整链路,并涉及数据库记录与DXF读写等细节。目前已有1159人学习下载,适合希望从零掌握ObjectARX自定义实体开发流程、对照源码排查编译与注册问题的开发者参考。
1. 从“画个会自己动的块”说起:ObjectARX 自定义实体到底解决什么问题
如果你在 AutoCAD 二次开发里待过一阵,大概率遇到过这种憋屈事:用普通图元拼出来的“设备”“阀门”“标注块”,看着像那么回事,可一旦要改参数、要联动、要按业务规则重算几何,就得写一堆遍历和替换逻辑,改一次炸一次。ObjectARX 自定义实体(Custom Entity)就是冲着这个痛点来的——它让你定义一个全新的图元类型,拥有自己的数据、自己的绘制逻辑、自己的夹点和捕捉行为,AutoCAD 把它当亲儿子一样对待,而不是一堆散装线条的临时组合。
这篇讲的是“简单实例”,所以我不打算一上来就扯 AcDbEntity 的完整继承体系,而是带你从零跑通一个最小可用的自定义实体:一个能显示、能选中、能拖动夹点、能存盘再打开的自定义图元。适合谁看?写过一点 C++、装过 ObjectARX SDK、被 LISP 或 .NET 的图元操作卡过脖子的工程师。看完你至少能判断:这东西值不值得在你的项目里用,以及第一个能跑起来的版本该怎么搭。
2. 动手前先把地基打牢:环境、协议与最小类骨架
2.1 为什么自定义实体必须走“协议扩展”这条路
AutoCAD 的图元系统本质是一套“数据库 + 协议”的架构。AcDbEntity 派生出来的类,光有数据不够,还得告诉 AutoCAD 怎么画、怎么选、怎么存。这套机制叫协议扩展(Protocol Extension),核心是几个虚函数和宏:dwgOut/dwgIn 负责序列化,worldDraw 负责显示,subGetGripPoints 和 subMoveGripPointsAt 负责夹点,getGeomExtents 负责范围框。你不实现它们,实体就是数据库里一条看不见摸不着的记录。
常见做法是继承 AcDbEntity,然后用 ACRX_DXF_DEFINE_MEMBERS 宏注册类。这个宏干了三件事:声明运行时类信息、定义克隆函数、把类挂到 ObjectARX 的类工厂上。少了它,反序列化时 AutoCAD 找不到你的类,文件打开就是一堆“无法识别的图元”。
我一般会先把类骨架写出来,哪怕函数体是空的,先保证编译通过、能注册、能 new 出来。这一步跑不通,后面全是空中楼阁。
2.2 最小类骨架与注册代码
// MySimpleEnt.h #pragma once #include "dbents.h" #include "dbmain.h" class MySimpleEnt : public AcDbEntity { public: ACRX_DECLARE_MEMBERS(MySimpleEnt); MySimpleEnt(); virtual ~MySimpleEnt(); // 协议扩展必须实现的接口 virtual Adesk::Boolean worldDraw(AcGiWorldDraw* mode) override; virtual Acad::ErrorStatus getGeomExtents(AcDbExtents& extents) const override; virtual Acad::ErrorStatus subGetGripPoints( AcDbGripDataPtrArray& grips, const double curViewUnitSize, const int gripSize, const AcGeVector3d& curViewDir, const int bitflags) const override; virtual Acad::ErrorStatus subMoveGripPointsAt( const AcDbVoidPtrArray& gripAppData, const AcGeVector3d& offset, const int bitflags) override; // 序列化 virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* filer) const override; virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler* filer) override; virtual Acad::ErrorStatus dxfOutFields(AcDbDxfFiler* filer) const override; virtual Acad::ErrorStatus dxfInFields(AcDbDxfFiler* filer) override; // 业务数据:一个中心点 + 一个半径 AcGePoint3d mCenter; double mRadius; };// MySimpleEnt.cpp #include "MySimpleEnt.h" #include "dbgrip.h" #include "giworlddraw.h" #include "giworldgeometry.h" ACRX_DXF_DEFINE_MEMBERS( MySimpleEnt, AcDbEntity, AcDb::kDHL_CURRENT, AcDb::kMReleaseCurrent, 0, MYSIMPLEENT, MyApp); MySimpleEnt::MySimpleEnt() : mCenter(0, 0, 0), mRadius(10.0) { } MySimpleEnt::~MySimpleEnt() {} Adesk::Boolean MySimpleEnt::worldDraw(AcGiWorldDraw* mode) { // 画一个圆环示意,实际项目里换成你的业务几何 AcGePoint3d pts[2] = { mCenter, mCenter + AcGeVector3d(0, 0, 1) }; mode->geometry().circle(mCenter, mRadius, AcGeVector3d(0, 0, 1)); return Adesk::kTrue; }这段代码里,ACRX_DXF_DEFINE_MEMBERS 的第三个参数是 DXF 组码范围,第四个是维护版本号,第五个是应用名。应用名必须和你的 ARX 模块名一致,否则注册表里对不上。worldDraw 里我用了 AcGiWorldDraw 的 geometry() 接口,这是即时绘制,不生成数据库图元,性能好但不会被 AutoCAD 的显示缓存优化。简单实例够用,复杂几何建议走 AcGiWorldDraw 的 subEntityTraits 配合 shell。
2.3 注册命令与创建实体的最小闭环
光有类不够,得有个命令能把它塞进模型空间。
// 注册命令 extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: acrxDynamicLinker->unlockApplication(pkt); acrxRegisterAppMDIAware(pkt); MySimpleEnt::rxInit(); acrxBuildClassHierarchy(); acedRegCmds->addCommand(L"MYSIMPLE_CMD", L"MYCREATE", L"MYCREATE", ACRX_CMD_MODAL, createSimpleEnt); break; case AcRx::kUnloadAppMsg: acedRegCmds->removeGroup(L"MYSIMPLE_CMD"); deleteAcRxClass(MySimpleEnt::desc()); break; } return AcRx::kRetOK; } void createSimpleEnt() { MySimpleEnt* pEnt = new MySimpleEnt(); pEnt->mCenter = AcGePoint3d(0, 0, 0); pEnt->mRadius = 50.0; AcDbBlockTable* pBlockTable = nullptr; acdbHostApplicationServices()->workingDatabase() ->getBlockTable(pBlockTable, AcDb::kForRead); AcDbBlockTableRecord* pModelSpace = nullptr; pBlockTable->getAt(ACDB_MODEL_SPACE, pModelSpace, AcDb::kForWrite); pBlockTable->close(); pModelSpace->appendAcDbEntity(pEnt); pModelSpace->close(); pEnt->close(); }这里有个血泪经验:appendAcDbEntity 之后必须 close,否则实体一直挂在打开状态,后续保存会报“对象已打开”的错误。另外 rxInit 和 acrxBuildClassHierarchy 的顺序不能反,反了类工厂里找不到你的类,反序列化直接崩。
3. 让实体“活”起来:绘制、夹点与序列化的三个关键实现
3.1 worldDraw 里到底该画什么、不该画什么
worldDraw 是显示回调,AutoCAD 在重生成、平移、缩放时都会调它。原则是:只画几何,不碰数据库。我见过有人在 worldDraw 里开事务查数据,结果一缩放就卡死,这就是典型的翻车现场。
简单实例里画个圆就够了,但你要知道 circle 的第三个参数是法向量。如果传零向量,AutoCAD 会画不出来,而且不报错,就是一片空白。这个坑我踩过,排查了半天才发现是法向量没给。
Adesk::Boolean MySimpleEnt::worldDraw(AcGiWorldDraw* mode) { // 法向量必须非零,否则几何不可见 AcGeVector3d normal(0, 0, 1); if (normal.length() < 1e-9) return Adesk::kFalse; mode->geometry().circle(mCenter, mRadius, normal); // 如果需要在特定视图下隐藏,用 mode->regenType() 判断 if (mode->regenType() == AcGiRegenType::kAcGiStandardDisplay) { // 标准显示下的额外绘制 } return Adesk::kTrue; }参数说明:mCenter 是圆心,mRadius 是半径,normal 是圆所在平面的法向量。regenType 可以区分标准显示和隐藏线显示,做复杂实体时用得上。
3.2 夹点:让用户能拖、能改、能后悔
夹点是自定义实体交互的核心。subGetGripPoints 返回夹点位置和标识,subMoveGripPointsAt 处理拖动后的数据更新。简单实例里我给圆心一个夹点,拖动时改 mCenter。
Acad::ErrorStatus MySimpleEnt::subGetGripPoints( AcDbGripDataPtrArray& grips, const double curViewUnitSize, const int gripSize, const AcGeVector3d& curViewDir, const int bitflags) const { // 圆心夹点,appData 传 nullptr 表示用索引区分 AcDbGripData* pGrip = new AcDbGripData(); pGrip->setAppData(nullptr); pGrip->setGripPoint(mCenter); grips.append(pGrip); return Acad::eOk; } Acad::ErrorStatus MySimpleEnt::subMoveGripPointsAt( const AcDbVoidPtrArray& gripAppData, const AcGeVector3d& offset, const int bitflags) { // 只有一个夹点,直接偏移圆心 mCenter += offset; return Acad::eOk; }注意:subMoveGripPointsAt 里不要调 worldDraw,AutoCAD 会自动重绘。另外 offset 是视图坐标下的偏移量,如果你的实体有旋转或缩放,需要转换到世界坐标。简单实例没做变换,直接加就行。
3.3 序列化:存盘再打开,数据不能丢
dwgOutFields 和 dwgInFields 是 DWG 格式的读写,dxfOutFields 和 dxfInFields 是 DXF 的。简单实例里存两个 double 和一个点。
Acad::ErrorStatus MySimpleEnt::dwgOutFields(AcDbDwgFiler* filer) const { assertWriteEnabled(); filer->writePoint3d(mCenter); filer->writeDouble(mRadius); return Acad::eOk; } Acad::ErrorStatus MySimpleEnt::dwgInFields(AcDbDwgFiler* filer) { assertReadEnabled(); filer->readPoint3d(&mCenter); filer->readDouble(&mRadius); return Acad::eOk; }这里有个后悔药:assertWriteEnabled 和 assertReadEnabled 在 Release 下是空宏,但在 Debug 下会检查打开状态。如果你在只读状态下调 dwgOutFields,Debug 版直接断言失败,Release 版静默写脏数据。所以序列化函数里千万别做业务逻辑,只读写字段。
DXF 的读写类似,但要注意组码。简单实例里我用 10/20/30 存点,40 存半径。组码选错了,别的软件读你的 DXF 就是乱码。
4. 避坑与排查:自定义实体最容易翻车的五个地方
4.1 实体创建后看不见,命令行也不报错
现象:命令执行成功,模型空间里啥也没有。原因通常是 worldDraw 返回了 kFalse,或者法向量为零,或者几何在视图范围外。解决:先在 worldDraw 里加一个固定的测试圆,确认回调被调用了;再检查法向量和坐标。我一般会在 worldDraw 开头写一句 acutPrintf 打印坐标,虽然土但管用。
4.2 存盘再打开,实体变成“代理对象”
现象:DWG 里显示“代理实体”,无法编辑。原因是类没有正确注册,或者 ACRX_DXF_DEFINE_MEMBERS 的应用名和模块名不一致。解决:检查 acrxEntryPoint 里 rxInit 是否调用,检查宏的最后一个参数是否和 ARX 文件名一致。另外,如果实体是在别的机器上创建的,目标机器没装你的 ARX,也会变代理,这是正常现象。
4.3 夹点拖动时实体闪烁或位置跳变
现象:拖动夹点,实体跟着动但位置不对,或者松手后弹回原位。原因是 subMoveGripPointsAt 里改了数据但没触发重绘,或者 offset 坐标系搞错了。解决:确保 mCenter 是成员变量,改完直接返回 eOk;如果实体有变换矩阵,用 AcGeMatrix3d 转换 offset。简单实例没矩阵,直接加。
4.4 删除实体时崩溃
现象:选中实体按 Delete,AutoCAD 直接退出。原因是析构函数里访问了已释放的资源,或者 getGeomExtents 返回了非法范围。解决:析构函数里只做 delete 自己的成员,不碰数据库;getGeomExtents 里确保 extents 被正确设置,别返回空范围。
4.5 多文档环境下实体数据串了
现象:开两个 DWG,在 A 里改实体,B 里的实体也跟着变。原因是用了全局变量或静态成员存数据。解决:所有业务数据必须是实例成员,跟 AcDbEntity 走。ObjectARX 的多文档机制下,静态变量是共享的,这个坑我见过不止一次。
5. 进阶技巧:用 getGeomExtents 和 subIntersectWith 把实体做“真”
5.1 getGeomExtents 不只是范围框,还是选择集的依据
getGeomExtents 返回实体的包围盒,AutoCAD 用它做窗口选择、缩放范围、打印裁剪。简单实例里返回圆心加减半径就行,但要注意 Z 轴范围。如果返回的 extents 是空的,实体选不中,打印也丢。
Acad::ErrorStatus MySimpleEnt::getGeomExtents(AcDbExtents& extents) const { AcGePoint3d minPt(mCenter.x - mRadius, mCenter.y - mRadius, mCenter.z); AcGePoint3d maxPt(mCenter.x + mRadius, mCenter.y + mRadius, mCenter.z); extents.set(minPt, maxPt); return Acad::eOk; }参数说明:set 的两个点必须 min 小于 max,否则 extents 无效。Z 轴如果实体是平面的,给同一个值就行。
5.2 subIntersectWith:让自定义实体参与修剪和延伸
默认情况下,自定义实体不参与 IntersectWith 操作,修剪、延伸、圆角都找不到它。要实现这个,得重写 subIntersectWith。简单实例里我让它和直线求交,返回交点。
Acad::ErrorStatus MySimpleEnt::subIntersectWith( const AcDbEntity* pEnt, AcDb::Intersect intType, AcGePoint3dArray& points, Adesk::GsMarker thisGsMarker, Adesk::GsMarker otherGsMarker) const { // 只处理直线,其他类型返回 eOk 不处理 if (pEnt->isKindOf(AcDbLine::desc())) { AcDbLine* pLine = AcDbLine::cast(pEnt); if (pLine) { // 简化处理:把直线投影到圆所在平面,求与圆的交点 AcGePoint3d p1 = pLine->startPoint(); AcGePoint3d p2 = pLine->endPoint(); // 这里省略具体几何计算,实际项目用 AcGeCircArc3d 求交 // 交点 append 到 points } } return Acad::eOk; }这个函数是自定义实体从“能看”到“能用”的分水岭。没有它,你的实体就是个孤岛,用户没法用常规编辑命令操作它。实现时注意:intType 区分实际交点和延伸交点,thisGsMarker 和 otherGsMarker 用于子实体级别的求交,简单实体传 0 就行。
5.3 一个验证习惯:用 DXF 组码反查序列化是否正确
我一般会在实体存盘后,用 AutoCAD 的 DXFOUT 命令导出 DXF,然后搜自定义组码。如果组码对不上,说明 dxfOutFields 写错了。这个习惯帮我省了无数次“文件打不开”的排查时间。另外,DWG 的序列化版本号要和 AcDb::kMReleaseCurrent 对齐,跨版本兼容时尤其注意。
最后说个教训:自定义实体的第一个版本,千万别堆功能。我见过有人一上来就做带孔、带槽、带阵列的复杂实体,结果夹点逻辑和序列化搅在一起,改一个参数崩三个地方。先把“一个点 + 一个半径”跑通,存盘、打开、拖动、删除都正常了,再往上加业务。这个顺序反了,后面全是还债。希望帮到你。
本文还有配套的精品资源,点击获取