简介:本资源是面向CAD二次开发工程师与.NET平台开发者的Teigha 4.0基础应用实战项目,聚焦DWG文件的读取、新建及后台批量文字替换三大核心场景,解决工业设计、图纸自动化处理等实际工程中常见的非交互式CAD数据操作需求。压缩包共63个文件,含21个运行依赖DLL(Teigha核心库与ODA接口)、11个TX/TXV格式技术文档(含API说明与许可配置)、6个C#源码文件(Form1.cs、Program.cs等构成完整WinForms工程)、3个可执行EXE(含RepText主程序)、以及sln/cspoj/obj/bin等标准Visual Studio项目结构文件,整体大小6.57MB,结构规范,开箱即用。目前已有1259人学习下载。读者可直接编译运行RepText实例,掌握Teigha环境初始化、模型空间遍历、实体类型识别、文字对象定位与SetText批量替换等关键流程,并复用其工程框架快速构建自有CAD工具。
1. Teigha基本应用实例:不是CAD插件,而是你手边最轻量的DWG/DXF解析黑匣子
很多刚接触工业软件二次开发的工程师,第一反应是“Teigha?是不是那个要配OCX控件、还得装AutoCAD运行时的庞然大物?”——错。Teigha(现由ODA维护,全称Open Design Alliance Teigha)本质是一套纯C++实现的、无GUI依赖的DWG/DXF读写SDK,它不依赖任何CAD桌面环境,也不需要用户安装AutoCAD或BricsCAD。我去年在某高校实验室帮A同学做设备图纸元数据提取时,用它把327份含图层+文字+块引用的DWG文件批量转成JSON结构化数据,全程静默运行,单机CPU占用峰值不到45%,连虚拟机都能扛住。它适合三类人:需要离线解析CAD图纸的嵌入式系统开发者、做BIM轻量化前端预览的后端工程师、以及像我这样常被临时拉去救火、得在没有CAD许可证的服务器上快速扒出图纸关键信息的“图纸翻译员”。这不是教你怎么画图,而是教你如何让图纸开口说话。
2. 环境准备与最小可运行工程:从零构建一个能打开DWG的控制台程序
Teigha不是开箱即用的npm包,它是一套需要手动集成的C++ SDK。它的核心价值恰恰在于“可控”——你可以精确决定加载哪些模块、是否启用加密支持、甚至裁剪掉不需要的实体类型以减小二进制体积。下面以Windows平台(Visual Studio 2019+)、Teigha v24.0(当前稳定版)为例,搭建一个能读取DWG文件头信息的最小工程。Linux/macOS路径逻辑一致,仅需替换库名和链接方式。
2.1 下载与目录结构解压
ODA官网提供Teigha SDK下载(需注册免费账号),下载包名为TeighaSDK_x64_24.0.0.0.zip(x64位,对应VS2019工具链)。解压后关键目录如下:
TeighaSDK/ ├── inc/ # 所有头文件,必须加入项目包含路径 ├── lib/ # 静态库(.lib)与动态库(.dll/.so) │ ├── x64/ # 64位版本 │ │ ├── TD_Db.lib # 核心数据库模块(实体、图层、块表等) │ │ ├── TD_Gi.lib # 图形接口(渲染相关,本例暂不需) │ │ └── TD_Root.lib # 基础运行时(必须链接) ├── samples/ # 官方示例,重点看ConsoleApp和DbRead └── readme.txt # 版本说明与许可条款(务必通读)提示:Teigha采用模块化链接策略。本例仅需
TD_Db.lib和TD_Root.lib,切勿盲目链接全部lib,否则会导致符号冲突或启动失败。TD_Gi.lib仅在需要光栅化预览时才引入。
2.2 Visual Studio项目配置四步法
新建空的Win32控制台应用程序(注意:选择“空项目”,取消预编译头)。按顺序执行以下四步配置:
步骤1:包含目录设置
项目属性 → C/C++ → 常规 → 附加包含目录 → 添加:$(TeighaSDK)\inc
步骤2:库目录与附加依赖项
项目属性 → 链接器 → 常规 → 附加库目录 → 添加:$(TeighaSDK)\lib\x64
项目属性 → 链接器 → 输入 → 附加依赖项 → 填入:TD_Root.lib;TD_Db.lib
步骤3:运行时库统一
项目属性 → C/C++ → 代码生成 → 运行时库 → 必须设为/MT(多线程静态链接)
原因:Teigha官方发布的lib均以/MT编译,若项目用/MD(动态链接CRT),链接时会报LNK2038不匹配错误。这是新手踩坑率最高的第一步。
步骤4:DLL部署(仅调试阶段)
将$(TeighaSDK)\lib\x64\*.dll(主要是TD_Root.dll,TD_Db.dll)复制到你的.exe同目录下。发布时需随程序分发,或改为静态链接(需ODA商业授权)。
2.3 编写第一个“Hello DWG”程序
创建main.cpp,粘贴以下代码(已去除所有异常处理,聚焦主干逻辑):
#include "tdb.h" // Teigha核心头文件 #include "dbmain.h" #include "dbdict.h" #include <iostream> #include <string> int main(int argc, char* argv[]) { if (argc != 2) { std::cout << "Usage: " << argv[0] << " <dwg_file_path>\n"; return -1; } // 1. 初始化Teigha运行时(必须调用,且仅一次) if (!odrxInitialize()) { std::cerr << "Failed to initialize Teigha runtime.\n"; return -2; } // 2. 创建数据库对象(不加载图形,仅读头信息) OdDbDatabasePtr pDb = odDbCreateDatabase(); if (pDb.isNull()) { std::cerr << "Failed to create database object.\n"; odrxUninitialize(); return -3; } // 3. 读取DWG文件(只读模式,不解析实体) ODA_ASSERT(pDb->readFile(argv[1], L"", false) == eOk); // 4. 输出基础信息 std::wcout << L"DWG Version: " << pDb->version() << L"\n"; std::wcout << L"Units: " << pDb->insunits() << L"\n"; std::wcout << L"Model Space Block Name: " << pDb->getModelSpaceBlockTableRecordId().toString().c_str() << L"\n"; // 5. 清理 pDb.release(); odrxUninitialize(); return 0; }关键参数说明:
readFile()第二个参数L""是密码(空字符串表示无密码);第三个参数false表示不加载实体数据(仅读头),这是提速关键。若需遍历所有线条,才设为true。pDb->version()返回整型枚举值(如kDwgVersionAC1032=2024版),需查OdDb::DwgVersion定义转换。insunits()返回单位代码(如kUnitsMillimeters=4),非字符串,直接输出是数字,需映射。
编译运行后,传入任意DWG文件路径,你将看到类似:
DWG Version: 32 Units: 4 Model Space Block Name: 1000000000000000这证明Teigha已成功握手DWG文件——不是靠AutoCAD,而是靠自己解析二进制结构。
3. 核心功能实战:遍历图层、提取文字、定位块引用坐标
上一节只是“握手”,这一节才是真刀真枪干活。我们以一份典型设备布置图(DWG格式)为样本,完成三个高频需求:列出所有图层名称、提取所有MTEXT实体内容、获取指定名称块(如“VALVE_001”)在模型空间中的插入点坐标。所有操作均基于OdDbDatabase对象,无需渲染,纯内存解析。
3.1 图层表遍历:获取所有激活图层及其状态
图层信息存储在layerTable中,它是数据库的命名对象字典之一。注意:Teigha中“表”(Table)是命名对象容器,不是数据库表。
void listLayers(OdDbDatabasePtr pDb) { OdDbLayerTablePtr pLayerTable = pDb->getLayerTable(Adesk::kForRead); if (pLayerTable.isNull()) return; OdDbLayerTableIteratorPtr pIter = pLayerTable->newIterator(); while (!pIter->done()) { OdDbLayerTableRecordPtr pLayer = OdDbLayerTableRecord::cast(pIter->getRecord(Adesk::kForRead)); if (!pLayer.isNull()) { std::wcout << L"Layer: " << pLayer->name() << L" | On: " << (pLayer->isFrozen() ? L"False" : L"True") << L" | Color: " << pLayer->color().colorIndex() << L"\n"; } pIter->step(); } }参数深挖:
getLayerTable(Adesk::kForRead):以只读模式打开图层表,避免锁表。生产环境严禁用kForWrite。pLayer->isFrozen():返回图层是否被冻结(不可见),注意不是isOff()(关闭),二者语义不同。pLayer->color().colorIndex():返回ACI颜色索引(1-255),非RGB。若需RGB,调用pLayer->color().rgb()。
3.2 文字实体提取:MTEXT与TEXT的统一处理
DWG中文字分两类:TEXT(单行)和MTEXT(多行富文本)。Teigha将它们抽象为OdDbText基类,但MTEXT需额外处理段落。
void extractTexts(OdDbDatabasePtr pDb) { OdDbBlockTablePtr pBlockTable = pDb->getBlockTable(Adesk::kForRead); OdDbBlockTableRecordPtr pModelSpace = OdDbBlockTableRecord::cast( pBlockTable->getAt(OD_T("Model"), Adesk::kForRead) ); if (pModelSpace.isNull()) return; // 遍历模型空间所有实体 OdDbBlockTableRecordIteratorPtr pEntIter = pModelSpace->newIterator(); while (!pEntIter->done()) { OdDbObjectId id = pEntIter->objectId(); OdDbObjectPtr pObj = id.openObject(Adesk::kForRead); if (pObj.isNull()) { continue; } // 判断是否为文字类实体 if (pObj->isKindOf(OdDbText::desc())) { OdDbTextPtr pText = OdDbText::cast(pObj); std::wcout << L"TEXT: " << pText->textString() << L" @ (" << pText->position().x << L"," << pText->position().y << L")\n"; } else if (pObj->isKindOf(OdDbMText::desc())) { OdDbMTextPtr pMText = OdDbMText::cast(pObj); // MTEXT可能含换行符,用\r\n分割段落 std::wstring content = pMText->contents(); size_t pos = 0; while ((pos = content.find(L"\r\n", pos)) != std::wstring::npos) { content.replace(pos, 2, L" | "); pos += 3; } std::wcout << L"MTEXT: " << content << L" @ (" << pMText->location().x << L"," << pMText->location().y << L")\n"; } pEntIter->step(); } }避坑点:
pText->position()对TEXT有效,pMText->location()对MTEXT有效,二者不能混用。MTEXT的contents()返回的是带格式代码的字符串(如{\fArial|b0|i0|c0|p34;Valve ID}),本例仅做简单换行替换。如需真实渲染,需调用OdDbMText::parseContents()。
3.3 块引用定位:根据名称查找所有插入点
块(Block)是DWG复用的核心机制。INSERT实体指向块定义,并记录插入点、比例、旋转角。
void findBlockInsertions(OdDbDatabasePtr pDb, const wchar_t* blockName) { OdDbBlockTablePtr pBlockTable = pDb->getBlockTable(Adesk::kForRead); OdDbBlockTableRecordPtr pModelSpace = OdDbBlockTableRecord::cast( pBlockTable->getAt(OD_T("Model"), Adesk::kForRead) ); if (pModelSpace.isNull()) return; OdDbBlockTableRecordIteratorPtr pEntIter = pModelSpace->newIterator(); int count = 0; while (!pEntIter->done()) { OdDbObjectId id = pEntIter->objectId(); OdDbObjectPtr pObj = id.openObject(Adesk::kForRead); if (pObj.isNull()) { continue; } if (pObj->isKindOf(OdDbBlockReference::desc())) { OdDbBlockReferencePtr pRef = OdDbBlockReference::cast(pObj); OdDbBlockTableRecordPtr pDef = OdDbBlockTableRecord::cast( pRef->blockTableRecord().openObject(Adesk::kForRead) ); if (!pDef.isNull() && wcscmp(pDef->name(), blockName) == 0) { OdGePoint3d insPt = pRef->position(); OdGeVector3d scale = pRef->scaleFactors(); double rot = pRef->rotation() * 180.0 / OdaPI; // 弧度转角度 std::wcout << L"Block '" << blockName << L"' found at (" << insPt.x << L"," << insPt.y << L") " << L"Scale=(" << scale.x << L"," << scale.y << L") " << L"Rot=" << rot << L"°\n"; count++; } } pEntIter->step(); } std::wcout << L"Total matches: " << count << L"\n"; }关键逻辑:
pRef->blockTableRecord()返回块定义ID,需openObject()才能获取名称。scaleFactors()返回XYZ三向缩放,通常Z为1,XY即实际缩放值。rotation()返回弧度,OdaPI是Teigha定义的π常量(3.14159265358979323846)。
4. 避坑指南:五个血泪经验换来的Teigha生存法则
Teigha文档以精炼著称,但精炼背后是大量隐性约束。以下是我在线上环境连续翻车三次后总结的硬核避坑清单,每一条都对应一个真实崩溃现场。
4.1 现象:程序启动即崩溃,报错Access violation reading location 0x0000000000000000
原因:odrxInitialize()未调用,或调用后odrxUninitialize()过早执行,导致后续odDbCreateDatabase()返回空指针,而代码未判空直接使用。
解决:在main()开头强制添加初始化检查:
if (!odrxInitialize()) { std::cerr << "Teigha init failed! Check DLL path and CRT version.\n"; return -1; } // ... 业务逻辑 ... odrxUninitialize(); // 必须在所有OdDbDatabase释放后调用4.2 现象:readFile()返回eInvalidInput,但文件路径绝对正确
原因:DWG文件被其他进程(如AutoCAD、SolidWorks)独占打开,或文件权限为只读且Teigha尝试写临时日志。
解决:
- 检查文件是否被占用:用
Process Explorer搜索该文件句柄; - 以只读方式打开:
pDb->readFile(path, L"", true)中第三个参数设为true(忽略写权限); - 或改用
readDwgFile()替代readFile(),前者更严格校验,后者兼容性略高。
4.3 现象:遍历实体时pEntIter->step()后pEntIter->done()始终为false,死循环
原因:pEntIter迭代器在遍历时,其内部游标未推进,常见于pEntIter->step()前pObj未正确释放,导致迭代器状态卡死。
解决:严格遵循“打开-使用-释放”三步:
OdDbObjectPtr pObj = id.openObject(Adesk::kForRead); if (!pObj.isNull()) { // ... 处理 pObj ... } // pObj在此处自动释放(智能指针析构) pEntIter->step(); // 确保在 pObj 作用域结束后调用4.4 现象:pLayer->name()返回乱码(如??),但图层名在CAD里显示正常
原因:Teigha默认使用UTF-16编码,而某些老版本DWG(如R14)用ANSI编码存储图层名,name()方法未自动转码。
解决:强制获取原始字节并转UTF-16:
OdString rawName = pLayer->name(); std::wstring wName; if (rawName.isAscii()) { wName = rawName.c_str(); } else { // 调用ODA提供的转换函数 wName = OdStringToWString(rawName); }4.5 现象:pRef->position()返回(0,0,0),但CAD中块明显不在原点
原因:INSERT实体的position()是相对于其所在块定义坐标系的局部坐标,而非模型空间全局坐标。若块引用嵌套在另一个块内,则需递归变换。
解决:获取全局坐标需调用pRef->transformedPosition():
OdGePoint3d globalPt = pRef->transformedPosition(); // 注意:此方法需确保pRef已完全加载(readFile时第三个参数为true)5. 进阶技巧:批量处理性能优化与内存泄漏防控
当处理数百份DWG时,朴素的“打开-处理-关闭”循环会触发频繁的磁盘IO和内存分配,导致速度断崖式下跌。我在某跨平台图纸管理系统中,将单文件平均处理时间从3.2秒压到0.47秒,核心就靠以下三招。
5.1 复用数据库对象:避免重复初始化开销
odrxInitialize()和odDbCreateDatabase()是重量级操作。对同一批文件,应复用同一个OdDbDatabase实例:
// 错误:每次循环都新建 for (auto& path : dwgPaths) { OdDbDatabasePtr pDb = odDbCreateDatabase(); pDb->readFile(path.c_str(), L"", false); // 每次都重读 process(pDb); } // 正确:复用单例数据库 OdDbDatabasePtr pDb = odDbCreateDatabase(); for (auto& path : dwgPaths) { pDb->purgeAll(); // 清空内存,但保留对象结构 pDb->readFile(path.c_str(), L"", false); process(pDb); }purgeAll()比close()更快,它不清除数据库结构,只释放实体数据,下次readFile()时跳过头信息解析。
5.2 实体过滤加载:用OdDbHostApplicationServices::setLoadDwgSettings()精准控制
默认readFile()会加载所有实体类型。若只需图层和文字,可禁用几何实体加载:
// 全局设置(在odrxInitialize()后调用一次) OdDbHostApplicationServices* pServices = odDbHostApplicationServices::setHostApplicationServices(); OdDbLoadDwgSettings settings; settings.setLoadEntities(false); // 不加载LINE/CIRCLE等几何实体 settings.setLoadBlocks(false); // 不加载块定义(除非需块引用) settings.setLoadLayers(true); // 必须加载图层 settings.setLoadTexts(true); // 必须加载文字 pServices->setLoadDwgSettings(settings);此设置对后续所有readFile()生效,大幅降低内存占用。
5.3 内存泄漏铁律:所有openObject()必须配对close()或依赖智能指针
Teigha使用引用计数智能指针(OdSmartPtr<T>),但仍有陷阱:
| 场景 | 是否安全 | 说明 |
|---|---|---|
OdDbObjectId id; auto pObj = id.openObject(); | ✅ 安全 | pObj离开作用域自动close() |
OdDbObjectId id; auto pObj = id.openObject(Adesk::kForWrite); | ⚠️ 危险 | 写模式需显式pObj->close(),否则锁表 |
OdDbObjectId id; auto pObj = id.objectId(); | ❌ 错误 | objectId()返回ID,非对象,不能当指针用 |
验证泄漏的实操方法:
在main()末尾添加:
std::cout << "Objects alive: " << odRxObject::numObjects() << "\n"; odrxUninitialize();若numObjects()> 0,说明有对象未释放。此时启用Teigha调试版(TD_Debug.lib),它会在控制台打印未释放对象类型。
5.4 生产环境兜底:超时控制与损坏文件隔离
DWG文件可能损坏或格式异常。在服务端必须加超时和异常隔离:
#include <thread> #include <future> #include <chrono> bool safeReadFile(OdDbDatabasePtr pDb, const wchar_t* path, int timeoutMs = 5000) { auto future = std::async(std::launch::async, [pDb, path]() { return pDb->readFile(path, L"", false) == eOk; }); if (future.wait_for(std::chrono::milliseconds(timeoutMs)) == std::future_status::timeout) { std::wcerr << L"Timeout reading " << path << L"\n"; return false; } return future.get(); } // 使用 if (!safeReadFile(pDb, L"corrupted.dwg")) { // 记录日志,跳过该文件,继续下一份 continue; }从那以后我每次批量处理DWG,都强制走一遍purgeAll()+setLoadDwgSettings()+超时包装三件套。哪怕遇到一份加密又损坏的DWG,整个进程也不会卡死,只会安静地记下日志然后滑向下一份——这才是工业级脚本该有的脾气。希望帮到你。
本文还有配套的精品资源,点击获取