☰
ObjectARX二次开发:数据库基础操作全解析(符号表、实体、事务)
2026/10/19 5:49:54 网站建设 项目流程

做 ObjectARX 二次开发,绕不开的第一块基石就是数据库基础操作。先说明一下,标题里的 objectArx 其实就是 ObjectARX,很多刚上手的朋友一看到“数据库”三个字就以为要去写 SQL、连服务器,其实完全不是一回事。ObjectARX 说的数据库,指的是 CAD 图形文件背后那一整套对象存储体系,DWG 打开后驻留在内存里的那个 AcDbDatabase。图层、块、文字样式、标注样式,包括你画的每一条直线、每一个圆,最终都以对象的形式存在这个数据库里。

这篇文章我不会讲多高深的东西,就把最常用、最基础但也是最重要的部分拆开过一遍:怎么拿到数据库对象、怎么读符号表、怎么加实体、怎么遍历数据库里的对象、事务和锁到底是怎么回事,最后附上我自己踩过的坑。文中的例子统一以“新建一个指定图层、往模型空间添加一个红色圆、再统计当前模型空间实体数量”这个小目标来演示,代码是 C++,环境是 Windows 下的 ObjectARX 开发环境。适合刚开始接触 ARX 的开发者,也适合从 LISP 或者 .NET 迁移过来想写原生插件的老手。

1. 先理解 ObjectARX 的“数据库”到底是个什么东西

1.1 对象数据库与 DWG 文件的关系

很多人在学习 ObjectARX 时,第一个概念就卡在这:数据库和图形到底是什么关系。你可以这样理解,DWG 文件在磁盘上是一个二进制文件,你用 CAD 打开它之后,文件内容被解析到内存中,组织成一个完整的数据结构,这个数据结构就是 AcDbDatabase。你在屏幕上看到的图形窗口,只是这个数据库内容的一种可视化渲染。

后面你写的代码里,不管是获取当前文档的数据库,还是新建一个数据库,本质上都是在操作这个内存里的对象树。保存的时候,数据库再把内存里的内容写回 DWG 文件。所以 ObjectARX 开发者常说的“增删改查”,查的是对象,改的也是对象,而不是传统关系数据库里的表记录。

这里有个很重要的类比:传统数据库有一张张表,表里有一行行记录;ObjectARX 的数据库是一棵对象树,根是 AcDbDatabase,往下挂的是各种符号表和字典,再往下才是具体的图元和非图形对象。你平时新建一个图层,就往图层符号表里加一条记录;你画一条直线,就往模型空间对应的块表记录里追加一个图元。搞清楚这棵树的层次,后面写代码会顺很多。

1.2 数据库的三层骨架:符号表、字典和对象

ObjectARX 的数据库内容大体可以归纳成三类东西:符号表、字典、对象。符号表是固定的集合,存放“定义类”的东西,比如图层、线型、文字样式、标注样式、块定义。每一类符号表都有对应的 C++ 类,这是你操作它们的人口。

符号表类存储内容常见操作
AcDbLayerTable图层定义新建图层、遍历图层、改颜色
AcDbBlockTable块定义获取模型空间、创建块定义
AcDbTextStyleTable文字样式查询和新建文字样式
AcDbDimStyleTable标注样式查询和新建标注样式
AcDbLinetypeTable线型定义查询线型、给图层指定线型

字典则是一种更灵活的容器,名字和对象 ID 的映射,适合存自定义数据和扩展数据,比如“组”就是存在组字典里的。而对象是数据库里真正干活的东西,包括图元(AcDbEntity 的派生类)和非图元对象(比如图层表记录这种)。理解这层结构以后,你看到代码里的 AcDbLayerTable、AcDbBlockTable 就不会觉得陌生,它们只是数据库里特定位置的那张表。

1.3 获取数据库的三种常见方式

获取数据库对象的途径基本上有三种,每一种对应不同的使用场景。

第一种最常用,就是获取当前激活文档的数据库:

AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();

这个指针你不用手动释放,它是宿主程序管理的。你也可以用acdbCurDwg(),效果类似,本质上是同一个东西,但 workingDatabase 写起来更直白,我习惯用这个。注意它返回的是当前文档的数据库,如果在非文档环境下调用,拿到的可能不是你想要的那个文档,这一点在对话框中尤其要注意。

第二种是自己新建一个内存数据库:

AcDbDatabase* pNewDb = new AcDbDatabase();

默认构造函数会创建一个带有初始设置的空白数据库,相当于在内存里开了一个空的 DWG。如果你想完全不带任何默认设置,可以传Adesk::kTrue参数,这种通常是用来做纯数据处理的。自己 new 出来的数据库,用完要自己 delete。

第三种是读取磁盘上的 DWG 文件到内存:

AcDbDatabase* pDb = new AcDbDatabase(); if (pDb->readDwgFile(_T("D:/test.dwg")) == Acad::eOk) { pDb->closeInput(Adesk::kTrue); // 后续可以对 pDb 进行读写操作 }

readDwgFile成功之后要调用closeInput,告诉数据库输入流已经结束,之后才能正常进行读写。如果你忘了这一步,后续对数据库做写操作可能会失败。保存文件则用pDb->saveAs(_T("D:/output.dwg")),读写完成后自己 new 的数据库记得 delete。

2. 数据库基础操作的第一关:符号表读写

2.1 符号表操作前必须搞懂的“打开与关闭”

ObjectARX 里所有从数据库取出来的对象,都有一个“打开”和“关闭”的概念。很多人第一次写 ARX 程序,最困惑的就是为什么一个对象还要 open、close,不能直接 new 完就用。道理其实很简单:AutoCAD 是有状态的宿主程序,数据需要保持一致性,所以在某个时刻只能允许一个模块以特定方式访问对象。

你调用 getSymbolTable 拿到符号表指针,这个操作内部其实已经完成了“打开”。用完必须调用 close() 释放。如果忘记关闭,轻则对象一直占着内存,重则后续对同一对象的写操作直接崩溃。我见过最典型的问题就是:代码写了一大段,运行第一次没问题,第二次就报“无法打开对象”,原因就是上一次没有正确关闭。

打开模式一般分三种:AcDb::kForRead只读、AcDb::kForWrite写、AcDb::kForNotify通知。判断一个对象是否存在用读模式就够了,新建或修改才需要写模式。写模式更加严格:同一时间只能有一个写者,不允许一边遍历一边写入。所以操作符号表时,先读后写是常见节奏。

2.2 新建图层:完整代码与逐行拆解

我直接给出一段新建图层的标准代码,这是数据库基础操作里最经典的一个场景:

void CreateDemoLayer() { AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); const TCHAR* layerName = _T("DEMO_LAYER"); // 以写模式打开图层表 AcDbLayerTable* pLayerTable = nullptr; if (pDb->getSymbolTable(pLayerTable, AcDb::kForWrite) != Acad::eOk) return; // 先判断同名图层是否已经存在 if (pLayerTable->has(layerName)) { pLayerTable->close(); return; } // 创建新的图层表记录 AcDbLayerTableRecord* pLayerRec = new AcDbLayerTableRecord(); pLayerRec->setName(layerName); // 添加到图层表 Acad::ErrorStatus es = pLayerTable->add(pLayerRec); if (es != Acad::eOk) { delete pLayerRec; pLayerTable->close(); return; } // 关闭记录和符号表 pLayerRec->close(); pLayerTable->close(); }

这里有几个细节值得讲。第一, new 出来的AcDbLayerTableRecord是暂时“游离”的对象,先设置名称,再通过图层表的 add 方法放进数据库。add 成功之后,数据库接管这个对象,所以后面关闭用的是 close(),不是 delete。如果 add 失败,说明没有真正进入数据库,此时必须手动 delete,否则内存泄漏。

第二,在添加之前一定先用 has 判断是否已存在。别嫌这一步多余,很多实际程序里的图层混乱、图面污染,都是因为重复创建同名图层造成的。has 判断和 add 之间最好不要再混入其它写操作,免得图层表状态发生变化。

2.3 遍历图层:迭代器和关闭顺序

有时候你要写一个批量处理工具,把所有图层名称列出来,或者统一清理空图层。遍历符号表就要用到迭代器。这里我给大家一段遍历图层的示例:

void ListAllLayers() { AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); AcDbLayerTable* pLayerTable = nullptr; if (pDb->getSymbolTable(pLayerTable, AcDb::kForRead) != Acad::eOk) return; AcDbLayerTableIterator* pIter = nullptr; if (pLayerTable->newIterator(pIter) != Acad::eOk) { pLayerTable->close(); return; } for (; !pIter->done(); pIter->step()) { AcDbObjectId layerId; pIter->getRecordId(layerId); AcDbLayerTableRecord* pRec = nullptr; if (acdbOpenObject(pRec, layerId, AcDb::kForRead) == Acad::eOk) { // 取图层名 const TCHAR* name = pRec->getName(); pRec->close(); } } delete pIter; pLayerTable->close(); }

迭代器本身是一个 C++ 对象,不是数据库对象,所以最后用 delete 而不是 close。这一点经常有人搞混。如果你在遍历过程中同时还要对当前记录做写操作,建议先把需要的对象 ID 收集到数组里,退出遍历之后再逐个打开、修改、关闭。在符号表迭代器处于激活状态时修改符号表内容,很容易导致迭代器失效。

2.4 块表和块表记录:为什么添加实体要先走这一步

符号表里最特殊、也最常用的是块表AcDbBlockTable。模型空间和图纸空间本身就是块表里的两条特殊记录,所有看得见的图元,都必须挂在某一条块表记录下面。所以你要往模型空间加一条直线,步骤必然是先读块表,再取出ACDB_MODEL_SPACE这条记录,写模式打开,然后追加图元。

AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); AcDbBlockTable* pBlockTable = nullptr; pDb->getSymbolTable(pBlockTable, AcDb::kForRead); AcDbObjectId modelSpaceId; pBlockTable->getAt(ACDB_MODEL_SPACE, modelSpaceId); pBlockTable->close(); AcDbBlockTableRecord* pModelSpace = nullptr; acdbOpenObject(pModelSpace, modelSpaceId, AcDb::kForWrite);

这里不要尝试直接通过对象 ID 去读块表记录而忽略前面的块表查询,因为模型空间的 ID 每次运行都要从块表里查出来。块表用完之后先关闭,再用 ID 打开模型空间记录,避免两个对象同时处于打开状态。这看起来是绕了一步,但这是保证数据库对象状态一致性的标准做法。

3. 实体级操作的常用套路:从添加实体到批量遍历

3.1 往模型空间添加直线和圆

实体操作是数据库基础操作的高频场景。我以一个红色圆的例子串一下完整流程:取得模型空间记录、构造实体、设置属性、追加到模型空间、依次关闭。

AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); AcDbBlockTable* pBlockTable = nullptr; pDb->getSymbolTable(pBlockTable, AcDb::kForRead); AcDbObjectId modelSpaceId; pBlockTable->getAt(ACDB_MODEL_SPACE, modelSpaceId); pBlockTable->close(); AcDbBlockTableRecord* pModelSpace = nullptr; acdbOpenObject(pModelSpace, modelSpaceId, AcDb::kForWrite); AcDbCircle* pCircle = new AcDbCircle(); pCircle->setCenter(AcGePoint3d(100, 100, 0)); pCircle->setRadius(50); pCircle->setColorIndex(1); // 1 号颜色是红色 if (pModelSpace->appendAcDbEntity(pCircle) == Acad::eOk) { pCircle->close(); } else { delete pCircle; } pModelSpace->close();

实体加到块表记录用的是appendAcDbEntity而不是符号表那个add,这是一个容易记混的点。实体构造完成后,如果没有成功挂接进数据库,必须手动 delete,否则又是一个内存泄漏。如果你还要设置实体的线型、图层、线宽,可以在追加之前用对应的 set 方法,但要注意图层名称需要先在图层表里存在,否则设置无效。

3.2 遍历模型空间实体并统计数量

统计当前模型空间有多少实体,是很多批处理工具的第一步。遍历方法如下:

AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); AcDbBlockTable* pBlockTable = nullptr; pDb->getSymbolTable(pBlockTable, AcDb::kForRead); AcDbObjectId modelSpaceId; pBlockTable->getAt(ACDB_MODEL_SPACE, modelSpaceId); pBlockTable->close(); AcDbBlockTableRecord* pModelSpace = nullptr; acdbOpenObject(pModelSpace, modelSpaceId, AcDb::kForRead); int count = 0; AcDbObjectIterator* pIter = pModelSpace->newIterator(); for (; !pIter->done(); pIter->step()) { AcDbObjectId entId = pIter->objectId(); AcDbEntity* pEnt = nullptr; if (acdbOpenObject(pEnt, entId, AcDb::kForRead) == Acad::eOk) { // 这里可以对 pEnt 做只读操作,比如统计类型 count++; pEnt->close(); } } delete pIter; pModelSpace->close();

注意这段代码里,我是先取 objectId,再通过 acdbOpenObject 打开实体。你也可以从迭代器直接拿到实体指针,但更推荐只拿 ID,因为这样职责更清晰,也方便后续批量删除。

3.3 按类型过滤:统计直线数量

很多时候我们不是统计所有实体,而是想统计“模型空间里有多少条直线”。用 ARX 的判断方式是用isKindOf加 RTTI 描述类:

if (pEnt->isKindOf(AcDbLine::desc())) { lineCount++; }

这是 ObjectARX 推荐的判断方式,性能好,语义清楚。不推荐通过字符串比较实体类名来做,因为 COM 接口里经常有这类写法,但在 ARX 这种 C++ 对象体系里,用 desc() 这种描述类机制才是最地道的做法。你如果统计的是圆,就换成AcDbCircle::desc()。

3.4 修改与删除实体的正确姿势

修改一个已有实体,和添加差不多,区别只是你要先通过某种方式拿到它的对象 ID,然后用写模式打开,修改,关闭。比如把模型空间里所有直线改到指定图层,框架是这样的:

AcDbObjectId entId = ...; // 从迭代器拿到 AcDbEntity* pEnt = nullptr; if (acdbOpenObject(pEnt, entId, AcDb::kForWrite) == Acad::eOk) { pEnt->setLayer(_T("DEMO_LAYER")); pEnt->close(); }

这里有一个铁律:以写模式打开后,只要不再使用这个指针,就要立刻关闭。不要想着先改一个、放着、再改下一个,因为写模式是独占的,你同时打开多个写对象轻则报错,重则引发图纸状态异常。删除对象则用pEnt->erase(),然后照样 close。erase 之后对象会从图形中消失,但内存对象还在,必须 close 释放引用。

4. 事务与文档锁:很多人忽略的重中之重

4.1 为什么说 ObjectARX 的数据库操作天生带事务

用过数据库的人都熟悉事务的概念:一组操作要么全部成功、要么全部回滚。ObjectARX 里虽然没有显式的 BEGIN TRANSACTION,但每个命令本身就处于一个事务上下文中。你的 ARX 命令在执行过程中对对象做的修改,会被 AutoCAD 记录下来,命令正常结束时统一提交;命令执行中如果抛出异常或者没有正常结束,那么你这一轮操作修改的东西会整体撤销。

这也是为什么你实现一个命令,往模型空间画了几条直线,Running 之后用 Undo 一次就能全部撤销。学会利用这个机制,可以在批处理工具里得到非常好的“后悔药”效果。反过来,如果程序崩溃,也可能导致事务无法正常提交,出现图形文件里残留半截修改的情况。所以我会强烈建议:ARX 命令里不要自己捕获异常后假装什么都没发生,该让调用栈上抛就上抛,让 AutoCAD 的事务管理去处理回滚。

4.2 什么时候需要手动事务

既然命令本身自带事务,是不是就不用管了?大多数情况下确实不用。但有些特殊场景需要手动事务,比如你在一个命令里弹出了模态对话框,对话框内部需要多步操作,并且希望这些步骤作为一个整体提交或回滚;又比如你在命令里通过代码修改了大量对象,希望在这中间做一次明确的回滚。

ObjectARX 提供了事务管理器接口,使用方式类似:

AcDbTransactionManager* pTransMgr = acdbTransactionManager(); pTransMgr->startTransaction(); // 执行一组数据库操作 pTransMgr->commitTransaction();

不过这里我要提醒一句:手动事务不是新手友好的东西,它有一套自己的对象状态管理规则,如果在事务里同一个对象被重复打开,或者事务和对象自动打开机制发生冲突,会出现非常难排查的异常。我的建议是:能靠命令级事务解决的问题,绝不手动开事务。等你把对象生命周期理解得足够透彻,再考虑手动事务的高级玩法。

4.3 文档锁:跨文档和对话框场景下的必备知识

数据库操作还有一个容易被忽略的隐藏武器:文档锁。AutoCAD 是典型的单文档界面宿主,同一时刻通常只有一个文档处于编辑状态。如果你在命令里弹出了一个模态对话框,对话框弹出期间,当前文档的消息循环被阻塞,但其他文档仍然可以被用户切换。如果你的代码在对话框过程中还要去操作其他文档,就必须先锁定目标文档,否则很容易引发不可预料的崩溃。

锁定文档的常规做法是通过文档管理器:

AcApDocument* pDoc = acDocManager()->curDocument(); acDocManager()->lockDocument(pDoc); // 执行跨文档或对话框期间的数据操作 acDocManager()->unlockDocument(pDoc);

锁文档是一种协作机制,不是强制锁。所有参与文档操作的模块都应该遵循“操作前锁定、操作后解锁”的顺序。锁的顺序如果不一致,还可能造成死锁,比如两个命令互相等待对方释放文档。我用过的排查手段是:先在调试器里看调用栈,确认当前代码是否已经持有锁,再决定要不要加锁。

5. 实操中踩过的坑与排查思路

5.1 对象未关闭引发的随机崩溃

这是我见过最多、也最难定位的一类问题。它的表现很有迷惑性:程序第一次运行完全正常,第二次再执行或者处理到某个特定分支时突然崩溃,报错信息通常是“eWasNotClosed”之类。原因基本就是某个对象打开后没有 close。

你以为不过是少写一行 close,但在 ObjectARX 这种严格的对象管理机制下,后果可能延迟出现,甚至出现在不相干的代码段里,排查起来极其痛苦。我现在写代码时已经养成习惯:每个打开对象的分支,close 和 return 一定成对出现;如果一个函数里有多个提前返回的分支,我会优先考虑用 RAII 的思想封装打开关闭逻辑,确保 close 一定会被执行。

5.2 符号表记录重复创建

图层、文字样式、标注样式这类“定义类”对象,如果每次执行命令都无条件 new 一条再加入符号表,运行第二次就会遇到冲突报错,或者更隐蔽地,图形文件里出现大量同名的定义垃圾。解决思路很简单:先读模式查一次,不存在再写模式加。但有一个细节容易踩:你以读模式打开符号表后,如果需要写,应该先关闭再以写模式打开,不要试图在同一个符号表指针上“升级”访问模式,那不是一个稳定的操作。

5.3 遍历过程中删除实体导致崩溃

如果你在模型空间的迭代循环里,打开一个实体、erase、close、然后再 step,大概率会在某一步崩溃。原因是迭代器的内部状态依赖于实体集合的一致性,你在遍历过程中修改集合,迭代器就失效了。正确做法是两段式处理:第一遍遍历收集要删除的实体 ID,第二遍再逐个打开、erase、close。我常用的结构是这样的:

AcDbObjectIdArray idsToDelete; // 遍历收集 idsToDelete for (int i = 0; i < idsToDelete.length(); i++) { AcDbEntity* pEnt = nullptr; if (acdbOpenObject(pEnt, idsToDelete[i], AcDb::kForWrite) == Acad::eOk) { pEnt->erase(); pEnt->close(); } }

5.4 数据库基础操作常见问题速查表

现象可能原因处理方法
第二次运行命令报对象已存在符号表记录重复添加new 前先 has 判断
程序偶发崩溃,伴随未关闭错误码对象打开后未 close检查所有提前返回分支
修改实体后图形无变化对象以只读模式打开改为 kForWrite 打开
遍历实体时崩溃迭代器过程中删除了实体先收集 ID 再逐个删除
新画实体没有进入当前文档使用了 new 出来的独立数据库确认使用 workingDatabase
读取 DWG 后写操作失败忘记 closeInputreadDwgFile 成功后调用 closeInput
命令可以执行但一直 Undo 无效异常被吞,事务未正常提交不捕获不该捕获的异常

我自己排查 ARX 数据库问题时,还有一个习惯:不确定某个对象是否正常关闭,就先在调试器里看引用状态和对象 ID 是否合法,而不是直接试运行。很多时候,静下心看一遍代码里的 open 和 close 配对情况,比反复调试更快。

这次就先分享到这儿。我做了几年 ObjectARX 开发,最大的感受是数据库基础操作看似琐碎,但所有高级功能,包括块属性操作、扩展数据、事件回调、批处理转换,最后都会落到打开、关闭、添加、遍历这些基本功上。把每一次对象的打开都当作一次资源申请、把每一次关闭都当作一条纪律来执行,后面的路会顺畅很多。如果你最近正在学习 ARX,不妨先写一个“新建图层 + 添加红色圆 + 统计模型空间实体数”的练习命令,亲手把这些流程跑通,比看十篇文章都管用。

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

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

立即咨询