Skip to content

CAD 二次开发环境配置与 DWG 数据库基础

我们使用的对 CAD 进行二次开发的方式是:ZWCAD Runtime Extension,简称 ZRX

它可以实现用户交互、识图管理、几何算法、文档管理、图型数据库操作等方式。

开发环境配置时需要安装 ZRXSDK 来使用。

其目录结构如下:

txt
Inc:头文件
lib-x64:库文件
Arxport:ObjectArx的兼容文件
Doc/ Samples :文档/示例程序
utils:扩展库
Classmap:类派生关系图
Tools:向导安装包

这里我顺便安装了向导安装包,方便快速搭建项目。

项目初始化

注册/注销命令

下面的函数作为入口点,我们主要在 initapp() 函数内部实现命令的添加:

c++
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!

一、编写命令函数:

这里是命令执行的内部逻辑,封装成一个函数。如果需要注册另一个命令,则封装到另一个函数里。(分散注册

c++
void helloworld()
{
	acutPrintf(_T("\nHello World!"))
}

注册命令:

注:实际中全局名称应为 _T("_HelloGlobal"),这里可能是示例简化。

这里是进入到 initapp 函数,将上面实现的命令函数注册到这里。

如果有其他的命令,也都在这个函数里进行注册。(集中管理

cpp
void initapp()
{
	acedRegCmds->addCommand(cmd_group_name, _T("HelloLocal"), _T("HelloLocal"), ACRX_CMD_MODAL, helloworld);

注销命令:

这里对 initapp 注册的所有命令都进行销毁。(集中注销

cpp
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 中的使用方式
  1. 创建:通过选择一组对象并为其指定一个名称来定义一个块。
  2. 插入:将块插入到图纸的不同位置,每一个插入的实例称为块引用,所有引用都与原始块定义关联。
  3. 修改:如果修改了原始块的定义,图纸中所有该块的引用都会自动更新。
  4. 属性应用:例如,一个块可以包含文字或数字(如零件编号),这些属性可以在每次插入时单独调整。

模型空间和图纸空间

  • 模型空间:ZWCAD中的三维空间,常用于创建和编辑模型和草图
  • 图纸空间:图纸空间是ZWCAD中的二维空间,常用于出图(打印)
  • 特别说明:模型空间和图纸空间本身也是特殊的“块定义”。

数据库(Database)

  • 生活类比:把 DWG 文件想象成一个“大仓库”,里面装着图纸的所有东西:线条、文字、图层、块等。这个仓库就是“DWG 数据库”。
  • CAD 图纸:你打开一个 DWG 文件时,数据库把图纸的所有信息加载到电脑内存里,让你可以查看和编辑。

数据库基本操作

WARNING

参考课堂习题与作业解析里的任务一任务二

创建新数据库

cpp
auto pDb = new ZcDbDatabase(); // new一个新数据库,包含有默认的图层、线型、字体样式、文档等等

解释

Details
  • new ZcDbDatabase():新建一个空白的 DWG 文件,里面自带一些默认设置(比如图层 0、默认线型)
  • CAD 场景:相当于在 CAD 软件里点“新建图纸”。这相当于在 CAD 软件中点击“文件”菜单,选择“新建”来创建一个新的图纸文件。新创建的图纸通常会带有默认设置,例如默认图层(如“0”层)、线型和单位设置。

从 DWG 文件中读取:

cpp
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 文件加载到软件中进行查看或编辑。

获取当前正在使用的数据库:

cpp
auto pDb = zcdbHostApplicationServices()->workingDatabase();

解释

Details
  • zcdbHostApplicationServices()->workingDatabase():获取你当前在 CAD 软件里正在编辑的图纸的数据库。
  • CAD 场景:这相当于在 CAD 软件中,你已经打开了一个图纸文件,并且当前正在编辑它。这个操作会返回当前活动图纸的数据库,类似于你在软件界面上看到并操作的当前图纸。

保存数据库

cpp
pDb->saveAs(_T(“D:\Sample.dwg”));

解释

Details
  • pDb->saveAs("D:\\Sample.dwg"):把内存里的数据库保存到硬盘上的 DWG 文件。
  • CAD 场景:相当于在 CAD 里点“另存为”。这相当于在 CAD 软件中关闭一个图纸文件,释放它占用的内存资源,类似于点击“文件”菜单中的“关闭”选项。

删除数据库

cpp
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)
  • 命名词典(NamedobjectDictionary)
    • 词典(Dictionary)
      • 字符串(Key)-Object ld(Value)
      • 词典
        • 字符串-0bject ld
        • ...

DANGER

txt
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,因为对象归数据库管,不能随便删。
    • 不关闭会让数据库“卡住”,影响稳定性。

获取表和表记录

cpp
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

参考课堂习题与作业解析里的任务三

cpp
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) 的直线。

修改数据库中的对象:删除直线

cpp
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

参考课堂习题与作业解析里的任务四

cpp
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 的“插入块”对话框中,只能通过代码插入。

创建匿名块

cpp
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 软件中创建一个临时的匿名块(例如用于标注的箭头或符号),然后在模型空间中插入这个块的引用。

插入匿名块

cpp
// 一般用于块名不重要的场景,比如标注的箭头就是匿名块
// 块名以*开头,一般是*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): 将块引用添加到模型空间。
  • 关闭操作: 用完后关闭所有打开的对象,避免资源泄漏。

词典操作

获取命名词典及遍历词典

cpp
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 或另一个词典)。它类似于一个“自定义文件夹”,用来组织和管理数据。词典可以嵌套,即一个词典里可以包含其他词典。

cpp
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 软件中向自定义数据分组添加一个新的条目,例如为图纸添加一个自定义属性,如“项目编号”或“设计者姓名”。

cpp
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>
cpp
pObj->setXData(pRb);  // 设置实体的扩展数据
  • pRb: 一个 resbuf 链表,包含要存储的数据。
  • 用途: 比如给一条线添加“材质:钢”的标记。

读写扩展词典

cpp
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编辑器中打开的图纸数据库,并遍历其“模型空间”中的所有实体(如直线、圆等),最后在命令行打印出实体的类型和总数。

算法思路

  1. 获取当前工作数据库的指针。

  2. 打开数据库中的“块表”(BlockTable)。

  3. 从块表中获取指向“模型空间”(Model Space)的记录。

  4. 创建一个迭代器来遍历模型空间中的所有实体。

  5. 在循环中,获取每个实体,打印其类型,并计数。

  6. 释放所有打开的对象和迭代器。

代码解析

cpp
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文件的内容到这个数据库中,然后同样遍历模型空间的实体并打印信息。

算法思路

  1. 创建一个新的、空的数据库对象。

  2. 调用 readDwgFile 方法读取指定路径的DWG文件。

  3. 后续步骤与 testCurrentDb 完全相同:获取块表 -> 获取模型空间 -> 创建迭代器 -> 遍历实体。

  4. 断开与文件的连接并删除数据库对象,释放内存。

代码解析

cpp
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() vs workingDatabase(): 前者用于在内存中创建全新的或用于读取文件的数据库;后者用于获取CAD当前正在编辑的数据库。

    • readDwgFile(): 这是将磁盘上的静态文件加载到内存中进行操作的关键函数。

    • delete pDb: 因为这个数据库是我们自己 new 出来的,它不属于CAD应用程序管理,所以当我们用完后,必须 delete 它来释放内存。这与 workingDatabase() 返回的数据库指针不同,那个不能 delete


任务三:在模型空间添加和删除直线

这个任务进入到修改数据库的阶段,演示了最基础的“写”操作:创建实体并添加到模型空间,然后再将其删除。这对应课堂上讲的“添加对象到数据库”和“修改数据库中的对象”。

功能说明: 在当前图纸的模型空间中绘制两条交叉的直线,然后删除其中的第一条。

算法思路

  1. 获取当前工作数据库。

  2. 以“可写”模式打开块表和模型空间记录。

  3. 创建两个 AcDbLine 对象(定义起点和终点)。

  4. 使用 appendAcDbEntity 将两条直线添加到模型空间,并获取它们的 ObjectId

  5. 使用获取到的 ObjectId,以“可写”模式重新打开第一条直线。

  6. 调用该直线对象的 erase() 方法将其标记为删除。

  7. 关闭所有打开的对象。

代码解析

cpp
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 vs kForWrite):获取对象时必须明确意图。如果只是查看,用 kForRead;如果要修改或向其内部添加/删除内容,必须用 kForWrite

    • appendAcDbEntity(): 这是向块表记录(如模型空间)添加新实体的标准函数。它会将 new 出来的对象的所有权转移给数据库。

    • AcDbObjectId: 这是对象在数据库中的唯一标识(在一次编辑会话中)。后续要操作这个对象,就需要通过它的ID来打开。

    • erase(): 删除实体并不是 delete 指针,而是调用其 erase() 方法。真正的物理删除通常发生在图纸关闭或执行 PURGE 命令时。


任务四:创建块定义并插入块引用

这是一个更综合的任务,涉及到CAD中一个非常重要的概念——块。它对应课堂上讲的“创建块”和插入“块引用”。

功能说明

  1. 创建一个名为 "Sample" 的新块定义。 2r2

  2. 在这个块定义中,添加一个半径为50的圆。

  3. 在模型空间的原点位置,插入一个这个 "Sample" 块的实例(即块引用)。

算法思路

  1. 以可写方式打开块表,因为要向其中添加新的块定义。

  2. 创建一个新的 AcDbBlockTableRecord 对象,并设置其名称 "Sample" 和基点。

  3. 将这个新的块记录添加到块表中。

  4. 创建一个 AcDbCircle 对象。

  5. 将这个圆 append 到刚刚创建的块记录中(而不是模型空间!)。

  6. 关闭所有对象。

  7. 为了插入块引用,需要再次打开模型空间(可写)和块表(可读)。

  8. 从块表中通过名字 "Sample" 找到我们刚刚创建的块定义的 ObjectId

  9. 创建一个 AcDbBlockReference 对象,指定插入点和块定义的ID。

  10. 将这个块引用 append 到模型空间。

  11. 关闭所有对象。

代码解析

cpp
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。


任务五(作业):

这是最复杂的作业,综合了前面几乎所有的知识点,并引入了更高级的“词典”和“扩展词典”概念,这对应课堂上讲的“词典操作”。

功能说明

  1. 创建一个名为 "Concentric255" 的块定义。

  2. 在块定义中,循环创建255个同心圆,半径从1到255,并且每个圆的颜色索引也从1到255。

  3. 为每个圆创建“扩展词典”,并在其中用一个 Xrecord 记录下该圆的颜色索引值。

  4. 在数据库的“命名对象词典”下,创建一个新的词典 "CircleCenters"。

  5. 在这个新词典中,存入255条记录,每条记录用一个 Xrecord 存储圆的中心坐标(虽然都是原点)。

  6. 最后,在模型空间插入这个 "Concentric255" 块的引用。

算法思路

  1. 创建块定义:与任务五类似,先创建名为 "Concentric255" 的 AcDbBlockTableRecord

  2. 循环创建圆

    • for 循环 255 次。

    • 在循环中,new AcDbCircle,设置半径和颜色。

    • 将圆 append 到块定义中。

    • (难点1)写入扩展词典

      • 检查圆有无扩展词典,没有则 createExtensionDictionary()

      • 打开扩展词典(可写)。

      • 创建 AcDbXrecord,用 resbuf 链表构建要存储的数据(颜色索引)。

      • setAt 方法将 Xrecord 存入扩展词典,键为 "ColorIdx"。

      • 关闭所有相关对象。

  3. 创建和写入命名词典

    • (难点2) 打开数据库的根词典——命名对象词典 (namedObjectsDictionaryId)。

    • 在其中 setAt 一个新的 AcDbDictionary,键为 "CircleCenters"。

    • 打开这个 "CircleCenters" 词典。

    • 循环255次,每次都创建一个 Xrecord 来存储原点坐标,并以 "C001", "C002"... 为键 setAt 到词典中。

    • 关闭所有相关对象。

  4. 插入块引用:与任务五完全相同,获取 "Concentric255" 的ID,在模型空间创建 AcDbBlockReference 并添加。

代码解析

cpp
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 链表的辅助函数,非常方便。