简介面向AutoCAD二次开发者的ObjectARX自定义实体入门实例基于VC环境通过从AcDbLine派生自定义直线实体完整演示自定义实体从项目创建、类定义、DWG/DXF序列化、worldDraw绘制、数据库注册到命令调用与调试的流程。压缩包共70个文件涵盖h/cpp源文件、dsw/dsp工程文件、rc资源脚本及生成Arx/Dbx等编译产物核心代码与Visual Studio工程结构一目了然约5.08MB。目前已有1159人学习适合初学ObjectARX或需要理解自定义实体内部机制的CAD开发者参考。通过该实例可掌握acedRegApp注册、dwgIn/OutFields读写、dxf序列化等关键接口写法为后续开发复杂自定义实体打下基础。 做AutoCAD二次开发跑通ObjectARX的入门例子不难画条线、加个圆、注册个命令半天就能搞定。但需求一旦变成“这个图元得自己记住参数被选中、被拖动、保存再打开之后还能保持状态”常规套路就开始吃力了。这时候就得碰自定义实体Custom Entity。这篇文章我会用一个最简单的ObjectARX自定义实体实例——一个带半径和文字标记的圆——把从类定义、序列化、显示到调试的完整链路走一遍顺便把“ObjectARX无法调试”这个高频问题也拆开讲清楚。适合已经会用ObjectARX写基础命令、想在图纸里放进“活”图元的朋友。1. 先搞懂自定义实体到底比块强在哪1.1 块引用和自定义实体的本质区别很多刚入门的朋友会问我要做一个带参数的图形用块加属性不就行了吗为什么非要碰自定义实体块BlockReference加属性AttributeDefinition确实是大部分图纸场景的标准答案它不需要写代码、编辑也方便。但块的本质是“几何模板 属性表”属性只是附着在插入点上的数据和图形本身没有强绑定关系。你要做一个设备符号每个设备有编号、温度、状态用块属性当然能存但属性值的校验、联动、夹点拖动、实时刷新全套逻辑都得靠LISP或ARX在外部维护保存打开后的关联性也比较脆弱。自定义实体就完全不一样。它本质是一个继承自AcDbEntity的C类实体内部的数据就是图形数据的一部分。存入DWG时这些数据自动序列化打开图纸时这些数据自动还原用鼠标点选时它就是一个普通图元通过特性面板或夹点编辑时行为完全由你自己控制。一句话块像是在图纸上贴便签自定义实体是把数据直接刻进图纸里。1.2 什么场景才值得上自定义实体不是所有需求都值得写自定义实体。我的判断标准很简单图形本身需要“智能”且这种智能不太适合用块或XData表达。举几个典型场景参数化设备符号比如阀门、传感器、工装夹具几何外观完全由几个参数驱动改一个参数整体联动。带状态数据的图元比如工位布局图里的设备要记录坐标、编号、状态值并且要在选中时高亮、编辑时刷新。需要特殊夹点和拖动行为的对象比如管道标注、梁截面标注夹点位置和吸附规则要完全定制。反过来如果只是“几根线加一个文字”用Block是最稳妥的。自定义实体一旦写不好最轻的是图形显示异常严重的会直接把图纸存坏属于“收益高、风险也高”的技术选型要用在刀刃上。1.3 选择自定义实体需要承担的成本这个成本很难绕开第一个是序列化你自己类里的成员变量必须通过dwgInFields和dwgOutFields手动读写漏一个成员图纸保存再打开就丢数据。第二个是显示世界的worldDraw函数决定图元在屏幕上的样子画得不讲究会导致缩放旋转后变形或闪烁。第三个是缓存与拷贝语义实体被复制、镜像、写块时ObjectARX会调用对应的克隆方法如果成员里有指针就得深拷贝否则两个实体共享同一块内存跑起来就是幽灵Bug。这也是为什么我建议新手从“无指针的普通成员”开始练先把序列化和显示链路跑通再上复杂数据。2. 环境配置与项目骨架别在VS和SDK上翻车2.1 版本配对是第一条红线ObjectARX开发最先要确认的不是代码而是AutoCAD版本和Visual Studio版本必须严格对应。每个版本的ObjectARX SDK都会在文档里明确写出支持的VS版本实际开发中常常有人因为VS版本不对编译出来的ARX一加载就让CAD崩溃而且错误信息完全不指向版本问题。稳妥的做法是打开你安装的AutoCAD在命令行输入VERNUM查看副版本号然后到Autodesk官网下载对应年份的ObjectARX SDK再按SDK文档指定的VS版本安装。以我手头的项目为例用的SDK明确要求VS对应关系我就在同一台机器上装了两个版本的VS专门应对不同CAD版本的插件开发。另外一个高频坑是位数。AutoCAD有32位和64位两个分支ARX插件必须跟CAD位数一致。现在新版本CAD基本是64位但不少老项目还在32位环境跑编译配置里x86和x64一定要跟实际CAD匹配否则加载时报“无法找到模块”或者直接不加载你排查半天发现是配置选错了。2.2 创建项目还是手动搭骨架ObjectARX SDK安装后会提供项目向导模板装好之后新建项目直接选对应模板即可。如果你用的VS版本没找到模板也别慌手动建一个MFC DLL或Win32 DLL项目改几个配置就行。我个人的经验是无论向导还是手动最后都要检查以下几项头文件目录加上SDK的inc目录比如C:\ObjectARX\inc。库文件目录加上SDK的lib目录并选择对应CAD位数的那份。附加依赖项根据SDK版本添加rxapi.lib、acdb*.lib、acui*.lib等具体名字以SDK里的lib文件夹为准。字符集ARX内部使用宽字符建议项目统一使用Unicode字符集避免到处转码。这些配置看着琐碎但漏掉任何一项都会出现“LNK2019无法解析的外部符号”或者编译能过、加载即崩的情况。编译能过但加载即崩十有八九是库目录配错了版本。2.3 入口函数和命令注册的标准结构ARX插件入口是acrxEntryPoint。一个最小可用的入口大概是这样的#include acedCmd.h #include MyMarkEntity.h static void markCreate() { MyMarkEntity* pEnt new MyMarkEntity(); pEnt-setCenter(AcGePoint3d(0, 0, 0)); pEnt-setRadius(100.0); pEnt-setLabel(_T(MARKER)); AcDbDatabase* pDb acdbHostApplicationServices()-workingDatabase(); AcDbBlockTable* pBlockTable nullptr; if (pDb-getBlockTable(pBlockTable, AcDb::kForRead) ! Acad::eOk) { delete pEnt; return; } AcDbBlockTableRecord* pRecord nullptr; AcDbObjectId objId; if (pBlockTable-getAt(ACDB_MODEL_SPACE, pRecord, AcDb::kForWrite) Acad::eOk) { if (pRecord-appendAcDbEntity(objId, pEnt) Acad::eOk) pEnt-close(); else delete pEnt; pRecord-close(); } pBlockTable-close(); } extern C AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: acrxDynamicLinker-unlockApplication(pkt); acrxDynamicLinker-registerAppMDIAware(pkt); MyMarkEntity::rxInit(); // 关键不注册类保存后的图形无法识别 acedRegCmds-addCommand(_T(MYMARK), _T(MYMARK), _T(MYMARK), ACRX_CMD_MODAL, markCreate); break; case AcRx::kUnloadAppMsg: acedRegCmds-removeGroup(_T(MYMARK)); break; } return AcRx::kRetOK; }这里有个特别容易漏的步骤MyMarkEntity::rxInit()。很多新人写完了类也实现了序列化和显示但忘了在入口函数里调用类的rxInit()注册结果实体创建后一旦保存文件、关掉CAD再打开实体会变成一个“代理对象”看不到数据也编辑不了。原因很简单AutoCAD打开图纸时发现这个实体的类没有对应ARX模块加载只能把它当成代理对象处理。3. 核心代码拆解一个带文字标记的圆怎么“活”起来3.1 类定义和运行时宏注册自定义实体的类定义需要继承AcDbEntity并使用ARX提供的一组宏来生成运行时类型信息。以我的MyMarkEntity为例头文件是这样写的// MyMarkEntity.h #pragma once #include dbents.h class MyMarkEntity : public AcDbEntity { public: ACRX_DECLARE_MEMBERS(MyMarkEntity); MyMarkEntity(); virtual ~MyMarkEntity() default; virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler* pFiler) override; virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* pFiler) const override; virtual Adesk::Boolean worldDraw(AcGiWorldDraw* mode) override; virtual Acad::ErrorStatus transformBy(const AcGeMatrix3d xform) override; void setCenter(const AcGePoint3d pt); void setRadius(double dRadius); void setLabel(const ACHAR* pszLabel); private: AcGePoint3d m_ptCenter; double m_dRadius; ACHAR m_szLabel[64]; };源文件里用ACRX_DXF_DEFINE_MEMBERS宏来生成类的运行描述#include MyMarkEntity.h #include dbsymtb.h #include dbapserv.h #include acgi.h #include acedCmd.h const Adesk::Int32 kEntityVersion 1; ACRX_DXF_DEFINE_MEMBERS(MyMarkEntity, AcDbEntity, AcDb::kDHL_CURRENT, AcDb::kMReleaseCurrent, AcDbProxyEntity::kNoOperation, MYMARKENTITY)注意最后一个参数MYMARKENTITY是DXF类名全局唯一。如果你在同一个CAD进程里加载了两个都叫这个名的实体类哪怕它们在不同ARX模块里也会产生类名冲突后果常常是加载第二个模块时直接崩。取名字的时候尽量带项目前缀比如你公司缩写加实体名别用太通用的词。3.2 序列化DWG文件保存和打开的数据通道自定义实体区别于普通图元的核心就在于序列化。新建实体时你在内存里设置的半径、标签这些数据如果不写到DWG文件里保存再打开就全丢了。ARX的序列化通过重写dwgOutFields和dwgInFields两个虚函数实现。先看写出数据的实现Acad::ErrorStatus MyMarkEntity::dwgOutFields(AcDbDwgFiler* pFiler) const { assertWriteEnabled(); Acad::ErrorStatus es AcDbEntity::dwgOutFields(pFiler); if (es ! Acad::eOk) return es; // 第一件事写版本号以后类升级时这里有大用处 pFiler-writeInt32(kEntityVersion); pFiler-writeDouble(m_ptCenter.x); pFiler-writeDouble(m_ptCenter.y); pFiler-writeDouble(m_ptCenter.z); pFiler-writeDouble(m_dRadius); pFiler-writeString(m_szLabel); return pFiler-filerStatus(); }读取数据时一定要先读版本号再做分支处理。虽然当前版本只有1但等实体升到第2版、第3版时如果没有版本号控制旧图纸里的老实体会因为数据格式不一致而读出乱码。Acad::ErrorStatus MyMarkEntity::dwgInFields(AcDbDwgFiler* pFiler) { Acad::ErrorStatus es AcDbEntity::dwgInFields(pFiler); if (es ! Acad::eOk) return es; Adesk::Int32 nVersion 0; pFiler-readInt32(nVersion); if (nVersion 1 || nVersion kEntityVersion) return Acad::eMakeFilerError; double x 0.0, y 0.0, z 0.0; pFiler-readDouble(x); pFiler-readDouble(y); pFiler-readDouble(z); m_ptCenter AcGePoint3d(x, y, z); pFiler-readDouble(m_dRadius); pFiler-readString(m_szLabel, sizeof(m_szLabel) / sizeof(ACHAR)); return pFiler-filerStatus(); }序列化的读写顺序必须严格一致先写什么就先读什么顺序对不上轻则数据错位重则图档损坏。写完后测试时一定要养成“保存文件 - 关掉CAD - 重新打开 - 查看实体属性”的完整验证习惯只盯着内存看是发现不了序列化问题的。3.3 worldDraw控制图元在屏幕上的显示显示函数是自定义实体的门面。worldDraw里画的内容就是你在CAD里看到的内容它会在重生成、缩放、移动时被反复调用性能也要注意。我的实现很朴素画一个圆再加一行文字Adesk::Boolean MyMarkEntity::worldDraw(AcGiWorldDraw* mode) { if (mode nullptr) return Adesk::kFalse; // 画圆 mode-geometry().circle(m_ptCenter, AcGeVector3d::kZAxis, m_dRadius); // 画文字 if (m_szLabel[0] ! \0) { AcGiTextStyle textStyle; textStyle.setHeight(m_dRadius * 0.3); textStyle.setFont(_T(simplex.shx), Adesk::kFalse, Adesk::kFalse); mode-rawGeometry()-text(m_ptCenter, AcGeVector3d::kZAxis, AcGeVector3d::kXAxis, m_szLabel, static_castint(_tcslen(m_szLabel)), textStyle); } return Adesk::kTrue; }这里有两个细节值得注意第一画圆用mode-geometry()画文字用mode-rawGeometry()。geometry()里的坐标会跟随视图变换适合圆、线这类需要随视图缩放的几何rawGeometry()则保持原始比例适合文字避免文字跟着视图拉伸变形。第二worldDraw返回Adesk::kTrue表示自己已经完成了所有绘制逻辑。如果返回kFalseARX会认为你放弃绘制转而尝试用视图相关绘制或者干脆不显示。新手最常见的困惑就是“实体明明创建了但屏幕上什么都没有”检查一下这个返回值往往就能找到问题。3.4 transformBy实体移动和夹点变形的响应当用户用MOVE命令或者夹点拖动实体时ARX会调用transformBy传入变换矩阵。如果你的实体没有自己的几何数据直接调基类默认实现就能正常工作。但我这个实体保存了中心点坐标所以必须把这个矩阵应用到中心点上否则会出现“夹点显示变了圆还是在原地”的诡异现象Acad::ErrorStatus MyMarkEntity::transformBy(const AcGeMatrix3d xform) { m_ptCenter.transformBy(xform); return AcDbEntity::transformBy(xform); }这里先更新自己的成员再调基类用矩阵处理实体的基本变换。如果实体内部还有向量、法向量这种方向型数据要注意矩阵变换时向量和点用的是不同方法点用transformBy向量用transformBy但忽略平移分量。我在项目里就踩过一次把圆心当点更新了结果圆弧的法向量没转导致三维视角下圆显示成一条线。4. 调试实战解决“ObjectARX无法调试”的经典场景4.1 加载ARX模块是调试的前提很多人说“ObjectARX无法调试”我先问一句你的ARX模块真的加载进CAD了吗调试ARX和调试普通程序不一样ARX不是独立EXE它必须跑在AutoCAD进程里。直接按F5Visual Studio会因为找不到可执行文件而报错或者启动了一个空项目。正确的做法有两种。第一种把Visual Studio的调试启动程序设为acad.exe调试命令行参数可以不填启动后CAD会自己加载ARX然后你就能下断点调试。第二种先手动在CAD里用APPLOAD或NETLOAD加载ARX然后在“调试 - 附加到进程”里选中acad.exe再触发命令断点同样能命中。注意一个容易被忽略的问题如果项目里同时存在Debug和Release配置一定要确保加载的是Debug版本。Release版本经过优化很多变量会被优化掉断点打不上或者命中不了是常事而且报错信息也不直观。4.2 断点打不上的四个排查方向我整理过一份排查清单几乎覆盖了“无法调试”的常见原因现象可能原因对策按F5没有反应调试启动程序没设置设置成acad.exe并确认路径正确断点显示“未加载符号”加载的模块没有PDB调试文件切换到Debug配置勾选“启用本机代码调试”断点灰显不可用代码没有执行到或模块未加载先确认命令真的被调用再检查加载的DLL文件路径能加载但一执行就崩库目录版本不匹配检查SDK的lib目录是否对应AutoCAD版本和位数实际操作中“未加载任何符号”最常见。Debug编译后会生成.pdb文件Visual Studio就靠它把机器码映射回源代码。如果你手滑把PDB文件漏了或者输出了错误目录断点当然打不上。处理办法也很简单项目属性里确认“调试信息格式”选择“程序数据库”然后清空输出目录重新编译。4.3 加载即崩溃时怎么用日志快速定位ARX最怕的不是编译错误而是加载ARX的瞬间CAD直接崩溃。这种崩溃往往发生在acrxEntryPoint的kInitAppMsg分支里可能是某个类没注册、某个命令组重名、或者依赖的库没有加载。我的习惯是在入口处加一段防御性代码把初始化步骤写到日志文件里。比如case AcRx::kInitAppMsg: try { acrxDynamicLinker-unlockApplication(pkt); acrxDynamicLinker-registerAppMDIAware(pkt); WriteLog(_T(开始注册实体类)); MyMarkEntity::rxInit(); WriteLog(_T(开始注册命令)); acedRegCmds-addCommand(...); WriteLog(_T(初始化完成)); } catch (...) { WriteLog(_T(初始化异常)); return AcRx::kRetError; } break;当CAD崩溃时打开日志文件看最后一条写到了哪里问题范围基本就能缩小到那一行附近。这个方法在调试任何ARX插件时都非常管用尤其是你面对别人写的一堆代码时日志能帮你从“一团乱麻”里快速找到第一个断点。4.4 调试中的两个小习惯第一不要每次改完代码都重启CAD。ARX的调试过程中频繁重启CAD会消耗大量时间。在入口函数里做了正确的资源释放之后可以用APPLOAD反复加载卸载大部分代码修改都能热更新。第二检查命令组名是否唯一。如果你在AutoCAD里加载过旧版本插件旧命令组名可能还残留在当前会话里新插件注册同名命令时会失败。卸载不干净时还会出现“加载完成但命令不存在”的假象。所以我在卸载分支里一定会调用acedRegCmds-removeGroup并且每个命令都加上公司前缀降低冲突概率。5. 高频问题速查与避坑经验总结5.1 高频问题速查表我把这几年在自定义实体上踩过的坑整理成一张表遇到问题先对照这个查问题原因对策保存再打开实体变成代理对象ARX类没注册或运行时类没加载确认rxInit()已调用保存时ARX文件要随图分发图形能选中但屏幕不显示worldDraw返回了kFalse改为返回Adesk::kTrue检查绘制几何代码移动实体后图形留在原地transformBy里没更新自己的坐标成员在transformBy里对m_ptCenter调用transformBy加载ARX后CAD直接崩溃版本不匹配或类名重复核对SDK版本和VS版本检查DXF类名是否唯一打开图纸提示缺少ARX程序图里包含自定义实体但模块没加载分发插件给使用方或设置代理对象可编辑策略文字显示乱码字符编码不统一项目统一用TCHAR/ACHAR不要混用char实体无法复制或写块指针成员没有深拷贝重写subDeepClone等克隆方法实现深拷贝5.2 几个值得长期坚持的习惯自定义实体项目做得越久越会发现“能跑”和“稳定”是两回事。我给自己定了三条规则一是每次给实体类增加成员变量一定同步做四件事更新dwgOutFields、更新dwgInFields、更新transformBy、升版本号。这四件事漏掉任何一件都是等到用户把图纸发回来反馈“数据变了”时才被发现。二是别在生产图纸上直接测试。自定义实体一旦在序列化或克隆逻辑上出错可能把图纸文件弄坏而这种损坏往往是不可逆的。我习惯在一个专门的测试DWG里反复做“新建实体 - 保存 - 打开 - 编辑 - 再保存”的闭环全部通过之后才拿到真实项目里用。三是认识assertReadEnabled和assertWriteEnabled的作用。ARX内部有并发和写时复制机制在新版多文档环境下这两个断言能帮助你尽早发现实体访问权限问题。虽然我的简单实体没用到复杂权限但习惯性在读取成员时加assertReadEnabled()、在修改前加assertWriteEnabled()能避免很多只有特定操作顺序才会触发的离奇Bug。写自定义实体这件事真正难的不是代码量而是把ObjectARX当成一套完整图形数据库来理解。类注册管实体身份序列化管图形存储worldDraw管屏幕呈现transformBy管交互行为每一环都扣在一起。跟着这个实例跑通之后建议你再试着加一个夹点或者一个特性面板往实体里丢一个AcDbObjectId成员——能处理好这些进阶需求才算真正把自定义实体玩明白了。本文还有配套的精品资源点击获取
