简介:PDMS二次开发教程面向三维工厂设计与化工、石油、制药等行业的从业者及脚本开发入门者,聚焦如何用内置宏语言PML扩展软件功能、适配项目个性化需求这一实际问题。内容从变量与数据类型、条件与循环控制、函数与子程序,到对象属性访问与事件响应,逐层讲清PML语法基础,并配合自动布置管道阀门、定制物料平衡与管路长度报表、扩展工具栏菜单、设计数据合规校验、工作流任务与审批自动化等典型场景,帮助读者把脚本思路落到日常设计流程中。压缩包共59个文件、约237KB,以doc与ppt讲解资料、pmlfrm与pmlobj窗体对象文件、pmlfnc函数文件、PML语法高亮配置文件为主,另含cs与cpp源码、sln与vcproj工程文件及少量exe、dll,可用于DotNet与Dars接口开发的对照实践。已有2757人学习下载,适合希望从语法入门过渡到脚本编写的设计人员按模块查阅、边学边练。
1. 从命令行到脚本:PDMS 二次开发到底在写什么
接手一个改造项目,第一个动作往往不是建模,而是先把上一版留下来的一堆元件理一遍:命名规则不统一、描述字段缺一半、报表还要按新图签重新拆。手工点当然能干,一下午点两千个元件,三天也能点完,但下次改图再来一遍。PDMS 二次开发要解决的就是这类事——不用在外部另写一套程序去对接数据库,直接用 PDMS 自带的 PML 宏语言,在命令行里、在自带的窗体里,对当前设计数据做查询、校验和批量修改。
它适合两类人:常年在 PDMS 里画图、想把重复操作变成一键执行的建模工程师;以及要给设计小组做小工具、又不想碰外部接口的技术人员。整条路线很清晰:先学会 PML 的变量和循环,再把它包进一个 .pmlfrm 窗体,最后挂到菜单上让别人也能点。下面按“语法骨架 → 窗体 → 挂菜单 → 批量任务 → 避坑”的顺序拆,代码都能直接抄下来改成自己的。
2. PML 语法骨架:变量、判断、循环与元素遍历
PML 不太像现代脚本语言,它更像一套挂着表达式外衣的命令行指令。前面几小时内会很快发现,卡住人的地方一般不是逻辑本身,而是这套语言长得太不合常规;习惯一旦拧过来,写起来其实比想象中快。
2.1 变量与字符串:!前缀和单引号是硬规矩
最容易被劝退的,是变量写法。局部变量必须以!开头,全局变量!!开头,字符串只能用单引号包起来,双引号在 PML 里不是字符串边界。拿其他语言的直觉写name = 'xx',解释器会直接报语法错。
-- 局部变量:只在当前脚本或方法内有效,脚本跑完就没了 !pipeName = 'PIPE-0001' !count = 12 -- 全局变量:同一个 PDMS 会话内跨脚本可见,适合放日志路径这类配置 !!logPath = 'c:/pdms_work/report.csv' -- 数字转字符串必须走 .string,直接拼接会报类型不匹配 !label = '管线 ' & !pipeName & ' 共 ' & !count.string & ' 段' $p 输出: $!label -- 字符串比较用 eq / ne,不是 == 和 != if (!pipeName eq 'PIPE-0001') then $p 命中目标管线 endif这段里有几个必须记住的点。&是字符串拼接符,不是逻辑与;$!在输出上下文里做变量展开,把变量值塞进字符串;$p往命令窗口打一行,是 PML 里最原始的调试手段,等价于别处的 print;--起头是注释,整行有效。
提示:变量名别跟 PDMS 保留字撞车,像
name、type、date这类,能用但读起来极容易混,养成加前缀的习惯,比如!myName、!inType。
还有一个新手常忽略的细节:PML 不强制你声明变量,但强烈建议在每个脚本开头把变量集中写一遍并给初值。原因不是规范好看,而是 PML 里未赋值变量的行为在不同上下文下表现不一致——在窗体回调里可能拿到空串,在命令行脚本里可能直接抛错,排查起来非常浪费时间。
2.2 判断与循环:把“点一下”翻译成“跑一遍”
条件语句的结构是if ... then ... elseif ... endif,then不能省,endif必须有,少了任何一个都是语法错。这套写法比 C 系语言啰嗦,但好处是嵌套多层时反而容易看清边界。
!dia = 150 !spec = '' if (!dia > 100) then !spec = 'A' elseif (!dia > 50) then !spec = 'B' else !spec = 'C' endif $p 口径 $!dia 对应规格 $!spec -- 计数循环:处理 1 到 20 号支架 do !i from 1 to 20 $p 正在处理第 $!i 个支架 enddo参数说明:!dia是数值型,跟数字比较用>、<、>=这类符号;跟前一节说的一样,字符串才用eq/ne。混用是高频错误——拿eq比较两个数字不会报错,但结果经常不是你要的。
遍历集合用另一种do写法,values关键字带一个集合对象:
-- 遍历一个名称列表,逐个判断前缀是否合规 !names = object ARRAY() !names.append('PIPE-0001') !names.append('VALVE-0002') !names.append('PIPE-0003') do !n values !names if (!n.index('PIPE') eq 1) then $p 前缀合法: $!n else $p 前缀不合法: $!n endif enddoobject ARRAY()建一个动态数组,.append()往尾部追加,do !x values !arr逐个取出。.index()返回子串第一次出现的起始位置,返回 1 表示从第一个字符就命中——这是判断前缀最省事的写法,比手工截子串稳妥。
2.3 元素遍历:!!ce是所有操作的起点
前面的代码都是纯逻辑,跟 PDMS 数据还没关系。真正干活的那一步,是拿到元素对象再读属性。
!!ce是 PDMS 的“当前元素”,跟着界面上选中的东西走。这个设计很方便,也是坑最多的地方——脚本换个人执行,当前元素可能完全不是你以为的那个层级。
-- 先确认拿到的是什么,再决定要不要往下遍历 !ce = !!ce if (!ce.Name ne '') then $p 当前元素类型: $!ce.Type $p 当前元素名称: $!ce.Name -- 遍历当前元素的直接成员 do !child values !ce.Members !cn = !child.Name !ct = !child.Type $p 成员: $!cn ($!ct) enddo else $p 没有选中任何元素,先在设计树里点一个 endif参数说明:.Name、.Type是元素上的属性,.Members是它的直接子元素集合。这三个名字看起来通用,实际项目里不一定完全对得上,属性名和集合名一定要跟着自己的数据模型查,别凭印象敲。查的办法很简单:选中一个元素,在命令行里把它的属性打出来看,对上了再往脚本里写。
遍历时建议先打两行确认层级,再执行业务逻辑。这一步看着多余,但它能省掉后面“脚本跑完了但一个都没改”的排查时间——那种情况九成是压根没进到你想遍历的那一层。
3. 第一个能交付的窗体:.pmlfrm 的结构与回调
命令行脚本自己用没问题,但要让组里其他人也用,就得给个界面。PML 自带窗体机制,一个.pmlfrm文件就是一个小窗口,不需要额外安装任何东西,也不用打包。
3.1 窗体骨架:setup form加 gadget 就是全部
一个最小可运行的窗体长这样,一个输入框、一个按钮、一个日志框,足够跑通“输入参数 → 执行 → 看结果”的完整闭环。
-- 文件名: pipecheck.pmlfrm -- 一个最小可运行的窗口:输入前缀 + 触发检查 + 显示日志 setup form !!PipeCheck title '管道命名检查' gadget .txtPrefix text 'PIPE' width 16 gadget .btnRun text '开始检查' call .doCheck gadget .txtLog text '' width 60 height 8 exit逐项拆:setup form !!PipeCheck定义窗体对象,!!前缀意味着这个对象在整个会话里全局可见,后面显示窗口、读取控件都靠它;title是窗口标题;每个gadget .xxx定义控件,点号后面的名字是你自定义的控件标识;call .doCheck把按钮的点击事件绑到名为.doCheck的方法上;最后一个exit结束窗体定义,不能漏。
width和height的单位是字符宽度和行数,不是像素。日志框给 60×8 一般够看十几行输出,再多就要考虑加滚动条或写文件了。
3.2 回调方法:按钮点下去之后发生了什么
按钮本身不做事,做事的是它绑的方法。方法名必须和call后面写的一字不差,包括那个点号。
-- 按钮回调,方法名与 gadget 上 call 指定的名字必须一致 define method .doCheck() -- 从控件里取值 !prefix = .txtPrefix.text -- 输入校验放在最前面,不要等执行一半才失败 if (!prefix eq '') then .txtLog.text = '前缀不能为空,请先填写' return endif !okCount = 0 !badCount = 0 !log = '' -- 遍历当前元素的成员,逐个比对命名前缀 do !child values !!ce.Members !n = !child.Name if (!n.index($!prefix) eq 1) then !okCount = !okCount + 1 else !badCount = !badCount + 1 !log = !log & $!n & ' ' endif enddo -- 循环跑完才回填界面,一次写入 .txtLog.text = '合格 $!okCount 个,不合格 $!badCount 个: $!log' endmethod逻辑说明:先从控件读参数,再校验,校验不过就提前return,别让它带着空值往下跑。统计和日志拼接全在循环里完成,界面只在最后写一次——这一点很关键,循环里每轮都去刷新控件,几百个元素之后窗口就会明显卡顿。最后.txtLog.text = ...一次性回填,用$!展开变量拼出完整信息。
.txtPrefix.text这种写法里的前导点号不能省,它表示“窗体对象上的控件”,省了就变成当前元素的属性,报错信息会很难懂。
3.3 显示、关闭与重复打开
窗体定义完不会自己弹出来,得显式调用:
-- 显示窗口 !!PipeCheck.show() -- 关掉窗口 !!PipeCheck.close()一个常见的现场问题是:同一个窗口点第二次没反应,或者直接弹出第二个。前者是因为窗体对象已经存在,show()只是把它提到前台;后者出现在用不同方式重复定义窗体的脚本里。稳妥的做法是先判断再显示:
-- 已存在就复用,不存在才重新加载定义文件 if (!PipeCheck eq '') then !!PipeCheck.close() endif注意:调试窗体时,改完
.pmlfrm文件记得先close()再重新加载,否则窗口里跑的仍是上一版定义。这个坑在改回调逻辑时尤其隐蔽——代码明明改了,行为一点没变。
4. 落到工程上用:菜单挂载、批量改属性与报表导出
到这一步,脚本能跑,窗体也能开,但只有你自己知道怎么启动。要让它真正进入日常工作流,还得解决三个问题:放哪、怎么点、跑完的产出落在哪。
4.1 脚本放哪、菜单怎么挂
文件的存放位置决定这个工具的生命周期。放本机临时目录,换个项目就找不着;放项目目录下的PDMSUI子目录,跟着项目走,但项目归档后要重新部署;放本机公共 UI 目录,团队能共享,但版本同步得自己管。我的习惯是:稳定成型的放项目PDMSUI,还在打磨放本机公共目录,两边用同一个文件夹结构,方便后面搬家。
| 存放位置 | 适合什么 | 代价 |
|---|---|---|
| 项目目录下的 PDMSUI 子目录 | 已成型、跟项目绑定的工具 | 换项目要重新部署 |
| 本机公共 UI 目录 | 还在打磨、多项目复用的工具 | 需要自己维护版本 |
| 任意临时目录 | 一次性脚本 | 下次登录就忘了在哪 |
菜单挂载的常见做法是找到对应模块的菜单定义文件,追加一条指向自己脚本的条目,登录时自动加载。临时测试阶段更简单,直接在命令行执行宏:
-- 临时测试:直接在命令行执行外部宏,路径按自己的目录替换 $m /c/pdms_work/pipecheck.pml路径分隔符建议统一用正斜杠,反斜杠在 PML 字符串里容易出转义问题。另外,不同环境对相对路径的基准目录理解不一致,能写绝对路径就写绝对路径,写不了就在脚本开头把当前目录打出来确认一遍。
4.2 批量改属性:先过滤、再判断、最后写
批量写是二次开发里最容易出事的动作。跑错一次,几千个元件的描述全被覆盖,后悔药是没有的。我一般强制自己按三段式写:先过滤类型,再判断当前值,最后才写。
-- 批量补齐描述字段:只处理管道,已有内容的一律不动 !updated = 0 !skipped = 0 do !child values !!ce.Members !n = !child.Name !t = !child.Type -- 第一段:类型不对的跳过 if (!t ne 'PIPE') then !skipped = !skipped + 1 else -- 第二段:已有描述的不能覆盖,那是别人留的备注 if (!child.Description eq '') then -- 第三段:确认空值才写 !child.Description = '自动补齐-' & !n !updated = !updated + 1 else !skipped = !skipped + 1 endif endif enddo $p 更新 $!updated 个,跳过 $!skipped 个参数说明:!child.Description是可写属性,赋值语句本身就是写操作;!updated和!skipped两个计数器用来做事后核对,跑完一定要看这两个数,加起来等于遍历总数才算逻辑闭环。中间那段空值判断是最容易被省掉的,省掉之后的后果是:别人手工填的备注被脚本安静地替换掉,一周后才有人发现问题。
调试这类脚本,我一般先改成“只统计不写”,把两个计数器打出来,确认筛选结果符合预期,再放开赋值那一行。多花五分钟,少一次事故。
4.3 导出 CSV:文件句柄、路径与异常
批量导出报表是另一个高频需求。PML 里用文件对象写文件,套路固定:打开、逐行写、关闭。
-- 导出当前元素下的成员清单,路径按自己环境改 !out = object FILE('c:/pdms_work/pipe_list.csv') !out.open('WRITE') -- 表头 !out.writeRecord('NAME,TYPE,DESCRIPTION') do !child values !!ce.Members !line = !child.Name & ',' & !child.Type & ',' & !child.Description !out.writeRecord($!line) enddo !out.close() $p 导出完成: c:/pdms_work/pipe_list.csv逻辑说明:object FILE(...)创建文件对象,open('WRITE')以覆盖方式打开,writeRecord()每调一次写一行,最后必须close()——不关文件,内容可能还在缓冲区里,打开 CSV 看到的是空的。
三个必须处理的边界:一是目录不存在,open直接失败,脚本里最好先用目录检查或提前建好;二是字段里本身带逗号,拼出来的 CSV 列会错位,稳妥做法是把字段里的逗号替换掉再拼;三是中文编码,不同环境的默认编码不一样,导完先用表格软件打开看一眼,乱码就改编码或换导出格式。
5. 避坑与排查:PML 工具上线前必须过的五个坑
下面这五条都是从实际翻车里抠出来的,共同点是:脚本不报错,命令行还回显“执行完成”,但结果就是不对。这类问题最难查,因为没有任何错误信息给你。
5.1 脚本跑完什么都没改:作用域搞错了
现象:命令行回显执行完毕,界面上的数据一点变化都没有,重新打开还是原样。
原因:绝大多数情况是!!ce指向的层级不对。操作者点的是一个大的装配节点,脚本却按“直接成员”去遍历,真正要改的元件在下面两三层,遍历自然一个都没碰到。
解决:在脚本开头加两行输出,把当前元素的名称和类型打出来,跟预期对比。确认层级后,别依赖操作者当前选中的是什么,改成从固定入口往下找,或者要求执行前必须先选中指定层级并做校验。
5.2 窗体第二次点没反应:全局对象重复创建
现象:第一次打开正常,关掉再点菜单,窗口不出现,命令行也没报错。
原因:!!PipeCheck这个全局对象在会话里还留着,show()只是把它提到前台,而它可能已经被关到某个不可见状态;另一种情况是脚本重新走了一遍setup form,新老两个对象打架。
解决:显示之前先判断对象是否已存在,存在就先close()再重新加载定义,或者干脆复用已有对象只做show()。养成“先清理再创建”的习惯,窗体类脚本能少一半玄学问题。
5.3 几千个元素跑到一半卡死:循环里刷新界面
现象:小范围测试很快,一放到整个区域就卡住,窗口变灰,鼠标转圈,最后只能强退。
原因:PML 是单线程的,循环里每轮都去改控件文本或弹提示,界面线程被反复打断,元素一多就积压。另外循环里做字符串拼接如果用&反复追加超长文本,开销也是平方级增长。
解决:循环里只做数据累计,界面刷新全部移到循环之后一次性完成。日志特别长的话,改成写文件,界面只显示“共处理 N 条,详见 xxx 文件”。
5.4 同事机器上找不到文件:路径写死了
现象:自己机器上跑得好好的,别人一执行就报文件打不开。
原因:脚本里写了绝对路径,指向的是你自己的本地目录。
解决:路径统一提到脚本顶部的变量里,方便替换;能放项目目录就放项目目录,用相对路径加运行前打印当前目录的方式确认基准位置。路径分隔符统一正斜杠,别混着写。
5.5 属性写进去又变回去:写错了对象或没确认状态
现象:脚本执行完,$p显示更新了 200 个,过一会儿再查,只有几十个是新的,其余全恢复原值。
原因:一类是写到了临时的副本元素上,不是真正在设计数据里的那个;另一类是脚本执行时所在模块或状态不对,写操作被后续的事务回滚掉了。还有一种最隐蔽的:组里另一个人同时在跑他自己的批处理脚本,两边互相覆盖。
解决:写完立刻抽样回读三到五个元素,把读到的值打出来,确认落库;批量写之前先确认当前模块和权限;多人协作时约定批处理窗口,别同时跑。
6. 进阶:把脚本变成能放心交给别人的工具
工具写到能跑只是及格线,能交给同事放心点下去才算交付。这一步靠的不是更复杂的语法,而是给脚本加自检和收敛。
6.1 用“空跑模式”给每次执行兜底
我后来给所有会写数据的脚本都加了一个开关,默认只统计不落库,确认数字对了再手动打开写入。实现方式很简单,脚本顶部放一个!dryRun = 1,所有赋值语句外面套一层判断:
-- 空跑开关:1 只统计不写,0 真正写库 !dryRun = 1 do !child values !!ce.Members if (!child.Description eq '') then !updated = !updated + 1 if (!dryRun eq 0) then !child.Description = '自动补齐-' & !child.Name endif endif enddo $p 预计影响 $!updated 个元素,空跑模式: $!dryRun这段代码的价值不在于技术含量,而在于它把“先看后写”变成了默认行为。习惯一旦养成,误伤率会明显下降。
6.2 性能:几千个元素之后怎么不卡
PML 的性能瓶颈集中在三处,优化手段都不复杂:
| 瓶颈点 | 典型表现 | 处理方式 |
|---|---|---|
| 循环内刷新界面 | 窗口变灰、进度条卡住 | 数据累计完再一次性回填 |
| 循环内反复读同一属性 | 元素越多越慢,呈线性放大 | 循环外先读一次存到变量里 |
| 超长字符串反复拼接 | 上千条之后明显变慢 | 分段写文件,或改用数组暂存后合并 |
还有一条经验:能一次遍历解决的,别拆成三次遍历。读属性、判断、写值放在同一个do循环里,比跑三遍快得多,也少三份出错的可能。
6.3 交付前必做的一次验证
我现在的固定动作是:脚本写完,拿一个已知答案的小范围跑一遍,把输出跟自己手点的结果对一次;再拿一个边界范围跑一遍,比如全是空描述的元素、或全是带逗号的名字。两遍都对上,才敢放到菜单上。
有一次给一个改造项目做批量命名工具,小范围测试全部通过,直接推给组里用。第二天有人反馈“部分元件名字被截断了”——原因是某个元件的名称里带了一个特殊字符,拼接时被当成分隔符处理掉了。那次之后我每次交工具前,都会专门构造一批带特殊字符、带空值、带超长的脏数据跑一遍,跑通了才发出去。
如果你手上正好有一堆重复劳动等着做,先从命令行里把那段逻辑写成脚本,再包成窗体,最后挂到菜单上。整个过程不需要额外的环境,PDMS 本身就够了。希望帮到你。
本文还有配套的精品资源,点击获取