干过几年AutoCAD二次开发的人迟早会遇到这样一个需求想在图纸的某个图元上偷偷挂一段自己的数据。设备编号、验收日期、负责人、报价信息这些数据要是写进文字对象用户随手一改就乱套要是存到外部数据库图纸一脱库、一外发数据就全丢了。这时候XData就是最省事的解决方案。XData全称eXtended Data扩展数据是AutoCAD从R11时代就提供的图元数据扩展机制专门用来在图形实体上附加自定义业务数据。它不需要额外文件随着DWG图纸一起保存、一起打开程序在读取图元时就能把数据原样取回来。在.Net环境下用起来非常简单注册一个应用名、拼一个结果缓冲区、调两个方法读写就完成了。不管你是做设备台账、图档管理系统还是给设计院的图纸加审核信息XData都是绕不开的入门必修课。这篇文章我直接用C#把读写、改造、避坑一次讲透代码你拿去就能抄。1. XData是个什么东西——先搞清它要解决什么问题先别急着写代码把XData的底子摸清楚后面踩坑的机会能少一半。很多人第一次接触到XData是在网上搜怎么给实体存附加数据搜出来一堆DXF组码看得一头雾水。实际理解起来并不复杂。AutoCAD里的每个图元Entity比如直线、圆、块引用、文字本质上都是一堆DXF数据的组合。AutoCAD在设计的时候就考虑到了扩展性允许开发者在这些标准数据之外再追加一段完全自定义的数据块。这段数据不参与图形几何计算不影响显示不参与打印它就是一个纯粹的信息挂架。你往上面挂什么它就保存什么用户用正常的CAD操作根本看不见只有通过程序才能读写。这正是XData最大的价值它能让图元在保持外观不变的前提下偷偷带上业务语义。数据随DWG文件走这是XData和外部数据库最本质的区别。图纸从工程师A手里传到工程师B手里XData跟着DWG一起走不依赖任何外部环境。你不需要额外维护一张关联表也不用担心重命名、移动路径导致关联失效。这一点在图纸外发、归档、审图时特别有用我做过一个项目客户要求把设计变更单号直接挂到变更涉及的所有图元上后续审计时只要点一下图元就能看到对应的变更单整个过程完全靠XData实现。1.1 一张随图漂移的便签纸XData的存储原理XData在AutoCAD内部到底是怎么存的说白了它就是一个由组码group code 值value构成的有序链表。组码决定了值的类型值就是实际的数据内容。在.Net环境下这个结构被封装成了ResultBuffer结果缓冲区和TypedValue类型化值两个类。TypedValue是单条记录ResultBuffer是一组记录的集合对应一段完整的XData。用生活里的东西类比一张图纸像一块白板图元像白板上的简笔画。XData就是在某幅简笔画下面贴了一张便利贴便利贴右上角写了一个命名标签便利贴正文才是你真正想写的业务数据。AutoCAD的图元可以贴无数张这样的便利贴但每张便利贴必须有不同的命名标签。这个标签就是注册应用名RegAppName。这里有一个非常关键的限制XData的便利贴不能乱贴。在AutoCAD的数据库里有一张专门的注册应用表RegAppTable所有允许使用的应用名都要先在这张表里登记。你没有注册就尝试写XDataAutoCAD会毫不犹豫地拒绝你抛出一个eRegAppNotFound异常。这其实是一个很好的设计它防止了不同插件之间数据互相污染。你想啊如果随便一个插件都能往图元上挂数据而且还能用相同的名字覆盖别人的数据那图的稳定性就没法保证了。所以应用名实际上起到了命名空间隔离的作用。XData还有几个硬性限制开发前必须刻进脑子里。一是单条字符串长度不能超过255个字符这个限制最容易被忽略很多人写设备备注时一写就是几百字运行直接报错。二是整段XData的数据量不能超过16KB更精确地说是16383字节这个体量放几十条短字符串没问题真要放大量明细数据就吃力了。三是二进制数据块单块不能超过127字节想直接往XData里塞一个复杂对象的二进制序列化结果基本不现实。这些限制决定了XData只适合承载轻量级业务数据数据量一大就要考虑扩展字典甚至外部数据库方案。1.2 组码即语言XData支持的数据类型全解既然XData的存储单位是组码那你就必须认识这些组码。这是XData的基础语言写代码的时候天天跟它打交道。我把常用组码整理成一张表直接照着用就行。组码数据类型说明与注意事项1001应用注册名XData数据头必须以它开头标识数据归属1000字符串最长255字符使用的频率最高1002控制字符串只能用来放{或}用于数据分组嵌套1003图层名存一个图层名称字符串1004二进制数据块最大127字节适合加密的短信息1005数据库句柄存另一个实体的ObjectId对应的句柄字符串1010-1013三维点依次对应点的X、Y、Z坐标经常成组出现1040实数双精度浮点数1041距离本质上还是实数语义上是距离1042比例因子同样是实数语义上是比例107016位整数取值范围-32768到32767107132位整数取值范围更大但注意它占4字节实际开发中最常用的组合就是1001 1000 1040 1070。比如要给一个图元记录编号CH-001长度12.5数量3那对应的TypedValue依次就是new TypedValue(1001, MY_APP) // 应用名 new TypedValue(1000, CH-001) // 编号字符串 new TypedValue(1040, 12.5) // 长度实数 new TypedValue(1070, 3) // 数量整数有一点必须提醒你应用名注册名在DXF规则里是字符串但如果用TypedValue来构造TypeCode用1001值直接传字符串就行。另外1071组码虽然能放32位整数但如果你是在老版本CAD环境上做数据交换建议优先用1070因为部分老工具对1071的支持不够好。真要用大整数时直接把数字转成字符串用1000存反而是最稳妥的。2. 动手前的准备开发环境与项目引用套路XData本身不复杂但搞AutoCAD二次开发环境的搭建经常能卡住新手一整天。我见过太多人在代码逻辑上没写错结果项目引用就配错了一调试就报无法加载程序集。这块我把我的习惯操作写出来照着做基本不会出问题。2.1 环境依赖与版本选型开发AutoCAD .Net插件基础环境是三件套安装了AutoCAD的电脑、Visual Studio、以及.NET Framework开发组件。AutoCAD版本对应着不同的.NET Framework版本选错版本插件加载时就会直接给你报程序集版本不匹配。以目前最主流的AutoCAD 2020到2024版本为例对应的是.NET Framework 4.7或4.8因此Visual Studio选择2019或2022都可以建项目时要选.NET Framework模板而不是.NET Core/5/6别选错了。如果你还在用AutoCAD 2016到2019对应的目标框架是.NET Framework 4.5到4.7老项目尽量别直接改框架版本牵一发动全身。需要引用的核心程序集有四个路径一般在AutoCAD安装目录下例如C:\Program Files\Autodesk\AutoCAD 2024下的AcDbMgd.dll托管封装核心数据库操作实体、事务、表记录都靠它AcMgd.dll托管封装图形系统与命令交互Editor、Application相关AcCoreMgd.dll核心命令与服务较高版本才引入AcCui.dll菜单等界面资源一般用不到右键项目引用添加引用浏览到AutoCAD目录把这几个dll选上然后在属性面板把复制本地改为False。这一条非常重要因为AutoCAD目录下的dll本来就有你没必要把它们输出到plugin目录有时候甚至会因为复制过去导致文件被占用连重新编译都报错。2.2 一个能跑起来的项目模板项目结构上新建一个C#类库项目就行输出类型是类库DLL不是可执行文件。AutoCAD本身是宿主程序你的DLL会被它加载。写完代码后要调试建议在项目属性→调试选项卡里把启动外部程序设置为C:\Program Files\Autodesk\AutoCAD 2024\acad.exe。这样按F5就会启动AutoCAD并自动加载你的程序集配合断点就能debug。代码里最基础的结构是这个样子using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Runtime; namespace XDataDemo { public class Commands { [CommandMethod(DEMO_HELLO)] public void Hello() { Document doc Application.DocumentManager.MdiActiveDocument; Editor ed doc.Editor; ed.WriteMessage(\n插件加载成功); } } }编译完成后在AutoCAD命令行输入NETLOAD选择生成的dll文件加载成功后输入DEMO_HELLO命令行能打印出信息就说明你的开发链路完全打通了。之后所有XData的开发都基于这个模板往里加逻辑。3. 核心实操XData的写入、读取与修改代码直接抄模板跑通了接下来就是正菜。这一章我把XData的增、查、改、删四套操作全部给你写出来。代码我尽量保持精简但关键的点一个不少。3.1 第一步注册应用名别漏了注册应用名是往图元上写XData的前置条件可以在每次写数据之前检查一下不存在就创建。下面的方法就是标准的注册逻辑可以单独抽出来复用private void EnsureRegApp(Transaction tr, Database db, string appName) { RegAppTable rat (RegAppTable)tr.GetObject(db.RegAppTableId, OpenMode.ForRead); if (!rat.Has(appName)) { rat.UpgradeOpen(); RegAppTableRecord rar new RegAppTableRecord(); rar.Name appName; rat.Add(rar); tr.AddNewlyCreatedDBObject(rar, true); } }注意两点第一rat.Has(appName)这个检查是必要的每次写XData前都跑一遍成本极低但能避免那个烦人的eRegAppNotFound异常。第二注册表记录的添加是在事务里完成的配合tr.AddNewlyCreatedDBObject(rar, true)事务提交后才会真正写入数据库。有些从老VB/VBA转过来的开发者可能习惯在程序初始化时就注册应用名这当然可以但如果插件某个功能先用到某个新应用名而初始化流程没覆盖运行时就可能踩坑。按需注册是最稳妥的。3.2 写入XData给图元挂数据注册完应用名下一步就是往目标图元上挂数据。用Editor的GetEntity方法让用户拾取一个图元然后构造ResultBuffer调用实体的SetXData方法。先看完整代码[CommandMethod(XDATA_WRITE)] public void WriteXData() { Document doc Application.DocumentManager.MdiActiveDocument; Database db doc.Database; Editor ed doc.Editor; PromptEntityResult per ed.GetEntity(\n请选择一个图元: ); if (per.Status ! PromptStatus.OK) return; using (Transaction tr db.TransactionManager.StartTransaction()) { EnsureRegApp(tr, db, MY_APP); Entity ent (Entity)tr.GetObject(per.ObjectId, OpenMode.ForWrite); ResultBuffer rb new ResultBuffer( new TypedValue(1001, MY_APP), new TypedValue(1000, CH-001), new TypedValue(1000, 离心泵), new TypedValue(1040, 7.5), new TypedValue(1070, 3) ); ent.SetXData(rb); tr.Commit(); ed.WriteMessage(\nXData写入成功共写入5条记录。); } }几个关键点展开说。第一是打开模式。tr.GetObject(per.ObjectId, OpenMode.ForWrite)里的OpenMode.ForWrite不能写成ForRead因为SetXData是写操作以只读方式打开实体会在运行时抛出对象已被打开供读取的异常。反过来读取XData时用ForRead就够了。第二是ResultBuffer的构造。多个TypedValue直接在构造器里传系统会按顺序组装成缓冲区数组。如果你需要动态追加就使用new ResultBuffer()后逐个rb.Add(new TypedValue(...))。顺序很重要1001必须是第一条这是XData数据的固定头格式不能颠倒。第三是SetXData是整体覆盖。这一点非常容易踩坑。SetXData会把实体上已有的所有XData全部替换掉不光是替换你自己应用名下的那部分其他应用名挂的数据也会被一并覆盖清除。所以如果图纸里已经存在别的插件写的XData你无脑调用SetXData就会误伤。正确的做法是先读取现有的完整XData把自己应用名下的数据从链表里摘出来更新再把全部数据写回去。这个我在后面问题排查章节专门讲。3.3 读取XData把数据取回来读XData用GetXData方法整个逻辑更简单不需要ForWrite只用ForRead[CommandMethod(XDATA_READ)] public void ReadXData() { Document doc Application.DocumentManager.MdiActiveDocument; Database db doc.Database; Editor ed doc.Editor; PromptEntityResult per ed.GetEntity(\n请选择要读取的图元: ); if (per.Status ! PromptStatus.OK) return; using (Transaction tr db.TransactionManager.StartTransaction()) { Entity ent (Entity)tr.GetObject(per.ObjectId, OpenMode.ForRead); ResultBuffer rb ent.GetXData(MY_APP); if (rb null) { ed.WriteMessage(\n该图元上没有MY_APP应用名下的XData。); return; } foreach (TypedValue tv in rb) { ed.WriteMessage(\n组码 {0}, 值 {1}, tv.TypeCode, tv.Value); } } }GetXData方法接收一个字符串参数作用是只返回这个应用名下的XData。如果你传null则返回图元上的全部XData不区分归属。返回值是ResultBuffer遍历它就能取出每一个TypedValue。遍历的时候有个小细节TypedValue.TypeCode是short类型不要直接和int比对时想当然。提取值时对象是装箱的里面可能是字符串、double、int需要根据组码做类型转换。例如foreach (TypedValue tv in rb) { switch (tv.TypeCode) { case 1000: string strVal tv.Value.ToString(); // 处理字符串 break; case 1040: double dblVal (double)tv.Value; // 处理实数 break; case 1070: short intVal (short)tv.Value; // 处理16位整数 break; } }之前有朋友问我为什么他读出来的1070值强制转换成int会报错就是因为1070组码对应的是16位整型在AutoCAD .Net API里被包装为short应该先转short再隐式转int。这种小坑一天不踩一次你都会怀疑自己写的是不是C#。3.4 修改与清除XData覆盖写入和定点移除修改XData没有专门的Update方法它的本质就是重新SetXData。最简单粗暴的更新就是重新构造一个ResultBuffer覆盖写。如果这个图元上只有你自己的应用名完全可以这样做[CommandMethod(XDATA_UPDATE)] public void UpdateXData() { // 选择图元、开始事务等前置步骤省略 // 重新构造新的ResultBuffer ResultBuffer rb new ResultBuffer( new TypedValue(1001, MY_APP), new TypedValue(1000, CH-001), new TypedValue(1000, 离心泵), new TypedValue(1040, 11.0), // 功率改为11.0 new TypedValue(1070, 5) // 数量改为5 ); ent.SetXData(rb); tr.Commit(); }覆盖写唯一要警惕的就是误伤他人的数据。在实际业务中一张图纸往往有多个插件在同一个图元上挂不同的XData。这时候你要做的是局部更新。思路是这样// 1. 读出现有XData全部内容 ResultBuffer allRb ent.GetXData(null); // 2. 遍历复制除目标应用名以外的TypedValue同时把目标应用名替换成新内容 ResultBuffer newRb new ResultBuffer(); bool replaced false; for (int i 0; i allRb.AsArray().Length; i) { if (allRb.AsArray()[i].TypeCode 1001 allRb.AsArray()[i].Value.ToString() MY_APP) { // 找到目标应用名用新数据替换 newRb.Add(new TypedValue(1001, MY_APP)); newRb.Add(new TypedValue(1000, CH-002)); replaced true; } else { newRb.Add(allRb.AsArray()[i]); } } // 3. 如果原图没有目标应用名则追加新数据 if (!replaced) { newRb.Add(new TypedValue(1001, MY_APP)); newRb.Add(new TypedValue(1000, CH-002)); } // 4. 写回 ent.SetXData(newRb);这里用到了allRb.AsArray()方法把ResultBuffer转成TypedValue数组遍历。这是处理复杂链表时最实用的手段掌握了这招你在XData世界里基本畅通无阻。清除XData也很简单给实体设置一个只包含1001应用名的空ResultBuffer就相当于把这个应用名下的所有XData都清掉如果直接ent.SetXData(null)或ent.SetXData(new ResultBuffer())则清空整个实体的所有XData这操作要慎用。4. 实战案例给设备块挂台账把XData用起来理论说了一大堆不如跑一个完整案例。这个案例我做过很多次非常典型给设备块挂设备台账每台设备是一个块引用需要在块上记录设备编号、设备名称、额定功率、数量和安装日期。做设计的人都知道这种信息放块属性里虽然用户看得见但容易被误改而XData对用户完全不可见程序读起来又方便。4.1 场景需求与整体设计需求拆开就是两件事一是把数据写进块引用二是能从块引用读出来方便统计和校验。数据项用XData来设计核心就是把业务字段映射成TypedValue记录。这里我会倾向混合使用类型而不是全都用字符串目的是让你感受一下不同类型组码的用法业务字段组码类型示例应用名头1001字符串DEVICE_INFO设备编号1000字符串CH-001设备名称1000字符串离心泵额定功率1040实数7.5数量107016位整数3安装日期1000字符串2025-01-15设计上我故意把设备编号和安装日期都放成字符串一个是编号可能带字母不宜用数值型一个是日期格式比较自由用DateTime对象不利于跨平台跨语言互读所以都统一成字符串。功率用实数数量用整数这样读出来能直接参与计算不用再做字符串解析。4.2 完整代码写入与读取命令写入命令核心逻辑就是前面注册应用名加SetXData的组合只是业务字段更丰富[CommandMethod(DEVICE_WRITE)] public void WriteDeviceData() { Document doc Application.DocumentManager.MdiActiveDocument; Database db doc.Database; Editor ed doc.Editor; PromptEntityResult per ed.GetEntity(\n请选择设备块: ); if (per.Status ! PromptStatus.OK) return; using (Transaction tr db.TransactionManager.StartTransaction()) { EnsureRegApp(tr, db, DEVICE_INFO); Entity ent (Entity)tr.GetObject(per.ObjectId, OpenMode.ForWrite); ResultBuffer rb new ResultBuffer( new TypedValue(1001, DEVICE_INFO), new TypedValue(1000, CH-001), new TypedValue(1000, 离心泵), new TypedValue(1040, 7.5), new TypedValue(1070, 3), new TypedValue(1000, 2025-01-15) ); ent.SetXData(rb); tr.Commit(); ed.WriteMessage(\n设备台账写入成功。); } }读取命令把XData里的内容解析成可读的字段[CommandMethod(DEVICE_READ)] public void ReadDeviceData() { Document doc Application.DocumentManager.MdiActiveDocument; Database db doc.Database; Editor ed doc.Editor; PromptEntityResult per ed.GetEntity(\n请选择要读取的设备块: ); if (per.Status ! PromptStatus.OK) return; using (Transaction tr db.TransactionManager.StartTransaction()) { Entity ent (Entity)tr.GetObject(per.ObjectId, OpenMode.ForRead); ResultBuffer rb ent.GetXData(DEVICE_INFO); if (rb null) { ed.WriteMessage(\n未找到设备台账信息。); return; } string devId , devName , installDate ; double power 0; int quantity 0; bool hasData false; foreach (TypedValue tv in rb) { switch (tv.TypeCode) { case 1000: if (!hasData) { devId tv.Value.ToString(); hasData true; } else if (devName ) { devName tv.Value.ToString(); } else { installDate tv.Value.ToString(); } break; case 1040: power (double)tv.Value; break; case 1070: quantity (short)tv.Value; break; } } ed.WriteMessage(\n设备编号: {0}, devId); ed.WriteMessage(\n设备名称: {0}, devName); ed.WriteMessage(\n额定功率: {0} kW, power); ed.WriteMessage(\n数量: {0}, quantity); ed.WriteMessage(\n安装日期: {0}, installDate); } }这个解析逻辑里我通过hasData布尔变量来区分1000组码的三个字符串依次填到编号、名称、日期里。实际项目里字段顺序请务必固定或者在每条字符串前再挂一个字段标识比如用IDCH-001这种格式然后程序按前缀解析。实践下来在只有一个应用名、字段又固定的前提下按顺序解析最快也够用。4.3 运行效果与验证把上面的命令装到插件里netload加载后画一个块引用命令行输入DEVICE_WRITE拾取块写入成功再输入DEVICE_READ拾取同一个块命令行按顺序打印出设备编号、名称、功率、数量、日期。这个过程就完成了XData的整个闭环。如果你想确认数据真正落到了DWG文件里可以把图纸保存、关闭、重新打开再执行DEVICE_READ数据依然在。这就是XData最让人放心的地方它不是存在内存里的临时数据而是实实在在写进了DWG文件的扩展区。用LIST命令查看块引用你看不到这些信息这正是我们希望的效果——业务数据对普通用户隐藏对程序可见。5. 常见问题与排查技巧实录XData本身不复杂但实际用起来各种奇怪问题还是层出不穷。我把这些年踩过的坑集中整理了一下按频率从高到低排了个序你照着排查基本能秒杀。5.1 eRegAppNotFound没注册就敢写报错信息类似eRegAppNotFound或者中文环境下的程序未注册。九成情况都是同一个原因构造ResultBuffer时用了某个应用名但RegAppTable里还没有这个名字的记录。排查流程是这样先在代码里查一下你是否调用了注册逻辑如果只是手工在DWG里执行了一次注册那换个图纸又没了。AutoCAD的应用注册表是存在DWG文件里的不同图纸之间互不相通所以每次写XData前都调一遍EnsureRegApp是最保险的。还有一种隐蔽的情况应用名大小写不一致。AutoCAD的注册名是区分大小写的注册时叫MY_APP使用时写my_app系统就认为两个不同的名字。这个问题在代码里很难一眼看出来建议所有应用名统一用常量字符串例如private const string APP_NAME MY_APP;杜绝手写带来的大小写不一致。5.2 XData写进去了读出来不对这类问题分几种症状。第一种是读取时返回null。最常见的原因是你GetXData时传了应用名但实体的XData里根本没有这个应用名。可以先改成ent.GetXData(null)试试如果传null能读到内容说明应用名不匹配检查大小写和前后空格。第二种是遍历时抛异常比如指定的转换无效。前面提过1070是short类型1040是double1000是string转换之前先判断TypeCode再强转不要一股脑ToString()或者直接转int。每一条TypedValue的Value类型都是和组码绑定的组码错了值就拆箱失败。第三种是数据被别的插件覆盖了。这种情况最隐蔽用户反馈XData明明写了过一会儿就没了。排查思路是看是否有其他插件也在对同一个图元调SetXData。由于SetXData是整体覆盖哪怕别的插件只写自己的应用名也会把你的XData清掉。解决办法就是在公共图元上采用读取-合并-回写的方式绝不直接SetXData把误伤的概率降到最低。5.3 16KB限制与字符串长度字符串超255字符会怎样写入时直接抛异常提示数据类型或值无效。16KB超限呢XData会被拒绝或者导致图形文件打开时报数据异常。这都不是吓唬你我都碰到过真实事故。所以做数据设计时有个原则XData只放标识性、摘要性数据明细数据放外部数据库或扩展字典。如果非要存长文本用分块存储每条字符串拆成200字符一段段号用另一个字段标识读取时再重组。这个方法能绕过字符串长度限制但也会让读取逻辑更复杂非必要不建议这么干。还有一点工程实践里经常有人把超长XML塞进XData。我真见过客户把BOM表序列化成XML写进XData的结果数据量轻松超过16KB后来被迫迁移到扩展字典。所以设计阶段就要把数据量预估进去不要等上线了再改。5.4 XData、扩展字典、块属性到底该用谁这是我被问得最多的问题。三种方案各有定位选错了后面麻烦不断。对比项XData扩展字典块属性数据量限制16KB/实体基本无限制受字符串长度限制数据类型基础类型二进制块任意DBObject仅字符串用户可见性不可见不可见可见且可编辑随图保存是是是开发门槛低中低适用场景轻量属性、标记类复杂业务对象、嵌套数据用户需要填写的属性我的选择建议很简单只是给图元加几个短标记字段比如状态、编号、审核人果断用XData数据结构复杂比如要挂子对象、嵌套列表用扩展字典用户在设计过程中需要自己看到和编辑的信息比如块的设计参数用块属性。这里补充一句XData和扩展字典还有块属性不是互斥关系完全可以同时用。比如块属性负责让用户填设备名称XData负责存程序侧的创建时间、校验状态各干各的互不干扰。我经手的项目里这种混合方案占了很大比例。6. 最后说点实在的XData这套API不难难的是在真实项目里怎么把数据设计得稳定、可维护。我在实际开发中最大的体会就是应用名的规划要趁早。一个项目里用哪些应用名每个应用名下挂哪些字段字段顺序是什么这些规则一定要在项目启动时定下来写成设计文档。否则等插件多了、图纸流转多了你再想统一改名或者调整字段顺序需要遍历海量图元做数据迁移那酸爽我体验过一次就不想再体验。另一个经验是写XData数据时顺手把写入版本号或时间戳也挂进去。比如在每个应用名下加一个整数字段作为版本号。这样后期排查数据异常、做数据升级时你一眼就能判断这份数据是哪一代程序写的该不该走迁移逻辑。这个习惯成本极低收益却非常实在。最后再分享一个小技巧XData里的句柄引用。如果你需要在XData中记录这个图元关联了另一个图元不要直接存ObjectId因为ObjectId在每次打开图纸时会变化跨会话不可靠。正确做法是存目标实体的Handle字符串组码1005使用时通过db.GetObjectId(false, new Handle(handleStr), 0)转回ObjectId。这个方案我测试过图纸关闭再打开后依然稳定可用。掌握这个小技巧XData就不只是挂几个字符串那么简单了你完全可以用它在图元之间建立一套轻量级关联网络很多业务流程都可以在这个基础上搭起来。