PDMS二次开发入门:用PML脚本实现模型批量自动化处理
简介面向PDMS二次开发人员的系统教程聚焦PML脚本语言及DARS、.NET等外部接口开发适用于化工、石油、制药等行业中需要扩展PDMS能力的工程设计人员。包内共59个文件、约237KB以PML基础语法、Dars接口开发、PDMS12 dotNet接口开发等文档为主含C/C#工程源码MFC窗口模式与控制台示例、PML窗体与函数定义、可执行程序及UE语法高亮配置便于对照实践。已有2743人学习下载。内容从PML变量、控制结构、函数与对象属性访问讲起延伸到自动化布置、定制报告、界面扩展、数据校验与工作流自动化并给出DARS调用和.NET集成的具体实现路径能帮助读者从示例入手掌握编写宏、构建外部程序的关键方法适合想系统学习并落地项目的初中级PDMS二次开发人员。1. 做pdms二次开发先别一头扎进C和.NETPML才是多数项目里最省力的杠杆做pdms二次开发很多人的第一反应是去翻C或者.NET的接口文档结果环境配置就劝退了一半。实际上PDMS内置的PML语言才是多数设计场景下最值得先投入的方向它在PDMS进程内直接运行不用单独装SDK也没有编译打包那一整套工程。我拆过一套以PML为主线的PDMS二次开发教程资源学习路径基本是四段PML语法基础、模型对象导航、Form表单交互、调试与交付。这套东西解决的问题很具体——设计模型里几百根管线的名称、描述、长度需要批量调整手工改一天脚本几分钟跑完。适合两类人一类是被重复操作耗到想自动化的设计员另一类是学过一点PML但始终写不出完整工具的入门工程师。这篇实操版复盘把教程里最容易卡住的参数和坑都标了出来所有脚本可以直接改后套用。2. 先把PML的脾气摸清楚解释型执行、加载方式与第一个可用脚本2.1 为什么“解释型”这个属性决定了你的调试方式PML全称Programmable Macro Language是PDMS内置的宏语言解释执行不经过编译。这意味着两件事第一你可以在PDMS命令窗口里逐行输入命令立刻看到返回结果第二错误通常要到运行到那一行才会暴露不会像编译型语言那样在构建阶段给你提示。所以调试PML的常态是“脚本自己跑一段然后停在某个属性上返回了空值”看起来像数据不对实际是前面某一行写错了对象路径。PDMS里加载PML的方式主要有三种直接在命令输入框敲入单行命令通过“自定义”菜单加载宏文件写在启动文件里让PDMS启动时自动执行。新手最容易忽略的是文件后缀名函数模块保存为.pmlfnc宏文件保存为.pmlmac两者加载机制不同。函数文件一般要先加载才能调用宏文件则可以直接执行。如果你把函数写进了.pmlmac再按函数的调用方式去命令窗口敲大概率会得到“未定义”的提示这一点在学习教程时值得注意。先看一段最基础的命令框验证代码!name DEMO-PIPE !msg 当前宏演示对象: !name !!msg变量名以半角感叹号开头字符串必须用单引号加号做拼接!!的作用是把结果输出到命令回显区。这个回显是PML学习阶段的“眼睛”也是之后所有调试的基础。你可以在PDMS任意一个命令输入框里逐行执行或者保存为.pmlmac文件后用命令调用。把这三行执行通说明你的PML运行环境基本可用。2.2 object()是进入模型世界的唯一入口但空对象是头号陷阱PML里几乎所有针对模型数据的操作都要从一个对象引用开始。最常用的写法是object 意思是“把当前设计环境中选中的对象作为操作对象”。这个设计很朴素却让很多初学脚本的人栽过跟头界面上一旦没有选中任何东西object 返回的是一个空对象引用而不是报错。你后续访问它的任何属性都会返回空值或空串脚本既不报错也不中断看起来像“黑匣子”。所以写任何PML脚本的第一步不应该是取属性而应该是判空!ce object IF !ce.IsMember THEN !!ce.Name ELSE !!warn(没有选中任何模型元件请先拾取一个管件或分支) ENDIFIsMember是PML中判断对象是否有效且属于模型成员的方法。如果整个库都比较庞大取出的是一个Zone或SiteIsMember也会返回相应结果但返回逻辑和具体元件不同。这里提醒两点一是不要指望object 在无选中时会抛异常它只会安静地返回空二是如果你的脚本是给设计员用的务必在入口处加上类似上面的判空否则后面所有批量逻辑都可能在空对象上跑完界面却毫无动静用户会以为脚本坏了。2.3 循环与数组一切批量操作的基本骨架PML里的循环结构和常见语言类似但要记住它的大写关键字风格。FOR ... ENDFOR、IF ... ENDIF这些闭合标签是必须写全的。数组用ARRAY()创建长度可以在运行时自动增长元素通过append方法追加。一个非常典型的入门循环是统计当前对象下所有成员的数量!all ARRAY() FOR !m IN object().Members !all.append(!m.Name) ENDFOR !!all.length参数说明Members属性返回当前对象的全部直接子节点!all.length返回数组长度。这里有一个大多数教程不会特意提醒的点PDMS数据库内部存储的成员顺序和设计树界面显示的顺序并不一定一致。如果你的脚本逻辑依赖“处理顺序和界面看起来一样”不要把希望寄托在默认顺序上后面第5.2节有个更具体的坑这里先建立一个意识批量脚本必须先明确排序规则或者对顺序不敏感。2.4 用函数封装逻辑避免全局名称污染写了几段脚本之后你会发现所有代码都推在宏文件里可读性会变得很差而且不同脚本之间变量作用域经常互相干扰。PML提供了函数文件机制扩展名.pmlfnc每个函数用Function开头EndFunction结束。函数可以通过参数传入对象也可以通过-后面的参数把结果带出来。这是一个可运行的函数骨架Function CountPipeBranches(!zone - !count) !count 0 IF !zone.IsMember THEN FOR !m IN !zone.Members IF !m.SPEC PIPE THEN !count 1 ENDIF ENDFOR ENDIF EndFunction调用方式是在命令框或宏里写!!CountPipeBranches(object )。这里!zone是传入的对象参数!count是返回参数如果传入的不是有效成员函数内部判空后返回0。需要特别说明的是!m.SPEC PIPE这一行不同PDMS版本的属性字典和类型名称有差异有些环境里管道元素类型对应的不是PIPE而是本地化描述这是一处典型版本陷阱。我第一次按资深同事的脚本抄SPEC写法时在自己的环境里跑出来统计值全是0之后才认识到要先在目标环境里实际打印一次类型值。经验是凡涉及类型关键字、属性关键字先写一个十来行的属性转储脚本跑一遍目标模型看看真实返回值再进入正式逻辑。函数文件多了之后命名冲突会很常见。PML在加载多个函数文件时如果存在同名函数后加载的会把先加载的覆盖掉。为了避免这种问题我给每个项目都加固定的函数前缀比如某个项目的工具类函数一律写成proj_开头。这个习惯在团队开发中非常值得坚持。3. 模型对象导航成员、属性与批量更新三类高频任务都在这章3.1 先把PDMS的对象树在脑海里画出来PML的对象导航本质上就是在PDMS的模型树里移动和筛选。PDMS模型顶层的结构大致是Database数据库下挂Site、Site下挂ZoneZone里再放管线、设备、结构等具体对象。这些具体对象又包含成员比如一条管线下面可以挂分支、管嘴、支撑分支下面再挂管件和元件。为什么要先理解这个层级因为几乎所有批量脚本要做的第一步都是确定“我在哪个层级取数”。比如你取Zone.Members拿到的不一定是管线可能是设备也掺杂其中而你取Pipe.Members拿到的才更可能是分支。层级错了后面所有循环条件都要跟着白跑。这个道理看起来简单但在真实项目里对象层级往往因为多人协同建模而不那么规整。有一次我在某项目的联合装置区域统计管道支吊架数量直接遍历了Zone下的所有成员结果把设备上的平台支腿也算成了支吊架整个统计表后续返工这就是对象层级没做条件过滤的血泪教训。3.2 从当前选中对象向上回溯Zone做一次分区统计日常操作中最常见的导航逻辑之一是从当前选中的元件回溯到所属Zone然后对整个Zone做统计。这样设计员在画布里随手选一个分支脚本就知道该处理哪个区域。代码可以这样写!ce object !zone !ce.Zone !pipeCount 0 FOR !item IN !zone.Members IF !item.TYPE PIPE THEN !pipeCount 1 ENDIF ENDFOR !!pipeCount参数说明!ce.Zone是PML提供的属性回调可以从任意对象向上找到它所属的ZoneTYPE是对象内置类型关键字PIPE表示管线对象。这里同样存在版本差异问题建议你先做一轮打印验证。如果你的模型里管道类型存成了别的名称把PIPE换成你那边打印出的实际类型关键字即可。这个脚本还有一个变体可以在真实项目里帮你快速看清模型组成结构。打印出某个Zone下所有成员的TYPE和Name形成一份临时目录方便你确认筛选条件写的是否准确。3.3 批量更新分支长度和描述把手工逐条操作改成循环第2章打完基础这一章开始就上更典型的实际操作。设计项目中经常发生管廊调整一个区段几十条分支的长度需要统一改到某个安全值。如果人工作业选中一个分支、改一个属性、再选中下一个五十个对象要十几分钟。用PML写一个循环时间开销是瞬时完成的。!changed 0 FOR !bm IN object().Members IF !bm.IsMember AND !bm.HasAttribute(LENGTH) THEN IF !bm.LENGTH 4500 THEN !bm.DESCRIPTION LONG-自动标记 !bm.LENGTH 4500 !changed 1 ENDIF ENDIF ENDFOR !!changed逻辑说明外层循环遍历当前对象的所有成员HasAttribute(LENGTH)先判断该对象是否有长度属性避免在管嘴、支撑等没有长度属性的成员上报错然后判断如果长度大于4500毫米就批量修改描述文字并截断长度。参数说明4500是阈值你可以按项目标准改成任意数值DESCRIPTION是描述字段在部分PDMS版本里也可能是其他关键字脚本中要留意当前环境的属性字典。这个脚本的隐患在于再次运行时会重复修改已经改过的对象。如果同一个脚本要被执行两次建议判断条件里加上“描述已经包含某个标记则跳过”IF LEFT(!bm.DESCRIPTION, 4) LONG THEN !bm.DESCRIPTION LONG-自动标记 !bm.LENGTH 4500 !changed 1 ENDIF这段修改体现了PDMS批量脚本的关键思路幂等性。批量更新脚本最好能接受重复运行不会把设计员已经调整过二次的内容覆盖掉这一点是教程资源里很容易被忽略的价值点。3.4 从CSV文件驱动的批量改名脚本批量操作还会经常面对外部数据源最常见的方式是CSV文件。PML本身提供了文件读取相关函数常见做法是先用FOPEN打开文件句柄再用循环逐行读取并切割字段最后把新名称写回对象。这里给一个可以套用的骨架!fp FOPEN(rename_list.csv, R) IF !fp 0 THEN FGETS !fp !line WHILE NOT FEOF(!fp) !parts SPLIT(!line, ,) !oldName !parts[1] !newName !parts[2] !obj object !oldName IF !obj.IsMember THEN !obj.Name !newName ENDIF FGETS !fp !line ENDWHILE FCLOSE !fp ENDIF参数说明FOPEN的第二个参数R表示只读FGETS把一行内容读入变量SPLIT按逗号切割得到原对象名和新对象名object !oldName用路径或名称定位对象。注意这里不同版本的文件函数可能名不同有的版本用FREADLINE代替FGETS。建议在拿到教程资源后先跑一个最小读取样例确认当前环境的函数名称。这个脚本有一个必须注意的边界条件CSV文件的编码。PDMS在不同区域版本里对中文编码支持不一样曾经有某公司同事导出的CSV里包含中文字符读进PML后全部乱码也没报错结果批量改名产生了大量含乱码的对象名最后只能靠备份恢复。遇到中文CSV建议先做编码转换或者改用UTF-8格式配合当前版本对应的读取方式。4. 表单FORM进阶把脚本封装成设计员愿意天天用的面板工具4.1 为什么要用Form以及PML表单的基本结构命令框里的脚本虽然强大但让设计员每天打开命令框敲函数并不太现实。PML提供了表单FORM机制可以在PDMS里绘制出标准的窗口界面包含输入框、按钮、下拉列表最终把之前写好的批量逻辑封装在一个可视化的面板里。一个典型的表单文件从结构上分为定义区和动作区。定义区写窗口布局动作区绑定按钮事件或初始化事件。下面是一个用于批量更新分支数据的表单定义骨架FORM fmBatchUpdater TITLE 批量更新分支数据 ALT FMBATCHUPDATER WIDTH 460 HEIGHT 320 BEGIN_FORM GROUP 目标范围 FIELD fldZone 区域路径 TYPE TEXTLONG FIELD fldBranchNo 分支号过滤 TYPE TEXTLONG GROUP 更新内容 FIELD fldLength 长度 TYPE NUMBER FIELD fldDesc 描述 TYPE TEXTLONG BUTTON btnRun 运行脚本 COMMAND !!run_batch_update() BUTTON btnClose 关闭窗口 COMMAND !!close_form() END_FORM逻辑说明FORM声明表单名ALT指定内部别名BEGIN_FORM到END_FORM之间是控件定义区。FIELD控件用于输入BUTTON控件绑定命令字符串当按钮被点击时PML会执行命令字符串里写的函数。参数说明TEXTLONG表示长文本输入框NUMBER表示数字输入框WIDTH和HEIGHT控制窗口尺寸。4.2 初始化回调打开表单时自动带入当前选中对象一个好的表单工具打开时应该自动把当前画布中的选中信息带进来减少设计员手动输入。这就需要在表单加载完成后执行一个初始化函数。常见做法是在表单定义里增加一个bev或form load相关的事件声明然后在函数文件中写出对应的初始化逻辑。Function fmBatchUpdater_init() !ce object IF !ce.IsMember THEN f.fldZone.Value !ce.Zone.Name ELSE f.fldZone.Value ENDIF EndFunction逻辑说明f前缀是PML表单字段的标准访问方式f.fldZone.Value表示给表单里的区域路径输入框赋值。参数说明如果当前没有选中对象则把输入框置空等待设计员手动填写。这里有一个值得注意的地方不同PDMS版本触发初始化的动作名称略有不同有的版本需要在FORM定义中显式声明动作绑定有些则默认执行同名初始化函数。教程资源一般会给出对应版本的声明方式如果发现窗口打开后没有预填数据优先检查这一处绑定设置。4.3 在按钮事件里调用批处理函数并回显结果表单最大的价值是把之前裸奔的脚本变成一个可以反复使用的工具。下面是按钮绑定的批处理函数对应4.1节里COMMAND !!run_batch_update()中的调用Function run_batch_update() !branchFilter f.fldBranchNo.Value !newLength f.fldLength.Value !count 0 FOR !bm IN object().Members IF !bm.IsMember THEN IF !branchFilter THEN IF !bm.Name !branchFilter THEN CONTINUE ENDIF ENDIF !bm.LENGTH !newLength !count 1 ENDIF ENDFOR !!warn(更新完成共处理 !!count 个对象) EndFunction逻辑说明先从表单两个输入框里取过滤条件和目标长度然后遍历当前对象的所有成员如果分支号过滤项非空则只处理名称匹配的分支否则全部处理。最后用!!warn弹出一个结果窗口显示一共处理了多少个对象。参数说明CONTINUE用于跳过本次循环中不满足条件的对象继续下一个。结果回显是做表单工具时不应该省掉的环节。如果脚本是静默执行的设计员不知道它是否把不相关的对象也改了。回显只是一个雏形更进一步可以把处理前后的对象清单写入日志文件这部分在第6章展开。4.4 数值校验防止设计员把明显不合法的参数提交给数据库表单打开了输入框就必然要面对人的误操作。如果设计员把长度填成-500或者99999999脚本会非常老实地把所有分支都改成那个数值这种错误尤其让人头大。所以按钮执行函数里要先做一轮数值合理性校验IF !newLength 0 OR !newLength 20000 THEN !!warn(长度超出合理范围请检查输入) RETURN ENDIF逻辑说明RETURN在PML函数里表示直接结束当前函数后面的代码不会执行。参数说明上限20000要根据项目实际情况调整如果项目里有超过20米的分支就把阈值相应放大。这一段直接放在run_batch_update函数体最前面即可。4.5 把面板做成菜单项设计员不用碰命令行表单封装好之后下一步是把它挂到PDMS的菜单栏上让设计员通过点击菜单打开工具而不是在命令框敲加载语句。常见做法是通过PDMS的自定义菜单系统完成不同版本有不同的菜单配置入口基本原理都是写一行类似!!form_load fmBatchUpdater的命令把这个动作作为菜单按钮的点击事件。团队里统一部署时我通常会把这行加载语句写进项目启动宏文件里这样PDMS启动即获得菜单入口。如果你只是自用在命令框敲!!form_load fmBatchUpdater也可以完成加载。可这样一来你觉得时间久了就会把Form加载方式淡忘建议还是花十分钟把菜单入口做好。这方面学到的是一个工具只有当设计员不用记任何命令就能用起来时它才真正有交付价值。5. 常见问题排查PML脚本开发中的四个经典疑难与处理记录5.1 函数文件加载后提示“未定义”或调用无响应现象把函数文件放进PDMS加载路径后在命令窗口执行!!myfunc()系统提示找不到该函数或者毫无反应。原因最常见的是文件扩展名不对。函数应保存为.pmlfnc但很多人按习惯保存成了.pmlmac。PDMS对不同扩展名的处理方式不同.pmlmac会被当作宏直接执行里面如果只有函数定义而没有调用动作执行结果就是“什么也没发生”你再去调函数自然无响应。另一个常见原因是函数文件加载顺序不对你在宏文件里已经写了!!myfunc()但包含myfunc定义的.pmlfnc文件还没有被加载。解决先确认文件扩展名再手动加载函数文件最后再执行宏。命令行里加载函数文件的常见写法类似!!load_function_names具体以当前版本的加载命令为准。建议团队内部统一一个加载文件把项目所有.pmlfnc放在同一个加载清单里按依赖顺序排列避免加载顺序导致的不稳定。还有一个额外建议函数名尽量带项目前缀例如proj_batch_update避免和PDMS自带的函数重名。5.2 遍历Members返回的成员顺序和设计树界面对不上现象用FOR !m IN object().Members遍历当前对象脚本输出的顺序和PDMS设计树上看到的看起来不是同一个顺序甚至统计出来的数量也和界面上数出来的不一致。原因PDMS数据库中Members返回的顺序是内部存储顺序它不等同于界面导航树所展示的层级顺序。在多人协同设计环境下不同设计员的视图过滤条件不同也会让界面显示结果和脚本统计结果产生差异。解决如果脚本逻辑不依赖顺序过滤条件里明确做好类型判断即可。如果必须按某种顺序处理一般会先把成员名称放进数组再用SORTUP排序最后按排序后的数组取对象!list ARRAY() FOR !m IN object().Members !list.append(!m.Name) ENDFOR !sorted SORTUP(!list) FOR !name IN !sorted !!object(!name).Name ENDFOR这一段操作目的很明确先收集全部符合范围的对象名排序后再遍历。排序关键字除了名称也可以是长度、位号等数值字段具体取决于你的业务目标。如果发现排序结果里的数字按字典序而非数值序排列例如10排在9前面就先对字段做一次数值格式化再排序。5.3 表单加载正常但字段数据为空按钮怎么点都没反应现象表单能弹出来文本框里却是空的按钮点击以后界面没有变化也没有任何报错。原因这一类问题多数出在字段名称不一致。例如表单定义里的字段名是fldBranchNo但函数里访问时写成了fldbranchnoPML对部分环境的大小写并不宽容。还有一种是按钮事件里的命令字符串没有正确触发比如命令前后缺少一对双引号或者命令调用的函数名与实际函数名不一致。解决把字段名统一为fld前缀所有访问保持一致。按钮事件排查时第一步先在按钮命令里临时改成弹出一个提示框BUTTON btnRun 运行脚本 COMMAND !!warn(button clicked)如果点击按钮能弹出这个提示说明事件绑定没有问题问题出在后续函数内部。如果连这个提示都不弹说明表单事件解析或函数加载方面有故障回头检查加载顺序。5.4 批处理脚本处理大量对象时越跑越慢最后像“卡死”一样现象脚本在几十个对象的小范围上运行很流畅一旦放到整个装置区段跑前面处理很快后面越来越慢界面像卡死了一样。原因PML在逐条修改模型属性时PDMS会同步触发模型数据库更新和界面刷新。对象数量大了以后界面刷新消耗的时间远远超过脚本逻辑本身。如果脚本里还嵌套了多层循环每次循环都对全库做一次查询数据量会呈几何级增长。解决第一调整批处理范围尽量把对象集缩小到当前要修改的分支或某个特定类型第二在循环外部临时关闭无关的显示刷新机制处理完成后再恢复。不少老工程师会用SET Q这一类命令来关闭数据库查询和界面提示不同版本命令有所不同。具体语法需要以你环境的命令速查表为准但思路是一致的大规模批处理时要降低PDMS的交互刷新频率。第三如果数据量真的很大可以把脚本拆成分段批处理每处理200个对象暂停一次并输出进度避免一次长时间运行。5.5 模型数据改错了想撤销发现PDMS的撤销机制帮不上忙现象脚本跑完设计员发现批量改值有误按撤销快捷键发现只能退回很小一步大量修改已经写入数据库。原因PDMS内置的撤销机制对脚本批量修改的支持有限尤其当脚本通过成员遍历逐条写入属性时撤销操作可能只记录最后一次修改。解决这是所有PML批量工具在交付前最值得认真对待的问题。通常的做法是在运行任何写操作脚本前先对当前模型作一次完整备份或者用PDMS自带的备份能力把相关区域数据导出为一个中间文件这样出错后有后悔药可以回滚。这个习惯我已经坚持了很久。现在每次给设计员交付批量工具我都会在运行按钮里先执行一次安全备份再进入正式逻辑。6. 把脚本做成可交付的工具样本验证、日志追踪与部署习惯工具函数写完之后距离真正交给设计员使用还有一步验证。这一步在某些教程里常被压缩成一句“测试一下”但实际工程里没做过验证的工具上线后出错的代价远比多花半小时测试要高。通常我会准备一个固定的试算样本。这个样本里包含几十根分支、不同类型的管嘴、一两个设备以及少量边界数据。样本固定有两个好处一是每次修改脚本后跑一遍样本的输出结果可以和上次对比二是当模型数据出问题时可以快速确认脚本在新的数据集上是否符合预期。固定样本对同一个脚本多跑几次如果结果一致再拿真实项目里的一个小区段跑一遍对比设计员手工统计的结果两次对得上脚本才具备交付资格。这里有一个可以作为习惯的技巧在批处理脚本里落一行日志。日志不需要复杂记录处理时间、处理对象名、修改前后的关键数值就够用!logPath C:\work\batch_run_ string(DATE(yyyymmdd)) .log !fp FOPEN(!logPath, A) FWRITE(!fp, !time 已处理 !obj.Name 长度 string(!newLen) \n) FCLOSE !fp参数说明FOPEN的A参数表示追加写入不覆盖上次日志string()把数值和日期转成字符串用于拼接FCLOSE务必执行否则文件句柄会一直占用。日志能让你在脚本跑完之后回溯每一笔操作这在多人共用同一个设计环境时特别有用。有一次某成员不小心覆盖了我的函数定义文件导致当天导出的数据整体错位当时全靠日志定位到出错的时间点和对象及时把影响范围控制住了。部署方面建议把函数文件、表单文件、启动宏文件放在同一个项目目录并用一个加载宏统一管理。PDMS启动时加载这个加载宏所有工具函数和菜单即可生效。每次更新脚本后重跑一次固定样本确认没有破坏旧功能再把文件同步到团队成员的工作目录。从那以后我每次交付脚本都会强制走一遍最小样本回放再补一条日志到本地文件。这个习惯救过我很多次。希望这篇文章能帮你在PDMS二次开发上少走几个弯路也希望你第一次跑脚本前就把备份功夫做在前面。本文还有配套的精品资源点击获取