
简介面向化工、石油、制药等行业的PDMS设计及二次开发人员资料系统讲解基于PML扩展PDMS核心功能的方法覆盖语法基础、对象模型、自动化设计、定制报告与界面扩展等典型应用场景。压缩包共59个文件以doc接口文档、cpp/h源码、cs工程以及pmlfrm/pmlfnc窗体函数文件为主另含sln解决方案、exe演示程序与ppt共237KB结构清晰便于查阅。目前已有2739人学习。资料内提供pml基础语法、dotNet接口开发、DARS接口开发等完整文档并附CsvFixer、dars_MFC窗口、testdotnet等可运行示例工程能帮助读者快速掌握PML编程与PDMS API调用直接应用于管道布置、报表统计、数据校验等生产环节。1. PDMS 二次开发是什么PML 在解决哪一类重复劳动做配管或建模的人应该都遇到过这种场景一条 200 根管道的管线设计变更后要挨个改标注、换材料描述、同步属性纯手工做要半天还容易漏。PDMS 二次开发要解决的就是这类重复劳动——通过 PML 脚本批量读属性、改属性、生成报表甚至把整个物料清单自动导出。PDMS 的官方脚本语言是 PML它长得很像 BASIC但跑在 PDMS 进程内部能直接操作当前数据库里的元件。这套教程的核心就是把 PML 语法、对象模型、对话框和外部接口串起来让你从「会操作 PDMS」变成「能让 PDMS 自己干活」。适合配管工程师、后台支持和想接管道类项目的开发人员。2. 先学会和 PML 打交道语法、运行时与调试手段2.1 PML 的变量、字符串与循环和 C# 完全不同的习惯PML 分 PML2 和 PML.NET 两代早期教程基本以 PML2 为主。PML2 的变量名统一用!!开头局部变量和全局变量靠前缀区分这跟 C# 或 Python 的声明方式完全不同第一次接触的人最容易在这里懵。!!开头的是脚本内变量::开头的是跨脚本共享的全局变量。下面是一段典型的 PML2 变量与循环代码-- 变量赋值不需要声明类型 !!projectName DEMO_PLANT !!totalCount 0 !!items list(PIPE-001, PIPE-002, PIPE-003) -- FOREACH 遍历列表 FOREACH !!item IN !!items !!totalCount !!totalCount 1 MESSAGE 当前项: !!item 计数: !!totalCount END FOREACH -- DO 循环注意从 1 开始而不是从 0 DO !!i FROM 1 TO 5 !!square !!i * !!i MESSAGE 平方值: !!square ENDDO这段代码里有三个关键点。第一字符串拼接用的是操作符不是数字和字符串混拼时 PML 会自动做类型转换但转换规则比较隐蔽建议都用显式拼接。第二列表类型list()的索引从 1 开始不是 0这是 PML 对老式 BASIC 的继承后面写遍历逻辑时如果按习惯取list[0]会直接拿不到数据。第三MESSAGE是 PML 里最基础的输出命令运行时会弹消息框调试阶段它就是你的printf。我在给新人讲这段时一定会强调PML 的循环不像 C# 的for那么灵活它本质上是「带计数器的重复执行块」循环体内不要试图修改循环变量!!i否则某些版本会静默忽略或者出现难以排查的跳变。教程后面的练习里很多「奇怪的结果」十有七八是循环写法不够规矩导致的。2.2 列表与 FOREACH处理一堆元件的基本姿势PDMS 二次开发里处理元件几乎离不开FOREACH。它的作用是把一个集合里的元素逐个取出来执行循环体集合可以是元件的子成员列表也可以是查询得到的临时集合。写法和 2.1 里的遍历一致但有一点要注意FOREACH中的元素变量是引用不是拷贝。如果你在循环体内修改了元素属性改的是真实数据库里的元件这在批量修改场景下是好事但在做数据统计时如果误改了属性后果比较严重。-- 获取当前选中元件 !!current CE -- 遍历当前元件的所有子成员 FOREACH !!child IN !!current.Members MESSAGE 子元件名: !!child.Name END FOREACHCE是 PML 内置的「当前元素」标识对应你在 PDMS 界面里选中的那个元件。.Members返回当前元件的直接下级集合。这段常见代码的价值在于打通「界面选中 → 脚本拿到对象 → 遍历子项」的链路。实际做工具时我不会直接用CE而是先让用户通过选择框指定一个范围再把选择结果传给遍历逻辑这样脚本的可控性高很多。教程里有专门一节讲COLLECTION类型的组装核心是把多个元件放进一个集合统一处理。2.3 对话框与 message第一段能跑的交互脚本PML 的交互能力来自FORM对话框它可以在脚本里定义一个窗口放输入框、下拉框、按钮然后让你从界面拿参数。最简单的一档是用DISPLAY配合内置对话框函数弹出输入框不需要手动排表单。下面的代码展示了最基础的参数输入交互-- 弹出输入框把用户输入存到变量 !!userInput TEXTBOT( 请输入管道前缀, PIPE ) MESSAGE 用户输入的是: !!userInput -- 下拉选择 !!selOption OPTIONBOX( 选择处理方式, list(导出清单, 批量改名, 仅统计) ) MESSAGE 你选择了: !!selOptionTEXTBOT和OPTIONBOX是 PML 内置的简易对话框函数第一个参数是提示文字第二个参数是默认值。它们的好处是不用写复杂的FORM定义脚本短、跑得快适合工具原型验证。如果要做一个让同事日常使用的完整工具还是得回到FORM表单把参数区和按钮区都排好这一步教程里有完整的表单代码。交互脚本是「能不能把技术落地」的分水岭。只会跑固定逻辑的脚本只能自己用带上参数输入和结果展示的脚本才能交付给别人。我一般建议新手先把MESSAGE和TEXTBOT这组搭配跑熟再去啃FORM表单——从「能跑」到「好用」中间差的正是这一段。2.4 调试手段$? 与 message 是最朴素好用的断点PDMS 的 PML 脚本没有像 Visual Studio 那样的图形化断点调试主要靠三种手段MESSAGE输出关键值、$?检查上一条命令是否执行成功、以及 PDMS 命令行的报错信息。$?是个特殊变量系统每执行完一条指令都会刷新它为 0 表示成功非 0 表示出错。很多老脚本里会在关键步骤后面加一句检查!!result !!element.Name -- 如果取属性失败直接弹窗终止 IF $? ! 0 THEN MESSAGE 取属性失败请检查是否有权限 EXIT ENDIFEXIT表示结束当前脚本。这个模式在整个教程里反复出现它的价值是让错误在发生的第一时间暴露而不是等到最后输出一堆奇怪数据才反查。另外 PDMS 命令行的报错信息是按行号定位的报错时记住「第几行出错」然后再去看对应行的变量类型和拼写比肉眼扫全篇高效得多。我习惯在每段脚本开头加一个MESSAGE 开始执行: 第X步跑完再删这比在代码里猜哪一步翻车要省时间。3. 对象模型与属性体系从 World 遍历到 PIPE 的路径3.1 层级结构World、Site、Zone、Equipment 与 PIPE 的关系PDMS 数据库不是一张扁平表而是一棵严格分层的树。顶层是 World下面挂 SiteSite 下面挂 ZoneZone 下面才是 Equipment、PIPE、BRANCH 这类实际建模元件。做二次开发时你操作任何元件都要先搞清楚它在树上的位置因为遍历逻辑是从父节点往下递归的。教程里给了这样一张层级对照表层级典型类型作用WorldWORLD整个项目根节点SiteSITE厂区划分ZoneZONE区域划分可挂管道和设备EquipmentEQUIPMENT设备模型PipePIPE管道逻辑对象由分支组成BranchBRANCH管道分支下面才挂管件Component管件/阀门弯头、三通、法兰等这张表告诉你两件事遍历时要从高层往低层递归一般只到 PIPE 和 BRANCH 这一层写脚本时判断类型要用TYPE字符串比较比如判断一个元件是不是管道要检查!!elem.Type PIPE。实际项目里还有个容易踩的细节Equipment 下面不一定直接是模型有时会嵌套 Equipment 再挂 Zone层次不齐。所以教程强调面向真实工程写脚本时别默认层级固定要么用递归遍历要么让用户手动指定根部元件。3.2 用 FOREACH 和属性读取拿下一张图纸上的所有管道属性读取是二次开发里最高频的操作。PML2 访问属性的语法很直接元素.属性名。比如要读管道名称、管径和材料写法如下-- 先定位到一个 Zone 作为根 !!root CE -- 递归不够时先做一层遍历只看直接子级里有没有 PIPE FOREACH !!item IN !!root.Members IF !!item.Type PIPE THEN MESSAGE 发现管道: !!item.Name MESSAGE 管径: !!item.Bore MESSAGE 材料: !!item.Material ENDIF END FOREACH这里三个属性对应 PDMS 里最常用的信息Name是元件名Bore是公称管径Material是材料代码。注意Bore读出来往往是字符串形式的尺寸代码比如DN100或者8具体格式和项目模板有关。教程里特别提醒判断一个属性是否存在不要靠猜先在 PDMS 的元件属性面板里找到对应字段再确认它在 PML 里的属性名很多属性是缩写比如Material在部分版本里是MAT。如果你的根元件是 Zone直接子级里大概率没有 PIPEPIPE可能挂在 Equipment 或者更深的层级之下。这时候要写一个递归遍历函数把整棵子树都扫一遍。把整张图纸的所有管道扫出来的结果是后续导出清单、批量修改的基础教程这一章配了递归函数模板我建议直接抄下来背住后面所有批量处理的脚本都用得上。3.3 属性读写的边界哪些能改哪些改了会翻车读属性容易写属性才是雷区。PML 里给属性赋值的语法看起来很简单!!pipe.USER_DESC 已加急USER_DESC是用户自定义描述字段大多数版本允许直接写。但有些属性不能改改了就报「属性只读」比如Name在某些项目里不可写Bore一旦管道建好就不允许通过脚本改因为 PDMS 会认为你改变了管道设计参数会连带影响分支和管件的匹配关系。教程里给了非常明确的分类可安全写的属性包括USER_DESC、自定义扩展属性、部分标注字段不可写或需要走设计模块操作的属性包括Bore、Spec、材料连接关系。还有一类是「写了但不报错结果却是错的」的属性。比如你给 BRANCH 写了一个不存在的材料代码PDMS 不一定立即报错但后续抽 ISO 图或做应力分析时会拉出完全错误的结果。针对这种情况教程的写法是批量写入前先把候选值用VALIDATE一类的手段检查一遍或者先从模板里GET一个合法值再做赋值。我在实际交付脚本时会在脚本里加一个「先统计后写入」开关默认只输出将要修改的清单人工确认后再真正写入。这不是胆小是给自己留后悔药——批量写坏了设计库恢复的成本远高于多跑一遍脚本。4. 实战批量修改管道标注并导出材料清单的 60 行脚本4.1 目标拆解框选、遍历、归并、输出 CSV到了实战环节教程给了一个非常典型的场景把选中的一批 PIPE 元件的描述字段统一加上「已审核」标记同时把管径、材料、长度导出成 CSV 清单。这个场景几乎是管道类项目二次开发应用案例的标配。拆解下来有四步框选处理范围、遍历范围内所有 PIPE、读取并修改属性、把结果写进文件。先定处理范围。我不会写死成某个固定 Zone而是让用户在 PDMS 里选中根元件脚本从根开始遍历。这样既照顾了全厂统计的需求也允许只处理某条管线。遍历采用递归方式避免因为层级不统一而漏掉 PIPE。归并这一步最容易被忽略一根 PIPE 下面通常有多条 BRANCH如果只统计 PIPE 名称而不把分支数据合并导出的清单会多出几十行重复数据。教程的思路是以 PIPE 为主键把下属分支的长度求和后归并到 PIPE 行。输出格式我选 CSV因为 Excel、WPS 直接能打开不会给配管专业的同事增加学习成本。CSV 列的顺序建议是管道名称、公称管径、材料、总长度、描述。这个顺序对应设计人员审核时的阅读习惯。4.2 完整脚本与关键行说明-- 批量处理 PIPE标注描述并导出材料清单 -- 使用前先选中根元件Zone / Equipment / Pipe 均可 !!root CE !!outPath D:/pdms_export/pipe_list.csv !!header PipeName,Bore,Material,Length,Description CHAR(13) !!body -- 自定义递归函数扫描当前元件下所有 PIPE FUNCTION !!scan(!!elem) FOREACH !!child IN !!elem.Members IF !!child.Type PIPE THEN -- 只在描述字段为空时写入避免重复覆盖 IF !!child.USER_DESC THEN !!child.USER_DESC 已审核 ENDIF -- 收集导出数据长度先取分支累加值没有则写 0 !!totalLen 0.0 FOREACH !!brch IN !!child.Members IF !!brch.Type BRAN THEN !!totalLen !!totalLen !!brch.Length ENDIF END FOREACH !!body !!body !!child.Name , !!child.Bore , !!child.Material , !!totalLen , !!child.USER_DESC CHAR(13) ENDIF -- 递归进入下一层 !!scan(!!child) END FOREACH ENDFUNCTION -- 从根开始递归 !!scan(!!root) -- 写入文件 !!file FileOpen(!!outPath, WRITE) FileWriteLine(!!file, !!header !!body) FileClose(!!file) MESSAGE 处理完成共导出管道清单到: !!outPath这个脚本是教程里最值得反复抄写的模板。逻辑上有三个关键设计。第一递归函数!!scan让脚本不依赖具体层级根是 Zone 还是 Equipment 都能跑通这是应对真实工程数据最稳妥的写法。第二写入描述前先判空保护了已经被人工标注过的记录脚本重复运行时不会把早有审核标记的数据覆盖掉。第三长度信息从 BRANCH 层累加避免直接读 PIPE 属性读到一个总长为 0 的坑。参数上要注意CHAR(13)是 CSV 的换行符字符串拼接全用中间不要混加号。FileOpen的第一个参数路径写死在脚本里是为了演示实际交付时应该改成用TEXTBOT让用户输入路径。FileOpen的模式WRITE是覆盖写如果文件存在会被清空重写这符合「导出一次性清单」的预期但如果要做增量日志就要用APPEND。4.3 跑完怎么验证复查工具与常见误判脚本跑完不代表可以收工。我在教程里反复强调脚本能跑和结果正确是两回事。CSV 导出后至少要核对三件事。第一管道数量是否和期望一致——从 PDMS 的元件树里数一下根下面的 PIPE 数量对比 CSV 行数数量对不上大概率是递归漏层。第二抽几行长度数据在 PDMS 里手工点开对应 PIPE 属性核对长度值不能差太多。第三确认描述字段已经写入再跑一次脚本看判空逻辑是否生效——正常情况下第二次运行时不会重复写入。常见误判有两种。一种是Bore读出来是数字或代码比如DN50但 CSV 里显示成了50这是因为部分 PDMS 模板将 Bore 存成了数值脚本里需要做一次格式化。另一种是总长度为 0反复检查发现是 BRANCH 的Length属性名拼错写成Length而模板里实际叫LENGTHPML 属性名对大小写不敏感但还是建议统一大写核对。教程里配了一张「运行前检查清单」每次交付脚本前照单检查一遍能挡掉八成低级事故。5. PDMS 二次开发避坑手册五个最容易翻车的现场5.1 坑一变量名大小写与命令缓冲区的玄学现象脚本在你自己机器上跑得好好的复制到同事机器上却报「未知命令」报错行指向变量赋值那一行。排查了半天发现是同事把脚本里的!!ProjectName改成了!!projectname然后命令就失效了。原因PML 的命令解析分两段先做命令查表再执行脚本体。变量名的解析在部分版本里对大小写敏感尤其在命令缓冲区和函数名层面MESSAGE写成message都可能不被识别。很多老教程里代码全大写不只是书写习惯而是为了兼容不同版本的解析规则。解决变量名统一用小写字母加数字PML 关键字统一大写函数名统一大写。交付脚本时附带一个说明不要手工改动变量名大小写。另外脚本第一行加一句!!SYS 1这类无关赋值来刷新命令缓冲区的做法是老一辈流传的「玄学」「换行前强制刷新解析状态」在个别版本确实有效但正规做法还是改完脚本后完整重启 PDMS 会话再测试。5.2 坑二list 的拷贝是引用不是副本现象脚本里写!!backup !!pipeList然后对!!backup做删除操作结果发现!!pipeList里的元素也变少了后续循环直接跳过了一部分管道导出清单少了几十行。原因PML 的list类型变量在赋值时传递的是引用不是复制。!!backup !!pipeList只是给同一个列表对象起了个别名两个变量指向同一份内存。这在 C# 里等价于把一个List对象赋给另一个变量而不是ToList()拷贝。解决需要真正副本时手动新建列表再逐项加入。稳妥写法如下!!pipeList list(PIPE-A, PIPE-B) !!backup list() FOREACH !!item IN !!pipeList !!backup !!backup list(!!item) END FOREACH!!backup list(!!item)是把新元素拼接到列表尾部。这样改出来的!!backup才是独立副本改它不会影响原列表。从那以后我只要遇到「要保留原始数据再做过滤」的场景一律用这个方式重建列表绝不再用等号直接拷。5.3 坑三属性写入后界面不刷新现象脚本批量改了USER_DESC跑完后回到 PDMS 图形界面点开管道属性看到的还是旧值或者三维模型上没有显示出标注变化。有人以为脚本写失败又跑一遍结果旧值被覆盖成「已审核已审核已审核」。原因属性写入底层数据库成功但图形界面和属性浏览器的缓存没有刷新。PDMS 不会像 Web 页面那样自动重渲染。部分属性在 Design 模块和 Draft 模块的数据链路不一致Draft 里看到的标注内容来自模型缓存不是实时从数据库读的。解决脚本末尾加一行刷新命令不同模块对应的刷新指令略有不同Design 模块用UPDATE ALLDraft 模块用REFRESH。更稳妥的做法是脚本跑完弹窗提示「属性已写入请手动刷新视图」把决策权交给使用者。批量写入前也要先记录原值清单万一刷新后发现问题还能对照恢复这算是脚本工具的基本职业素养。5.4 坑四PML2 与 PML.NET 混用时的对象类型不一致现象用 PML.NET 写了一段 C# 代码去读取 PML2 传过来的元件对象方法签名接收的是Element实际运行时报类型转换失败或者拿到一个看起来什么方法都没有的对象。原因PML2 的元件对象和 PML.NET 的元件对象是两套类型系统。PML2 脚本里拿到的是 PML 原生句柄PML.NET 里拿到的是 .NET 包装对象两者之间需要显式转换不是自动兼容。很多教程分开讲 PML2 和 PML.NET但没有讲清楚它们在同一进程里怎么协作混用时就容易翻车。解决混用场景下统一在 C# 侧接收object类型参数再通过对岸提供的转换函数转成 .NET 的Element。或者在 PML 侧先做完数据整理把要传给 C# 的裸数据字符串、数字、列表准备好PML.NET 只做文件读写、复杂计算这类 PML 不擅长的事尽量减少对象跨边界传递。教程里给的建议很实在能用 PML2 写完的场景不要强行上 PML.NET引入第二种技术栈就要承担两套调试成本。5.5 坑五脚本在同事机器上跑不起来现象脚本交付给同事双击运行报「文件未找到」或「路径无效」。自己机器上明明用的是同一个安装包路径检查了好几遍没错。原因八成是脚本里写了绝对路径。常见的是D:/pdms_export/这种写死的导出目录或者引用了安装目录下的固定路径不同电脑的 PDMS 安装盘符和目录层级不一样。还有一种情况是用户没有目标目录的写入权限在公司域环境里尤其常见。解决导出的路径用TEXTBOT让使用者每次输入或者用 PDMS 提供的系统变量取安装根目录再拼相对路径。脚本里凡是涉及文件读写的位置统一封装一个获取输出路径的函数避免到处散落绝对路径。目录不存在时先创建目录再写入PDMS 的文件函数不会自动建目录这一步漏了脚本会安静地失败——不报错但文件根本没生成。验证阶段一定换一台干净的机器跑一遍完整流程别只在自己环境里自嗨。6. 把脚本变成团队工具命名规范与自动加载脚本从「自己能跑」到「团队能用」中间就差一个标准化的加载流程。PML 脚本在 PDMS 里默认从个人 PML 目录加载通常路径在安装目录下的PML子目录。你把写好的pipe_export.pml放进去在 PDMS 命令行直接敲!!pipe_export就能运行不带扩展名。这个机制决定了命名规范的重要性文件名就是命令名全小写、不带空格、不跟安装目录里的内置脚本重名。我习惯在每个脚本文件头部加一段固定注释写明功能、入参、输出文件和适合的 PDMS 版本。这样脚本在团队里流传时同事不用翻代码也能知道怎么用。PDMS 启动时会自动加载个人 PML 目录下的初始化脚本把常用工具的调用命令注册成菜单按钮的方式也依赖这个加载入口——在初始化脚本里判断当前模块再挂上对应的按钮调用关系。更进一步的方向是用 PML.NET 把复杂逻辑挪到 C# 里PML 只做界面和调用这样文件读写性能更好也方便写单元测试。教程最后这部分讲的就是这套分层思路。从那以后我每次交付脚本都强制走一遍三件事换一台机器跑通全流程、把路径全部改成运行时可输入、清掉所有调试用MESSAGE。靠这套习惯我交付的脚本很少再收到「跑不了」的反馈。希望帮到你。本文还有配套的精品资源点击获取