简介:基于 Open Design Alliance 官方 Teigha 4.0 开发的 RepText 应用实例,完整演示了在 .NET 环境下读写与新建 DWG 文件、遍历模型空间实体以及后台批量文字替换的核心流程。资源面向具备一定 C# 基础的 CAD 二次开发人员,适合用作 Teigha 入门与项目起步的参考工程。压缩包共 63 个文件,大小 6.57MB,主体为 21 个运行所需的 dll 库文件、6 个 C# 源码文件、3 个可执行程序以及若干配置文件与解决方案文件,目录结构清晰,可直接用 Visual Studio 打开工程进行编译或对照学习。工程覆盖 DWG 打开、图元遍历、图层创建、文字查找替换、保存关闭等关键环节,并包含可运行的 exe 与调试信息,便于读者验证效果并进一步扩展为定制化的 CAD 工具。已有 1259 人学习,对于希望快速掌握 Teigha API 调用方式的开发者来说是一份高性价比的入门资料。
1. 把 Teigha 当二开框架用,方向就反了
第一次接触 Teigha 的人,十有八九会把它当成 AutoCAD 的替代品,或者是某种可以直接画图的控件。我最早也是这么以为的,直到某次需要在不装 AutoCAD 的服务器上批量处理 DWG 文件,翻了一晚上资料才反应过来——Teigha 的本质是一套面向开发者的图形数据访问库,它的核心能力是「读写 DXF/DWG 文件并在内存里操作几何与实体数据」,而不是一个可视化编辑器。这意味着你可以用它写一个后台服务,定时把图纸里的图框信息抽出来入库,或者把旧版图纸批量升级成新版格式,也可以在 Web 后端生成 DXF 预览文件。这套资源的价值,恰恰在于它把这些基本用法拆成了可以直接跑通的实例,适合刚接触 CAD 数据开发的从业者,也适合需要在服务端处理图纸的架构师快速评估技术路线。
2. 实例包结构:先搞清楚每段代码解决什么问题
2.1 一个最小工程的基本组成
拿到 Teigha 基本应用实例这套资源后,先别急着编译,花十分钟把目录结构过一遍是值得的。我见过的实例工程通常包含四类东西:一是用 CMake 或 Visual Studio 工程文件组织的基础框架,二是针对核心 API 的示例代码文件,三是几个测试用的 DXF 或 DWG 文件,四是说明文档或编译脚本。初步判断这套资源也按此组织。
编译前的关键一步是检查库文件路径。Teigha 的 SDK 一般会给你 include 目录和 lib 目录,实例工程里引用的头文件路径与实际的 SDK 安装目录需要一致。如果拿到的是源码包,里面通常附带一个指向 SDK 根目录的环境变量配置,比如通过属性表或 CMake 变量传入。
提示:先把 SDK 目录结构完整看一遍,确认存在 Inc、Lib、Bin 三个标准子目录后再动编译器,步骤能省很多力气。
2.2 初始化顺序是第一个坑
Teigha 的程序不是直接 new 一个数据库对象就能开始操作,它要求先完成运行时的初始化。实例代码里通常在 main 函数开头就会出现这类逻辑——创建全局的 HostAppServices 子类实例,并在进入业务逻辑前调用初始化函数。我一般会写成:
#include "OdaCommon.h" #include "OdKernelServices.h" #include "DynamicLinker.h" class MyHostAppServices : public OdHostAppService { public: virtual OdString getWinSysDir() override { return L"C:\\Program Files\\TeighaSDK\\Bin"; } }; ODRX_DECLARE_STATIC_MODULE(MyHostAppServices); ODRX_BEGIN_STATIC_MODULE_MAP(MyHostAppServices) ODRX_DEFINE_STATIC_APPLICATION(OdKernelModule) ODRX_DEFINE_STATIC_APPLICATION(OdDynamicLinkerModule) ODRX_END_STATIC_MODULE_MAP(MyHostAppServices) int main() { MyHostAppServices app; OdStaticRxObject<MyHostAppServices> svc; odInitialize(&svc); // 读取文件、处理实体的业务逻辑写在这里 odUninitialize(); return 0; }这段代码的逻辑固定,基本上没有太多可改的空间。MyHostAppServices必须继承自OdHostAppService并实现getWinSysDir()方法,这里返回的路径是 SDK 运行时依赖库所在的目录,编译完拷贝 exe 之后,系统是靠这个路径去加载 Teigha 的动态库。odInitialize负责把所有基础模块挂载到内存中,随后的odUninitialize在程序退出时释放这些资源。如果顺序反了或者根本没有调用初始化,后面无论执行什么操作都会直接在内存访问上崩掉。
参数说明中唯一需要关注的是getWinSysDir()的返回值。示例工程里可能写的是相对路径或者写死的绝对路径,但你在自己的环境上部署时,最好根据实际安装位置来设置。另一个容易忽略的选择是static module map里要注册哪些模块,这个根据你用到的功能来增减,只做基本文件读取时,保留内核和动态加载器即可。
2.3 头文件包含与链接库的对应关系
看完初始化代码,下一步要对照的是链接库。Teigha 的类库划分得很细,不同的功能被拆到不同的动态库里。最常见的三个是:Db 库(包含数据库、实体对象相关类)、Rx 库(包含运行时类型信息、对象引用机制)和 Ge 库(包含向量、矩阵、几何计算类)。实例代码里如果出现了OdGePoint3d这类几何类,说明链接配置里必须有 Ge 库。
链接阶段的报错需要注意区分两种情况:一是链接时找不到符号,这多半是库没加全或者是引入了不存在的接口;二是运行时提示找不到 DLL,这时的排查重点放在 Bin 目录是否在 PATH 环境变量中。实例包通常会附带一个说明文件列出依赖清单,我建议在第一次编译前就对照清单逐项检查。
3. 读取 DXF/DWG 并遍历实体:实例里最常见的操作
3.1 打开文件并读入数据库
读取文件是整个应用的基础步骤。Teigha 提供了OdDbDatabase::readFile()接口来完成文件到内存数据库的加载,实例代码的处理流程一般是先创建空数据库,再调用读取接口,最后把数据库指针保存到某个地方等待后续遍历。
OdDbDatabasePtr pDb = odCreateDatabase(OdDbDatabase::kR2018); OdResult res = pDb->readFile(L"C:\\test\\drawing.dxf"); if (res == eOk) { // 文件读取成功,开始遍历实体 } else { odPrint(L"读取失败,错误码: %d", res); }odCreateDatabase创建一个空的数据库,参数指定默认的 DWG 版本格式。对于只读旧文件这种场景,版本参数并不会限制你能打开的文件格式,它更多影响的是当你在内存中新建实体时,最终写出的文件版本。readFile是真正做解析的地方,它的返回值是OdResult枚举,eOk表示成功,其他值分别对应文件不存在、版本不兼容、文件损坏等场景。需要注意的一点是,对于 DXF 文件,如果文件里包含中文字符,旧版本的 Teigha 需要在读取前设置代码页,否则文字实体的内容会变成乱码。
读取完成后,数据库指针被管理在一个智能指针里,后面所有的实体遍历都必须从这个数据库出发。
3.2 块表与模型空间的遍历逻辑
DWG 文件的层次结构可以这么理解:数据库是整个文件的根,它下面挂着块表,块表里每个块记录都对应图中的一个块,模型空间、图纸空间和每个块定义分别对应一条块表记录。所以遍历图纸上所有实体,本质上就是遍历块表中特定记录的实体集合。
OdDbBlockTablePtr pBlockTable = pDb->getBlockTableId().openObject(); OdDbBlockTableRecordPtr pModelSpace = pDb->getModelSpaceId().openObject(); OdDbObjectIteratorPtr pIter = pModelSpace->newIterator(); for (; !pIter->done(); pIter->step()) { OdDbEntityPtr pEntity = pIter->entity(); if (pEntity->isKindOf(OdDbCircle::desc())) { OdDbCirclePtr pCircle = pEntity; double radius = pCircle->radius(); odPrint(L"发现圆, 半径: %f", radius); } else if (pEntity->isKindOf(OdDbLine::desc())) { OdDbLinePtr pLine = pEntity; OdGePoint3d startPt = pLine->startPoint(); OdGePoint3d endPt = pLine->endPoint(); odPrint(L"发现直线, 起点坐标: (%f,%f), 终点: (%f,%f)", startPt.x, startPt.y, endPt.x, endPt.y); } }块表记录的获取方式是通过数据库的getBlockTableId()和getModelSpaceId()两个入口。前者拿到块表这个容器对象,后者直接拿到模型空间的块表记录。在 Teigha 的对象模型里,所有数据访问都要经过先取得对象 ID、再通过 ID 打开对象的方式,这和其他 CAD 二次开发接口的套路一致。
遍历器newIterator()会返回一个按顺序访问块表记录内所有实体的迭代器。循环体内先拿到的是通用实体指针,通过isKindOf()判断实际类型后,再用智能指针转换到具体类型并读取属性。这种写法最大的好处是安全,你不需要担心类型转换失败的问题。这里对圆的半径、直线的首尾点做了最简单的读取,目的只是让你看到数据是怎么从文件流到可编程对象的。
3.3 关于坐标系和单位,实例里不会明说但你必须知道
遍历代码里输出的坐标值有一个隐含前提:它们直接来自文件内的坐标系。DWG 文件默认使用世界坐标系(WCS),一个点的 X、Y、Z 值都以图纸单位表示。但注意,图纸单位不一定是毫米,也可能是英寸,这取决于图纸本身的设置。所以实例输出的数值你不能直接当毫米用,尤其是做尺寸校验之类的业务时。稳妥的做法是在读取文件后,先查询数据库中的MEASUREMENT或INSUNITS系统变量,再换算成你的业务单位。
提示:我用这个库做过一个面积统计工具,第一次拿到的数据比实际偏大,查了半天才发现是图纸单位为英寸导致。
4. 修改实体属性并另存新文件:从读进阶到写
4.1 获取并修改已有实体的属性
读懂了遍历代码之后,修改实体属性的逻辑就顺理成章了。核心思想是:遍历时保留当前实体的指针,调用修改方法改变属性值,最后用record()或事务机制把改动提交回数据库。
OdDbBlockTableRecordPtr pModelSpace = pDb->getModelSpaceId().openObject(); OdDbObjectIteratorPtr pIter = pModelSpace->newIterator(); for (; !pIter->done(); pIter->step()) { OdDbEntityPtr pEntity = pIter->entity(); if (pEntity->isKindOf(OdDbText::desc())) { OdDbTextPtr pText = pEntity; OdString oldStr = pText->textString(); if (oldStr == L"旧标题") { pText->setTextString(L"新标题"); pText->setHeight(5.0); } } }修改属性不需要额外的保存动作,因为 Teigha 的对象模型是直接映射到内存数据库的,指针指向的就是数据库里的对象本身。这与某些先从数据库复制一份出来、改完再提交的框架不同,更贴合 CAD 文件的数据组织方式。setTextString与setHeight是文字实体最常用的两个接口,前者改内容,后者改字高。注意文字内容必须使用宽字符字符串,直接用窄字符串会产生编译错误或乱码。
修改完对象的属性后,数据库的状态会标记为已修改,后续保存文件时,这些改动会自动写进去。这里有一个容易被误会的点:Teigha 没有显式的事务回滚机制,如果修改到一半程序崩溃,内存中的改动就丢了,原始文件并不会受任何影响,所以不必担心改坏了原文件,因为它从来不会被直接写回。
4.2 新建实体并写入数据库
除了修改已有实体,基本应用实例通常还会演示如何往模型空间添加全新对象。这个流程分两步:先创建对象并设置属性,再把它添加到块表记录的实体集合中。
OdDbLinePtr pLine = OdDbLine::createObject(); pLine->setStartPoint(OdGePoint3d(0.0, 0.0, 0.0)); pLine->setEndPoint(OdGePoint3d(100.0, 50.0, 0.0)); // 设置图层和颜色 pLine->setLayerId(pDb->getLayerTableId().openObject()->getAt(L"0")); pLine->setColor(OdCmColor::kRed); // 加入模型空间 OdDbBlockTableRecordPtr pModelSpace = pDb->getModelSpaceId().openObject(); pModelSpace->appendOdDbEntity(pLine); // 通知数据库对象已被追加 pLine->record();对象创建的第一行是通过createObject()静态方法完成的,此时它还是独立状态,不在任何数据库中。接下来设置几何属性,直线是最简单的实体,只需要起点和终点两个三维点。setLayerId需要的是图层表记录的 ID,这里通过图层表getAt()方法按名称获取。颜色设置使用的是OdCmColor对象,kRed是一组预定义枚举色值中最常用的一个。appendOdDbEntity()把实体正式挂到模型空间里,最后调用的record()方法提交更新,把这套操作完整落到数据库。不能省略最后一步,这是一个容易导致实体消失的坑。
4.3 按指定版本保存输出
处理完之后写回文件,使用数据库的writeFile()方法即可。注意用户期望的输出版本,Teigha 支持的版本范围取决于 SDK 版本,实例代码里一般会提供一个OdDbDatabase::save()或writeFile()的示例。
OdResult res = pDb->writeFile(L"C:\\output\\result.dxf", OdDbDatabase::kR2018, OdDbDatabase::kDxf); if (res == eOk) { odPrint(L"保存成功"); } else { odPrint(L"保存失败, 错误码: %d", res); }5. 实例实操避坑:四条高频错误与对应排查思路
5.1 初始化没做,直接读文件崩溃
现象:代码一执行到readFile就弹出访问违规异常,在 Debug 模式下提示_ASSERTE失败,堆栈信息指向一个未知模块的内部函数。
原因:最常见的原因是遗漏了odInitialize的调用,或者HostAppServices子类里返回的运行时路径不对,导致系统根本没能加载基础的模块映射表。另外还有一些没有被充分讨论的原因,比如在/MD与/MT运行库之间切换后没有重新编译依赖库,但在大多数情况下,这个问题和初始化不当直接相关。
解决方法:回到实例自带的可运行工程上验证一遍环境。确认getWinSysDir()返回的目录里有TD_Kernel.dll文件;在odInitialize调用所在行打断点,单步进去确认没有抛出异常。这两个检查能排除掉八成问题。
5.2 中文文字实体导出后变成乱码
现象:DXF 文件里中文正常,但经过 Teigha 读取再写出的文件,中文全部变成问号或乱码。同样的问题也可能出现在新建文字实体时。
原因:写入时没有设置正确的代码页,或者读入 DXF 时没有指定 DXF 编码。Teigha 对纯 DXF 文件有单独的编码控制逻辑,默认行为可能不识别 GBK;输出到旧版 DWG 格式时,文字内容也可能受版本支持的编码限制。
解决方法:读取前确认数据库的代码页设置,写文件时优先使用较新的版本格式,或显式设置代码页。
5.3 写出的文件在新版本软件中打不开
现象:程序提示保存成功,但拿到新版 CAD 软件里打开时,提示文件版本不识别或无响应。在旧版本里则完全正常。
原因:writeFile或save调用时指定的保存格式过旧,新版软件虽然兼容旧格式,但你用的 Teigha 版本在生成旧格式时存在兼容性缺陷,或者文件里包含了旧版本不支持的新对象类型,最常见的几种包括填充、注释性比例或自定义对象代理。
解决方法:把保存版本参数提升一到两档,改用新格式重出。如果必须导出旧版,检查图纸里是否含有高质量几何对象,必要时按类型分解后再导出。记住一点,覆盖测试不同版本组合是这套方案上线前必须完成的步骤。
5.4 不释放数据库句柄导致内存持续增长
现象:程序循环处理几百个文件,内存占用持续上升,直到接近系统内存上限后崩溃。
原因:OdDbDatabasePtr是智能指针,但如果循环体内每次都把新数据库赋值给同一个指针,而退出前没有正确释放块表和记录句柄,底层对象会残留。更常见的情况是把数据库指针存到了某个全局容器里,循环结束容器不清理。
解决方法:处理完一个文件后手动将指针置空,并调用OdDbObject::close(),确保所有打开的对象都已关闭。
6. 实例改造实战:批量图层重命名小工具
掌握了基本读写之后,最值得动手试的一个场景是批量处理业务:比如把一批旧图纸的图层名统一标准化。图纸里时常出现图层名不统一的情况,同一图层在部分文件里叫"墙线",在另一部分叫"WALL",靠人工一个一个改效率很低。用已经跑通的读写框架,实现成本其实很低。
void renameLayer(OdDbDatabasePtr pDb, const OdString& oldName, const OdString& newName) { OdDbLayerTablePtr pLayerTable = pDb->getLayerTableId().openObject(); if (pLayerTable->has(oldName)) { OdDbLayerTableRecordPtr pLayer = pLayerTable->getAt(oldName).openObject(); pLayer->setName(newName); pLayer->close(); } }这个工具的核心逻辑就一个函数:按旧名称从图层表中找到记录,把setName()改成新值。必须要加一层has()的存在性判断,图纸里没有这个图层时直接跳过,否则会抛出异常。此外,当图层被大量实体引用时,setName()的行为取决于 Teigha 内部如何处理引用关系,多数情况下是安全的——图层表记录的重命名会同步更新所有实体的关联,保留对象的setLayerId引用不变。如果你只是想让代码更稳妥,建议在所有修改完成前备份原始文件。
批量处理的循环框架代码直接套前面第 2 章的初始化结构即可。进入循环前先创建HostAppServices并初始化,然后对每个文件执行打开、遍历、判断、修改、另存的流程。输出文件名建议加上后缀区分,用原来的目录加_new避免覆盖源文件。
从那以后,我每次处理大批图纸之前,都会强制走一遍「先读一个文件 → 列出全部图层名 → 人工确认映射表 → 再跑全量」的流程。看似多花五分钟,实际上能挡住不少因为映射关系设置错误导致的批量翻车。希望这份实例资源能帮你把框架跑通,后面真正做出趁手的工具只是时间问题。
本文还有配套的精品资源,点击获取