简介:资料包围绕PDMS(三维工厂设计软件)的二次开发入门与进阶,面向已具备基本建模操作、希望用PML脚本扩展软件功能的化工、石油、制药行业设计人员与在校学习者。包内共59个文件,约237KB,除PML基础语法、二次开发讲义与培训PPT等文档外,还包含PML窗体(pmlfrm)、函数与对象(pmlfnc、pmlobj)示例,以及C#、C++接口工程源码与UE语法高亮配置,覆盖语法讲解、界面定制与外部程序调用等方向。内容以变量与数据类型、条件与循环、函数子程序、对象属性访问、事件响应等语法要点为线索,配合自动布置管道阀门、定制物料平衡与管路长度报告、工具栏菜单扩展、设计数据合规校验、工作流自动化等实例,帮助读者把脚本落到实际项目。资源已有2757人学习,适合作为查阅式手册与练手素材。
1. PDMS 二次开发到底在改什么:从每天半小时的重复操作说起
在 PDMS 里画完一个区的管道,剩下的活往往不是设计,而是核对:管件命名有没有按规范走、保温等级和管道等级对不对得上、材料表里的描述是不是又被谁改成了另一种写法。单看一个几秒钟,一个区几百根管子就是半天。PDMS 二次开发解决的正是这类事——用 PML(Programmable Macro Language)把「点进去、看属性、抄出来、比对」这串动作写成脚本,让软件自己去遍历模型、读属性、出报告,甚至弹一个小窗口让同事填参数点按钮。它适合两类人:天天用 PDMS 做设计、被重复核对拖住的工程师;以及要给团队做小工具、又不想上重型平台的实施人员。门槛不高,但边界很清楚,后面几章就是把这条边界画出来。
2. PML 语言底座:变量、对象与宏的三种运行形态
PML 是一门解释型语言,写一行执行一行,不需要编译。听起来很轻松,但真正决定脚本好不好维护的,是你把它放在哪种形态里跑、变量在哪个上下文里活。这一章先把语言层面的东西说清楚,后面所有脚本都建立在这几个概念上。
2.1 命令行、宏文件与表单:三种落点怎么选
实际干活时 PML 有三个落点,选错了会很别扭。
命令行是最轻的。模块下面那行输入框,直接敲!x = 1回车就能看到结果,适合验证一个属性名到底存不存在、某个导航表达式返回的是单个元素还是一个集合。我习惯在写任何脚本之前,先在命令行把关键表达式敲一遍,确认!!CE.MEMBERS到底吐出几个东西,后面写循环才有底。命令行的问题是它不留痕,关掉会话就没了,所以只能用来试探,不能用来交付。
宏文件是团队协作的主力形态。扩展名.pml,放在 PMLLIB 指向的目录里,用$M/宏名调用。PMLLIB 本质是一个路径列表,项目的配置文件里通常已经挂了几个目录,把你自己的脚本目录加进去,重开会话后$M/才找得到。宏文件天然可以进版本管理,这一点比命令行和表单都强。
表单的扩展名是.pmlfrm,用show !!表单名打开,把界面和逻辑塞在同一个文件里,适合给不写代码的同事用。表单的调试成本比宏高得多——界面布局、回调绑定、方法定义三处任何一处对不上,表现都是「点了没反应」,没有报错提示。
再往下是 PML1 和 PML2 的选择。PML1 语法偏脚本,字符串处理和界面控件都比较原始;PML2 支持面向对象,能调 .NET 那边的东西。我的判断标准很简单:只遍历模型、读属性、出清单,PML1 足够,写得快、同事也看得懂;要做复杂界面、要连外部数据库、要做多线程之类的事,再考虑 PML2。别一上来就上 PML2,团队里没人能接手维护是常态。
2.2 变量、字符串与作用域:几处反直觉的地方
PML 的变量统一用!打头,第一次赋值就自动声明,不写 VAR 也能跑。看起来是最松散的那类脚本语言,但有几处反直觉的地方,踩过一次就记住了。
字符串用单引号包,拼接用&,不是+。写成+有时候不报错,会按数值去算,结果是一串莫名其妙的数字,这种玄学错误最难查。数字和字符串混拼时 PML 会尝试隐式转换,转不了就给个空串,不报错,报告里就是一行空白,你还以为是数据本身缺失。
双感叹号!!打头的是 DBREF,指向数据库里的一个真实元素。!!CE是当前元素,会随着你在图形界面里点选而变,所以脚本里最好第一行就把它接住:!ce = !!CE。我见过有人在循环中间直接用!!CE,循环体里触发了别的命令把当前元素改掉了,后半段全跑偏,日志里看着还挺正常。
作用域这块,命令行和宏是两套上下文。命令行里敲过的变量留在当前会话里,紧接着敲的第二行还能用;宏是一次独立执行,上一次运行残留的变量不一定还在。所以宏的开头把要用的变量都显式赋一遍初值,别指望「上次跑过所以这次还有」。
提示:PML 的注释是
--,不是//也不是#。混用不会报错,但注释后面的代码可能被当成语句执行,排查起来很费时间。
2.3 第一个能跑的脚本:遍历成员并打印关键属性
概念说再多不如跑一遍。下面这段是几乎所有 PDMS 脚本的骨架:接住当前元素、遍历它的直接成员、按类型过滤、把关键字段打出来。
-- first_pass.pml -- 用途:遍历当前元素的直接成员,打印名称/类型/描述 -- 调用:把文件放进 PMLLIB 目录,命令行执行 $M/first_pass !ce = !!CE -- 先把当前元素接住,后面全程用它 !count = 0 -- 计数器必须显式初始化,别依赖上次运行的残留 DO !m VALUES !ce.MEMBERS !count = !count + 1 !line = !count & ' | ' & !m.NAME & ' | ' & !m.TYPE & ' | ' & !m.DESCRIPTION $!line ENDDO !msg = '处理完成,共 ' & !count & ' 个成员' $!msg逻辑其实就四步。第一步!ce = !!CE把当前元素固定下来,避免遍历过程中!!CE被别的操作改掉。第二步DO !m VALUES !ce.MEMBERS是 PML 里最常用的迭代写法,MEMBERS返回的是直接子元素集合,不含孙子辈,想要更深层得自己递归或者再套一层 DO。第三步在循环体里拼字符串,&负责连接,!m.NAME这类点号访问就是在读属性。第四步用$!变量把结果回显到命令行。
参数改动集中在三处。想换遍历范围,把!ce.MEMBERS换成!ce.OWNER就变成往上找父级,但要注意父级只有一个,这时候 DO 循环就不合适了。想换输出字段,改!m.NAME那一串属性名即可,前提是这些属性在你的字典里真实存在——用户自定义属性要在 Paragon 里定义过才读得到,读一个不存在的属性不报错,返回空串,这是新手最容易卡住的地方。想加过滤条件,在循环体最前面插一个IF (!m.TYPE EQ 'BRAN') THEN ... ENDIF,只处理分支。
还有个细节:$!line这一行如果拼出来的字符串里有特殊字符,回显可能被截断。真要输出干净结果,别靠命令行,直接写文件,这个放到第 4 章讲。
3. DBREF 对象模型导航:从一根管子找到它的全部属性
会写循环只是入门,能不能干活取决于你对 PDMS 对象模型的理解深度。PML 脚本里 80% 的报错和 90% 的「结果不对但没报错」,根子都在导航路径写错了。
3.1 三层结构:数据库、层级与元素
PDMS 的数据组织是三层。最上面是数据库(MDB),一个项目可以有多个;中间是层级,从 WORLD 往下是 SITE、ZONE,再往下才是设备和结构;最下面是元素,管件、分支、设备、轴网都算元素,每个元素在脚本里就是一个 DBREF。
这个层级关系决定了你写遍历时的起点。很多新手习惯从!!CE开始递归整棵树,在测试模型上跑得飞快,到了实际项目的 MDB 上直接卡死——因为 WORLD 底下挂着几十万个元素,脚本挨个读属性,每次读都可能触发一次数据库访问。
合格的做法是先想清楚「我要查的东西挂在哪一层」。查管道保温,起点应该是 ZONE,因为 BRAN 是 ZONE 的直接成员;查设备位号,起点是 EQUI 所在的 ZONE 或者 EQUIPMENT 层。层级想明白了,循环层数就定死了,性能问题自然消失大半。
元素的属性分两类。一类是系统属性,NAME、TYPE、OWNER、DESCRIPTION 这些每个元素都有;另一类是用户自定义属性(UDA),需要先在 Paragon 和 Lexicon 里定义,定义完之后元素上才有这个字段。脚本读 UDA 之前,先去字典里确认字段名,拼错一个字母不会报错,只会给你一片空白。
3.2 常用导航表达式与它们返回什么
下面这张表是我自己贴在显示器边上的,写脚本时天天要看。返回类型搞错了,后面的代码全白写。
| 表达式 | 返回类型 | 典型用途 |
|---|---|---|
!!CE | 单个 DBREF | 脚本入口,第一行接住 |
!el.NAME | 字符串 | 命名规范检查、去重 |
!el.TYPE | 字符串,如 BRAN / PIPE / EQUI | 循环里做类型过滤 |
!el.OWNER | 单个 DBREF | 往上找所属 ZONE 或 SITE |
!el.PURPOSE | 字符串 | 区分设计态与施工态元素 |
!el.MEMBERS | 元素集合 | 向下遍历,只含直接子级 |
!el.DESCRIPTION | 字符串 | 描述字段核对,常被手工改乱 |
有一点要特别提醒:MEMBERS返回的是集合,OWNER返回的是单个元素。这两个东西在语法上长得像,用错了报错信息还很含糊。判断方法很简单,看你在它前面写的是DO ... VALUES还是直接赋值——DO后面跟集合,赋值后面跟单个元素。
还有TYPE的取值,别凭印象写。不同类型元素的 TYPE 字符串不完全一样,最稳的办法是在命令行点选一个元素,然后敲$!CE.TYPE看它到底返回什么,照着抄。
3.3 写一个能出结果的检查器:过滤、计数、输出
把前面几块拼起来,就是一个真正能干活的检查器。下面这段统计当前 ZONE 下所有分支的保温等级缺失情况。
-- check_insulation.pml -- 用途:统计当前 ZONE 下 BRAN 的保温等级缺失数量 -- 前提:INSULATION 是字典里已定义的属性,未定义请换成实际字段名 !zone = !!CE !total = 0 !bad = 0 DO !br VALUES !zone.MEMBERS IF (!br.TYPE EQ 'BRAN') THEN !total = !total + 1 IF (!br.INSULATION EQ '') THEN !bad = !bad + 1 !line = '缺保温: ' & !br.NAME $!line ENDIF ENDIF ENDDO !msg = '分支总数 ' & !total & ',缺保温 ' & !bad $!msg逐段看一下。!zone = !!CE要求你在执行前先在图形界面里选中一个 ZONE,选错层级结果就是零,而且不报错。DO !br VALUES !zone.MEMBERS只遍历一层,因为 BRAN 就是 ZONE 的直接成员。IF (!br.TYPE EQ 'BRAN')做类型过滤,字符串比较用EQ,PML 里大小写敏感,写eq大概率不生效。
参数上最容易改错的是属性名。很多项目里保温不叫 INSULATION,叫INSUL、INSL或者别的缩写,具体看字典。改之前先在命令行选中一个分支,敲$!CE.INSULATION看返回什么,返回空串有两种可能:一是这个属性确实没填,二是这个属性名根本不存在。区分办法是换一个肯定有值的字段(比如 NAME)试一下,能返回就说明语法没问题,问题在属性名。
这段脚本还有个隐含限制:它只处理当前 ZONE。实际项目里一个区有几十个 ZONE,你不可能一个个点。扩展方向有两个,一是把!zone换成 SITE,遍历成员时判断类型再往下钻一层;二是把这段代码包成define method,用递归把整棵子树扫一遍。第二种写法更通用,但递归深度要设上限,遇到异常数据(比如父子关系成环)会无限循环,这是血泪经验,不是理论风险。
4. PML 表单工具化:把命令行脚本做成同事愿意点的按钮
脚本自己能跑,和同事愿意用,中间隔着一条鸿沟。让人记$M/xxx这种命令是不现实的,交付形态必须是一个窗口。
4.1 表单文件的结构:布局和方法写在同一个文件里
.pmlfrm文件分两段:前半段是setup form布局块,后半段是一串define method。文件名要和表单名一致,CheckForm.pmlfrm里定义的就该是!!CheckForm,不一致的症状是show的时候提示找不到表单,但不会告诉你是名字对不上。
-- CheckForm.pmlfrm -- 表单名必须与文件名一致:CheckForm setup form !!CheckForm dialog title '保温检查' frame .Main row 1 text .lblTip '先在模型里选一个 ZONE,再点扫描' width 40 row 2 button .btnScan '开始扫描' callback '!this.DoScan()' row 3 list .lstOut expand exit exit布局块的关键是frame .Main这一层容器,控件都挂在它下面,row决定行号,expand让列表控件随窗口拉伸。做工具的时候别在界面上堆控件,一个按钮加一个列表足够覆盖大部分场景,控件越多,不同分辨率下的布局越容易崩。
调用方式是把文件放进 PMLLIB 目录,命令行敲show !!CheckForm。第一次调试建议就手动敲,别急着做按钮,先把show调通。
4.2 回调绑定:按钮点下去之后发生什么
callback '!this.DoScan()'这一行是表单里最容易出错的地方。它要求同文件里存在一个叫DoScan的方法,方法名大小写必须一致,!this.前缀不能省。
-- 接在上面的布局块后面,同一个文件 define method .CheckForm.DoScan() !zone = !!CE !count = 0 !this.lstOut.clear() -- 每次点击先清空,避免结果累加 DO !br VALUES !zone.MEMBERS IF (!br.TYPE EQ 'BRAN') THEN !count = !count + 1 IF (!br.INSULATION EQ '') THEN !this.lstOut.add(!br.NAME) ENDIF ENDIF ENDDO !this.lblTip.text = '扫描完成,共 ' & !count & ' 个分支' endmethod方法体里访问控件用!this.控件名,和布局块里的.lstOut、.lblTip一一对应。clear()和add()是最常用的两个列表方法,add接受字符串,塞不进结构化数据,所以结果复杂的时候我会先拼成一行文本再塞进去。
!this.lblTip.text这个写法在不同版本的控件属性上略有差异,有的是.text,有的是.title。遇到赋值不生效,别硬试,去 Lexicon 里查一下这个 gadget 的属性定义,比翻资料快。
方法体里的变量是局部的,方法执行完就没了。想让多次点击之间记住状态(比如累计统计),得把值写到表单上隐藏的文本控件里,或者用全局变量。这一点和宏脚本不一样,别混。
4.3 结果落盘:格式、编码和路径的三个坑
列表里的结果只能看,不能交。真要交付,还是得生成文件。PML 里写文件通常用文件对象,写法在不同版本上有差异,常见的形态是这样:
-- export_csv.pml -- 用途:把当前 ZONE 下的分支清单写成 CSV -- 注意:路径用正斜杠,目录必须已存在,PML 不会自动建目录 !zone = !!CE !path = 'C:/temp/branch_list.csv' !f = !!file !path FOR WRITE !f.writeLine('NAME,TYPE,INSULATION') DO !br VALUES !zone.MEMBERS IF (!br.TYPE EQ 'BRAN') THEN !row = !br.NAME & ',' & !br.TYPE & ',' & !br.INSULATION !f.writeLine(!row) ENDIF ENDDO !f.close()第一个坑是路径。反斜杠在某些上下文里会被当转义符吃掉,统一写正斜杠最省事;目录不存在的时候不会自动创建,直接报错或者静默失败。第二个坑是没有引号包裹,元件名或描述里带逗号的时候,那一行会被 Excel 拆成多列,整个文件错位。稳妥的做法是给每个字段都套上双引号,或者干脆用制表符分隔存成.txt。第三个坑是编码,中文描述在 CSV 里被 Excel 打开经常是乱码。
| 输出格式 | 编码建议 | 适用场景 |
|---|---|---|
.csv逗号分隔 | 带 BOM 的 UTF-8 | 交给 Excel 直接双击打开 |
.txt制表符分隔 | ANSI 或 UTF-8 | 字段里有逗号、引号 |
.csv手工导入 | UTF-8 无 BOM | 走 Excel 的「自文本导入」指定编码 |
如果!!file在你那个版本上不可用,退路是把结果塞进列表控件,再用 Draft 那边的报表模板输出,或者干脆输出到命令行让人手工复制。工具能不能用,比工具先不先进重要得多。
5. 排查清单:PML 脚本上线前最容易翻车的五个场景
下面这五条都是我自己踩过的,按「现象 → 原因 → 解决」记下来,遇到问题先照着对一遍,比从头读代码快。
5.1 运行时报错类
现象一:命令行逐行敲能跑,存成.pml宏就报变量未定义。原因有两层。表层是命令行会话里残留了上一次的变量,宏是独立执行上下文,拿不到;深层是宏里某个变量只在 IF 分支里赋值,条件不成立时后面直接用了。解决办法是宏开头把所有要用的变量显式初始化一遍,尤其是计数器和累加字符串,赋成 0 和空串,别指望默认值。
现象二:$M/宏名提示找不到宏。常见原因三个:文件没放进 PMLLIB 覆盖的目录;文件名大小写和调用时写的不一致;文件被存成了xxx.pml.txt,扩展名多了一层。排查顺序是先确认$M/的搜索路径,再确认文件名,最后看扩展名。改完路径要重开会话,运行中改环境变量不生效。
现象三:表单点按钮完全没反应,也没有报错。这是最难查的一类,因为不报错。三个必查点:表单名和文件名是否一致;callback里的方法名和define method里的名字是否逐字符一致;方法是否真的写在了同一个.pmlfrm文件里。我遇到过把方法单独拆到另一个文件的情况,拆分本身没问题,但需要在表单里显式引用,漏了就是静默失败。
现象四:脚本跑完提示成功,重新打开模型发现属性没变。原因是改的东西没落到数据库里,只在内存副本上生效,或者改的是引用的临时对象。稳妥的做法是先在一根管子上单独验证,确认保存关闭再打开还在,再批量跑。批量修改前一定要导出备份清单,这个习惯救过我两次。
5.2 数据与性能类
现象五:脚本在测试模型上秒出结果,在实际项目的 MDB 上卡死。原因基本都是遍历范围失控:从!!CE开始无条件递归,每一层都读属性,属性读取又触发数据库访问,几十万次下来就是假死。解决办法有三条,按性价比排序:一是把起点收缩到 ZONE 或 SITE,别从 WORLD 开始;二是循环里先读TYPE做过滤,只对目标类型读重属性;三是把结果攒在内存里,循环结束后一次性输出,别在循环体里拼大字符串或者频繁刷新界面控件。
还有一个容易被忽略的:循环里如果调用了会弹出对话框或者刷新界面的方法,每次都会打断执行流,几百次循环下来体感就是卡死。界面更新一律放到循环外面。
6. 从能跑到敢用:给脚本加 dry-run、日志和版本管理的三个习惯
脚本能出结果只是第一步,敢让它在正式模型上跑,中间还差三个习惯。
第一个是 dry-run 开关。我在每个会修改数据的脚本开头都放一个常量,默认只读。写法和验证方式是这样:把!dryRun = 1放在文件最前面,凡是要写数据库的地方都包一层IF (!dryRun EQ 0) THEN ... ENDIF,只读分支里把「将要改成什么」逐条打印出来。第一次跑永远走 dry-run,把人眼能核对的清单拉出来,抽样对五到十条,确认逻辑没错再把开关改成 0。这个开关看着笨,但它是我唯一敢在别人项目上跑脚本的理由。
第二个是日志。每次运行生成一个带时间戳的文件,记录跑的是谁、哪个 MDB、遍历了多少元素、命中多少条、耗时多少。出问题的时候,日志比代码有用得多——它能告诉你脚本到底跑没跑完,还是卡在第 3000 个元素上。日志路径写在脚本顶部,别写死在 C 盘根目录,换个机器就失败。
第三个是版本管理。PML 文件都是纯文本,天生适合进仓库。我的目录习惯是这样:
| 目录或命名 | 用途 | 说明 |
|---|---|---|
pml/dev/ | 开发中,随时会改 | 只有自己用,不进 PMLLIB |
pml/release/v1.2/ | 已验收版本 | 按版本号建目录,不覆盖旧版 |
pml/archive/ | 下线脚本 | 保留但不加载,方便回溯 |
| 文件头注释 | 版本、作者、改动日期 | 三行以内,改一次更一次 |
回滚这件事,我吃过亏。早年改一个批量重命名脚本,直接覆盖了原文件,跑完之后发现命名规则写反了,想改回去的时候已经没有旧版本了,最后是拿导出清单一条条改回来的。从那以后我每次改脚本,都强制先复制一份到release下面按版本号落地,再动dev里的文件,确认没问题才提版本号。慢是慢了一点,但比手工改两百根管子快。
这套习惯不复杂,难的是每次都走一遍。希望帮到你。
本文还有配套的精品资源,点击获取