Teigha基本应用实例:从DWG读取到实体遍历的工程实战 简介Teigha 是 Open Design Alliance 推出的 CAD 开发工具提供读写与编辑 DWG 文件的 API。这份《Teigha基本应用实例》围绕“RepText”示例面向需要将 DWG 操作集成到自有程序中的开发者重点演示了读取现有图纸、后台遍历文字对象并批量替换、以及新建 DWG 文件并添加图层与几何实体等常见功能适合有一定 C# 基础并希望快速上手 Teigha 4.0 的读者。资源共 63 个文件约 6.57MB包含 21 个 dll 动态库、11 个 tx 相关文件、6 个 cs 源代码、3 个 exe 可执行程序以及项目配置、调试缓存和界面资源等整体是一个可直接打开编译的 Visual Studio 解决方案目录结构清晰便于对照学习。目前已有 1254 人浏览学习。通过该实例读者可以掌握 Teigha 初始化、模型空间遍历、文字查找替换和文件保存关闭的完整调用流程也可借此理解 CAD 二次开发中的对象模型与批处理思路为后续构建定制化 DWG 工具打下基础。 看一眼今天要聊的题目Teigha基本应用实例。如果你搞过CAD数据处理对这个名字应该不陌生——它是Open Design AllianceODA提供的DWG/DXF读写开发库后来官方统一归到ODA平台ODA Platform名下但老工程师还是习惯叫它Teigha。这篇文章不打算从官网文档的目录结构讲起而是用一个我实际跑过的项目线来串从环境配置、读图、遍历实体、写回文件到中间踩过的几个典型坑每一段都有可复现的代码和真实参数。适合两类人看一类是刚接到DWG解析需求、正在选型的技术负责人另一类是已经把Teigha跑起来、但总在细节上被折磨的开发者。1. 为什么选Teigha而不是直接解析DWG二进制1.1 一次真实的DWG处理项目经历事情是这样的。去年有个项目需要从一批存量DWG图纸里提取设备编号和坐标信息图纸格式从R12到2018都有有些还是繁体字编码的档案图纸。第一反应是直接用Python读二进制不可能的DWG的编码机制决定了它不是简单的流式文件每次版本升级文件头的加密和CRC校验规则都变。后来也考虑过先转DXF再做解析但转格式过程中丢失的扩展数据xdata和代理图元在部分图纸里腰斩了关键信息。最后选型落在了两个方向上要么用ODA平台的Teigha要么选择成熟的商业控件。考虑到预算和跨平台部署需求——现场服务器是Linux还要做日后的批处理服务——Teigha基本是当时最顺的选择。它能直接读写DWG和DXF不需要外部AutoCAD进程依赖还自带了一整套图形数据库对象模型。1.2 Teigha与ODA的关系以及授权模式的基本面貌很多刚接触的人容易混淆一件事Teigha到底是一个软件还是一种文件格式实际上它是ODA开放设计联盟维护的一个SDK名称历史上有过叫Teigha for AutoCADDWG兼容、Teigha for DGN、Teigha for PDF等不同分支后来ODA把所有SDK统一为ODA PlatformTeigha这个名字成了底层核心库的代称。授权方面ODA的会员制决定了它不是免费开源的需要申请会员并签署协议。如果你是小公司想先评估可以走ODA官方的评估版渠道或者用NuGet上由ODA官方发布的预编译包做功能验证但正式商用前必须把授权理清楚。这点我不展开讲细节只提醒一句项目立项时就要把SDK授权成本列进预算等到做完再谈很容易被动。从架构上看Teigha的核心是OdDbDatabase类它对应于一个DWG文件在内存中的数据库实例文件里的每一张图纸、每一个块、每一条线段都是这个数据库某个容器里的对象。理解这句话之后接下来的代码就好懂多了。2. 环境搭建与第一个最小可运行实例2.1 依赖包获取与工程配置我用的环境是Visual Studio 2019 C编译平台设为x64因为默认的win32在加载一些ODA依赖库时会有符号冲突。SDK解压后关键的目录是Inc头文件和Lib各平台的链接库。ODA的库分Debug和Release两个版本混用会在运行时直接抛出异常这类问题的排查成本比写代码本身还高所以从一开始就要在项目属性里单独配置两套链接路径。配置时需要注意头文件目录指向Inc下的根目录即可ODA内部已按模块分好了子目录。链接库依赖里加TD_Db、TD_Gs、TD_Root这几个核心库按实际功能增减。运行时产生的TD_开头的依赖DLL需要一并拷贝到输出目录或系统PATH中。如果你用CMakeODA官方提供了一组CMake辅助脚本能省掉手工配置VS属性的麻烦但我实测下来脚本对VS版本的检测比较敏锐VS2022高版本可能需要小改一下。2.2 读取DWG并打印实体清单的完整代码环境好了以后第一个实例做什么我建议不要一上来就搞复杂的三维体操作先写一个能打开文件、读取所有图元名和图层名的程序。这一步能验证SDK配置正确也方便新人理解数据库的组织方式。以下是一段精简但完整的示例基于ODA的C API#include OdaCommon.h #include OdDbDatabase.h #include OdDbBlockTable.h #include OdDbBlockTableRecord.h #include OdDbEntity.h #include DbHostAppServices.h #include OdPlatformSettings.h #include iostream class MyAppServices : public OdDbHostAppServices {}; // 需至少实现一个宿主类 int main() { OdDbHostAppServices* pApp new MyAppServices(); odInitialize(pApp); try { OdDbDatabasePtr pDb pApp-readFile(Lsample_2018.dwg); OdDbBlockTablePtr pBlockTable pDb-getBlockTableId().safeOpenObject(); OdDbObjectIteratorPtr pIter pBlockTable-newIterator(); for (; !pIter-done(); pIter-step()) { OdDbObjectId btrId pIter-objectId(); OdDbBlockTableRecordPtr pRecord btrId.safeOpenObject(); if (pRecord-isLayout()) { // 只处理布局里的实体 OdDbObjectIteratorPtr pEntIter pRecord-newIterator(); int count 0; for (; !pEntIter-done(); pEntIter-step()) { OdDbEntityPtr pEnt pEntIter-objectId().safeOpenObject(); std::cout Entity: pEnt-isA()-name().c_str() on layer: pEnt-layer().c_str() std::endl; count; } std::cout Layout block: pRecord-getName().c_str() has count entities. std::endl; } } } catch (const OdError e) { std::cout Error: e.description().c_str() std::endl; } odUninitialize(); delete pApp; return 0; }这段代码的核心逻辑就是拿到数据库的块表BlockTable遍历所有块表记录再对每个布局类型的记录遍历其中实体。safeOpenObject()是ODA里的标准打开方式它跟openObject()的区别在于返回的是一个智能指针能在异常时自动清理资源推荐优先用它。我第一次跑这段代码时就交了一次学费文件路径用相对路径程序的工作目录指向了输出目录但DWG放在源工程目录里结果readFile抛出一个不太具有提示性的错误。这算是个基础但高频的坑后面我单独讲。3. 从“能读”到“会用”实体遍历与属性提取3.1 块表、块表记录和实体的层级关系DWG文件的数据组织有点像文件系统OdDbDatabase是磁盘根目录OdDbBlockTable是目录列表OdDbBlockTableRecord是具体文件夹而实体OdDbEntity子类就是文件夹里的一个个文件。每个布局模型空间、图纸空间里的各布局其实都对应一条块表记录所以要遍历图纸里的可见实体先拿到对应布局的块表记录再从它那里开一个迭代器去读实体。这个模型对新人来说需要一点时间适应。它太像一个“数据库”而不像一个“绘图文件”了。不过一旦理解了就会明白为什么ODA能同时兼容DWG和DGN——它们本质上都是“数据库图形视图”的复合结构。3.2 实体类型的运行时识别与属性读取实际业务很少会只统计实体数量更多的需求是“把这个图层上所有文字提取出来”或“找出所有半径大于50的圆”。这就要做类型判断和属性读取。ODA中每个实体类的根是OdDbEntity具体的文字实体是OdDbText或OdDbMText圆是OdDbCircle直线是OdDbLine。运行时判断类型的方法有两种用pEnt-isA()返回的类名与OdDbText::desc()比较适合精确匹配。用pEnt-isKindOf(OdDbCircle::desc())做向上兼容判断适合查找一个父类下的所有子类。这里有一个小坑很多图纸里的文字是用OdDbMText多行文字存储的它的文字内容在contents()属性里而普通单行OdDbText的文字在textString()里。如果需求只说“提取文字”不把这两种类型都覆盖就会漏掉一大部分数据。我在写提取工具时通用了OdDbMText和OdDbText两个分支来读取并且对块引用的属性做了递归展开才能拿到嵌套块里的文字。多行文字还牵扯到格式控制码比如{\\fSimSun|b0|i0|c134|p2;中文},实际提取时要剥离这些控制字符再落库。这个清洗步骤看着小却直接影响下游数据分析的正确性。4. 写入与修改从只读到写回DWG的关键细节4.1 创建实体并保存新文件的完整流程Teigha的价值不只是读它也能改和写。常见场景包括批量给图元添加扩展数据、替换损坏的字体、按规则统一修改图层名等。这里给一个创建圆并保存为新DWG的例子只截取核心部分OdDbDatabasePtr pDb pApp-createDatabase(); OdDbBlockTableRecordPtr pModel pDb-getModelSpaceId().safeOpenObject(OdDb::kForWrite); OdDbCirclePtr pCircle OdDbCircle::createObject(); pCircle-setCenter(OdGePoint3d(10.0, 20.0, 0.0)); pCircle-setRadius(25.5); pCircle-setLayer(L0); pModel-appendOdDbEntity(pCircle); pDb-writeFile(Loutput_created.dwg);这里最需要注意的细节是openObject(OdDb::kForWrite)。在ODA对象模型里同一个对象可以以读或写两种模式打开读模式可以被多个请求共享写模式则是独占的。如果忘了指定kForWrite后面调用appendOdDbEntity时就会抛异常。很多“明明照着示例写但一跑就崩”的问题十有八九出在这个模式参数上。4.2 坐标系、单位与扩展数据的坑写CAD程序绕不开坐标系。ODA默认使用的是世界坐标系WCS圆心坐标是三维点Z值默认为0。但如果目标DWG包含用户坐标系UCS信息且你读取坐标后不做转换直接另存会出现旋转或偏移问题。更隐蔽的是单位问题。DWG文件内部默认以“图形单位”存储数值不强制要求是毫米还是英寸这个信息记录在OdDbDatabase::getUnits()里但它是一个查询接口不一定每个文件都正确设置。做几何计算前先确认文件的单位并换算一致坐标提取结果才不会在部署后被下游质疑。扩展数据xdata是CAD的经典功能应用名appname通常在写入时需要注册。ODA里通过appendXdata()来添加扩展数据代码本身很直接但真正的坑在于如果你想读的文件里存在未知应用名某些第三方库会直接报错或数据丢失Teigha对这部分的容忍度高很多会保留原样。这也是我在那次批量处理中坚持用它的原因之一。5. 三个实战中必然会踩的问题以及排查链路5.1 文件路径和编码导致的读取失败前面提到过第一次运行时连文件都打不开。我当时排查的顺序是第1步确认文件存在且能被AutoCAD正常打开。第2步检查代码里路径字符串是宽字符版本L前缀还是窄字符版本。ODA的readFile接受宽字符路径如果直接从命令行接收一个UTF-8字符串传进去非ASCII字符会乱。第3步检查工作目录。VS默认的工作目录是$(ProjectDir)但如果你把DWG放在$(SolutionDir)下的data目录路径拼错就很正常。解决方法是拼接绝对路径并且用_wgetcwd打印出当前工作目录做对比不要凭空猜测。这听起来很基础但在社区里经常有人问“为什么我的程序读不了DWG”十有八九都是这个问题。5.2 大文件遍历慢问题不在遍历本身后来处理一批单文件超过200MB的市政图纸时遍历所有实体特别慢。最初怀疑是newIterator效率低换成直接用OdDbBlockTableRecord::getBlockReferenceId等方式定位块引用结果提升很有限。真正的原因有两个一是遍历过程中频繁调用了isA()-name()这个操作会触发大量RTTI字符串比较二是我不小心在读取每个实体时都调用了record-isLayout()而该方法内部存在较重的查询链条。把这两个高频调用移出循环只在初始化时缓存布局列表性能才得到质的提升200MB图纸的遍历时间从12秒降到了3秒左右。5.3 保存时版本不匹配怎么办DWG版本有很多R12、R2000、R2004、R2007、R2010、R2013、R2018等。Teigha写入时默认用当前数据库的版本如果你读入的是一个R2018文件直接writeFile()会得到一个R2018版本。但现场下游系统只能读R2000那就需要另存时声明版本参数。writeFile的签名有重载可以传入OdSaveType之类的参数指定保存格式。这个能力很实用但要注意高版本图元降级到低版本时不可避免有特性丢失。比如填充边界和动态块行为在R2000里可能变成匿名块或代理图元。处理这类需求前一定要跟接收方确认可接受的“信息损失范围”否则验收时大概率挨骂。6. 我现在是怎么看待Teigha选型的6.1 适合的项目有哪些特征复盘下来Teigha在以下几类需求里非常值得选服务端批量处理DWG/DXF没有AutoCAD许可也不能弹交互界面。跨平台部署同一套代码同时跑Windows和Linux。对文件格式兼容性要求极高历史版本混杂。需要深度扩展比如自研CAD应用、图纸加密、格式转换服务。从我自己的经验看只要项目占上面任意两条Teigha都是优先选项。6.2 不适合的场景和替代思考反过来如果只是临时转几个文件或者只需要极简单的读取Teigha就偏重了——授权、SDK体积、学习成本都不划算。这种轻量需求我会选择DXF的纯解析方案或者先转成JSON再处理。另外如果你必须依赖AutoCAD本身的某些行为比如渲染效果、代理图元重建那Teigha能模拟的只是数据库层呈现层不一定完全一致。选型本质上没有一个“永远正确”的答案重要的是知道你手里的工具擅长什么。Teigha擅长的是把DWG当成数据库来操作而不是一个画板来可视化操作。最后分享一个我在所有Teigha项目里保留的习惯每次拿到一批新图纸先用官方自带的OdaFileConverter跑一遍确认这批文件“没坏到连库都打不开的程度”然后再进入代码开发。这么做的原因很现实——有时候不是代码问题而是文件本身在长期保存和版本迁移过程中已经存在不可修复的损坏。把这个前置检查做掉后面调试时能省掉一整类“疑似程序bug”的干扰项。本文还有配套的精品资源点击获取