版本声明(全系列统一口径)
本篇基线为AVEVA E3D Design 3.1。表单示例、回调机制表述与 gadget 限制均出自官方文档与官方培训材料的原文摘录(https://help.aveva.com/AVEVA_Everything3D/1.1/SOFTCG/SOFTCG14.14.1.html、https://help.aveva.com/AVEVA_Everything3D/3.1/PMLPUG/PMLCG14.14.1.html)。
凡本系列按官方已发表写法组合构造、未在本机跑通的代码,均标注**“按官方已发表的写法组合构造,需在本地 E3D Design 环境验证”;官方材料未给出确切语法的部分(如文本类 gadget 的声明形式、FMSYS/CMSYS的成员清单)一律不臆造,正文给出待核实标记与应查章节。
官方把 PML 交互入口称为Command Window(命令窗口):Tools 标签 → Display 组 → Commands。另需注意:官方不建议从标签页按钮直接调用 PML 宏**,界面入口应做成 command object +.uic。
一句话结论
表单在 PML2 里本身就是对象、用全局变量表示,所以"窗体名不能与既有全局变量重名"不是风格建议而是语法约束;而回调不是函数指针,是一段写进字符串里的 PML 代码——你写在竖线里什么,运行时就执行什么。
〇认知问题
- 为什么官方说"窗体名不能与其他对象/全局变量重名"?一个名字为什么会撞车?
setup form !!nameCE里的!!是必须的吗?为什么窗体不加!!就不对?- gadget 是控件还是变量?我在代码里怎么拿到一个按钮或文本框?
- 回调(callback)到底是什么?我能不能在里面写一整段脚本?
FMSYS和CMSYS是什么?为什么我在文档目录里能看到它们两个独立成章,却找不到用法说明?- 我能给 gadget 加自己的成员变量吗?能把某段状态挂到按钮上吗?
一机制解析
1.1 窗体就是对象,而且必须是全局变量
这是理解 PML2 表单的第一块地基,官方文档的原文说得非常直接:
在 PML2 中,窗体是一种由全局变量表示的对象——窗体的名字,例如
!!EntryForm……Gadget 对象是窗体对象的用户自定义成员。窗体的成员始终使用点号访问……文本框取值写作!!EntryForm.TextField.val。Gadgets 不支持用户自定义成员变量或方法。
这段话里包含了本节的三个关键结论:
- 窗体名就是全局变量名。所以
setup form !!nameCE里的!!不是装饰——它就是"这个窗体以哪个全局变量存在"的声明。这也直接解释了官方那条限制:窗体名不能与其他对象、全局变量或窗体重名,因为名字在全局命名空间里只有一份,撞名就是互相覆盖。 - gadget 是窗体的成员,不是独立对象。
- 成员访问一律用点号,这一点和第 05 篇讲的 PML2 成员访问规则完全一致。
1.2setup form与formTitle
官方培训材料的表单示例(原样摘录):
setup form !!nameCE !this.formTitle = |Name CE| button .button |Print Name Of CE| call |!this.print()| exit define method .print() q var !!ce.flnn endmethod从这两段能提炼出表单文件的基本骨架:
setup form !!<窗体名>:声明窗体。窗体名用全局变量形式书写。!this.formTitle = |Name CE|:设置窗体标题。!this指窗体的这个实例(与第 05 篇的对象方法语义一致);|Name CE|是 PML 的字符串定界写法(竖线包裹)——在表单里竖线是主力字符串定界符,因为它不容易和内部引号冲突。button .button |Print Name Of CE| call |!this.print()| exit:定义一个按钮 gadget。三个部分分别是:gadget 声明(button .成员名)、显示文字(|Print Name Of CE|)、行为(call |...| exit)。define method .print():为窗体对象定义方法,方法体里q var !!ce.flnn输出当前元素的信息(flnn属性名出自官方示例)。注意方法定义写在setup form块之外——它们是"给这个窗体对象挂方法",不是窗体声明的一部分。
1.3 gadget 是成员:点号访问与.val
把 1.1 的官方表述翻译成可操作规则:
- 声明:在
setup form块内用button .成员名 ...这类形式声明,.成员名就是它在窗体对象里的成员名。 - 访问:任何时候都写
!!窗体名.成员名,例如!!EntryForm.TextField;取值则再点一层.val——官方给出的写法就是!!EntryForm.TextField.val。
为什么.val这一层是必要的?因为 gadget 成员本身是"控件对象",而.val才是"控件里当前的值"。这与第 05 篇的"成员是数据容器"是同一套逻辑:对象 ≠ 值。
gadget 类型从哪里查?官方目录里有两处必看:
- PMLCG:Form Concepts、Form and Gadget Callbacks、Forms、Form Layout、Frames、Gadgets and their Attributes、FMSYS/CMSYS Object等章;
- SCRM:Summary of Gadget-Specific Methods、Gadget Syntax Graphs、Forms and Menus Objects、Object Type Details。
SCRM 的对象类型名单里真实存在的相关类型有:FORM、FRAME、BUTTON、COMBOBOX、CONTAINER、COLUMN、COLUMNFORMAT、BANNER、BAR、BLOCK、DATEFORMAT、DATETIME、FORMAT、FMSYS、CMSYS、FILE等。要声明某种 gadget,请以 SCRM 的 Gadget Syntax Graphs 给出的语法图表为准——本篇只对已核实的button写法给出可运行代码。
1.4 回调就是一段字符串(本篇核心机制)
官方原文对回调的表述是:
回调以文本字符串的形式提供,可以是任何合法的 PML 表达式、PML 函数或方法调用,包括任何窗体、gadget 或用户定义对象的方法。
这句话要拆成三点来用:
- “以文本字符串形式提供”:所以在表单里写回调用的是竖线定界符
call |...|。竖线里的内容不会被当作"编译好的函数",它首先是一段文本。 - “可以是任何合法的 PML 表达式”:这意味着你可以在回调里直接写表达式,而不必新定义一个函数。例如把一个简单计算直接写进回调。
- “包括任何窗体、gadget 或用户定义对象的方法”:这意味着回调的三种常见形态是——
- 表达式形态:
call |<某段 PML 表达式>|; - 窗体/gadget/用户对象的方法调用形态:
call |!this.print()|(官方示例就是这样); - 函数调用形态:调用
.pmlfnc里的函数。
- 表达式形态:
三个直接后果(也是三个坑):
- 回调里的写法不受"编译期"检查。字写错不会在加载表单时报错,而是点击时才报。这与第 04 篇"PML 错误延迟报出"的机制叠加之后更隐蔽:点错了按钮、错误又刚好被
handle接住,你会以为"这个按钮没用"。 - 回调字符串里不要塞长逻辑。官方允许你写"任何合法表达式",但把五十行逻辑塞进一个竖线里,等于放弃了可读性与可调试性。推荐做法:回调只负责"调一个方法",逻辑放进窗体方法或
.pmlfnc函数里——官方示例call |!this.print()| exit正是这个风格。 - 回调是字符串,所以它可以被拼接——这也意味着要小心注入问题(第 03 篇讲的分词风险在这里同样适用)。
顺便把exit说清:官方示例里按钮回调末尾跟了exit,它在表单语境下表示"这个按钮行为结束后关闭窗体"。把它当作 gadget 行为的一部分来读,而不是当作 PML 的语言关键字——具体语义请以 PMLCG 的 Form and Gadget Callbacks 章为准。
1.5FMSYS/CMSYS:知道它们存在,也知道该去哪查
官方目录里有一个独立章节叫FMSYS/CMSYS Object,并且FMSYS、CMSYS在 SCRM 的对象类型名单中都是正式的一等对象类型。这说明它们是窗体系统里的正式对象,不是别名或内部细节。
但必须诚实地说:本篇依据的材料没有给它们的作用描述与成员清单。因此:
- 不要根据名字去推断它们的功能;
- 要到 PMLCG 的FMSYS/CMSYS Object章与 SCRM 的Object Type Details中查确切成员与方法;
- 在学会它们之前,用"窗体方法 + 回调"这套已核实的机制就能完成绝大多数表单工具。
布局同理。PMLCG 有独立的Form Layout章,SCRM 有Gadget Syntax Graphs。布局参数(行列、间距、对齐)的确切语法请以这两处为准;本篇的可运行代码刻意只用"一行一个 gadget"的最简结构,避免给出看起来漂亮、其实靠猜的布局参数。
1.6 两条硬限制(务必先记住)
- Gadget 不支持用户自定义成员变量或方法。官方原文明确。这条限制的实践含义是:你不能把状态挂在按钮上(比如"我在按钮上存一个计数器")。正确的做法是——状态挂在窗体对象上(窗体是完整的对象,可以有成员和方法),gadget 只负责"触发"。
- 窗体名不能与其他对象、全局变量或窗体重名。因为窗体本身是全局变量。实践含义:窗体命名要有项目前缀,并且在定义之前先确认该名字没被占用。
另外提醒一条与本主题相邻的官方态度:PMLCG 的 Commands 章明确,命令可以做成为PML command object或core command object,通过.uic文件关联到界面,并且官方不建议从标签页按钮直接调用 PML 宏(https://help.aveva.com/AVEVA_Everything3D/3.1/PMLPUG/PMLCG24.27.01.html)。所以表单也不是"挂到主界面"的正规途径——它是"内部工具窗口",不是 E3D 主界面的定制方式,后者要走Command+ UIC。
二完整代码与逐行剖析
2.1 官方表单示例完整复刻
-- 文件:namECE.pmlfrm -- 来源:官方培训材料表单示例(原样复刻) -- 说明:窗体名 !!nameCE 是全局变量;方法 .print() 定义在 setup form 块之外。 setup form !!nameCE !this.formTitle = |Name CE| button .button |Print Name Of CE| call |!this.print()| exit define method .print() q var !!ce.flnn endmethod逐行剖析:
setup form !!nameCE:声明窗体。名字前必须带!!,因为窗体以全局变量形式存在。!this.formTitle = |Name CE|:设置标题。!this指窗体实例;|...|是字符串定界。button .button |Print Name Of CE| call |!this.print()| exit:这三段的分工是"声明成员名.button→ 按钮上的文字 → 行为"。- 成员名
.button是你在代码里的唯一抓手:之后要拿这个按钮,写!!nameCE.button。 - 回调
call |!this.print()|是方法调用形态——官方推荐风格:回调只调方法,逻辑不写在回调里。 - 末尾
exit是"行为结束后关闭窗体"。
- 成员名
define method .print()/q var !!ce.flnn/endmethod:方法体只做了"输出当前元素的flnn"。注意这里用的是endmethod——与第 05 篇官方对象示例里出现的endfunction不同(那处原文如此)。本篇照录,不做取舍;请在本机确认后统一规范。
2.2 三种回调形态(按官方回调定义构造,需在本地 E3D Design 环境验证)
官方说回调"可以是任何合法的 PML 表达式、PML 函数或方法调用",据此可以给出三种形态的对照。下表写法是按官方回调定义构造的,需在本地 E3D Design 环境验证。
-- 文件:callbackDemo.pmlfrm -- 说明:演示回调的三种形态;三种写法都源自官方"回调=文本字符串、可为表达式/函数/方法调用"的定义, -- 具体在本环境的可用性需自行验证。 setup form !!cbDemo !this.formTitle = |Callback Demo| -- 形态一:方法调用(官方示例风格,推荐) button .btnMethod |Call a form method| call |!this.showIt()| exit -- 形态二:函数调用(调用 PMLLIB 里 .pmlfnc 中的函数) button .btnFunc |Call a PML function| call |!this.showTag()| exit -- 形态三:表达式(把简单逻辑直接写进回调;长逻辑请勿这样写) button .btnExpr |Evaluate an expression| call |!this.n + 1| exit define member .n is REAL -- ← 窗体对象自己的成员(注意:是窗体成员,不是 gadget 成员) define member .tag is STRING define method .init() !this.n = 0 !this.tag = 'CB-DEMO' endmethod define method .showIt() P form method called. endmethod define method .showTag() P form tag = $!this.tag endmethod逐行剖析与重要警示:
- 三种 gadget 声明都用了已验证的
button写法,区别只在回调内容:方法调用 / 函数调用 / 表达式。 define member .n is REAL这一处必须特别说明:它意在演示"状态挂在窗体上"。但官方材料在本系列依据的摘录范围内,只明确给出了define object … member .x is 类型这种"对象成员"写法,并没有给出窗体对象上声明自定义成员的语法。因此上面define member .n is REAL属于结构示意,声明语法待核实——请到 PMLCG 的Forms章与 SCRM 的Forms and Menus Objects节确认窗体成员的正确声明方式后再使用。- 同理,
define method .init()里对!this.n、!this.tag的赋值,也只有当上一条的成员声明成立时才有效。在没有确认之前,请把"窗体状态"改为放在你自己的 PML2 对象里(用第 05 篇已验证的define object+member写法),让窗体只负责调用——这条路完全走在已核实的语法上。 P form tag = $!this.tag:把成员值注入输出文本。若本环境不接受$!this.成员组合,退回两步写法:先!t = !this.tag再P form tag = $!t。- 关于 “gadget 不能有自定义成员/方法” 的对照理解:上面所有成员与方法都挂在窗体上,没有一条挂在按钮上——这正是官方那条限制逼出来的写法。
2.3 实用表单:当前元素信息卡(只用已核实语法,需在本地 E3D Design 环境验证)
下面这个表单刻意不使用任何待核实语法:gadget 只用button,状态全部走"调用 → 立即读取 → 立即输出",不依赖窗体自定义成员。
-- 文件:ceCard.pmlfrm -- 用途:把当前元素的关键信息打印到 Command Window(只读,不改模型) -- 说明:gadget 只用 button(已验证写法);不在 gadget 上挂任何状态 setup form !!ceCard !this.formTitle = |CE Info Card| button .btnName |Print CE name| call |!this.printName()| button .btnType |Print CE type| call |!this.printType()| button .btnPos |Print CE position| call |!this.printPos()| button .btnAll |Print all and exit| call |!this.printAll()| exit define method .printName() q var !!ce.name endmethod define method .printType() q var !!ce.type endmethod define method .printPos() q var !!ce.position endmethod define method .printAll() P --- CE Info Card --- q var !!ce.name q var !!ce.type q var !!ce.position q var !!ce.flnn endmethod逐行剖析:
setup form !!ceCard:窗体名用了ceCard(6 字符,符合 ≤16 字符的命名限制)。- 四个
button声明:每个按钮对应一个"读一个属性"的方法。这样拆分的理由是——回调里只有方法调用,没有任何逻辑,出错时你能立刻定位"是哪个按钮坏了"。 !this.printName()这类方法名统一用print*前缀,便于在.methods()自省输出里一眼认出本表单自己的方法。- 方法体里全部用
q var输出。q var是调试友好形式:它把属性值与属性名一起打出来,便于人工核对。批量脚本里建议改成"键值对"形式(P element name : $!x),但表单调试阶段用q var更快。 - 结束语句统一用
endmethod:与 2.1 的官方表单示例一致(官方对象示例那处原文用的是endfunction)。请先在本机确认接受哪一种,再统一到团队规范里;同一批文件里混用两种写法是隐患。 btnAll的按钮回调末尾带exit,表示"打印完就关闭窗体"——这是官方示例里exit用法的直接沿用。- 模型的当前元素变了怎么办?这个表单是"即时读取"型:每次点按钮都重新读一次
!!ce,所以你可以在 E3D 里切换当前元素后继续点按钮。这是"不做快照"的有意设计——需要快照时,请用第 05 篇的CETYPEWATCH对象,两者互补。
2.4 文本框取值:官方给出的写法与待核实部分
官方原文明确给出的取值形式是:
-- 官方原文中的写法:文本框取值用 .val 取到"控件里当前的值" -- !!EntryForm.TextField.val -- ^窗体 ^成员 ^值 -- 待核实:TextField 这类 gadget 在 setup form 块内的"声明语法", -- 本篇依据的材料未给出 —— 请查 SCRM 的 Gadget Syntax Graphs -- 与 PMLCG 的 Gadgets and their Attributes 章后再补。怎么用这段信息:
- 取值方式是确定的:
!!窗体名.成员名.val。 - 声明方式未在本篇材料中确认:请到 SCRM 的Gadget Syntax Graphs找到该类 gadget 的语法图,按其语法补出声明行。
- 在确认之前,不要凭"猜一个像 TEXT 的关键字"去写——这是本系列最坚决的一条纪律:宁可留空,不给假语法。
三常见报错与排查
症状一:表单加载失败,报"名称已存在"或行为异常。
- 现象:
setup form !!xxx之后行为错乱,或旧的窗体/变量被覆盖。 - 原因:窗体名与既有对象、全局变量或窗体重名。官方明确:窗体本身就是由全局变量表示的对象,名字在全局命名空间里唯一。
- 处置:给窗体名加项目前缀(如
mtoCard、supTag),并在定义前用 Command Window 确认该名字未被占用。
症状二:按钮点了没有任何反应,也不报错。
- 现象:窗体显示正常,点击无效。
- 原因:回调字符串里的写法有问题(回调是文本字符串,加载时不检查,只在点击时执行并报错);或错误被匹配的
handle接住了(第 04 篇的延迟报错机制)。 - 处置:先在 Command Window 里手工把回调里的那段代码敲一遍,确认它本身可执行;再把回调简化为"仅调用一个方法"(
call |!this.xxx()|),把逻辑全部移进方法体;方法体内每一步加P输出以便定位。
症状三:想给按钮挂一个计数器 / 状态,报错说不支持。
- 现象:试图在 gadget 上声明自定义成员或方法,失败。
- 原因:官方原文明确——Gadgets 不支持用户自定义成员变量或方法。
- 处置:状态挂到窗体对象上(窗体是完整对象,可以有成员与方法;窗体成员的声明语法请以 PMLCG 的 Forms 章为准),或更稳妥地挂到你自己的 PML2 对象上(
define object+member,第 05 篇已验证)。gadget 只做"触发"。
症状四:把表单当成主界面定制,结果发现"挂不上去"。
- 现象:想把表单入口放进 E3D 主界面菜单/工具栏。
- 原因:表单是"内部工具窗口",不是 E3D 主界面的定制机制。官方对界面定制的口径是command object +
.uic文件,并不建议从标签页按钮直接调用 PML 宏。 - 处置:把功能实现为 command object(PML command object 或 core command object),用
.uic关联;表单作为被命令打开的辅助窗口。
四动手练习
练习 1(20 分钟):把 2.1 的官方示例跑通。合格判定:窗体标题显示为Name CE;按钮文字为Print Name Of CE;点击后 Command Window 出现当前元素的flnn输出,且窗体关闭。
练习 2(20 分钟):把 2.1 改成"两个按钮"版本(一个打印name、一个打印type),回调必须是方法调用形态。合格判定:两个按钮各自只输出对应属性,互不串台;且你能说清每个按钮的成员名(!!窗体名.成员名)与它绑定的方法名。
练习 3(15 分钟):验证"gadget 不能有自定义成员"这条限制。合格判定:能给出一次真实的报错原文(尝试给按钮加成员/方法),并结合窗体对象给出正确的替代写法(状态挂窗体或挂自定义对象)。
练习 4(25 分钟):到 SCRM 的Gadget Syntax Graphs与 PMLCG 的Gadgets and their Attributes章里,找出文本输入类 gadget 的声明语法,把 2.4 的待核实部分补全,写出一个"输入一个字符串 → 点按钮 → 输出该字符串"的最小表单。合格判定:!!窗体名.成员名.val能取到你输入的内容;把这次找到的声明语法记进团队笔记(这是本篇留给你的"作业型交付物")。
五小结与下一篇预告
本篇的核心是三条:窗体是对象、且以全局变量表示,所以窗体名不能与既有对象/全局变量/窗体重名;!this.formTitle设标题,gadget 是窗体成员、用点号访问、用.val取值;回调以文本字符串形式提供,可以是任何合法的 PML 表达式、函数或方法调用——加载时不检查、点击时才执行;gadget 不支持用户自定义成员变量或方法,状态要挂在窗体或自定义对象上。同时记住了两条官方态度:表单不是主界面定制途径,主界面要走 command object +.uic;FMSYS/CMSYS与布局语法的确切内容,要查 PMLCG 的 FMSYS/CMSYS Object 章与 Form Layout 章。
下一篇《数据访问入门:!!CE、DBREF与集合遍历》正式进入模型数据:!!ce的真实含义、元素属性读取、VAR … COLL ALL … FOR CE、DO … VALUE、MATCHW通配筛选、SIZE()与下标访问,并用一段 AVEVA Community 的真实宏(支吊架批量复制与重命名)做完整剖析——同时纠正一个高频误解:PML 的 collection 与界面上的 “My Collections” 不是一回事。
文末 FAQ
Q1:为什么官方说"窗体名不能与其他对象/全局变量重名"?一个名字为什么会撞车?
因为在 PML2 里窗体本身就是一种由全局变量表示的对象——官方举例说明窗体名字形如!!EntryForm。也就是说,窗体名不是"给界面起的一个标签",而是占用全局命名空间的标识符。全局命名空间里名字唯一,所以与既有对象、全局变量或窗体重名,就是互相覆盖。实践建议:窗体名加项目前缀,并在定义前确认名字未被占用。
Q2:setup form !!nameCE里的!!是必须的吗?为什么窗体不加!!就不对?
是必须的(按官方对窗体是全局变量表示的对象这一表述)。!!是 PML 的全局变量标记,setup form !!nameCE的语义是"这个窗体以名为nameCE的全局变量存在"。如果写成局部变量形式,就无法表达"窗体是一个在会话内独立存在的对象"这层含义。这也和官方那条限制一脉相承:正因为它是全局变量,才不能与别的全局变量/对象/窗体重名。
Q3:gadget 是控件还是变量?我在代码里怎么拿到一个按钮或文本框?
gadget 是窗体对象的成员——官方原文说 “Gadget objects are user-defined members of the form object”,并且"窗体的成员始终使用点号访问"。所以拿到按钮靠的是!!窗体名.按钮的成员名(成员名就是你在setup form块内声明时写的那个.成员名)。要取值(例如文本框里的内容)再点一层.val,官方给出的写法是!!EntryForm.TextField.val:窗体 → 成员 → 值。gadget 类型的完整清单与声明语法请查 SCRM 的 Gadget Syntax Graphs 与 PMLCG 的 Gadgets and their Attributes。
Q4:回调(callback)到底是什么?我能不能在里面写一整段脚本?
回调是以文本字符串形式提供的代码片段。官方原文的表述是:回调以文本字符串形式提供,可以是任何合法的 PML 表达式、PML 函数或方法调用,包括任何窗体、gadget 或用户定义对象的方法。所以技术上你确实能把较长的表达式写进回调里,但不要这么做,原因有两个:一是回调在表单加载时不检查,写错只在点击时才报错,长逻辑很难定位;二是与"PML 错误延迟报出"的机制叠加后,错误可能被handle接住而不显示,你会误以为按钮失效。推荐做法是回调只写方法调用(官方示例就是call |!this.print()|),逻辑放进方法或.pmlfnc函数。
Q5:FMSYS和CMSYS是什么?为什么我在文档目录里能看到它们两个独立成章,却找不到用法说明?
官方 PMLCG 的目录里有一个独立章节FMSYS/CMSYS Object,并且FMSYS、CMSYS在 SCRM 的对象类型名单里都是正式的一等对象类型——这说明它们是窗体系统里的正式对象。但本篇依据的材料没有给出它们的作用描述与成员清单,所以本篇不写"它们大概是干什么的"这类推测。正确的做法是到 PMLCG 的FMSYS/CMSYS Object章与 SCRM 的Object Type Details节查确切成员与方法;而在学会它们之前,用"窗体方法 + 回调"这套已核实的机制足以完成绝大多数内部工具表单。
Q6:我能给 gadget 加自己的成员变量吗?能把某段状态挂到按钮上吗?
不能。官方原文明确:Gadgets 不支持用户自定义成员变量或方法。所以"在按钮上挂一个计数器"这类写法行不通。替代方案有两条:一是把状态挂在窗体对象上——窗体是完整对象,可以有成员与方法(窗体自定义成员的声明语法请以 PMLCG 的 Forms 章为准);更稳妥的是第二条,把状态放进你自己的 PML2 对象里(用define object+member .x is 类型定义),让窗体与 gadget 只负责"触发"。记住这条分工原则:gadget 触发,对象存状态。