Teigha实战:CAD二次开发中的DWG/DXF读取与实体遍历

发布时间:2026/10/20 4:23:09
Teigha实战:CAD二次开发中的DWG/DXF读取与实体遍历 简介围绕 Teigha 4.0 的 CAD 二次开发实例面向需要读写 DWG 文件的 C# 开发者。资源以 RepText 工程为主线演示如何通过 Open Design Alliance 提供的 API 打开、新建 DWG 文件并在不打开图形界面的情况下批量查找并替换图纸中的文字适合刚接触 Teigha、希望快速落地批量处理工具的人群。压缩包为 rar 格式共 63 个文件包含 21 个 dll、6 个 cs 源码、11 个 tx以及 exe、config、resx、sln 等辅助文件源码、配置与编译产物并存便于按工程结构理解完整调用流程。已有 1259 人学习下载。通过该系统实例可以掌握 Teigha 环境初始化、模型空间遍历、实体类型判断、后台文字替换、文件保存与资源释放等关键环节为开发定制化 CAD 数据管理或图纸自动处理工具提供直接参考。1. 为什么我把 Teigha 看成 CAD 二次开发的“格式黑匣子”作为常年处理 CAD 数据的人我最不想接的活儿就是把图纸里的实体逐一导出来做统计。原因不是不会写代码而是 DWG 的二进制布局一直没有公开的稳定解释用偏移量去读文件永远是拆东墙补西墙。直到把 Teigha 的示例工程跑通我才第一次觉得这个“黑匣子”是可以安全调用的。Teigha 是一套面向 C 的 CAD 文件读写开发包它用“对象数据库”的思路把加载、遍历、属性提取和格式转换都封装成了类库接口你不用去抠字节排布只要按数据库的方式操作模型空间就行。它适合三种人做图纸内容提取的、做 DWG/DXF 格式转换的、在自有工具里批量读取图纸数据的。2. 从开发包到跑通第一个工程环境配置与最小 Demo2.1 解包后的目录结构先认清四类文件拿到开发包后我一般会先花五分钟把目录结构扫一遍而不是急着建工程。发布包的典型布局如下teigha/ ├─ include/ # C 头文件全部以 .h 形式提供 │ ├─ OdaCommon.h # 几乎所有示例都会先包含它 │ ├─ OdDb.h # 数据库核心实体、块表、数据库对象 │ ├─ OdGe.h # 几何运算点、向量、变换矩阵 │ ├─ OdGi.h # 图形接口渲染、绘制相关 │ └─ ... ├─ lib/ │ ├─ win64/ │ │ ├─ TD_Root.lib # 运行时基础库必须第一个链接 │ │ ├─ TD_Db.lib # 数据库与实体对象库 │ │ ├─ TD_Ge.lib # 几何对象库 │ │ └─ TD_Gi.lib # 图形接口库 │ └─ ... ├─ bin/ │ └─ *.dll # 运行时依赖需要放到可执行文件旁边 ├─ examples/ │ └─ console/ # 命令行示例适合先跑一遍 └─ docs/ └─ help/ # API 参考文档表基础库文件的功能划分库文件核心能力什么情况下需要TD_Root运行时、类型注册、系统服务任何时候都要TD_Db数据库、块表、实体读写 DWG/DXF 时必须TD_Ge点、向量、变换等几何运算处理坐标和几何形态时TD_Gi渲染与图形上下文只在做可视化预览时用头文件里最值得先打开的是OdaCommon.h里面定义了基础类型和全局函数声明。我第一次打开时差点被里面大段的宏定义劝退但实际用起来并不需要全看懂先记住“包含它才能编译通过”就够了。2.2 CMake 链接参数我只保留三个基础库我习惯用 CMake 组织这种跨目录的库工程因为它能把头文件目录、库目录和链接项写在一起换机器时只改一个根路径。最小可用的 CMakeLists 如下cmake_minimum_required(VERSION 3.10) project(teigha_demo) # TEIGHA_ROOT 指向解包后的根目录 if(NOT DEFINED TEIGHA_ROOT) set(TEIGHA_ROOT C:/SDK/teigha) endif() include_directories(${TEIGHA_ROOT}/include) add_executable(teigha_demo main.cpp) target_link_libraries(teigha_demo ${TEIGHA_ROOT}/lib/win64/TD_Root.lib ${TEIGHA_ROOT}/lib/win64/TD_Db.lib ${TEIGHA_ROOT}/lib/win64/TD_Ge.lib )逻辑说明include_directories负责让编译器找到头文件target_link_libraries决定链接哪些导入库。只做读取和遍历时我基本只保留三个库TD_Root、TD_Db、TD_Ge。加上TD_Gi的情况通常是你需要把实体交给渲染模块比如做预览程序。参数说明链接顺序上我习惯把TD_Root放最前面让基础运行时符号先被解析实际工程里即使顺序反了大多数情况下链接器也能自动处理但保持稳定顺序能减少排查告警的分心。如果你用的是 Visual Studio 而不是 CMake也没关系核心同样三步附加包含目录指向include附加库目录指向lib/win64附加依赖项写上面三个.lib文件名。另外建议在预处理定义里加上_CRT_SECURE_NO_WARNINGS能少看一堆关于 CRT 函数安全性的告警。2.3 初始化与退出odInitialize 和 odUninitialize 不能省我第一次写 Teigha 程序时觉得示例里的初始化代码是多余的删掉后直接创建数据库结果运行到一半就崩。后来才明白这两行承担的是整个运行时环境的创建和销毁。最小可运行程序如下#include OdaCommon.h #include OdDbHostAppService.h #include RxDynamicLinker.h int main() { // 初始化对象数据库运行时注册类型工厂分配全局状态 odInitialize(); // 把数据库模块加载进来。第二个参数为 false 表示仅加载不自动启动宿主服务 ::odrxDynamicLinker()-loadModule(TD_Db, false); // 业务代码创建数据库、读取文件、遍历实体 // 这些在下一章展开 // 反初始化释放运行时注册的资源 odUninitialize(); return 0; }逻辑说明odInitialize()可以理解成给整个 Rx 对象系统准备好“运行时容器”后面用到的数据库、实体、块表这些对象的类型信息都要靠它注册进去。loadModule则把TD_Db模块加载进来并注册相关对象工厂。如果漏掉loadModule常见的后果是调用OdDbDatabase::createODB()时拿到空指针。参数说明loadModule的第二个参数我几乎总是传false表示只加载模块、不自动创建宿主应用服务。这样做命令行批处理工具比较干净不会拉起多余的后台对象。3. 核心对象模型加载文件、遍历空间、读实体属性3.1 加载 DWG/DXFreadFile 之前的准备工作加载文件的第一件事不应该是直接读而是先确认文件路径合法。Teigha 的入口是数据库对象加载动作围绕OdDbDatabase展开。#include OdDbDatabase.h #include OdDbHostAppService.h #include OdRxSystemServices.h OdDbDatabasePtr loadDrawing(const wchar_t* path) { // 检查文件是否存在避免 readFile 抛出 IO 异常 if (OdRxSystemServices::get()-accessFile(path, 0) ! 0) return nullptr; // 用数据库工厂创建一个空数据库 OdDbDatabasePtr db OdDbDatabase::createODB(); // 读取 DWG 或 DXFTeigha 会按文件内容自动识别格式 db-readFile(path); return db; }逻辑说明createODB()创建的是空数据库内部已经建好了块表、图层表、标注样式表等基础结构只是没有内容。readFile(path)会根据文件扩展名和实际内容自动判断是 DWG 还是 DXF所以不要自己根据扩展名做分支。参数说明readFile其实有带更多参数的重载比如控制文件共享方式和“文件缺失时的行为”。我平时在批处理工具里只传路径即可遇到损坏文件时多参数版本能提供更大的容错空间具体语义可以查头文件里对应的注释。还有一个值得养成习惯的动作加载后检查数据库的文件版本信息。用db-originalFileVersion()可以看到这个文件是用哪个版本的格式写入的当后面遇到实体读不出来时这个字段能帮你快速判断是不是库版本跟不上。3.2 遍历模型空间迭代器的两个隐含规则Teigha 的实体不是平铺在一个大列表里而是按“数据库 → 块表 → 块表记录 → 实体”的层级组织的。我读取模型空间的惯用写法是#include OdDbBlockTableRecord.h #include OdDbEntity.h #include OdDbDatabase.h void scanModelSpace(OdDbDatabasePtr db) { // 1. 通过数据库拿到“模型空间”的对象 ID OdDbObjectId modelSpaceId db-getModelSpaceId(); // 2. 按 ID 打开块表记录kForRead 表示只读打开 OdDbBlockTableRecordPtr pModel; modelSpaceId.safeOpenObject(pModel, OdDb::kForRead); // 3. 创建迭代器 OdDbObjectIteratorPtr it pModel-newIterator(); // 4. 循环遍历模型空间中的所有对象 long long count 0; for (; !it-done(); it-step()) { OdDbObjectId entId it-objectId(); OdDbEntityPtr pEnt OdDbEntity::cast(entId.safeOpenObject()); if (!pEnt.isNull()) { count; } } // 注意迭代器走到 done 之后不能重置继续使用需要再扫一遍就重新 newIterator }逻辑说明getModelSpaceId()是快捷入口等价于“块表 → 模型空间块表记录”的查找省掉两层跳转。safeOpenObject按 ID 打开对象并自动填充智能指针不需要手动管理引用计数。参数说明newIterator()默认是浅迭代只遍历当前块表记录里的直接子对象不会进入嵌套块内部。这是最容易踩的坑之一——图纸里的实体如果被装进块里模型空间里只有一个块参照内部线条都藏在块表记录中。处理嵌套块的方案我会在第 4 章单独讲。3.3 读取实体属性图层、颜色、线型与几何坐标遍历的目的通常是取数据。实体公共属性集中在OdDbEntity基类上void dumpEntityBasicInfo(OdDbEntityPtr pEnt, int index) { // 图层是字符串直接对应到图层表里的名称 OdString layerName pEnt-layer(); // 颜色按索引方式读取返回基本颜色索引 int colorIndex pEnt-colorIndex(); // 线型保存的是线型名例如 CONTINUOUS OdString linetypeName pEnt-linetype(); // 打印到控制台方便比对 odPrintConsole(L[%d] layer%s colorIndex%d linetype%s, index, layerName.c_str(), colorIndex, linetypeName.c_str()); }逻辑说明colorIndex()返回的是颜色索引值和 CAD 里“按索引颜色”的色号一致。这里有个容易混淆的值0 表示随块BYBLOCK256 表示随层BYLAYER。真正显示时还需要查图层表或者块定义里继承的颜色设置所以只拿索引做报表可以要做样式还原就得再查一层。表常见颜色索引用法颜色索引含义适用场景1红常规实体255近似白白色背景图纸256BYLAYER随层从图层继承颜色0BYBLOCK随块从块定义继承颜色几何数据要看具体实体类型。我平时最常处理的是线段、圆、圆弧、单行文本四类#include OdDbLine.h #include OdDbCircle.h #include OdDbArc.h #include OdDbText.h void dumpGeometry(OdDbEntityPtr pEnt) { if (pEnt-isKindOf(OdDbLine::desc())) { OdDbLinePtr pLine OdDbLine::cast(pEnt); OdGePoint3d s pLine-startPoint(); OdGePoint3d e pLine-endPoint(); // 线段的基本几何信息就是两个端点 odPrintConsole(LLINE (%.3f,%.3f)-(%.3f,%.3f), s.x, s.y, e.x, e.y); } else if (pEnt-isKindOf(OdDbCircle::desc())) { OdDbCirclePtr pC OdDbCircle::cast(pEnt); OdGePoint3d c pC-center(); double r pC-radius(); // 圆心加半径圆所在平面的法向量决定朝向 odPrintConsole(LCIRCLE center(%.3f,%.3f) r%.3f, c.x, c.y, r); } else if (pEnt-isKindOf(OdDbArc::desc())) { OdDbArcPtr pA OdDbArc::cast(pEnt); double start pA-startAngle(); double end pA-endAngle(); // 角度单位是弧度做报表时需要转成度 odPrintConsole(LARC startAngle%.3f endAngle%.3f, start, end); } else if (pEnt-isKindOf(OdDbText::desc())) { OdDbTextPtr pT OdDbText::cast(pEnt); OdString text pT-textString(); // 注意文字内容可能带格式控制符比如 %%D 表示度数符号 odPrintConsole(LTEXT %s, text.c_str()); } }逻辑说明这里用isKindOf做类型判断而不是先cast再判空是因为isKindOf能处理继承关系——多行文本、属性文字都派生于单行文本光靠字符串比对类型名很容易出错。cast负责把基类指针安全转成具体类型的智能指针转换失败会返回空对象。参数说明所有几何数据都以图形单位返回不是毫米也不是英寸。CAD 状态栏里显示的坐标经过单位缩放直接和 Teigha 读出的数值对比经常会出现“差了一百倍”的情况。正确的做法是查看数据库里的insunits()设置这个参数在单位换算章节再展开。4. 避坑指南五个最常见的 Teigha 翻车现场4.1 库版本与图纸版本错配打开新版本文件直接崩现象用某版 Teigha 打开新版本 CAD 软件保存的 DWG程序要么在readFile阶段抛异常要么遍历到一半收到空指针日志里看不到任何业务错误。原因DWG 格式每个版本都有内部结构调整开发包对旧版支持很稳定但新版文件的某些写入特性它未必认识读不出来时就表现为崩溃或空对象。解决优先升级到当前能拿到的最新版开发包。如果受限于授权或工程环境不能升级就在工具入口处做版本边界校验——用db-originalFileVersion()读文件版本号高于支持范围的直接拒绝处理并给出明确提示避免在批处理中间崩掉整个任务。4.2 中文字符串乱码根源在代码页现象读 DXF 文件里的中文文字输出到控制台或写入文件后变成??或整段乱码读 DWG 有时正常有时又不正常看起来没有规律。原因DXF 文件本身可能有代码页标注而OdString内部是宽字符存储。如果源文件用的是 ANSI 代码页比如 GBK直接拷到宽字符串再输出就会错位。DWG 里中文乱码则往往和字体映射、形文件相关。解决先把OdString转成 UTF-8 字节串再交给上层输出或存储。如果读出来已经乱码先检查文件头里的$DWGCODEPAGE或 DXF 的代码页声明确认源编码后再转换。我在导出 JSON 时统一用 UTF-8国外软件打开就不会再乱。4.3 嵌套块跳过迭代器默认是浅扫描现象图纸里明明有大量线条遍历模型空间却只看到少量块参照线条一条都扫不到统计数据明显偏少。原因模型空间里保存的只是块参照对象真正的实体定义在对应的块表记录里。newIterator()默认只遍历当前层级的直接子对象不会下探到块定义内部。解决遇到OdDbBlockReference时取它的blockTableRecordId()再打开对应块表记录继续递归扫描。我一般会在工具里加一个深度参数控制最多下探几层防止块里套块导致死循环。4.4 一边遍历一边删除迭代器失效现象想在遍历过程中“顺手”删掉不需要的实体结果step()之后再次访问对象指针变成野指针程序直接崩溃。原因迭代器的游标和底层对象存储是绑定的删除当前对象会让迭代器状态失效。这类问题在 Debug 版会先报断言错误Release 版则直接崩。解决遍历阶段只收集对象 ID全部扫完之后再根据 ID 列表执行删除。删除用的 ID 列表要单独保存不要在迭代器存活期间做任何写操作。4.5 坐标数值对不上单位换算的坑现象用 CAD 状态栏核对点位发现 Teigha 读出的坐标和标注值完全对不上有时差 25.4有时差 1000且不是线性比例。原因CAD 图纸里的“单位”是一个显示层面的概念模型空间实际存储的是图形单位而每个文件可以声明INSUNITS表示意图单位毫米、英寸、英尺等。状态栏显示的是经标注比例换算后的值Teigha 直接给出原始图形单位两边自然不一致。解决读取db-insunits()拿到单位枚举根据图纸单位做换算。最常见的场景是毫米图纸存的是毫米但标注比例是 1:100这时要从标注样式里找比例因子而不是简单地乘以一个常数。我的做法是让工具输出“原始图形单位 文件单位标注”把是否换算的决定权交给使用方。5. 把导出逻辑收口做一个 DWG 转 JSON 的口袋工具5.1 封装遍历函数输出结构化数据把第 3 章的遍历逻辑收进一个函数输入是数据库指针输出是实体信息的数组。这样的工具函数独立于界面可以在控制台、服务程序里复用。struct EntityInfo { std::string type; std::string layer; double x, y; double value; // 线长/半径/角度等 }; void exportToJson(OdDbDatabasePtr db, const std::string outputPath) { std::vectorEntityInfo items; // 遍历模型空间并将实体信息填充到 items scanModelSpace(db, items); // 拼接 JSON 文本注意转义 std::string json {\entities\:[; for (size_t i 0; i items.size(); i) { char buf[256]; snprintf(buf, sizeof(buf), {\type\:\%s\,\layer\:\%s\,\x\:%.3f,\y\:%.3f}, items[i].type.c_str(), escapeJson(items[i].layer).c_str(), items[i].x, items[i].y); json buf; if (i ! items.size() - 1) json ,; } json ]}; // 写入文件…… }逻辑说明scanModelSpace负责从模型空间迭代器中取出实体并填到std::vectorEntityInfo函数只做一件事方便单独测试。导出部分单独走 JSON 拼接让“数据提取”和“数据输出”解耦。参数说明坐标输出前我保留了三位小数。对于 1:1 毫米图纸够用如果是大坐标地形图建议改成%.6f否则小数点后精度不够会造成几毫米的偏差。多实体的 JSON 体积会涨得很快百万实体级别的场景建议改用流式写入而不是一次性拼接字符串。5.2 JSON 转义与数字格式JSON 拼接最容易被忽略的是字符串转义。图层名里如果出现引号或反斜线直接拼进 JSON 会让结构损坏看起来比实体数少了一堆。std::string escapeJson(const std::string s) { std::string out; for (char c : s) { switch (c) { case : out \\\; break; case \\: out \\\\; break; case \n: out \\n; break; default: out c; } } return out; }逻辑说明只需要处理、\、换行这三类最常见的字符就能避免绝大多数解析失败。更完整的转义还需要处理\r、\t和控制字符但业务图层名里很少出现。5.3 验证两层检查确保数据没跑偏工具写完后不能直接交付我会强制走一遍两层验证。第一层是实体数量对比把导出 JSON 里的实体数打印出来在 CAD 里用 LIST 命令选中同类实体看数量是否一致。数量一致代表遍历路径没漏。第二层是点位抽查手工量取图纸里几个特征点坐标对比 JSON 里对应实体的 x、y 值坐标一致说明单位处理正确。从那以后我每接一个图纸解析的需求都会先花五分钟跑一遍“加载文件 遍历模型空间 抽查三个实体属性”的最小流程确认库版本和图纸单位匹配了再放心往下写业务。这套习惯帮我省掉的排查时间远比那五分钟多。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询