
简介Teigha是Open Design Alliance推出的CAD文件处理开发库核心能力覆盖DWG读取、新建与后台文字替换。这份“Teigha基本应用实例”面向需要在自研程序中集成CAD数据读写能力的.NET开发者适合学习如何初始化Teigha环境、遍历模型空间实体、创建图层与块以及批量定位并替换图纸文字。资源包共63个文件大小6.57MB包含21个dllTeigha核心库及依赖、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( Linput.dwg, appServices); // 获取块表BlockTable OdDbBlockTablePtr pBlockTable pDb-getBlockTable(); // 从块表中获取模型空间Model Space OdDbBlockTableRecordPtr pModelSpace pBlockTable-getAt(LModel); // 创建模型空间迭代器 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(LModel) 拿到了模型空间这是图纸里最常用的容器图纸上的可见图形基本都在这里。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(LModel); // 创建一条直线从 (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(Loutput.dwg);创建实体有两个关键步骤createObject 创建对象并设置属性appendOdDbEntity 把对象挂到模型空间。注意顺序不能反没有设置属性就挂到模型空间虽然不报错但会在图纸里留下一个位置为 (0,0,0) 的残次实体。另外createObject 创建的对象在挂到数据库之前不受数据库管理修改属性不会触发自动保存。这是一个和直觉相反的地方很多第一次用的人会在这里困惑为什么改了 line 的坐标再保存文件里没变化原因就是忘记 appendOdDbEntity。flush## 4.2 修改已有实体从查找到更新属性的完整流程现实中的需求不止是创建新图更多是修改已有图纸。例如批量把图纸中所有圆的半径改成原来的一半或者把所有直线的颜色改成红色。修改流程比创建多一步先找到要改的实体再改属性。这里要提醒一个关键操作实体修改之后需要调用 recordModified 或依赖事务提交让数据库知道这个实体发生了变化最简单的方式是直接调用实体对象的 recordModified()。下面的代码演示了如何遍历所有圆并修改半径// 假设 pDb 已经通过 readFile 读取了文件 OdDbBlockTablePtr pBlockTable pDb-getBlockTable(); OdDbBlockTableRecordPtr pModelSpace pBlockTable-getAt(LModel); 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(LCenterLine); pLayer-setColorIndex(2); // 黄色 // 把图层添加到图层的表中 pLayerTable-add(pLayer); // 将实体放到该图层 pLine-setLayer(LCenterLine);颜色索引是一个数字编号1 是红色2 是黄色3 是绿色7 是白色或黑色取决于背景。这个编号和 CAD 里的 ACI 颜色号一致。setLayer 接收的是图层名字符串如果指向一个不存在的图层程序不会立即报错但打开图纸时会有异常表现。所以创建图层和设置图层之间的顺序很重要先建立图层再设置实体不要反过来。一个常见的批量处理需求是把所有实体放到同一个图层。直接遍历并调用 setLayer 就能完成但性能上有优化空间大批量操作时可以先关闭数据库的自动记录功能全部改完再统一保存能显著加快速度。这个技巧在做几千个实体的批量修改时很有用基本应用里不强制需要。4.4 保存与格式选择save、saveAs 和 DXF 导出Teigha 支持保存为 DWG 或 DXF。saveAs 可以指定格式和版本。核心差异是保存格式决定目标文件的兼容性。导出低版本格式虽然兼容性好但可能丢失新版特性。下面的代码把数据库保存为 R2000 格式的 DXF 文件用于与老版本 CAD 交互// 保存为 DXF格式选择 R2000 pDb-saveAs(Loutput.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 本身可能正在使用同一张图纸此时不能直接覆盖。保存失败时留意异常信息中的 descriptionTeigha 的异常描述通常指明了失败原因。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 跑出了旧结果当场被问“为什么和代码不一致”非常尴尬。从那以后我每次交付前都强制走一遍“重新编译、清理临时文件、英文路径、完整用例”流程。希望这套流程和方案能帮到你。本文还有配套的精品资源点击获取