1. 从一次数据丢失事故说起为什么需要扩展记录几年前我在做一个建筑构件管理模块需要把每个图元的加工参数、材料批次、安装日期这些信息挂到CAD实体上。当时第一反应是用扩展数据XData毕竟这是最常规的做法。结果项目上线没多久就出问题了——有个客户的图纸里部分参数莫名其妙丢失排查了半天才发现是XData的容量限制和类型约束导致的。那次事故之后我开始认真研究扩展记录Xrecord才发现这东西才是真正适合存储结构化、大批量自定义数据的方案。扩展记录英文叫Xrecord是ObjectARX里专门用来存储任意类型数据的一种对象。它跟XData最大的区别在于XData是挂在实体上的附属数据有大小和类型限制而Xrecord是一个独立的数据库对象通过扩展字典Extension Dictionary挂接到实体或字典上能存储几乎任意类型、任意数量的数据。你可以把它理解成给CAD对象配了一个随身硬盘想存什么就存什么想存多少就存多少。这篇文章适合已经接触过ObjectARX基础开发、用过扩展数据但遇到瓶颈的开发者。如果你正在做需要给图元附加复杂业务数据的项目比如BIM构件信息管理、设备台账、工艺参数存储那Xrecord基本是绕不开的技术点。我会从设计思路、核心原理、代码实操到踩坑经验把这块内容完整拆一遍。2. 扩展记录的核心设计思路与选型考量2.1 为什么不用XData而选Xrecord很多人刚接触ObjectARX时习惯性地用XData解决所有自定义数据存储问题。XData确实方便一个setXData就能搞定但它有几个硬伤第一每个实体的XData总大小不能超过16KB第二数据类型受限只支持有限的几种组码类型第三所有数据挤在一个平铺列表里没有层级结构数据一多就难以维护。Xrecord的设计思路完全不同。它是一个独立的AcDbXrecord对象存储在对象的扩展字典里。扩展字典本身是一个AcDbDictionary你可以往里面放多个Xrecord每个Xrecord有自己的名字键相当于给数据做了分类归档。更关键的是Xrecord内部使用结果缓冲区链表Resbuf Chain来存储数据支持的数据类型覆盖了AutoCAD几乎所有基本类型——字符串、整数、实数、点坐标、对象ID、甚至嵌套的Resbuf列表。我个人的选型原则很简单数据量小、结构简单、只需要存几个标记值用XData就够了数据量大、结构复杂、需要分类管理、或者需要存储对象引用果断上Xrecord。实际项目里我基本是两者配合使用——XData存一个简单的索引标记真正的业务数据全部放Xrecord。2.2 扩展字典的层级结构设计理解Xrecord之前必须先搞清楚扩展字典的组织方式。每个AcDbObject包括实体都可以有一个扩展字典通过createExtensionDictionary()创建。这个字典是一个标准的AcDbDictionary你可以往里面添加任意多个条目每个条目可以是Xrecord也可以是嵌套的字典。我通常采用这样的层级设计实体的扩展字典下面先建一个子字典作为命名空间比如叫MYAPP_DATA然后在子字典里再放多个Xrecord分别存储不同类别的数据。这样做的好处是避免跟其他应用程序的数据冲突因为扩展字典是所有应用共享的如果你直接往根字典里塞东西键名很容易撞车。// 层级结构示意 // 实体 - 扩展字典 - MYAPP_DATA子字典 - Params Xrecord // - History Xrecord // - Materials Xrecord这种设计还有一个好处当你需要清理自己应用的数据时只需要删除MYAPP_DATA这个子字典所有相关数据一次性清空不会误删别人的东西。我在实际项目中吃过亏——早期直接把Xrecord挂在根扩展字典上后来要卸载模块时发现根本分不清哪些是自己的数据只能一个个判断键名前缀非常狼狈。2.3 数据类型的规划策略Xrecord内部的数据以Resbuf链表形式存储每个节点包含一个组码DXF Group Code和对应的值。组码决定了数据类型1是字符串70是16位整数40是双精度实数10/11/12是三维点330是对象ID等等。规划数据类型时我建议遵循两个原则。第一尽量用语义明确的组码不要全部用1字符串然后自己解析。比如存储一个尺寸值用40存实数比用1存字符串好得多读取时不需要做类型转换也不会因为区域设置导致小数点解析问题。第二对于复杂结构用嵌套的Resbuf列表。比如你要存一个材料信息包含名称、密度、成本三个字段可以构造一个子列表用组码0作为列表开始标记把三个字段包在一起。注意Xrecord的Resbuf链表中组码的选择虽然灵活但某些组码有特殊含义如0表示列表开始/结束使用时需要参考DXF组码规范避免踩到保留组码的坑。3. 核心API解析与关键细节3.1 AcDbXrecord类的核心方法AcDbXrecord类本身的方法并不多核心就是两个setFromRbChain()和rbChain()。前者用于把一条Resbuf链表的数据写入Xrecord后者用于把Xrecord的数据读出来变成Resbuf链表。这一写一读构成了Xrecord操作的全部基础。// 写入数据 AcDbXrecord* pXrec new AcDbXrecord(); resbuf* pRb ...; // 构造好的数据链表 pXrec-setFromRbChain(*pRb); // 读取数据 resbuf* pRbOut NULL; pXrec-rbChain(pRbOut); // 使用完毕后需要释放 acutRelRb(pRbOut);这里有个容易忽略的细节setFromRbChain()会复制传入的Resbuf数据所以调用之后你可以安全地释放自己的链表。但rbChain()返回的链表是动态分配的必须用acutRelRb()释放否则就是内存泄漏。我在早期代码里经常忘记释放跑批量处理时内存一路飙升最后CAD直接崩掉。3.2 扩展字典的创建与访问往实体上挂Xrecord标准流程是先获取或创建实体的扩展字典然后在字典里添加Xrecord条目。这里的关键是获取或创建这个逻辑——扩展字典可能已经存在比如其他应用创建的也可能不存在需要判断。AcDbObjectId getOrCreateExtensionDict(AcDbObject* pObj) { AcDbObjectId dictId pObj-extensionDictionary(); if (dictId AcDbObjectId::kNull) { pObj-createExtensionDictionary(); dictId pObj-extensionDictionary(); } return dictId; }拿到扩展字典ID后打开字典用setAt()方法添加Xrecord。setAt()的签名是setAt(const ACHAR* name, AcDbObjectId id)name就是键名id是Xrecord的对象ID。如果同名键已存在setAt()会覆盖旧值所以写入前最好先检查一下。3.3 Resbuf链表的构造技巧构造Resbuf链表是Xrecord操作中最繁琐也最容易出错的部分。标准做法是用acutNewRb()逐个分配节点然后串联起来。但这种方式代码冗长而且容易忘记设置链表末尾的NULL。我后来封装了一个辅助类用RAII的方式管理Resbuf的生命周期构造时自动串联析构时自动释放。这样代码简洁很多也不容易泄漏。对于嵌套列表需要特别注意组码0的使用——组码0在Resbuf中表示列表的开始和结束构造嵌套结构时要在子列表前后各加一个组码0的节点。// 构造一个包含字符串和实数的简单链表 resbuf* pHead acutNewRb(AcDb::kDxfText); // 组码1字符串 pHead-resval.rstring _tcsdup(_T(构件A)); pHead-rbnext acutNewRb(AcDb::kDxfReal); // 组码40实数 pHead-rbnext-resval.rreal 125.5; pHead-rbnext-rbnext NULL;提示字符串类型的Resbuf节点resval.rstring需要动态分配内存acutRelRb()释放链表时会自动释放这些字符串所以不要手动free否则会双重释放。4. 完整实操流程从零实现一个构件参数管理器4.1 场景定义与数据结构设计假设我们要做一个构件参数管理功能每个构件需要存储三类信息基本信息构件编号、名称、类型、几何参数长度、宽度、高度、材料信息材料名称、密度、成本。我用一个子字典COMPONENT_DATA来组织里面放三个Xrecord分别对应三类信息。数据结构设计如下表Xrecord键名存储内容组码规划BasicInfo编号、名称、类型1, 1, 70Geometry长、宽、高40, 40, 40Material材料名、密度、成本1, 40, 40这种拆分方式的好处是读取时按需加载比如只需要显示基本信息时不用把几何和材料数据也读出来。对于大型图纸这种优化能明显提升响应速度。4.2 写入Xrecord的完整代码实现先看写入部分的完整流程。核心步骤是获取实体扩展字典、创建或获取子字典、构造Resbuf链表、创建Xrecord并写入、把Xrecord添加到子字典。bool writeComponentData(AcDbObjectId entId, const CString code, const CString name, int type, double length, double width, double height, const CString matName, double density, double cost) { AcDbObjectPointerAcDbEntity pEnt(entId, AcDb::kForRead); if (pEnt.openStatus() ! Acad::eOk) return false; // 获取或创建扩展字典 AcDbObjectId extDictId pEnt-extensionDictionary(); if (extDictId AcDbObjectId::kNull) { pEnt-upgradeOpen(); pEnt-createExtensionDictionary(); extDictId pEnt-extensionDictionary(); } // 获取或创建子字典 AcDbObjectPointerAcDbDictionary pExtDict(extDictId, AcDb::kForRead); AcDbObjectId subDictId; if (pExtDict-getAt(_T(COMPONENT_DATA), subDictId) ! Acad::eOk) { pExtDict-upgradeOpen(); AcDbDictionary* pSubDict new AcDbDictionary(); pExtDict-setAt(_T(COMPONENT_DATA), pSubDict, subDictId); pSubDict-close(); } // 写入BasicInfo { resbuf* pRb acutNewRb(AcDb::kDxfText); pRb-resval.rstring _tcsdup(code); pRb-rbnext acutNewRb(AcDb::kDxfText); pRb-rbnext-resval.rstring _tcsdup(name); pRb-rbnext-rbnext acutNewRb(AcDb::kDxfInt16); pRb-rbnext-rbnext-resval.rint type; pRb-rbnext-rbnext-rbnext NULL; AcDbObjectPointerAcDbDictionary pSub(subDictId, AcDb::kForWrite); AcDbXrecord* pXrec new AcDbXrecord(); pXrec-setFromRbChain(*pRb); pSub-setAt(_T(BasicInfo), pXrec, pXrec-objectId()); pXrec-close(); acutRelRb(pRb); } // Geometry和Material的写入逻辑类似此处省略重复代码 // ... return true; }这段代码里有几个关键点值得展开说。第一AcDbObjectPointer是ObjectARX提供的智能指针模板能自动管理对象的打开和关闭避免忘记close导致的死锁。第二upgradeOpen()的使用时机很重要——当你以只读方式打开对象后需要修改必须先升级为写模式。第三setAt()添加Xrecord后Xrecord对象的所有权转移给了字典你只需要close自己的引用即可。4.3 读取Xrecord数据的实现读取流程跟写入对称打开实体、获取扩展字典、找到子字典、打开指定Xrecord、调用rbChain()获取数据、解析Resbuf链表。bool readBasicInfo(AcDbObjectId entId, CString code, CString name, int type) { AcDbObjectPointerAcDbEntity pEnt(entId, AcDb::kForRead); if (pEnt.openStatus() ! Acad::eOk) return false; AcDbObjectId extDictId pEnt-extensionDictionary(); if (extDictId AcDbObjectId::kNull) return false; AcDbObjectPointerAcDbDictionary pExtDict(extDictId, AcDb::kForRead); AcDbObjectId subDictId; if (pExtDict-getAt(_T(COMPONENT_DATA), subDictId) ! Acad::eOk) return false; AcDbObjectPointerAcDbDictionary pSubDict(subDictId, AcDb::kForRead); AcDbObjectId xrecId; if (pSubDict-getAt(_T(BasicInfo), xrecId) ! Acad::eOk) return false; AcDbObjectPointerAcDbXrecord pXrec(xrecId, AcDb::kForRead); resbuf* pRb NULL; if (pXrec-rbChain(pRb) ! Acad::eOk) return false; // 解析链表 resbuf* pCur pRb; if (pCur pCur-restype AcDb::kDxfText) { code pCur-resval.rstring; pCur pCur-rbnext; } if (pCur pCur-restype AcDb::kDxfText) { name pCur-resval.rstring; pCur pCur-rbnext; } if (pCur pCur-restype AcDb::kDxfInt16) { type pCur-resval.rint; } acutRelRb(pRb); return true; }解析链表时我习惯先判断restype再取值这样即使数据结构有变化比如版本升级加了字段也不会因为类型不匹配导致崩溃。另外rbChain()返回的链表一定要释放这个前面强调过了。4.4 数据更新与删除的处理更新Xrecord数据有两种方式一种是删除旧的Xrecord重新创建一个另一种是打开现有Xrecord调用setFromRbChain()覆盖。我推荐后者因为对象ID保持不变如果有其他地方引用了这个Xrecord的ID不会失效。// 更新方式打开现有Xrecord重新设置数据 AcDbObjectPointerAcDbXrecord pXrec(xrecId, AcDb::kForWrite); pXrec-setFromRbChain(*pNewRb);删除Xrecord就简单了从子字典里erase()掉对应的键即可。但要注意erase()只是从字典里移除条目Xrecord对象本身如果没有其他引用会被数据库回收。如果你想彻底删除还需要调用erase()后确保没有悬空引用。注意删除Xrecord时如果该Xrecord正被其他代码以写模式打开erase()会失败。实际项目中要做好错误处理不要假设删除一定成功。5. 常见问题与排查技巧实录5.1 Xrecord写入后读不到数据这是最常见的问题表现是写入操作返回成功但读取时找不到Xrecord或者数据为空。排查思路按以下顺序进行第一检查扩展字典是否真的创建成功。有些实体类型比如某些代理实体不支持扩展字典createExtensionDictionary()会静默失败。可以在创建后立即检查extensionDictionary()是否返回非空ID。第二检查子字典的键名是否一致。CString比较是大小写敏感的写入时用COMPONENT_DATA读取时用Component_Data就会找不到。建议把键名定义为常量统一引用。第三检查事务状态。如果你在事务中写入数据但没有提交事务数据不会真正保存到数据库。ObjectARX的事务机制要求显式commit()忘记提交是新手常犯的错误。5.2 内存泄漏与崩溃问题Xrecord操作涉及大量动态内存分配稍不注意就会泄漏。我总结了几条经验rbChain()返回的链表必须用acutRelRb()释放不要用free()或delete。setFromRbChain()会复制数据传入的链表可以立即释放。字符串类型的Resbuf节点resval.rstring由acutRelRb()负责释放不要手动释放。使用AcDbObjectPointer管理对象生命周期避免忘记close。下面这个表格整理了我遇到过的典型内存问题问题现象原因解决方法批量处理后内存持续增长rbChain返回的链表未释放每次读取后调用acutRelRb程序退出时崩溃对象未close就析构使用AcDbObjectPointer字符串乱码rstring未用_tcsdup分配统一用_tcsdup复制字符串双重释放手动释放了rstring交给acutRelRb处理5.3 跨版本兼容性处理不同版本的CADXrecord的API基本稳定但有些细节有差异。比如早期版本对Unicode字符串的支持不完善如果你的代码需要兼容多个版本字符串处理要特别小心。我的做法是统一使用AcString或CString配合_tcsdup宏让编译器根据项目配置自动选择正确的字符类型。另外Xrecord存储的数据格式如果有变化比如增加了字段读取旧数据时要做好兼容。我的策略是在数据链表开头加一个版本号字段读取时先检查版本再决定用哪种解析逻辑。这样即使数据结构升级旧图纸里的数据也能正确读取。5.4 性能优化建议Xrecord的读写性能总体不错但在处理大量实体时还是要注意几点。第一批量操作时尽量使用事务把多个写入操作包在一个事务里减少数据库提交次数。第二读取时按需加载不要一次性把所有Xrecord都读出来。第三如果某个Xrecord的数据量特别大比如存储了几百个点的坐标考虑拆分成多个Xrecord避免单个链表过长影响解析速度。我在一个包含上万构件的项目里做过测试采用子字典分类存储后按需读取的响应时间比全量读取快了将近一个数量级。这个优化对于交互式应用来说非常关键用户点选一个构件时不应该等待所有数据加载完毕。6. 几个实战中总结的进阶技巧6.1 用Xrecord存储对象引用Xrecord支持组码330软指针和340硬指针来存储其他对象的ID。这个特性非常有用比如你可以把构件的标注文字、引用的材料表对象ID存在Xrecord里建立对象之间的关联关系。硬指针340会阻止被引用对象被删除软指针330则不会。选择哪种取决于业务逻辑——如果引用对象被删除后你的数据就失效了用硬指针如果允许引用对象独立删除用软指针。6.2 嵌套字典实现复杂层级除了Xrecord扩展字典里还可以放子字典子字典里再放子字典理论上可以构建任意深度的层级结构。我在一个设备管理项目里用这种方式实现了系统-子系统-设备-部件四级结构每级都是一个字典最底层挂Xrecord存具体参数。这种设计的扩展性极好增加层级不需要改代码只需要在字典里加条目。6.3 数据校验与容错从Xrecord读取数据时永远不要假设数据一定存在或格式一定正确。图纸可能被其他程序修改过或者用户手动删除了某些数据。我的做法是每个读取函数都返回bool表示成功与否调用方根据返回值决定后续处理。对于关键数据读取失败时给出明确的提示而不是静默返回默认值——后者会让问题更难排查。6.4 调试技巧用LISP验证数据开发过程中我经常用AutoLISP来快速验证Xrecord的数据是否正确写入。LISP的dictsearch和entget函数可以直接查看扩展字典和Xrecord的内容比写C调试代码快得多。具体做法是在CAD命令行执行LISP表达式遍历实体的扩展字典打印出所有键名和数据。这个方法在排查数据写入了但读不到这类问题时特别有效能快速定位是写入端的问题还是读取端的问题。;; 查看实体扩展字典的LISP示例 (setq ent (car (entsel))) (setq dict (dictsearch (cdr (assoc -1 (entget ent))) COMPONENT_DATA)) (princ dict)这套调试方法帮我省了大量时间建议每个做ObjectARX开发的人都掌握基本的LISP调试技能。毕竟C编译一次要等半天LISP即时执行验证数据分分钟的事。6.5 数据迁移与版本升级当你的数据结构需要升级时比如增加字段、修改类型旧图纸里的数据需要迁移。我的做法是写一个批量处理工具遍历图纸中所有相关实体读取旧格式数据转换成新格式后重新写入。迁移过程中要保留原始数据备份万一转换出错还能回滚。这个工具最好做成独立命令让用户可以手动触发而不是在加载时自动执行——自动迁移一旦出问题影响面太大。实际项目中我还遇到过一种情况同一个图纸被不同版本的模块修改过数据格式混杂。处理这种问题时版本号字段就派上用场了。读取时先看版本号旧版本走兼容解析路径新版本走标准路径确保无论哪种格式都能正确读取。这种向前兼容的设计在长期维护的项目里几乎是必须的。
阅读完成 · 觉得有帮助?