简介:面向钢结构详图设计与TEKLA二次开发初学者的一份入门级源代码包,围绕图纸管理场景,提供载入模型图纸列表、选择模型图纸、打开模型图纸以及读取图纸数值等功能的实现参考。读者可借此了解TEKLA API与C#/VB.NET交互的基本方式,熟悉图纸数据读取、界面事件处理与视图调用思路,并为后续定制化工作流开发打基础。压缩包共20个文件,约74KB,以6个cs源代码文件为核心,配合csproj、sln项目文件及config、settings配置,另含resx资源、suo与vsidx等Visual Studio辅助文件,可直接作为小型WinForm示例工程参考目录组织与调试配置。已有793人学习,适合具备一定C#基础、希望从图纸模块切入TEKLA二次开发的工程技术人员对照源码理解功能拆分与代码结构,也可用于梳理图纸列表加载、选中操作和属性提取之间的调用关系。
1. 从两百个零件编号说起:这套图纸 API 源码到底解决什么
装配图出到一半,编号栏里还剩两百多个零件要一个个手标;图框上的工程名和版本号改了,三十几张图纸得挨个打开改一遍。这种活干过一次就不想干第二次,所以不少人会去搜 Tekla二次开发里图纸方向的入门源代码,但真正能跑通的没几个。这份资源是一套图纸模块的最小可运行示例集,把 DrawingHandler、Drawing、Sheet、View、DrawingObject 这条链子用代码串了起来,还带了批量遍历、改文字、改属性的骨架。它适合已经会写 C#、想快速摸清图纸对象模型的人;不适合指望装个软件插件就一键出图的施工员。看懂它,你才有底气去写属于自己的出图工具链。
2. 环境与工程骨架:把一个 .NET 类库塞进 Tekla 的插件目录
2.1 依赖清单:为什么必须是 .NET Framework 类库
先看技术栈。这套示例没有任何第三方包,全部靠宿主自带的程序集吃饭,所以新建工程那一步选错了模板,后面全是白费功夫。
| 组件 | 形态 | 在示例里的角色 |
|---|---|---|
| Tekla Structures | 本机已安装的宿主程序 | 提供全部 API 程序集,同时是运行容器 |
| .NET Framework 类库 | 与宿主同代的 4.x | 插件工程类型,不能选 .NET 6/8 |
| Tekla.Structures.dll | 安装目录下的 bin 目录 | Point、Identifier、AABB 等公共类型 |
| Tekla.Structures.Model.dll | 同上 | 取梁、板、构件编号,做图纸与模型的关联 |
| Tekla.Structures.Drawing.dll | 同上 | 图纸、图幅、视图、文字、线这些图元 |
| Tekla.Structures.Plugins.dll | 同上 | 插件基类与输入定义 |
关键点在最后一列的「不能选 .NET 6/8」。宿主进程跑的是 .NET Framework 运行时,插件是被它加载进去的,运行时对不上就直接BadImageFormatException或者干脆静默不加载。我一般打开旧的类库模板,改TargetFrameworkVersion,再把平台目标锁成 x64——32 位宿主早就没了,但模板默认值有时候还留着 AnyCPU。
2.2 五步挂载:从空类库到「应用和组件」里能搜到
工程建好之后,动作其实很机械,但每一步都有个容易漏的细节。
第一步,新建「类库(.NET Framework)」,取名随意,示例里是DrawingLab。第二步,添加对上面四张表里后四个 DLL 的引用,路径用宏变量指过去,别写死盘符,换机器就废。第三步,把输出目录直接指到宿主的 plugins 目录,省掉手工复制。第四步,生成一次,打开宿主,在「应用和组件」的搜索框里打插件名。第五步,附加调试。
工程文件的这几行是核心,Private=False尤其重要——不写的话编译会把 API 的 DLL 一起复制到输出目录,下次加载就可能出现两份同名不同版本的程序集,排查起来相当费劲:
<PropertyGroup> <!-- 输出直接落到宿主的插件目录,省掉手工拷贝 --> <OutputPath>$(TEKLA_BIN)\plugins\DrawingLab\</OutputPath> <TargetFrameworkVersion>v4.8</TargetFrameworkVersion> <!-- 宿主只有 64 位进程,这里必须锁死 --> <PlatformTarget>x64</PlatformTarget> <DebugType>full</DebugType> </PropertyGroup> <ItemGroup> <Reference Include="Tekla.Structures.Drawing"> <HintPath>$(TEKLA_BIN)\Tekla.Structures.Drawing.dll</HintPath> <!-- 关键:不要把宿主自己的程序集复制到输出目录 --> <Private>False</Private> </Reference> <Reference Include="Tekla.Structures.Plugins"> <HintPath>$(TEKLA_BIN)\Tekla.Structures.Plugins.dll</HintPath> <Private>False</Private> </Reference> </ItemGroup>$(TEKLA_BIN)这个宏我在系统环境变量里指到宿主的 bin 目录,比如装在某块盘上的某个版本目录下。好处是所有工程共用一份配置,升级版本只改环境变量,不用挨个工程翻属性页。
入口类长这样,特性(Attribute)决定了它能不能在界面里露脸:
using System.Collections.Generic; using Tekla.Structures.Plugins; using Tekla.Structures.Drawing; namespace DrawingLab { // 这个名字就是「应用和组件」里搜到的那一项 [Plugin("DrawingLab.QuickStamp")] public class QuickStampPlugin : DrawingPluginBase { // 图纸插件不需要用户在模型里点选,返回空集合即可 public override List<InputDefinition> DefineInput() { return new List<InputDefinition>(); } public override bool Run(List<InputDefinition> input) { // 真正的活都丢给业务类,插件类只当入口用 return new DrawingStamper().Run(); } } }提示:
DrawingPluginBase的抽象方法在版本之间动过,Run的返回类型和参数列表不一定和上面完全一致。写之前先按 F12 进基类看一眼,别硬抄。
注意插件类只做入口,真正的逻辑放在DrawingStamper里。这么拆的理由很实在:Run里一旦抛异常,界面上的反馈只是「执行失败」,堆栈得去日志里翻;把逻辑独立出来,就可以在控制台或者单元测试里直接调,调试效率差好几倍。
2.3 调试:附加进程比反复重启快得多
调试这一步,新手常犯的错是每次改代码都关宿主再开。实际上直接「调试 → 附加到进程」,进程列表里挑宿主那个 exe,勾上「托管代码」,然后在插件入口打个断点,回宿主里点一下插件就断住了。断不上一般是两个原因:DLL 输出到了别的地方,宿主加载的是旧文件;或者勾错了代码类型。
还有一个实用的技巧,宿主的日志文件会记录插件加载失败的原因,路径在模型目录或者公共环境目录下。遇到「插件出现了但点了没反应」,先翻日志,比在代码里到处加MessageBox快得多。
3. 图纸对象模型拆解:从 DrawingHandler 到 InsertionPoint 的四层结构
3.1 层级关系:Handler、Drawing、Sheet、View
图纸 API 的层级非常规整,记住这张树,一半的问题就能自己推出来:
DrawingHandler 入口,全局唯一,负责拿图纸、切图纸、保存 └─ Drawing 一张图纸,带 Name / Title1..3 / Mark / Identifier ├─ Sheet 图幅,一张图纸可能有多个图幅(比如续页) │ ├─ View 视图,有原点、比例、视图名 │ │ ├─ Text 文字 │ │ ├─ Line 直线 │ │ ├─ Rectangle 矩形框 │ │ └─ Mark/Dimension 零件标记、尺寸 │ └─ Table 表格对象(标题栏、材料表一类) └─ GetDrawingType() 区分装配图 / 零件图 / 布置图DrawingHandler是无状态的,每次用都 new 一个,不要试图缓存成静态字段——宿主切换图纸之后,缓存下来的对象引用会失效,后面所有操作都会抛异常,这种 bug 查起来像玄学。
Drawing通过GetDrawingType()返回一个枚举来区分图纸种类,示例里用的是DRAWINGTYPE_ASSEMBLYDRAWING、DRAWINGTYPE_SINGLEPARTDRAWING、DRAWINGTYPE_GA这几个值,具体拼写以本机 DLL 为准,枚举名字母大小写写错编译器会直接报。
Sheet这一层容易被忽略。多数单页图纸只有一个 Sheet,但一旦遇到图纸内容太多自动分页的情况,只取drawing.GetSheet()就只能拿到当前那一个图幅,剩下的图元你永远遍历不到。稳妥写法是把drawing.GetSheets()拿到的集合全跑一遍。
View下面才是图元。这里有个概念上的分界:图元可以直接挂在 Sheet 上(比如图框里的标题文字),也可以挂在 View 里(比如零件编号)。两种挂法的坐标系不一样,下一节细说。
3.2 DrawingObject 与它的子类:图元的统一接口
所有能画出来的东西都继承自DrawingObject,共同的成员有三组:
- 位置相关的插入点,
Text是InsertionPoint,Line是StartPoint/EndPoint,语义各不相同; - 增删改的三个方法:
Insert()、Modify()、Delete(); - 查询与比较:
IsEqual()、GetDoubleUserProperties()、GetUserProperty()。
这套接口设计的好处是批量处理时能统一调度:遍历拿到一堆DrawingObject,is Text判断一下再转型,就能只处理文字对象。坏处是属性分散在子类上,父类引用拿不到TextString,必须转型。
还有一个必须提前说清的:图纸对象和模型对象是两套完全独立的继承树。模型的Beam不会变成图纸上的任何图元,图纸上的Mark也不是模型的Part。两边靠Identifier关联:模型对象有Identifier,图纸对象也有Identifier,但两者的 Guid 是不同的体系,别想着直接对等比较。真要打通,走「模型里的构件编号(比如ASSEMBLY_POS)→ 图纸里同名文字」这条业务路径更现实。
3.3 坐标:图纸坐标和视图坐标不是一回事
这是入门阶段最容易出错的地方。new Point(100, 100)里的单位是毫米,原点在图幅左下角,往上往右都是正数。这是图幅坐标系。
而如果你把图元插到一个视图里,坐标就变成了相对该视图原点的局部坐标。同一句new Point(100, 100),挂在图幅上和挂在视图里,落点能差出半个图框。
我一般的做法是:跟图框、标题栏相关的元素统一挂图幅,用绝对坐标;跟视图内容相关的元素统一挂视图,坐标全部相对。两套坐标系绝不混用,代码里干脆分两个方法,一个叫AddSheetText,一个叫AddViewText,看到方法名就知道自己在哪个坐标系里。
4. 核心代码逐段讲:批量遍历图纸、改文字、再保存
4.1 拿到图纸:先判空,别急着往下走
所有操作的起点都是拿到一张图纸。听起来简单,但GetActiveDrawing()在两种情况下会返回 null 或直接抛异常:前台窗口是模型而不是图纸;图纸正被某个模态对话框霸占着。
public static bool TryGetActiveDrawing(out Drawing drawing) { drawing = null; try { var handler = new DrawingHandler(); // 当前前台是模型窗口时,这个方法返回 null drawing = handler.GetActiveDrawing(); } catch (Exception) { // 宿主没起、连接断了,或者图纸被模态窗口占用 return false; } return drawing != null; }这段的价值全在try/catch加out参数上。上层调用写成if (!TryGetActiveDrawing(out var d)) return false;,一行就把异常路径堵死了。我见过不少示例代码直接var d = handler.GetActiveDrawing();然后一路点下去,结果用户先点了模型窗口,整个插件报空引用,日志里只有一行堆栈,根本看不出是「没打开图纸」这么简单的原因。
4.2 遍历图元:枚举器必须 Select
拿到图纸之后,常规动作是把图幅上的对象捞出来。这里的枚举器有个特殊要求,不调Select()直接MoveNext()会拿不到东西:
public static List<Text> CollectTexts(Drawing drawing) { var result = new List<Text>(); var sheet = drawing.GetSheet(); if (sheet == null) return result; // 不传类型参数就是全部图元,数据量大时性能一般 DrawingObjectEnumerator all = sheet.GetAllObjects(); // 关键一步:枚举器要先 Select 才能 MoveNext all.Select(); while (all.MoveNext()) { var obj = all.Current as DrawingObject; if (obj == null) continue; // 只关心文字,其他图元直接跳过 if (obj is Text text) result.Add(text); } return result; }GetAllObjects()有个重载可以直接传Type,比如GetAllObjects(typeof(Text)),只返回文字对象。数据量小的时候无所谓,图纸上有几千个图元时,这个重载能把遍历时间砍掉一大半——因为过滤发生在宿主内部,省掉了跨进程序列化的开销。
要注意Current拿到的是object,必须转型。转型用as再判空,别用强制转换,遇到不认识的图元类型会直接抛。
4.3 改属性并落盘:Modify 与 Save 的先后顺序
改文字是最常见的需求:批量替换图层前缀、加统一后缀、把版本号从 A 改成 B。
public static int RenameTexts(Drawing drawing, string oldPrefix, string newPrefix) { int changed = 0; foreach (var text in CollectTexts(drawing)) { if (string.IsNullOrEmpty(text.TextString)) continue; if (!text.TextString.StartsWith(oldPrefix)) continue; // 只改内容,位置和字体保持原样 text.TextString = newPrefix + text.TextString.Substring(oldPrefix.Length); // 不调 Modify 就等于没改,内存里改了宿主也不知道 if (!text.Modify()) continue; changed++; } return changed; }三个参数里,oldPrefix和newPrefix建议从外部配置读,别写死在代码里。Modify()返回布尔值,与Insert()一样,失败时不会抛异常,只会返回 false。所以工程里一定要统计返回值,最后把成功条数打出来,否则你会遇到「跑完了但图纸没变」还找不到原因的情况。
改完之后还有一步:保存。图纸的改动在内存里,没保存的话切换图纸或者关掉宿主就没了。
var handler = new DrawingHandler(); // 保存当前活动图纸,返回 false 说明没保存成功 bool saved = handler.SaveActiveDrawing();顺序不能反:先所有Modify(),最后统一SaveActiveDrawing()一次。每改一条就保存一次,几百条下来会卡到让人怀疑人生。
4.4 批量跑所有图纸:切活动图纸是前提
单张图纸跑通之后,多数人第一反应是「遍历所有图纸挨个改」。这里有个硬约束:图纸的写操作基本只能在活动图纸上做。所以循环里必须先切图纸、再改、再存。
public static List<string> RunBatch(DrawingHandler handler, string oldPrefix, string newPrefix) { var log = new List<string>(); DrawingEnumerator drawings = handler.GetDrawings(); drawings.Select(); // 先收集,边遍历边切活动图纸会让枚举器状态错乱 var targets = new List<Drawing>(); while (drawings.MoveNext()) { if (drawings.Current is Drawing d && d.GetDrawingType() == DrawingTypeEnum.DRAWINGTYPE_ASSEMBLYDRAWING) { targets.Add(d); } } foreach (var d in targets) { // 切不过去就跳过,通常是图纸被锁定或正在被别人编辑 if (!handler.SetActiveDrawing(d)) { log.Add($"{d.Name} 切换失败"); continue; } int n = RenameTexts(d, oldPrefix, newPrefix); handler.SaveActiveDrawing(); log.Add($"{d.Name} 改了 {n} 处"); } return log; }这段里有两个设计点值得说。第一,先收集再处理,不要让枚举器和切图纸的动作交错,否则枚举器内部状态会乱,轻则漏图纸,重则抛异常。第二,SetActiveDrawing的返回值必须判断。多人协作环境下图纸被锁是常态,不判断就会在RenameTexts里对一张根本没切过去的图纸操作,改的其实是上一张。
DrawingTypeEnum用来筛图纸种类,实际项目里也可以按d.Name的前缀过滤,比如只处理以某个工程号开头的图纸。
4.5 抽一个工具类出来,别把逻辑写在插件入口里
到这里功能已经齐了,最后一步是把上面几段收进一个类。我的习惯是分成三层:DrawingStamper做流程编排,DrawingQuery只读不写负责查询,DrawingMutator只写不读负责修改。这样后面加需求时,改哪一层非常明确,也方便写单元测试——查询层可以对着一个固定的测试模型跑,不需要人肉点界面。
返回值统一用结果对象而不是 bool,把成功数、失败数、失败原因都带上,界面上直接弹一个汇总,比弹「执行完成」有用得多。
5. 避坑与排查:图纸 API 的五个高频翻车点
5.1 编译成功但「应用和组件」里搜不到
现象:工程生成成功,DLL 也在,宿主里搜插件名一片空白。
原因:多半是 DLL 没落在插件目录,或者落了但目录层级不对。宿主只在固定的 plugins 目录(及其子目录)扫描,输出路径写错一层就扫描不到。其次是[Plugin]特性漏写,或者特性里的名字和你在搜索框里打的字对不上——这个搜索是按名字前缀匹配的,打错了就是没结果。还有一种少见的:平台目标不是 x64,宿主直接拒绝加载。
解决:先去文件管理器确认 DLL 的落点,再用宿主的日志确认有没有加载记录。日志里没有记录,就是路径问题;有加载失败记录,就是位数或 .NET 版本问题。
5.2 一跑就 BadImageFormatException
现象:插件能出现在列表里,一点就报程序集格式错误。
原因:编译成了 x86 或 AnyCPU 且优先 32 位,而宿主是 64 位进程。或者引用的TargetFrameworkVersion和宿主运行时不一致。
解决:平台目标锁 x64,框架版本对齐宿主。改完记得清理一次bin和obj,有时候旧产物会被增量编译留下,改了配置也不生效,这个坑我踩过不止一次。
5.3 GetActiveDrawing 返回 null,但明明开着图纸
现象:用户说「图纸就在屏幕上」,代码里拿到的是 null。
原因:前台窗口的焦点不在图纸上。图纸和模型是两个窗口,用户切到模型看了一眼再切回来,某些情况下焦点状态不同步。也可能是有个模态对话框没关掉。
解决:入口处统一走TryGetActiveDrawing,拿不到就弹一句「请先激活一张图纸」。别去猜用户的操作路径,把提示写明白比什么兼容处理都管用。
5.4 改了属性,图纸上纹丝不动
现象:TextString赋了新值,界面上还是老内容。
原因:漏了Modify();或者Modify()返回了 false 但没人看;还有一种隐蔽的,对象引用是从上一次遍历里留下来的,宿主内部已经把它换掉了,对旧引用调Modify()静默失败。
解决:每次操作都重新查一遍对象,别跨方法传DrawingObject引用。同时把Modify()的返回值统计出来打到日志里,失败率不为零就一定是哪里有问题。
5.5 文字跑到图框外面去了
现象:想在图框右下角加一行字,结果它出现在图纸左上角,或者干脆看不见。
原因:坐标系用错了。把视图局部坐标当成了图幅坐标,或者反过来。单位也是坑,图纸 API 用毫米,而有些老代码沿用了模型那边的单位习惯。
解决:先在图纸上手工标一个参考点,把它的坐标打出来做基准,再反推自己的换算对不对。这个笨办法比看文档快。另外记得给Insert()的返回值判空,插入失败的对象后续所有操作都是空转。
6. 二次开发改造思路:从示例代码到能交货的工具
示例跑通只是起点,真正要交付,第一件事是把规则从代码里赶出去。我习惯把「哪些图纸要处理、前缀映射表、图框字段填什么」全部外置成一份配置,代码只负责读配置、执行、回写日志。这样现场改需求,运维改配置文件就行,不用重新编译发版。
{ "drawingNamePrefix": "ASM-", "textRules": [ { "match": "^版本[::]A$", "replace": "版本:B" }, { "match": "^工程名[::].*$", "replace": "工程名:示例工程" } ], "dryRun": true }dryRun这个开关强烈建议加上。打开时只统计会改多少处、打印前后对比,不真正调Modify()。第一次在正式模型上跑,我一定先 dry-run 一遍,看日志里的命中条数对不对,再关掉开关真跑。加这一个字段的成本是十行代码,省下的是半天恢复数据的功夫。
第二件事是幂等。批量修改最怕的是跑两遍结果不一样——第二遍把第一遍改过的内容又按规则匹配了一次,越改越乱。解决办法是让每条规则都带一个「已处理」的判据,改前先比对目标值,相等就跳过。这一步多写几行,能省掉后面无数扯皮。
第三件事是验证。改完不要只看一两张图纸就说没问题,写个校验方法,把改动前后的图元数量、文字总数、关键字段内容各统计一遍,前后对不上就报警。图纸数据不像模型数据那么容易被看到,一旦改错了,往往是出图之后才被发现,那时候返工成本就上去了。
我自己在这上面翻过一次车:批量改图框字段,规则里少写了一个排除条件,把零件标记里的同名文字也一起改了,几十张装配图的编号全乱。因为是Modify()一条条默默成功的,日志里全是绿色的成功计数,根本看不出问题。从那以后我每次写涉及批量落笔的功能,都强制先跑一遍 dry-run 加前后比对,宁可多花十分钟,也不想再花一整天去恢复。希望这些能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取