CAD 二次开发环境配置与 DWG 数据库基础
我们使用的对 CAD 进行二次开发的方式是:ZWCAD Runtime Extension
,简称 ZRX
。
它可以实现用户交互、识图管理、几何算法、文档管理、图型数据库操作等方式。
开发环境配置时需要安装 ZRXSDK
来使用。
其目录结构如下:
Inc:头文件
lib-x64:库文件
Arxport:ObjectArx的兼容文件
Doc/ Samples :文档/示例程序
utils:扩展库
Classmap:类派生关系图
Tools:向导安装包
这里我顺便安装了向导安装包,方便快速搭建项目。
项目初始化
注册/注销命令
下面的函数作为入口点,我们主要在 initapp()
函数内部实现命令的添加:
extern "C" AcRx::AppRetCode zcrxEntryPoint(AcRx::AppMsgCode msg, void* appId)
{
switch (msg)
{
case AcRx::kInitAppMsg:
{
acrxDynamicLinker->unlockApplication(appId);
acrxDynamicLinker->registerAppMDIAware(appId);
initapp();
}
break;
case AcRx::kUnloadAppMsg:
{
unloadapp();
}
break;
default:
break;
}
return AcRx::kRetOK;
}
创建命令
这里对命令管理的方式是:
- 分散注册,集中管理,集中注销
例如我需要注册一个命令,命令名称是:_HelloGlobal
HelloLocal
,可以实现在 CAD 命令行输出 Hello World!
。
一、编写命令函数:
这里是命令执行的内部逻辑,封装成一个函数。如果需要注册另一个命令,则封装到另一个函数里。(分散注册)
void helloworld()
{
acutPrintf(_T("\nHello World!"))
}
注册命令:
注:实际中全局名称应为
_T("_HelloGlobal")
,这里可能是示例简化。这里是进入到
initapp
函数,将上面实现的命令函数注册到这里。如果有其他的命令,也都在这个函数里进行注册。(集中管理)
void initapp()
{
acedRegCmds->addCommand(cmd_group_name, _T("HelloLocal"), _T("HelloLocal"), ACRX_CMD_MODAL, helloworld);
注销命令:
这里对
initapp
注册的所有命令都进行销毁。(集中注销)
void unloadapp()
{
acedRegCmds->removeGroup(cmd_group_name);
}
加载/卸载程序
手动加载
- Appload命令
- ZRX命令
自动加载
按需加载 – 启动时加载/执行命令时加载
[HKEY_LOCAL_MACHINE\SOFTWARE\ZWSOFT\ZWCAD\2026\zh-CN\Applications]
调试程序
通常使用附着的方式。
DWG 数据库
基础概念
- 块定义(AcDbblockTableRecord)是指将一个或多个对象组合成一个单一的对象,以便在图纸中多次使用
- 块引用(AcDbblockReference)是指在图纸中使用块定义的实例
- 模型空间是ZWCAD中的三维空间,常用于创建和编辑模型和草图
- 图纸空间是ZWCAD中的二维空间,常用于出图(打印)
- 模型空间和图纸空间是特殊的块定义
块(Block)
块”是由多个对象(如线条、弧线、文字,甚至其他块)组成的一个命名集合,这些对象被定义为一个整体。
Details
在 CAD 软件中,块就像一个可以多次插入到图纸中的模板,每次插入都被称为一个块引用(block reference)。
块的主要作用是帮助设计师快速重复使用相同的元素,例如符号、标准零件或复杂图形,从而提高效率并保持设计的一致性。
块定义(AcDbBlockTableRecord)
生活类比:想象你在做手工,设计了一个“桌子”的模板(由桌面和四条腿组成)。这个模板就是“块定义”,你可以拿它反复制作桌子,而不用每次都重新画腿和桌面。
CAD 图纸:在 CAD 中,你可以把一组图形(比如一个门窗的形状)定义成一个“块”,方便在图纸中多次插入,比如设计房屋时每个房间的门窗都用同一个块。
块引用(AcDbBlockReference)
生活类比:你用“桌子模板”在房间里放了三张桌子,每张桌子就是模板的一个“实例”。这些桌子就是“块引用”。
CAD 图纸:你在图纸中插入“门窗块”,每个门窗的位置是一个“块引用”,它们都指向同一个“块定义”,但位置和方向可以不同。
小结:块定义是“设计图”,块引用是“成品”。
块在 CAD 中的使用方式
- 创建:通过选择一组对象并为其指定一个名称来定义一个块。
- 插入:将块插入到图纸的不同位置,每一个插入的实例称为块引用,所有引用都与原始块定义关联。
- 修改:如果修改了原始块的定义,图纸中所有该块的引用都会自动更新。
- 属性应用:例如,一个块可以包含文字或数字(如零件编号),这些属性可以在每次插入时单独调整。
模型空间和图纸空间
- 模型空间:ZWCAD中的三维空间,常用于创建和编辑模型和草图
- 图纸空间:图纸空间是ZWCAD中的二维空间,常用于出图(打印)
- 特别说明:模型空间和图纸空间本身也是特殊的“块定义”。
数据库(Database)
- 生活类比:把 DWG 文件想象成一个“大仓库”,里面装着图纸的所有东西:线条、文字、图层、块等。这个仓库就是“DWG 数据库”。
- CAD 图纸:你打开一个 DWG 文件时,数据库把图纸的所有信息加载到电脑内存里,让你可以查看和编辑。
数据库基本操作
WARNING
参考课堂习题与作业解析里的任务一与任务二
创建新数据库
auto pDb = new ZcDbDatabase(); // new一个新数据库,包含有默认的图层、线型、字体样式、文档等等
解释:
Details
new ZcDbDatabase()
:新建一个空白的DWG
文件,里面自带一些默认设置(比如图层 0、默认线型)- CAD 场景:相当于在 CAD 软件里点“新建图纸”。这相当于在 CAD 软件中点击“文件”菜单,选择“新建”来创建一个新的图纸文件。新创建的图纸通常会带有默认设置,例如默认图层(如“0”层)、线型和单位设置。
从 DWG 文件中读取:
auto pDb = new ZcDbDatabase(false, true); //new一个空的数据库
pDb->readDwgFile(_T("D:\Sample.dwg"));
pDb->closeInput(true); //断开与文件的联系(即读取全部数据并关闭文件)
解释:
Details
new ZcDbDatabase(false, true)
:创建一个空的数据库壳子,准备装东西。pDb->readDwgFile("D:\\Sample.dwg")
:从硬盘读取一个 DWG 文件(比如你之前画的房子图纸)。pDb->closeInput(true)
:把文件内容全部加载到内存后,断开与硬盘文件的联系,防止文件被占用。- CAD 场景:这类似于在 CAD 软件中点击“文件”菜单,选择“打开”,然后浏览并选择一个现有的 DWG 文件加载到软件中进行查看或编辑。
获取当前正在使用的数据库:
auto pDb = zcdbHostApplicationServices()->workingDatabase();
解释:
Details
zcdbHostApplicationServices()->workingDatabase()
:获取你当前在 CAD 软件里正在编辑的图纸的数据库。- CAD 场景:这相当于在 CAD 软件中,你已经打开了一个图纸文件,并且当前正在编辑它。这个操作会返回当前活动图纸的数据库,类似于你在软件界面上看到并操作的当前图纸。
保存数据库
pDb->saveAs(_T(“D:\Sample.dwg”));
解释:
Details
pDb->saveAs("D:\\Sample.dwg")
:把内存里的数据库保存到硬盘上的DWG
文件。- CAD 场景:相当于在 CAD 里点“另存为”。这相当于在 CAD 软件中关闭一个图纸文件,释放它占用的内存资源,类似于点击“文件”菜单中的“关闭”选项。
删除数据库
delete pDb;
解释:
Details
delete pDb
:把内存里的数据库删掉,释放空间。- 重要:用 new 创建的东西用完后必须 delete,不然内存会“漏水”(占着不放)。
- CAD 场景:这相当于在 CAD 软件中关闭一个图纸文件,释放它占用的内存资源,类似于点击“文件”菜单中的“关闭”选项。
表和词典简介
表(Table)
- 概念:数据库里有 9 个表,分别存不同类型的东西,比如块、图层、文字样式、线型等。
Details
- 生活类比:想象一个文件柜,里面有 9 个抽屉,每个抽屉装一类东西(比如“图层抽屉”“块抽屉”)。
- CAD 图纸:
- 块表(BlockTable):存所有块定义,比如你的门窗模板。
- 图层表(LayerTable):存图层信息,比如“墙”“家具”层。
- 结构:每个表里有“表记录”(TableRecord),表记录里是具体“对象”(Object)。
- 类比:抽屉里有文件夹(表记录),文件夹里有文件(对象)。
词典(Dictionary)
- 概念:词典是一个特殊的“收纳箱”,用来存用户自定义的东西。
Details
- 生活类比:就像你的电话簿,里面写着“名字-电话号码”的键值对。
- CAD 图纸:比如你在图纸里加了个自定义标记(“项目编号”),可以用词典存起来。
- 结构:词典里有“键”(字符串)和“值”(对象 ID),还能嵌套更多词典。
- 类比:电话簿里可以有分组(子词典),分组里又有名字和号码。
结构
- 表(table)
- 表记录(tablerecord)
- 对象(0bject)
- 表记录(tablerecord)
- 命名词典(NamedobjectDictionary)
- 词典(Dictionary)
- 字符串(Key)-Object ld(Value)
- 词典
- 字符串-0bject ld
...
- 词典(Dictionary)
DANGER
DWG 数据库(大仓库)
├── 表(Table,分类抽屉)
│ ├── 块表(BlockTable)
│ │ ├── 块表记录(BlockTableRecord,如 *Model_Space, *Paper_Space, 自定义块“Sample”)
│ │ │ ├── 对象(Object,如直线、圆、块引用)
│ │ ├── ...
│ ├── 图层表(LayerTable)
│ ├── 文字样式表(TextStyleTable)
│ ├── ...(其他表)
├── 命名词典(NamedObjectDictionary,杂物箱)
│ ├── 词典(Dictionary,自定义分组)
│ │ ├── 键(字符串)- 值(Object ID)
│ │ ├── 嵌套词典
│ │ ├── ...
对象与实体、打开与关闭
对象(Object)和实体(Entity)
Details
- 对象:数据库里的基本单位,可以是任何东西(图形、样式等)。
- 实体:看得见的图形,比如线、圆、文字。
- CAD 图纸:一条直线是一个实体,一个图层设置是一个对象但不是实体。
- 标识:
- 句柄(Handle):对象的“身份证号”,在整个数据库里唯一且不变。
- ObjectId:对象的“临时编号”,图纸打开期间不变。
- 类比:句柄是你的名字,
ObjectId
是你在学校里的学号。
打开和关闭对象
Details
- 操作:
zcdbOpenAcDbObject()
或zcdbOpenAcDbEntity()
:打开对象,可以只读或读写。ZcDbObject::close()
:操作完后关闭对象。
- 生活类比:打开抽屉拿文件(打开对象),用完后关上抽屉(关闭对象)。
- 重要:
- 关闭是用
close()
,不是delete
,因为对象归数据库管,不能随便删。 - 不关闭会让数据库“卡住”,影响稳定性。
- 关闭是用
获取表和表记录
auto pDb = zcdbHostApplicationServices()->workingDatabase();
ZcDbBlockTable* pBT = nullptr;
pDb->getBlockTable(pBT, ZcDb::kForRead);//从数据库获取块表
if (pBT)//判断是否成功
{
ZcDbBlockTableRecord* pBTR = nullptr;
//ACDB_MODEL_SPACE = "*Model_Space"
pBT->getAt(ACDB_MODEL_SPACE, pBTR, ZcDb::kForWrite);//从块表获取模型空间的块表记录
pBT->close();//用完后马上关闭
if (pBTR)
{
... // 这里可以操作模型空间
pBTR->close();
}
}
逐行解释:
Details
pDb->getBlockTable(pBT, ZcDb::kForRead)
:从数据库打开“块抽屉”,只读模式。pBT->getAt(ACDB_MODEL_SPACE, pBTR, ZcDb::kForWrite)
:从块表里拿出“模型空间”文件夹,准备读写。pBT->close() 和 pBTR->close()
:用完后关上抽屉和文件夹。- CAD 场景:你想在模型空间里加东西,先找到模型空间的“文件夹”。这类似于在 CAD 软件中访问图纸的块表和模型空间。例如,你可能需要在模型空间中添加图形实体(如线条或圆),或者查看当前图纸的结构。
添加对象到数据库:画一条直线
WARNING
参考课堂习题与作业解析里的任务三
ZcGePoint3d startPt(4.0, 2.0, 0.0); // 定义起点坐标 (x=4, y=2, z=0)
ZcGePoint3d endPt(10.0, 7.0, 0.0); // 定义终点坐标 (x=10, y=7, z=0)
ZcDbLine *pLine = new ZcDbLine(startPt, endPt); // 创建直线对象
ZcDbObjectId lineId; // 定义直线的 ID
pBtr->appendZcDbEntity(lineId, pLine);//把直线添加到块表记录里 // 模型空间
pLine->close();//添加后记得关闭,注意不能把pLine给delete掉
逐行解释:
Details
ZcGePoint3d startPt(4.0, 2.0, 0.0)
:设置直线的起点,像在图纸上标一个点。new ZcDbLine(startPt, endPt)
:创建一条从起点到终点的直线。pBtr->appendZcDbEntity(lineId, pLine)
:把直线放进模型空间的“文件夹”,并给它一个 ID。pLine->close()
:保存直线并关闭,别用 delete(数据库会管它)。- CAD 场景:这相当于在 CAD 软件中使用“直线”工具,在模型空间中绘制一条从坐标 (4, 2, 0) 到 (10, 7, 0) 的直线。
修改数据库中的对象:删除直线
ZcDbLine* pLine = nullptr; // 定义直线指针
zcdbOpenZcDbEntity((ZcDbEntity*&)pLine, lineId, ZcDb::kForWrite);//修改的话要以写的方式打开
if (pLine)
{
pLine->erase();//删除其实只是设置标记 // 标记删除
pLine->close();//即使是删除后也得关闭,不能delete
}
逐行解释:
Details
zcdbOpenZcDbEntity(...)
:用直线的 ID 打开它,准备修改。pLine->erase()
:标记这条直线为“删除”,但它还在数据库里(像回收站)。pLine->close()
:保存修改并关闭。- CAD 场景:这类似于在 CAD 软件中选择一条已绘制的直线,然后按“删除”键将其从图纸中移除。
创建块:做一个“圆形模板”
WARNING
参考课堂习题与作业解析里的任务四
auto pDb = zcdbHostApplicationServices()->workingDatabase();
ZcDbBlockTable* pBT = nullptr;
pDb->getBlockTable(pBT, ZcDb::kForWrite); // 打开块表
if (pBT)
{
auto pBtr = new ZcDbBlockTableRecord(); // 创建新块记录
pBtr->setName( _T("Sample")); // 命名“Sample”
pBT->add(pBtr); // // 加到块表
auto pCir = new ZcDbCircle(ZcGePoint3d::kOrigin, ZcGeVector3d::kZAxis, 50); // 创建圆
pBtr->appendZcDbEntity(pCir); //区别在于不是加到模型空间的块表记录 // 把圆加到块里
pCir->close(); // 关闭圆
pBtr->close(); // 关闭块记录
pBT->close(); // 关闭块表
}
逐行解释:
Details
- pBtr->setName("Sample"):给新块起名叫“Sample”。
- new ZcDbCircle(...):创建一个半径 50 的圆,中心在原点。
- pBtr->appendZcDbEntity(pCir):把圆放进块里。
- CAD 场景:这相当于在 CAD 软件中创建一个新的块定义,命名为“Sample”,并在块中添加一个半径为 50 的圆,类似于使用“块编辑器”创建可重用的图形组合。
匿名块
匿名块是一种特殊的块,名字以 *
开头(通常是 *U
),由系统自动生成一个唯一的块名。它通常用于块名不重要的场景,比如标注的箭头或临时图形。匿名块不会出现在 CAD 的“插入块”对话框中,只能通过代码插入。
创建匿名块
pBtr->setName( _T("*U"));
ZcDbObjectId btrId;
pBT->add(btrId, pBtr);
Details
pBtr->setName(_T("*U"))
: 将块表记录(BlockTableRecord)的名称设为*U
,表示这是一个匿名块。系统会自动将其改为一个唯一名称(如*U1
、*U2
)。pBT->add(btrId, pBtr)
: 将这个块表记录添加到块表(BlockTable)中,并返回其 ID(btrId
)。 CAD 场景:这类似于在 CAD 软件中创建一个临时的匿名块(例如用于标注的箭头或符号),然后在模型空间中插入这个块的引用。
插入匿名块
// 一般用于块名不重要的场景,比如标注的箭头就是匿名块
// 块名以*开头,一般是*U,创建后会自动生成块名
// 不会列在Insert对话框里,需要用代码插入
ZcDbBlockTableRecord* pBTR = nullptr;
pBT->getAt(ZCDB_MODEL_SPACE, pBTR, ZcDb::kForWrite);// 打开模型空间
pBT->close(); // 关闭块表
if (pBTR)
{
auto pRef = new ZcDbBlockReference(ZcGePoint3d::kOrigin, btrId); // 创建块引用
pBTR->appendZcDbEntity(pRef); // 添加到模型空间
pRef->close(); // 关闭块引用
pBTR->close(); // 关闭模型空间
}
Details
pBT->getAt(ZCDB_MODEL_SPACE, ...)
: 打开模型空间的块表记录,准备写入。auto pRef = new ZcDbBlockReference(...)
: 在原点创建一个块引用,指向刚才创建的匿名块。pBTR->appendZcDbEntity(pRef)
: 将块引用添加到模型空间。- 关闭操作: 用完后关闭所有打开的对象,避免资源泄漏。
词典操作
获取命名词典及遍历词典
auto pDb = zcdbHostApplicationServices()->workingDatabase();
ZcDbDictionary* pNOD = nullptr;
zcdbOpenZcDbObject((ZcDbObject*&)pNOD, pDb->namedObjectsDictionaryId(), ZcDb::kForRead);//获取命名词典的Id并打开
if (pNOD)
{
auto pItr = pNOD->newIterator();//获取迭代器
while (!pItr->done()) // 循环直到结束
{
zcutPrintf(_T("\n%s"), pItr->name());//输出命名词典包含的词典名称 // 打印词典里的键
pItr->next(); // 下一个
}
delete pItr; //用完迭代器后要delete
pItr = nullptr;
pNOD->close(); //要记得关闭词典
}
Details
- 解释:
- pNOD 是数据库的“主电话簿”,这里遍历它,看看里面有哪些“分组”。
- CAD 场景:这类似于在 CAD 软件中查看图纸中的自定义数据或元数据,例如检查图纸属性中存储的项目信息或自定义设置。
创建词典
词典是一个存储“键值对”的数据结构,键是字符串,值是对象的 ID(可以是任何对象,比如 Xrecord 或另一个词典)。它类似于一个“自定义文件夹”,用来组织和管理数据。词典可以嵌套,即一个词典里可以包含其他词典。
auto pDict = new ZcDbDictionary(); // 新建词典
ZcDbObjectId dictId;
pNOD->setAt(_T("MyDict"), pDict, dictId); // 加到主词典
...//给新的词典添加数据
pDict->close();//添加后要记得关闭
Details
auto pDict = new ZcDbDictionary()
: 创建一个新的词典对象,就像新建一个空文件夹。pNOD->setAt(_T("MyDict"), pDict, dictId)
: 将这个词典以键 "MyDict" 添加到主词典(Named Object Dictionary,简称 NOD)中,并获取其 ID(dictId)。
NOD 是 DWG 数据库中的“总电话簿”,管理所有命名的对象。- 生活类比: 就像在总电话簿里加了一个新分组叫“MyDict”,你可以往这个分组里塞东西。
pDict->close()
: 操作完成后关闭词典,释放资源。- 主词典(NOD): NOD 是 DWG 文件中默认的主词典,所有的自定义词典通常都挂在它下面。
- 用途: 词典可以用来存储任何你想关联到图纸的数据,比如自定义属性、配置信息等。这相当于在 CAD 软件中创建一个新的自定义数据分组,例如为图纸添加一个新的元数据类别,如“项目参数”或“设计信息”。
添加条目(entry)
条目是词典中的一个“键值对”,键是一个唯一的字符串,值是一个对象的 ID。添加的对象不能已经有“所有者”(owner),否则会报错。
这类似于在 CAD 软件中向自定义数据分组添加一个新的条目,例如为图纸添加一个自定义属性,如“项目编号”或“设计者姓名”。
if (!pDict->has(key))//先检查key是否已经存在,如果已存在还继续写入的话会覆盖原来的数据
{
auto pXrec = new ZcDbXrecord();//把数据存在xrecord里
resbuf* pRb = zcutBuildList(ZcDb::kDxfText,_T("Line"),
ZcDb::kDxfInt32,12,
ZcDb::kDxfReal,3.14,
ZcDb::kDxfXCoord,pt,
RTNONE);
pXrec->setFromRbChain(*pRb);
ZcDbObjectId xrecId;
pDict->setAt(key, pXrec, xrecId);//把xrecord存到词典里,并生成其id
pXrec->close();//记得要close
zcutRelRb(pRb);//resbuf链表复制到xrecord后要删除
pRb = nullptr;
}
Details
if (!pDict->has(key))
: 检查词典中是否已有这个键。如果键已存在,继续写入会覆盖原有数据,所以要先检查。auto pXrec = new ZcDbXrecord()
: 创建一个 Xrecord 对象,Xrecord 是一种特殊的容器,可以存储多种类型的数据(类似一张“数据卡片”)。resbuf* pRb = zcutBuildList(...)
: 构建一个 resbuf 链表,包含要存储的数据:ZcDb::kDxfText, _T("Line")
: 文本“Line”。ZcDb::kDxfInt32, 12
: 整数 12。ZcDb::kDxfReal, 3.14
: 实数 3.14。ZcDb::kDxfXCoord, pt
: 坐标点 pt。RTNONE
: 链表结束标志。
pXrec->setFromRbChain(*pRb)
: 将 resbuf 链表中的数据复制到 Xrecord 中。pDict->setAt(key, pXrec, xrecId)
: 将 Xrecord 以指定键(key)存入词典,并获取其 ID(xrecId)。pXrec->close()
: 关闭 Xrecord。zcutRelRb(pRb)
: 释放 resbuf 链表,避免内存泄漏。- Xrecord 的作用: Xrecord 是一种灵活的数据容器,专门用来存放在词典中,适合保存自定义的结构化数据。
- 键 hood: 如果键已存在,添加新条目会覆盖原数据,所以要谨慎使用。
拓展词典与拓展数据
每个实体(比如线、圆)都可以有自己的“扩展词典”和“扩展数据”,用来存储额外信息。这类似于在 CAD 软件中为一个图形实体(例如一条直线)添加扩展数据,例如为直线添加“材质”或“长度”等自定义属性。
Details
- 扩展数据(XData):
- 存储基本类型的数据(数字、字符串等)。
- 可用于选择过滤(比如筛选带特定 XData 的实体)。
- 容量有限。
- 扩展词典(Extension Dictionary):
- 可存储任意类型的数据,无容量限制。
- 只能通过代码读写,不能用于过滤。
扩展数据更直观,可用于选择过滤,但只能存储基本类型,且有容量限制。 扩展词典更强大,可存储任意类型,无容量限制,但不可用于过滤,且只能用代码读写。
读写扩展数据:
- 示例:
<samples\XData>
pObj->setXData(pRb); // 设置实体的扩展数据
pRb
: 一个 resbuf 链表,包含要存储的数据。- 用途: 比如给一条线添加“材质:钢”的标记。
读写扩展词典:
if (pObj->extensionDictionary().isNull())//判断该对象是否已经有扩展词典
{
pObj->createExtensionDictionary();//没有就创建
}
//之后就是常规词典操作
ZcDbDictionary* pXDict = nullptr;
zcdbOpenZcDbObject((ZcDbObject*&)pXDict, pObj->extensionDictionary(), ZcDb::kForRead);
if (pXDict)
{
...
pXDict->close();
}
pObj->extensionDictionary().isNull()
: 检查实体是否已有扩展词典。pObj->createExtensionDictionary()
: 如果没有,创建一个。zcdbOpenZcDbObject(...)
: 打开扩展词典,指定读写权限(这里是 kForRead)。- 操作: 打开后,可以像普通词典一样添加或读取条目。
pXDict->close()
: 操作完成,关闭词典。
课堂习题与作业解析
任务一:获取当前数据库并遍历实体
这是最基础的数据库操作,对应课堂上讲的“获取当前正在使用的数据库”。它教我们如何与当前打开的DWG图纸进行交互。
功能说明: 获取当前CAD编辑器中打开的图纸数据库,并遍历其“模型空间”中的所有实体(如直线、圆等),最后在命令行打印出实体的类型和总数。
算法思路:
获取当前工作数据库的指针。
打开数据库中的“块表”(BlockTable)。
从块表中获取指向“模型空间”(Model Space)的记录。
创建一个迭代器来遍历模型空间中的所有实体。
在循环中,获取每个实体,打印其类型,并计数。
释放所有打开的对象和迭代器。
代码解析:
void testCurrentDb()
{
// 1. 获取当前活动的 DWG 数据库
// 这就是你在CAD里正在操作的那个图纸文件。
AcDbDatabase* pDb = zcdbHostApplicationServices()->workingDatabase();
if (!pDb) {
acutPrintf(_T("\n无法获取当前数据库"));
return;
}
acutPrintf(_T("\n当前数据库指针:%p\n"), pDb);
// 2. 获取块表(BlockTable)
// 数据库中所有“块定义”(包括模型空间)都存储在块表中。
AcDbBlockTable* pBlockTable = nullptr;
// 以只读(kForRead)方式打开块表,因为我们只是查看,不修改。
pDb->getBlockTable(pBlockTable, AcDb::kForRead);
// 3. 获取模型空间(Model Space)的块表记录
// 模型空间是一个特殊的块,其名字是 ACDB_MODEL_SPACE (*Model_Space)。
AcDbBlockTableRecord* pModelSpace = nullptr;
pBlockTable->getAt(ACDB_MODEL_SPACE, pModelSpace, AcDb::kForRead);
pBlockTable->close(); // 块表使用完毕,立刻关闭,这是个好习惯。
// 4. 创建迭代器遍历模型空间
AcDbBlockTableRecordIterator* pIter = nullptr;
pModelSpace->newIterator(pIter); // 为模型空间创建一个新的迭代器
int count = 0;
// 5. 循环遍历
for (; !pIter->done(); pIter->step()) { // 当迭代器没有结束时,继续下一步
AcDbEntity* pEnt = nullptr;
// 获取当前迭代器指向的实体
pIter->getEntity(pEnt, AcDb::kForRead);
// pEnt->isA()->name() 获取实体的类型名,如 "AcDbLine", "AcDbCircle"
acutPrintf(_T("\n模型空间实体:%s"), pEnt->isA()->name());
pEnt->close(); // 实体对象使用完毕,必须 close()!不能 delete!
count++;
}
// 6. 清理工作
delete pIter; // 迭代器是用 new 创建的,所以需要 delete
pModelSpace->close(); // 模型空间记录也要关闭
acutPrintf(_T("\n模型空间实体总数:%d\n"), count);
}
核心知识点:
zcdbHostApplicationServices()->workingDatabase()
: 获取当前活动数据库,这是与用户当前工作交互的入口。打开/关闭原则:任何从数据库获取的对象(如
pBlockTable
,pModelSpace
,pEnt
),使用完毕后都必须调用close()
方法来关闭。这只是释放了你对该对象的操作权,对象本身仍然存在于数据库中。忘记关闭会导致数据库不稳定。迭代器:
AcDbBlockTableRecordIterator
是遍历块定义(如模型空间)中所有实体的标准方式。用newIterator()
创建,用delete
销毁。
任务二:从文件读取 DWG 并遍历实体
这个任务与上一个类似,但演示了另一种获取数据库的方式:直接从硬盘读取一个.dwg
文件。这对应课堂上讲的“从DWG文件中读取”。
功能说明: 在内存中创建一个新的空数据库,从指定路径读取一个.dwg
文件的内容到这个数据库中,然后同样遍历模型空间的实体并打印信息。
算法思路:
创建一个新的、空的数据库对象。
调用
readDwgFile
方法读取指定路径的DWG文件。后续步骤与
testCurrentDb
完全相同:获取块表 -> 获取模型空间 -> 创建迭代器 -> 遍历实体。断开与文件的连接并删除数据库对象,释放内存。
代码解析:
void test()
{
// 1. 创建一个空的数据库对象
// 参数 (false, true) 表示创建一个不带默认设置的空数据库,准备用于读入文件。
auto pDb = new AcDbDatabase(false, true);
// 2. 从文件读取DWG内容
// pDb->readDwgFile() 会尝试打开并解析指定的DWG文件。
Acad::ErrorStatus es = pDb->readDwgFile(_T("E:\\CAD\\homework\\Sample.dwg"));
if (es != Acad::eOk) {
acutPrintf(_T("\nDWG 读取失败,错误码: %d"), es);
delete pDb; // 读取失败,清理并返回
return;
}
acutPrintf(_T("\nDWG 读取成功!\n"));
// 3. 获取模型空间并遍历实体 (这部分逻辑与 testCurrentDb 完全一样)
AcDbBlockTable* pBlockTable = nullptr;
pDb->getBlockTable(pBlockTable, AcDb::kForRead);
AcDbBlockTableRecord* pModelSpace = nullptr;
pBlockTable->getAt(ACDB_MODEL_SPACE, pModelSpace, AcDb::kForRead);
pBlockTable->close();
AcDbBlockTableRecordIterator* pIter = nullptr;
pModelSpace->newIterator(pIter);
int count = 0;
for (; !pIter->done(); pIter->step()) {
AcDbEntity* pEnt = nullptr;
pIter->getEntity(pEnt, AcDb::kForRead);
acutPrintf(_T("\n发现实体类型:%s"), pEnt->isA()->name());
pEnt->close();
count++;
}
delete pIter;
pModelSpace->close();
acutPrintf(_T("\n实体总数:%d\n"), count);
// 4. 断开文件连接并清理
pDb->closeInput(true); // 读取操作完成后,断开与物理文件的关联。
delete pDb; // pDb 是用 new 创建的,必须用 delete 销毁以释放内存。
}
核心知识点:
new AcDbDatabase()
vsworkingDatabase()
: 前者用于在内存中创建全新的或用于读取文件的数据库;后者用于获取CAD当前正在编辑的数据库。readDwgFile()
: 这是将磁盘上的静态文件加载到内存中进行操作的关键函数。delete pDb
: 因为这个数据库是我们自己new
出来的,它不属于CAD应用程序管理,所以当我们用完后,必须delete
它来释放内存。这与workingDatabase()
返回的数据库指针不同,那个不能delete
。
任务三:在模型空间添加和删除直线
这个任务进入到修改数据库的阶段,演示了最基础的“写”操作:创建实体并添加到模型空间,然后再将其删除。这对应课堂上讲的“添加对象到数据库”和“修改数据库中的对象”。
功能说明: 在当前图纸的模型空间中绘制两条交叉的直线,然后删除其中的第一条。
算法思路:
获取当前工作数据库。
以“可写”模式打开块表和模型空间记录。
创建两个
AcDbLine
对象(定义起点和终点)。使用
appendAcDbEntity
将两条直线添加到模型空间,并获取它们的ObjectId
。使用获取到的
ObjectId
,以“可写”模式重新打开第一条直线。调用该直线对象的
erase()
方法将其标记为删除。关闭所有打开的对象。
代码解析:
void testLineAddDelete()
{
// 1. 获取当前DWG数据库
AcDbDatabase* pDb = zcdbHostApplicationServices()->workingDatabase();
// 2. 获取块表
AcDbBlockTable* pBT = nullptr;
// 注意这里是 kForRead,因为我们只是从块表里“获取”模型空间,不修改块表本身。
if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk || !pBT) {
return;
}
// 3. 获取模型空间块表记录
AcDbBlockTableRecord* pBTR = nullptr;
// 这里必须是 kForWrite,因为我们要向模型空间里“添加”和“删除”实体。
if (pBT->getAt(ACDB_MODEL_SPACE, pBTR, AcDb::kForWrite) != Acad::eOk || !pBTR) {
pBT->close();
return;
}
pBT->close(); // 块表用完即关
// 4. 添加第一条直线
AcGePoint3d pt1(0, 0, 0);
AcGePoint3d pt2(10, 10, 0);
AcDbLine* line1 = new AcDbLine(pt1, pt2); // 在内存中创建直线对象
AcDbObjectId idLine1; // 用于接收新实体的ID
// 将直线实体“追加”到模型空间记录中,数据库会为它分配一个ID。
pBTR->appendAcDbEntity(idLine1, line1);
line1->close(); // 添加后,关闭直线对象。数据库现在是它的所有者。
// 5. 添加第二条直线 (同上)
AcGePoint3d pt3(0, 10, 0);
AcGePoint3d pt4(10, 0, 0);
AcDbLine* line2 = new AcDbLine(pt3, pt4);
AcDbObjectId idLine2;
pBTR->appendAcDbEntity(idLine2, line2);
line2->close();
acutPrintf(_T("\n添加两条直线成功"));
// 6. 删除第一条直线
AcDbLine* delLine = nullptr;
// 使用 acdbOpenAcDbEntity 函数,通过 ObjectId 以可写方式打开实体。
if (acdbOpenAcDbEntity((AcDbEntity*&)delLine, idLine1, AcDb::kForWrite) == Acad::eOk) {
delLine->erase(); // 调用 erase() 方法。这只是给对象打上“删除”标记。
delLine->close(); // 即使是删除了,也要关闭!
acutPrintf(_T("\n删除第一条直线成功"));
}
pBTR->close(); // 所有操作完成,关闭模型空间记录。
}
核心知识点:
读写模式 (
kForRead
vskForWrite
):获取对象时必须明确意图。如果只是查看,用kForRead
;如果要修改或向其内部添加/删除内容,必须用kForWrite
。appendAcDbEntity()
: 这是向块表记录(如模型空间)添加新实体的标准函数。它会将new
出来的对象的所有权转移给数据库。AcDbObjectId
: 这是对象在数据库中的唯一标识(在一次编辑会话中)。后续要操作这个对象,就需要通过它的ID来打开。erase()
: 删除实体并不是delete
指针,而是调用其erase()
方法。真正的物理删除通常发生在图纸关闭或执行PURGE
命令时。
任务四:创建块定义并插入块引用
这是一个更综合的任务,涉及到CAD中一个非常重要的概念——块。它对应课堂上讲的“创建块”和插入“块引用”。
功能说明:
创建一个名为 "Sample" 的新块定义。 2r2
在这个块定义中,添加一个半径为50的圆。
在模型空间的原点位置,插入一个这个 "Sample" 块的实例(即块引用)。
算法思路:
以可写方式打开块表,因为要向其中添加新的块定义。
创建一个新的
AcDbBlockTableRecord
对象,并设置其名称 "Sample" 和基点。将这个新的块记录添加到块表中。
创建一个
AcDbCircle
对象。将这个圆
append
到刚刚创建的块记录中(而不是模型空间!)。关闭所有对象。
为了插入块引用,需要再次打开模型空间(可写)和块表(可读)。
从块表中通过名字 "Sample" 找到我们刚刚创建的块定义的
ObjectId
。创建一个
AcDbBlockReference
对象,指定插入点和块定义的ID。将这个块引用
append
到模型空间。关闭所有对象。
代码解析:
void testCreateBlockAndInsert()
{
AcDbDatabase* pDb = zcdbHostApplicationServices()->workingDatabase();
// --- Part 1: 创建块定义 "Sample" ---
// 1. 获取块表,准备添加块定义 (必须可写)
AcDbBlockTable* pBT = nullptr;
if (pDb->getBlockTable(pBT, AcDb::kForWrite) != Acad::eOk) return;
// 2. 创建一个新的块定义记录
AcDbBlockTableRecord* pBtr = new AcDbBlockTableRecord();
pBtr->setName(_T("Sample")); // 设置块名
pBtr->setOrigin(AcGePoint3d(0, 0, 0)); // 设置块的基点
// 3. 把块定义添加到块表中
if (pBT->add(pBtr) != Acad::eOk) {
// 添加失败通常是因为名字重复
delete pBtr; pBT->close(); return;
}
// 4. 在块定义里添加实体(一个圆)
AcDbCircle* pCir = new AcDbCircle(AcGePoint3d::kOrigin, AcGeVector3d::kZAxis, 50);
pBtr->appendAcDbEntity(pCir); // 注意:是添加到 pBtr,不是模型空间
pCir->close();
// 块定义创建完成,关闭相关对象
pBtr->close();
pBT->close();
// --- Part 2: 在模型空间插入块引用 ---
// 5. 打开模型空间(可写)
pDb->getBlockTable(pBT, AcDb::kForRead);
AcDbBlockTableRecord* pModelSpace = nullptr;
pBT->getAt(ACDB_MODEL_SPACE, pModelSpace, AcDb::kForWrite);
pBT->close();
// 6. 获取块定义 "Sample" 的 ObjectId
AcDbObjectId blkId;
pDb->getBlockTable(pBT, AcDb::kForRead);
if (pBT->getAt(_T("Sample"), blkId) != Acad::eOk) {
// 找不到块定义
pModelSpace->close(); pBT->close(); return;
}
pBT->close();
// 7. 创建块引用并插入到模型空间
// 构造函数需要插入点和块定义的ID
AcDbBlockReference* pRef = new AcDbBlockReference(AcGePoint3d(0, 0, 0), blkId);
pModelSpace->appendAcDbEntity(pRef);
pRef->close();
pModelSpace->close();
acutPrintf(_T("\n块定义已创建,块引用已插入"));
}
核心知识点:
块定义 vs 块引用:
AcDbBlockTableRecord
是“模板”或“定义”,它本身不可见,存在于块表中。AcDbBlockReference
是“实例”或“引用”,是可见的实体,存在于模型空间或图纸空间,它指向一个块定义。两步操作:创建和使用块通常分两步:先定义块(往
AcDbBlockTableRecord
里加东西),再插入块(在模型空间创建AcDbBlockReference
)。pBT->add(pBtr)
: 将新的块定义记录添加到块表中。new AcDbBlockReference(insertionPoint, blockDefinitionId)
: 创建块引用的关键,需要提供插入位置和它所引用的块定义的ID。
任务五(作业):
这是最复杂的作业,综合了前面几乎所有的知识点,并引入了更高级的“词典”和“扩展词典”概念,这对应课堂上讲的“词典操作”。
功能说明:
创建一个名为 "Concentric255" 的块定义。
在块定义中,循环创建255个同心圆,半径从1到255,并且每个圆的颜色索引也从1到255。
为每个圆创建“扩展词典”,并在其中用一个
Xrecord
记录下该圆的颜色索引值。在数据库的“命名对象词典”下,创建一个新的词典 "CircleCenters"。
在这个新词典中,存入255条记录,每条记录用一个
Xrecord
存储圆的中心坐标(虽然都是原点)。最后,在模型空间插入这个 "Concentric255" 块的引用。
算法思路:
创建块定义:与任务五类似,先创建名为 "Concentric255" 的
AcDbBlockTableRecord
。循环创建圆:
for
循环 255 次。在循环中,
new AcDbCircle
,设置半径和颜色。将圆
append
到块定义中。(难点1)写入扩展词典:
检查圆有无扩展词典,没有则
createExtensionDictionary()
。打开扩展词典(可写)。
创建
AcDbXrecord
,用resbuf
链表构建要存储的数据(颜色索引)。用
setAt
方法将Xrecord
存入扩展词典,键为 "ColorIdx"。关闭所有相关对象。
创建和写入命名词典:
(难点2) 打开数据库的根词典——命名对象词典 (
namedObjectsDictionaryId
)。在其中
setAt
一个新的AcDbDictionary
,键为 "CircleCenters"。打开这个 "CircleCenters" 词典。
循环255次,每次都创建一个
Xrecord
来存储原点坐标,并以 "C001", "C002"... 为键setAt
到词典中。关闭所有相关对象。
插入块引用:与任务五完全相同,获取 "Concentric255" 的ID,在模型空间创建
AcDbBlockReference
并添加。
代码解析:
void testConcentric255()
{
// 1. 获取数据库并创建块定义 "Concentric255"
AcDbDatabase* pDb = zcdbHostApplicationServices()->workingDatabase();
AcDbBlockTable* pBT = nullptr;
pDb->getBlockTable(pBT, AcDb::kForWrite);
AcDbBlockTableRecord* pBTR = new AcDbBlockTableRecord();
pBTR->setName(_T("Concentric255"));
pBT->add(pBTR);
pBT->close();
// 2. 循环创建255个同心圆并添加到块定义
for (int i = 1; i <= 255; i++) {
// 创建圆,半径和颜色索引都为 i
AcDbCircle* pCir = new AcDbCircle(AcGePoint3d::kOrigin, AcGeVector3d::kZAxis, static_cast<double>(i));
pCir->setColorIndex(i);
pBTR->appendAcDbEntity(pCir);
// --- 难点1: 在圆的扩展词典里记录颜色索引 ---
// 检查并创建扩展词典
if (pCir->extensionDictionary().isNull()) {
pCir->createExtensionDictionary();
}
// 打开扩展词典
AcDbDictionary* pXDic = nullptr;
acdbOpenAcDbObject((AcDbObject*&)pXDic, pCir->extensionDictionary(), AcDb::kForWrite);
if (pXDic) {
// 创建 XRecord 来存储数据
AcDbXrecord* pXRec = new AcDbXrecord();
// resbuf 是一个灵活的数据链表,这里只存一个整数 i
resbuf* pRb = acutBuildList(AcDb::kDxfInt16, i, RTNONE);
pXRec->setFromRbChain(*pRb);
acutRelRb(pRb); // resbuf 数据被复制后,原链表应被释放
// 将 XRecord 以 "ColorIdx" 为键,存入扩展词典
pXDic->setAt(_T("ColorIdx"), pXRec, AcDbObjectId()); // ID可以不接收
pXRec->close();
pXDic->close();
}
pCir->close();
}
pBTR->close(); // 块定义内容添加完毕,关闭
// --- 难点2: 在命名词典下新建字典 "CircleCenters" 并存入坐标 ---
// 4. 打开数据库的命名对象词典 (NOD)
AcDbDictionary* pNOD = nullptr;
acdbOpenAcDbObject((AcDbObject*&)pNOD, pDb->namedObjectsDictionaryId(), AcDb::kForWrite);
if (pNOD) {
// 新建一个词典对象,并以 "CircleCenters" 为键添加到NOD中
AcDbDictionary* pCCDic = new AcDbDictionary();
AcDbObjectId ccDictId;
pNOD->setAt(_T("CircleCenters"), pCCDic, ccDictId);
pCCDic->close();
pNOD->close();
// 打开刚才新建的 "CircleCenters" 词典
AcDbDictionary* pOpenCC = nullptr;
acdbOpenAcDbObject((AcDbObject*&)pOpenCC, ccDictId, AcDb::kForWrite);
if (pOpenCC) {
for (int i = 1; i <= 255; i++) {
// 同样用 XRecord 存储一个点坐标
AcDbXrecord* pXR = new AcDbXrecord();
resbuf* pPtRb = acutBuildList(AcDb::kDxfXCoord, AcGePoint3d::kOrigin, RTNONE);
pXR->setFromRbChain(*pPtRb);
acutRelRb(pPtRb);
// 生成键名,如 "C001", "C002", ...
TCHAR key[16];
_stprintf_s(key, _T("C%03d"), i);
pOpenCC->setAt(key, pXR, AcDbObjectId());
pXR->close();
}
pOpenCC->close();
}
}
// 5. 最后,在模型空间插入块引用 (这部分是标准操作)
pDb->getBlockTable(pBT, AcDb::kForRead);
AcDbBlockTableRecord* pMS = nullptr;
pBT->getAt(ACDB_MODEL_SPACE, pMS, AcDb::kForWrite);
pBT->close();
AcDbObjectId blkId;
pDb->getBlockTable(pBT, AcDb::kForRead);
pBT->getAt(_T("Concentric255"), blkId);
pBT->close();
AcDbBlockReference* pRef = new AcDbBlockReference(AcGePoint3d::kOrigin, blkId);
pMS->appendAcDbEntity(pRef);
pRef->close();
pMS->close();
}
核心知识点:
扩展词典 (Extension Dictionary):每个实体都可以附带一个自己的私有词典,用来存储与该实体相关的任意自定义数据。这是挂在实体上的。
命名对象词典 (Named Object Dictionary, NOD):这是数据库级别的“总词典”,用于存储图纸范围内的、非图形的、命名的对象,比如我们自定义的 "CircleCenters" 词典。这是挂在数据库上的。
Xrecord:
AcDbXrecord
是一个通用的数据容器,通常作为“值”被存储在词典中。它可以容纳通过resbuf
链表定义的各种数据类型。acutBuildList
/acutRelRb
: 这是创建和释放resbuf
链表的辅助函数,非常方便。