简介:Teigha是Open Design Alliance推出的CAD文件处理开发库,核心能力覆盖DWG读取、新建与后台文字替换。这份“Teigha基本应用实例”面向需要在自研程序中集成CAD数据读写能力的.NET开发者,适合学习如何初始化Teigha环境、遍历模型空间实体、创建图层与块,以及批量定位并替换图纸文字。资源包共63个文件,大小6.57MB,包含21个dll(Teigha核心库及依赖)、6个cs(源码)、3个exe(可执行程序)等,另有配置与编译中间文件,整体是一个可直接运行的Visual Studio示例工程。已有1259人学习下载。通过该实例,读者可以掌握用Teigha 4.0打开现有DWG、从模型空间获取图形对象、新建文件并写入自定义元素,以及在不打开图形界面的情况下完成文字批量替换的核心流程,同时了解保存、关闭与异常处理等注意事项,适合CAD二次开发入门者参考和复用。
1. 需求与场景:为什么我放弃了手写 DXF 解析,转向 Teigha
接手某 CAD 二次开发小项目时,需求听起来很简单:把一批 DWG 图纸里的直线、圆和图层信息提取出来。我最初的想法是自己写 DXF 解析器,毕竟 DXF 是文本格式,按段读了就行。结果一跑真实图纸就翻车:块表结构、匿名块、扩展数据 XData、代理实体全部冒出来,解析器越改越长,连最基本的圆和圆弧都分不清方向。后来换了思路,直接用 Teigha(现在很多资料里也叫 Drawing SDK)做底层解析。Teigha 是专门读写 DWG、DXF 文件的开发库,自带了完整的对象模型,不用自己处理文件格式。这份“Teigha 基本应用实例”要解决的,就是从读取文件、遍历实体到创建图形并保存的全套流程。适合正在做 CAD 相关课设、毕设,或者要快速交付一个图纸处理工具的人参考。整套方案围绕一个目标:不碰文件格式细节,直接用 API 完成图纸的读取、修改和写回。
2. 环境搭建与工程骨架:SDK 初始化、库配置和第一个可运行程序
2.1 从下载包到可用工程:文件结构与库路径配置
拿到 Teigha 的 SDK 压缩包后,第一件事不是写代码,而是看清目录结构。一个典型的发行包会包含 Include、Lib、Bin 三个核心目录。Include 里是头文件,Lib 里是导入库,Bin 里是运行时动态库。我见过不少人在这一步就踩坑:把 Lib 路径配到了 64 位目录,程序编译过了但链接失败;或者把 Bin 里的 DLL 丢到系统目录,结果把系统原有的同名文件覆盖了。稳妥的做法是把依赖路径全部配置在工程文件里,不要碰系统目录。
以 Visual Studio 工程为例,需要配置三处:
// 头文件目录(附加包含目录) // SDK安装目录/Include // 库文件目录(附加库目录) // SDK安装目录/Lib/win64 // 运行时动态库(调试与发布都要) // SDK安装目录/Bin/win64常见的 SDK 发行包会提供 win32 和 win64 两个平台的库,选择哪个取决于目标程序要跑在什么环境。如果做毕设演示机是 64 位系统,直接使用 64 位库;如果你的项目要兼容老机器或者被其他 32 位进程加载,那就要用 32 位库。这里有一个容易被忽略的点:Teigha 是面向 C++ 的 SDK 为主,但也提供 .NET 和 Java 的封装。如果熟悉 C++,直接使用 C++ API 是最省事的路径,因为示例代码几乎都是 C++。
核心库模块有一个需要区分的概念:不是所有功能都在同一个 DLL 里。通常会有核心对象库、图形系统库、渲染库等。做基本应用实例只需要核心库,不需要全部加载。表 1 列出了最常用的几个模块:
| 模块 | 头文件特征 | 作用 |
|---|---|---|
| 数据库核心 | Db*.h | 管理图形数据库,读写 DWG/DXF |
| 几何工具 | Ge*.h | 点、向量、矩阵等数学对象 |
| 宿主服务 | OdDbHostApp.h | 提供文件访问、环境设置等支撑 |
| 事务机制 | OdDbTransaction.h | 实体修改与提交的底层支持 |
2.2 宿主程序与初始化顺序
Teigha 在正式使用前必须完成初始化,这个初始化不是简单的 new 一个对象,而是调用 odInitialize()。这个函数会加载内部的服务组件,建立程序运行的环境。与之对应,程序退出前调用 odUninitialize() 释放资源。很多初学者漏掉这一步,运行到读取文件时才报错,而且报的错误很隐晦,看起来像内存越界。实际上只是初始化没做。
初始化之外还要创建一个宿主服务对象 OdDbHostAppServices。这个对象的作用是让 SDK 知道程序运行在什么上下文里,比如文件找不到时往哪里写日志、临时文件放在哪个目录。在示例程序里,可以直接使用系统提供的默认实现。
下面是第一个可运行程序的完整骨架,这个程序完成一件事:读取一个 DWG 文件,并在控制台输出文件中模型空间的实体数量。这个骨架是后续所有实例的基础,建议把它做成模板工程,不要每次复制粘贴。
#include "OdaCommon.h" #include "OdDbHostApp.h" #include "OdDbDatabase.h" #include "OdDbBlockTable.h" #include "OdDbBlockTableRecord.h" #include "OdDbBlockTableRecordIterator.h" int main() { // 初始化 Teigha 运行环境 ::odInitialize(); try { // 创建宿主服务,负责文件访问等底层操作 OdDbHostAppServices appServices; // 读取 DWG 文件 OdDbDatabasePtr pDb = OdDbDatabase::readFile( L"input.dwg", &appServices); // 获取块表(BlockTable) OdDbBlockTablePtr pBlockTable = pDb->getBlockTable(); // 从块表中获取模型空间(Model Space) OdDbBlockTableRecordPtr pModelSpace = pBlockTable->getAt(L"Model"); // 创建模型空间迭代器 OdDbBlockTableRecordIteratorPtr pIter; pModelSpace->newIterator(pIter); // 遍历并统计实体数量 int entityCount = 0; for (; !pIter->done(); pIter->step()) { entityCount++; } printf("实体数量: %d\n", entityCount); } catch (const OdError& e) { // SDK 异常统一从这里获取 printf("错误: %ls\n", e.description().c_str()); } // 释放 Teigha 运行环境 ::odUninitialize(); return 0; }这段代码的核心逻辑分成四步:初始化、读文件、获取模型空间、遍历。readFile 的第二个参数是宿主服务指针,如果不传,部分 SDK 版本会直接崩溃,因为它需要宿主服务来处理文件流。getAt(L"Model") 拿到了模型空间,这是图纸里最常用的容器,图纸上的可见图形基本都在这里。newIterator 返回一个迭代器,用于逐个访问实体。最后一个 try-catch 非常重要:Teigha 在文件不存在或者文件版本不兼容时,不是返回空指针,而是抛出 OdError 异常。如果不捕获,程序会直接终止。
编译运行前还有一个必须做的配置:把 Bin 目录下的 DLL 放到可执行文件同目录,或者把 Bin 目录加入系统 PATH 环境变量。这一步决定了程序能否正常运行。常见的错误是程序编译成功,双击运行时提示“找不到动态链接库”,原因就是 DLL 路径没配置好。
2.3 解决一个黑匣子问题:读取失败时如何定位
如果 readFile 报错,通常不是代码问题,而是环境问题。从我的排查经验看,集中在三类:DLL 版本不匹配、文件版本比 SDK 支持的版本新、路径中包含中文或特殊字符。DLL 版本不匹配的表现是编译时选用的库是 32 位,但程序编译成了 64 位;或者反过来。文件版本不支持的表现是抛出“读文件失败”异常,例如用旧版 SDK 去打开新版 CAD 保存的文件。中文路径的问题则比较微妙,表现是文件明明存在,但 readFile 抛出找不到文件的错误。
3. 核心对象模型与文件读写:从 Database 到 BlockTable 的遍历链
3.1 对象体系:一张图看清 Database、BlockTable、BlockTableRecord 的关系
Teigha 的对象模型模仿了 CAD 内部的数据组织方式,理解这套层级是写任何实例的前提。最顶层是 OdDbDatabase,代表一个完整的图形数据库,对应内存中的一个 DWG 文件。数据库里有块表 OdDbBlockTable,块表是一个索引容器,按名字保存了所有的块定义。每个块定义是一个 OdDbBlockTableRecord,模型空间就是一种特殊的块定义记录。真正的实体,比如直线、圆、文字,存放在 BlockTableRecord 中。
这个层级关系和文件系统很像:Database 是磁盘,BlockTable 是根目录,BlockTableRecord 是文件夹,Entity 是文件。遍历实体时,必须按照这个链条一层层取。很多初学者直接尝试从 Database 拿实体,编译都不通过,就是因为没理解这个层级。模型空间对应的名字是“Model”,图纸空间对应的是“Paper”,这两个名字是固定的。块表中其他名字是用户自定义的块定义,比如常见的图框块、标题栏块。
为什么要把实体放在块表记录里而不是直接放数据库?因为 CAD 的实体要么在模型空间直接绘制,要么属于某个块定义,块定义可以被多次插入(称为块引用)。如果实体直接挂在数据库下,块引用就无法实现了。这就是为什么读写文件时,遍历路径必须是 Database -> BlockTable -> BlockTableRecord。
3.2 遍历 ModelSpace 并识别实体类型
遍历模型空间是基本应用中最常见的操作。读取实体后,下一步通常是根据类型做不同处理。实体类型判断用 isKindOf 函数,每个实体类都有对应的 desc() 静态方法。下面的代码展示了如何遍历并统计直线、圆、圆弧的数量:
#include "OdDbLine.h" #include "OdDbCircle.h" #include "OdDbArc.h" int lineCount = 0; int circleCount = 0; int arcCount = 0; for (; !pIter->done(); pIter->step()) { // getEntity 返回当前实体 OdDbEntityPtr pEnt = pIter->getEntity(); // 判断实体类型 if (pEnt->isKindOf(OdDbLine::desc())) { lineCount++; } else if (pEnt->isKindOf(OdDbCircle::desc())) { circleCount++; } else if (pEnt->isKindOf(OdDbArc::desc())) { arcCount++; } } printf("直线: %d, 圆: %d, 圆弧: %d\n", lineCount, circleCount, arcCount);isKindOf 本质上是做 RTTI 类型识别,它比 C++ 的 dynamic_cast 更安全,因为 Teigha 的对象模型支持跨模块类型判断。desc() 返回一个描述对象类型的静态结构,Teigha 内部用这个结构做类型树判断。有一个需要注意的陷阱:实体的类型是继承关系。例如多段线 OdDbPolyline 继承自曲线 OdDbCurve,而曲线又继承自实体 OdDbEntity。用 isKindOf(OdDbCurve::desc()) 判断时,多段线也会返回 true。所以在做类型过滤时,要先判断具体类型,再判断父类型,避免重复统计。
3.3 提取几何数据:拿直线的起点终点和圆的圆心半径
类型识别只是第一步,很多时候需要把实体的几何参数提取出来做计算,例如算图纸中所有直线的总长度。下面是提取几何数据的示例:
#include "OdGePoint3d.h" double totalLength = 0.0; for (; !pIter->done(); pIter->step()) { OdDbEntityPtr pEnt = pIter->getEntity(); if (pEnt->isKindOf(OdDbLine::desc())) { // 将基类指针转到直线类 OdDbLinePtr pLine = OdDbLine::cast(pEnt); // 获取起点和终点 OdGePoint3d startPt = pLine->startPoint(); OdGePoint3d endPt = pLine->endPoint(); // 计算两点距离 double dx = endPt.x - startPt.x; double dy = endPt.y - startPt.y; double length = sqrt(dx * dx + dy * dy); totalLength += length; } else if (pEnt->isKindOf(OdDbCircle::desc())) { OdDbCirclePtr pCircle = OdDbCircle::cast(pEnt); // 获取圆心和半径 OdGePoint3d center = pCircle->center(); double radius = pCircle->radius(); printf("圆心: (%lf, %lf), 半径: %lf\n", center.x, center.y, radius); } }这里使用 cast 做类型转换,这是 Teigha 里推荐的安全指针转换方式。startPoint() 和 endPoint() 返回的是 OdGePoint3d,也就是三维点,x、y、z 分别对应三个坐标轴。即使图纸是二维的,z 值通常也是 0,但不要默认它是 0,从旧版图纸读出的数据偶尔会有非零 z 值。
关于直线长度的计算,看起来简单,但实际项目中我一般不会自己写距离公式,因为 Teigha 的 OdGePoint3d 已经重载了减法运算符,可以直接算向量,然后用 length() 方法。自己写公式也可以,只要注意浮点精度即可。这个基本应用实例里保留手写公式,是为了把几何对象的使用方法讲清楚。
4. 实体创建与修改:从画一条线到完整写回图纸
4.1 创建新图:从一个空白数据库开始
只读文件不够,很多场景要求程序自动生成图纸,比如批量导出坐标点到 CAD 图纸。这时需要从零创建数据库。下面的代码创建了一个空白数据库,并且向模型空间写入一条直线和一个圆:
#include "OdDbLine.h" #include "OdDbCircle.h" // 创建空白数据库 OdDbDatabasePtr pDb = OdDbDatabase::createObject(); // 获取模型空间 OdDbBlockTablePtr pBlockTable = pDb->getBlockTable(); OdDbBlockTableRecordPtr pModelSpace = pBlockTable->getAt(L"Model"); // 创建一条直线:从 (0,0,0) 到 (100,50,0) OdDbLinePtr pLine = OdDbLine::createObject(); pLine->setStartPoint(OdGePoint3d(0, 0, 0)); pLine->setEndPoint(OdGePoint3d(100, 50, 0)); // 将直线添加到模型空间 pModelSpace->appendOdDbEntity(pLine); // 创建一个圆:圆心 (50,25,0),半径 20 OdDbCirclePtr pCircle = OdDbCircle::createObject(); pCircle->setCenter(OdGePoint3d(50, 25, 0)); pCircle->setRadius(20.0); // 将圆添加到模型空间 pModelSpace->appendOdDbEntity(pCircle); // 保存为 DWG 文件 pDb->saveAs(L"output.dwg");创建实体有两个关键步骤:createObject 创建对象并设置属性,appendOdDbEntity 把对象挂到模型空间。注意顺序不能反:没有设置属性就挂到模型空间虽然不报错,但会在图纸里留下一个位置为 (0,0,0) 的残次实体。另外,createObject 创建的对象在挂到数据库之前,不受数据库管理,修改属性不会触发自动保存。这是一个和直觉相反的地方,很多第一次用的人会在这里困惑:为什么改了 line 的坐标再保存,文件里没变化?原因就是忘记 appendOdDbEntity。
flush:## 4.2 修改已有实体:从查找到更新属性的完整流程
现实中的需求不止是创建新图,更多是修改已有图纸。例如批量把图纸中所有圆的半径改成原来的一半,或者把所有直线的颜色改成红色。修改流程比创建多一步:先找到要改的实体,再改属性。这里要提醒一个关键操作:实体修改之后,需要调用 recordModified 或依赖事务提交让数据库知道这个实体发生了变化,最简单的方式是直接调用实体对象的 recordModified()。
下面的代码演示了如何遍历所有圆并修改半径:
// 假设 pDb 已经通过 readFile 读取了文件 OdDbBlockTablePtr pBlockTable = pDb->getBlockTable(); OdDbBlockTableRecordPtr pModelSpace = pBlockTable->getAt(L"Model"); OdDbBlockTableRecordIteratorPtr pIter; pModelSpace->newIterator(pIter); for (; !pIter->done(); pIter->step()) { OdDbEntityPtr pEnt = pIter->getEntity(); if (pEnt->isKindOf(OdDbCircle::desc())) { OdDbCirclePtr pCircle = OdDbCircle::cast(pEnt); // 获取原半径并减半 double oldRadius = pCircle->radius(); pCircle->setRadius(oldRadius / 2.0); // 通知数据库该实体已修改 pCircle->recordModified(); } } // 保存覆盖原文件 pDb->save();recordModified 是一个很容易被忽略的调用。Teigha 的数据库内部有缓存机制,appendOdDbEntity 之后,数据库知道有新实体加入,但实体加入之后如果属性变化,数据库不一定会感知。有些版本会自动处理,有些不会。最稳妥的写法是任何属性修改后都调用 recordModified。这种情况属于典型的“玄学问题”:不调用好像也能用,偶尔会失败,失败时根本找不到原因。
4.3 处理图层、颜色和线型:实体的非几何属性
实体不仅有几何数据,还有图层、颜色、线型等显示属性。在 Teigha 中,设置图层不是直接给实体赋值一个字符串,而是先建立图层记录,再把实体指向该图层。下面是最常用的设置方式:
// 设置实体颜色为红色(颜色索引 1) pLine->setColorIndex(1); // 创建新图层 OdDbLayerTablePtr pLayerTable = pDb->getLayerTable(); OdDbLayerTableRecordPtr pLayer = OdDbLayerTableRecord::createObject(); pLayer->setName(L"CenterLine"); pLayer->setColorIndex(2); // 黄色 // 把图层添加到图层的表中 pLayerTable->add(pLayer); // 将实体放到该图层 pLine->setLayer(L"CenterLine");颜色索引是一个数字编号,1 是红色,2 是黄色,3 是绿色,7 是白色或黑色(取决于背景)。这个编号和 CAD 里的 ACI 颜色号一致。setLayer 接收的是图层名字符串,如果指向一个不存在的图层,程序不会立即报错,但打开图纸时会有异常表现。所以创建图层和设置图层之间的顺序很重要:先建立图层再设置实体,不要反过来。
一个常见的批量处理需求是把所有实体放到同一个图层。直接遍历并调用 setLayer 就能完成,但性能上有优化空间:大批量操作时可以先关闭数据库的自动记录功能,全部改完再统一保存,能显著加快速度。这个技巧在做几千个实体的批量修改时很有用,基本应用里不强制需要。
4.4 保存与格式选择:save、saveAs 和 DXF 导出
Teigha 支持保存为 DWG 或 DXF。saveAs 可以指定格式和版本。核心差异是保存格式决定目标文件的兼容性。导出低版本格式虽然兼容性好,但可能丢失新版特性。下面的代码把数据库保存为 R2000 格式的 DXF 文件,用于与老版本 CAD 交互:
// 保存为 DXF,格式选择 R2000 pDb->saveAs(L"output.dxf", OdDb::kDxf, OdDb::kR2000);参数说明:第二个参数是保存类型,kDxf 表示 DXF 文件,kDwg 表示 DWG 文件;第三个参数是版本号,kR2000 对应 AutoCAD 2000 格式。除 R2000 外,常见的还有 kR12、kR14、kR2004、kR2007、kR2010、kR2013 和 kR2018。如果目标环境是较新的 CAD,直接使用 kR2018 可以保留全部特性。如果只是内部存档,用 kR2004 是稳妥选择。
关于文件保存还要注意一个细节:saveAs 是重命名写入,会覆盖同路径同名文件。如果希望保留原文件,调用前先做好备份,或者把路径改成新文件。实际项目中我一般在保存前用标准库复制一份原文件,相当于给数据上一个保险。这不是 SDK 的限制,而是工程习惯。
5. 避坑与常见问题排查:初始化失败、保存翻车和黑匣子里的玄学
5.1 现象:程序启动后 odInitialize 就崩溃或卡死
原因:odInitialize 内部会加载和注册服务模块,如果 Bin 目录下的 DLL 不在可执行文件的搜索路径中,程序会直接崩溃;另一个常见原因是初始化被调用了两次,第二次调用破坏了内部状态。
解决:在 main 函数入口处只调用一次 odInitialize,并在程序退出时只调用一次 odUninitialize。排查路径问题时用 Dependencies 工具检查可执行文件依赖的 DLL 是否都能找到,不要凭感觉在工程配置里反复添加路径。
5.2 现象:readFile 明明文件存在,却抛出文件不存在的异常
原因:路径中包含中文字符或特殊字符,SDK 内部使用宽字符 API 时对本地编码处理不一致;或者文件是只读属性,SDK 以读写方式打开失败。
解决:把测试文件和输出路径放在纯英文目录下,例如 D:\TestData\input.dwg。不要在桌面路径下测试,桌面的用户名如果是中文,很可能触发这个坑。如果必须要支持中文路径,考虑使用短路径或者先把文件复制到临时目录再读取。这一点是很多初学者最容易困惑的:程序在别人的电脑上能跑,自己电脑上跑不起来,最后排查出来的原因是系统用户名是中文。
5.3 现象:遍历实体时程序抛出“集合已修改”之类的异常
原因:在遍历迭代器过程中对同一个 BlockTableRecord 执行了 appendOdDbEntity 或者删除操作。迭代器内部维护了位置索引,集合结构变化会导致索引失效。
解决:不要在遍历循环内修改实体的集合结构。正确做法是先在遍历过程中收集需要操作的实体指针到一个容器,比如 std::vector,遍历结束后再对容器中的实体执行添加、删除或修改。删除实体时使用 OdDbEntity::erase,但要在模型空间的记录上操作,通常是调用 pModelSpace->erase(pEnt) 或直接调用 pEnt->erase()。
5.4 现象:saveAs 保存失败,文件变成一个 0 字节的空文件
原因:保存路径不存在。SDK 不会自动创建多级目录,如果输出目录不存在,会直接写入失败,并在目标位置留下一个空文件。另一个常见原因是目标文件被其他程序占用,CAD 正在打开同一个文件。
解决:保存前检查目录是否存在,不存在就创建完整的目录链。同时检查目标文件是否被占用。如果程序是在 CAD 插件里运行的,还要考虑 CAD 本身可能正在使用同一张图纸,此时不能直接覆盖。保存失败时留意异常信息中的 description,Teigha 的异常描述通常指明了失败原因。
5.5 现象:程序能正常读写,但在释放时崩溃
原因:智能指针的循环引用导致资源没有及时释放。OdDbDatabasePtr 和实体指针内部使用的是引用计数机制,如果两个对象互相持有对方,引用计数不会归零。
解决:循环引用场景主要在实体反向持有数据库的扩展数据时出现,普通应用不太会遇到。如果遇到,在 odUninitialize 前执行一次 pDb->closeInput() 或明确将智能指针置为空,例如 pModelSpace = nullptr; pBlockTable = nullptr; pDb = nullptr; 然后再调用 odUninitialize。这个操作相当于手动释放资源,避免最终清理时因为内部指针纠缠而崩溃。
6. 验收清单与答辩准备:怎么证明这套实例能可靠交付
验收时不看代码,只看行为和输出。我的验收方法是用同一组测试文件跑完五个用例,每个用例对应一个基本功能点。测试文件在纯英文路径下,包含一个直线、圆、圆弧、多段线和一个块引用组成的简单图纸。五个用例分别是:读取并统计实体数量、提取直线端点与圆参数、创建新图并保存、修改已有圆的半径、批量设置图层颜色。每个用例执行完成后,用 CAD 打开输出文件人工比对。
验收标准按照表 2 执行:
| 用例 | 预期结果 | 验收方式 |
|---|---|---|
| 读取统计 | 输出实体数量与图纸一致 | 与 CAD 特性面板对照 |
| 参数提取 | 坐标值精确到 0.01 | 与随机选点测量值对照 |
| 新建保存 | 输出 DWG 能被正常打开 | 直接双击打开 |
| 修改半径 | 半径变为原来一半 | 用特性面板测量 |
| 图层设置 | 实体显示在对应图层 | 查看图层面板 |
答辩准备时,有一个一定会被问到的问题:为什么用 Teigha 而不是直接操作 DXF 文件?我的回答思路是:DXF 是公开格式,但真实图纸远比文本格式复杂,块表、字典、扩展数据、代理实体这些内容用文本方式维护成本极高;Teigha 把文件读写封装成对象模型,开发效率更高,出错率更低。这个回答能说服答辩老师,也体现方案的技术选型是经过对比的。
另一个高频问题是实体如何遍历。建议准备一张手绘的对象层级图,从 Database 画到 BlockTable,再到 BlockTableRecord,最后到 Entity,讲清楚这个链条。这张图比任何口述都有说服力。
还有一条血泪经验:不管代码多简单,一定在验收前做一次全量重新编译,用新的 exe 完成一遍完整流程。我有一次在答辩演示时用旧 exe 跑出了旧结果,当场被问“为什么和代码不一致”,非常尴尬。从那以后,我每次交付前都强制走一遍“重新编译、清理临时文件、英文路径、完整用例”流程。希望这套流程和方案能帮到你。
本文还有配套的精品资源,点击获取