版本声明(全系列统一口径)
本篇基线为AVEVA E3D Design 3.1。对象示例GETELEMENTINFORMATION、成员访问与自省方法出自官方培训材料与官方文档原文摘录;PML2 的定位表述出自官方文档(https://help.aveva.com/AVEVA_Everything3D/3.1/PMLPUG/PMLCG2.02.3.html)。
凡本系列按官方已发表写法组合构造、未在本机跑通的代码,均标注**“按官方已发表的写法组合构造,需在本地 E3D Design 环境验证”**;官方材料未给出的语法一律不臆造,只在正文中显式留出待核实标记。
官方把 PML 交互入口称为Command Window(命令窗口):Tools 标签 → Display 组 → Commands。
一句话结论
PML2 的"对象"是写 PML 的人自己定义的数据载体,成员访问一律用点号、把自己叫!this——它和 .NET 的对象不是一回事;而methods()与attributes()这两个自省方法,让你在 Command Window 里就能把一个陌生对象"看穿"。
〇认知问题
- PML2 的对象和 C# 的对象是同一个概念吗?能互相传吗?
define object里的member有类型吗?能声明哪些类型?- 方法体里的
!this到底指谁?和!!CE是什么关系? - 一个对象定义好之后,我怎么知道它有哪些成员和方法?难道要回去翻源码?
- 对象定义要放在什么文件里?
.pmlobj是必须的吗? - 我已经会用 PML1 了,为什么还要学 PML2 对象?什么时候用它更划算?
一机制解析
1.1 PML2 是"为界面而生的面向对象扩展"
先把官方定位抄下来:PML2 是面向对象语言,专为定制 AVEVA 产品的 Forms and Menus 而设计。并且官方说明它与 PML1 的关系是:当前版本常称 PML2,可视为对 PML1 能力的扩展,但**“许多编程任务的做法不同”,而某些任务用 PML1 更高效**(https://help.aveva.com/AVEVA_Everything3D/3.1/PMLPUG/PMLCG2.02.1.html)。同时,PML1 的 expressions package 在数据库手册(DBRM)中单独成章,官方口径是"一般来说这些设施与 PML 2 兼容"(https://help.aveva.com/AVEVA_Everything3D/1.1/DBRM/DBRM9.10.01.html)。
三句话概括这段官方表述的实践含义:
- PML2 不是另一门语言,是同一体系的两代风格;
- PML2 的目标场景是"界面与对象",不是"更快地跑循环";
- 官方明确承认 PML1 在某些任务上更高效——所以"全都改成 PML2"不是进步,是没读懂官方定位。
1.2define object与成员类型
官方培训材料的对象示例(原样摘录):
define object GETELEMENTINFORMATION member .element is DBREF member .name is STRING member .type is STRING member .position is POSITION endobject结构要点:
define object <名字>…endobject:定义一个对象类型。对象名同样处在全局命名空间里,所以第 04 篇讲的"变量名不能与既有对象/全局变量重名"在这里同样适用——定义一个与已存在对象同名的类型,会互相覆盖。member .名字 is 类型:成员变量声明。.名字前面的点号是成员声明语法的一部分(和访问时写的点号是配套的)。- 成员是带类型的:示例中出现的是
DBREF、STRING、POSITION三种。DBREF:数据库引用,即"指向模型里某个元素的引用"。这是二次开发里最重要的成员类型,因为模型数据访问的入口就是它。STRING:字符串。POSITION:位置(坐标)。
- 为什么成员要带类型?因为对象是"数据的容器"。类型声明让你在写方法时能确认"这个成员拿到的是元素引用、不是名字字符串"——这是 PML2 相对 PML1 的实质性收益之一。
文件放哪:按官方培训材料口径,对象放在.pmlobj文件里(与.pmlfnc、.pmlfrm并列),依赖环境变量PMLLIB定位。惯例同样是"对象名即文件名"。
1.3define method与!this
官方同一份示例中的方法部分(原样摘录,注意其结束语句见下方说明):
define method .getinformation() !this.element = !!ce !this.name = !!ce.name !this.type = !!ce.type !this.position = !!ce.position endfunction逐层拆解:
define method .getinformation():方法名以点号开头,表示"这是对象的成员方法"。括号里是参数列表(这里为空)。!this:指"调用这个方法的那个对象实例"。所以!this.element = !!ce的语义是"把当前元素(!!ce)存进我这个对象实例的element成员"。!!ce与!this的关系:!!ce是全局的当前元素——它是"环境状态",不是这个对象的成员。方法体里做的事正是"把环境状态快照进对象"。这是对象化最典型的价值:把"此刻的当前元素"固化成一份可携带的快照,之后无论当前元素怎么变,这份快照都不变。!!ce.name/!!ce.type/!!ce.position:读取当前元素的三类属性。注意这三个属性名是元素的属性,不是你自己对象的成员;写法上与!this.name很像(都是点号),但一个是元素属性、一个是你自己的成员——这是新手最容易混淆的地方。- 关于结束语句的一处必须点明的细节:本篇依据的摘录在这里用的是
endfunction,而官方另一处表单方法示例用的是endmethod。两处原文如此,本系列照录并不做取舍。实践建议:在你本机的 Command Window 里各写一次,确认本环境接受哪个,并在团队代码规范里统一——这属于"看起来很琐碎、实际会导致文件加载失败"的细节。
1.4 自省:methods()与attributes()
自省是 PML2 最实用的能力之一,官方材料给出的两个方法(原样摘录):
!exampleObject.methods() !exampleObject.attributes().methods():列出该对象可用的方法。.attributes():列出该对象的成员(属性)。
这两个方法的价值在于"反向可读":拿到别人写的对象(或者自己半年前写的对象),你不必回去翻源码,直接在 Command Window 里创建实例、调.methods()和.attributes(),就能把它的接口看明白。这在排查"这个方法名到底叫什么"时,比翻文档快得多。
进一步:SCRM 才是元数据的权威来源。当你需要确认"某个对象类型有哪些成员与方法"时,查 SCRM(Software Customisation)的这几节:
- Summary of Objects, Members and Methods
- Methods Available to All Objects
- Forms and Menus Objects
- Summary of Gadget-Specific Methods
- Gadget Syntax Graphs
- Object Type Details
SCRM 里真实存在的对象类型名(节选)包括:ALERT、ARC、ARRAY、BANNER、BAR、BLOCK、BOOLEAN、BORE、BUTTON、CMSYS、COLLECTION、COLUMN、COLUMNFORMAT、COMBOBOX、COMMAND、CONTAINER、DATEFORMAT、DATETIME、DB、DBREF、DBSESS、DIRECTION、EXPRESSION、FILE、FMSYS、FORM、FORMAT、FRAME……
注意观察这份名单的两个"信号":
- 里面有
COLLECTION——集合也是对象类型,这与第 07 篇要讲的数据访问直接相关; - 里面有
COMMAND——官方对"交付入口"的定位就是把功能做成 command object。
另外,SCRM 还给出了"用object关键字创建对象实例"的真实写法(官方示例):!dim = object measure('mass'),该对象具备default()/suspend()/unsuspend()/reinstate()方法。这个例子很值得看一眼,因为它说明:PML2 里很多"对象"是产品内置的,你既能定义自己的对象,也会大量用到内置对象。
1.5 反直觉点:PML2 对象 ≠ .NET 对象
这是本篇最需要扭正的观念。三条边界:
- 不是一个东西。PML2 对象是在 PML 会话里用
define object定义的数据载体,由 PML 解释器管理;.NET 对象由编译后的程序集定义、由公共语言运行时管理。名字都叫"对象",但生命周期、类型系统、可见性规则完全不同。 - 不能随便互相传递。官方明确列出 PML.NET 的限制:不能把
DIRECTION、ORIENTATION等 PML 对象传给 .NET;但数据库引用(DB 引用)可以以数组或字符串形式传递。也就是说,要穿过 PML/NET 的边界,你得用"可序列化的形态"(数组或字符串),而不是直接把 PML 对象递过去。 - 模块切换不保持 .NET 对象。官方限制之一——这意味着"在 A 模块里创建的 .NET 对象,切到 B 模块后不能指望还在"。设计跨模块功能时必须显式重建。
实践结论:把 PML2 对象当作"结构清晰的数据容器 + 方法集合"来用,不要把它当作"能跨语言传的通用对象"。
1.6 什么时候该用 PML2 对象
| 场景 | 建议 | 理由 |
|---|---|---|
| 一次性批量操作 | PML1 宏 | 官方明确"某些任务用 PML1 更高效",宏无需定义类型 |
| 需要把多个属性绑成一份"记录" | PML2 对象 | member+ 类型声明让数据有形状,避免一堆散变量 |
| 需要把同一份逻辑作用在很多元素上 | PML2 对象 | 方法可复用,!this提供稳定上下文 |
| 要做表单界面 | PML2 | 官方定位:PML2 为 Forms and Menus 设计;窗体本身就是全局变量表示的对象 |
| 要对接 .NET | PML.NET(不是 PML2 对象) | 跨边界只能传数组/字符串等可传递形态 |
| 单纯做数值计算 | PML1 函数(.pmlfnc) | 函数有类型化签名,比对象更轻 |
一条判断口诀:“要不要一份带形状的数据”——要,就用 PML2 对象;只是"跑一段过程"——用 PML1。
二完整代码与逐行剖析
2.1 官方对象示例完整复刻
-- 文件:GETELEMENTINFORMATION.pmlobj -- 来源:官方培训材料对象示例(原样复刻) -- 说明:结束语句按原文照录为 endfunction;官方另一处表单方法示例用的是 endmethod, -- 请在本机确认本环境接受哪个,并统一团队规范。 define object GETELEMENTINFORMATION member .element is DBREF -- 指向模型元素的数据库引用 member .name is STRING -- 元素名 member .type is STRING -- 元素类型 member .position is POSITION -- 元素位置 endobject define method .getinformation() !this.element = !!ce -- 把"当前元素"存进本实例(快照) !this.name = !!ce.name -- 读元素名 !this.type = !!ce.type -- 读元素类型 !this.position = !!ce.position -- 读元素位置 endfunction2.2 使用与逐行剖析
在 Command Window 里使用它(创建实例 → 调方法 → 读成员):
-- 第 1 步:创建对象实例(对象名处在全局命名空间) !info = object GETELEMENTINFORMATION() -- 第 2 步:把当前元素的信息快照进来 !info.getinformation() -- 第 3 步:读出快照(此时与 !!ce 已经无关,改选别的元素也不影响它) q var !info.name q var !info.type q var !info.position逐行剖析:
!info = object GETELEMENTINFORMATION():用object <对象名>()的形式创建实例。这与官方 SCRM 里!dim = object measure('mass')是同一种构造写法(后者是内置对象)。该构造写法源自官方示例,需在本地 E3D Design 环境验证你的自定义对象是否同样适用。!info.getinformation():调用方法。方法名前是实例变量、方法名后带括号——!info.getinformation(),而不是getinformation(!info)。这是 PML2 与 PML1 在调用风格上的最大区别之一。q var !info.name/!info.type/!info.position:读成员。成员访问一律点号,且可与q var组合做调试输出。- 验证"快照"语义:先跑上面三步,然后在模型里把当前元素切到别的元素,再重复执行第 3 步的三行——如果输出的仍是原元素的信息,就证明方法体里的赋值确实是"拷贝到了本实例",而不是"存了个引用到当前元素"。
2.3 自省演示(官方写法)
-- 自省:把这个对象"看穿" !info.methods() -- 列出可用方法(官方写法) !info.attributes() -- 列出成员/属性(官方写法)预期用途:.methods()会列出你定义的方法名(如getinformation),.attributes()会列出element、name、type、position四个成员。注意:具体输出格式与是否包含继承来的方法,本篇依据的材料没有说明,请以本机实际输出为准并记录——这正是自省的价值:让"我猜"变成"我看到"。
2.4 一个实用对象:元素类型校验器(按官方已发表的写法组合构造,需在本地 E3D Design 环境验证)
这个对象解决一个真实问题:在批量脚本前先确认"当前元素是不是我期望的类型",避免在错误的元素上执行写操作。
-- 文件:CETYPEWATCH.pmlobj -- 说明:只使用官方示例里出现过的成员类型(DBREF / STRING / POSITION)与方法写法, -- 属"按官方已发表的写法组合构造",需在本地 E3D Design 环境验证。 -- 用途:抓取当前元素的类型快照,并输出一条人可读的结论。 define object CETYPEWATCH member .snapref is DBREF -- 快照时的元素引用 member .snapname is STRING -- 快照时的元素名 member .snaptype is STRING -- 快照时的元素类型 endobject define method .capture() !this.snapref = !!ce !this.snapname = !!ce.name !this.snaptype = !!ce.type endfunction define method .report() P --- CETYPEWATCH report --- P element name : $!this.snapname P element type : $!this.snaptype endfunction逐行剖析:
- 三个成员都是"快照字段":一个
DBREF(保留引用)+ 两个STRING(人可读信息)。为什么不只存STRING?因为DBREF是后续操作的入口——要真正操作这个元素(而不只是打印名字),必须持有引用。 !this.snapref = !!ce:与官方示例完全同构的一行,把当前元素存进实例。define method .report():第二个方法。注意到这里已经出现两个方法——这正是对象相对"散变量"的收益:数据和作用于数据的逻辑被绑在一起。P element name : $!this.snapname:把成员值注入输出文本。$!this.snapname用的是第 03 篇讲过的变量注入机制。这里的!this.成员与$!变量的组合写法是按已知机制组合的,需在本机验证;若你的环境不接受这种写法,退回到"先赋值给局部变量再注入"的两步写法(!t = !this.snaptype然后P element type : $!t),这一点同样需要本机确认。- 报告方法里不要写
q var:q var更适合调试时看原始值,而写成"键值对"形式便于日志检索与人工核对。批量作业的日志规范应当统一成这种形式。
三常见报错与排查
症状一:对象定义之后,创建实例时报"找不到对象"。
- 现象:
!x = object MYOBJECT()报错。 - 原因:
.pmlobj文件所在目录不在PMLLIB里,或者文件没被加载;也可能对象名与已有对象重名导致覆盖。 - 处置:用第 02 篇的
PMLLIB探针确认目录已覆盖;检查对象名是否与既有对象/全局变量重名;确认文件的扩展名为.pmlobj。
症状二:方法定义处直接报语法错。
- 现象:
define method ...块加载失败。 - 原因:结束语句不一致。官方材料的两处示例分别用了
endfunction与endmethod,不同环境/版本可能只接受其中一种。 - 处置:在 Command Window 里用最小样例各试一次,确定本环境接受的写法,并在团队规范里统一;同时确认方法名以点号开头。
症状三:!this.xxx读出来是空的。
- 现象:
!info.name为空,但元素明明有名字。 - 原因:没有先调用
getinformation()(或capture())。成员初始为空,只有调了"抓取方法"才有值。这是"数据容器"语义的自然结果,不是 bug。 - 处置:在建实例的下一步立刻调抓取方法;养成"先快照、后使用"的固定顺序。
症状四:把 PML 对象往 C# 里传,报类型不匹配。
- 现象:PML.NET 调用时参数转换失败。
- 原因:官方限制明确——不能把
DIRECTION、ORIENTATION等 PML 对象传给 .NET;跨边界要使用可传递形态。 - 处置:数据库引用可以用数组或字符串形式传递,改用这种方式;同时记住模块切换不保持 .NET 对象,跨模块功能必须显式重建对象。
四动手练习
练习 1(20 分钟):把 2.1 的对象与 2.2 的三步使用流程跑通。合格判定:q var !info.name打印出的名字与调用时刻的当前元素一致;随后把当前元素切成另一个元素,再重复第 3 步,输出不变(证明快照语义成立)。
练习 2(15 分钟):在 Command Window 里对!info执行.methods()与.attributes()。合格判定:能列出至少 1 个方法名与 4 个成员名;把本机的实际输出格式抄下来(这一条很重要,因为官方材料没有给出输出格式)。
练习 3(20 分钟):找出本环境接受的"方法结束语句"。合格判定:分别用endfunction与endmethod定义一个最小方法,各加载一次,记录哪一次成功、哪一次报什么错,并写出一行团队规范建议。
练习 4(25 分钟):把 2.4 的CETYPEWATCH落地,并加一条"类型不符就警告"的逻辑:用IF (!this.snaptype eq '...') THEN的结构(类型标识请用你自己模型里真实存在的类型)判断。合格判定:在两种不同类型的元素上各跑一次,报告内容不同,且不一致时能看到明确提示。
五小结与下一篇预告
本篇钉住四点:PML2 是面向对象的扩展,专为 Forms and Menus 设计,官方承认有些任务 PML1 更高效;对象用define object定义、成员用member .x is 类型声明(示例中出现的是DBREF/STRING/POSITION)、方法用define method定义、方法体里!this指对象实例;成员访问一律点号,methods()/attributes()是现成的自省入口,SCRM 是元数据权威来源;PML2 对象与 .NET 对象不是一回事,跨边界只能传数组或字符串等可传递形态。
下一篇《表单开发(.pmlfrm)》进入 PML2 的主场:setup form与formTitle、gadget 作为窗体对象的成员、点号访问与.val取值、回调其实是"文本字符串形式的 PML 表达式/函数/方法调用"、FMSYS/CMSYS的作用与布局要点——并揭穿两条硬限制:gadget 不支持用户自定义成员变量或方法,以及窗体名不能与既有全局变量重名。
文末 FAQ
Q1:PML2 的对象和 C# 的对象是同一个概念吗?能互相传吗?
不是同一个概念。PML2 的对象是用define object在 PML 里定义的数据容器,由 PML 解释器管理;C# 的对象由编译后的 .NET 程序集定义、由运行时管理,两者的类型系统与生命周期规则不同。互传也受限:官方明确 PML.NET 的限制之一是不能把DIRECTION、ORIENTATION等 PML 对象传给 .NET,而数据库引用可以以数组或字符串形式传递;官方另一条限制是模块切换不保持 .NET 对象。所以跨语言时要传"可传递形态"(数组、字符串、DB 引用),而不是直接把 PML 对象递过去。
Q2:define object里的member有类型吗?能声明哪些类型?
有类型,写法是member .名字 is 类型。官方培训材料的对象示例中出现的是三种:DBREF(数据库引用,指向模型元素的引用,最常用于数据访问)、STRING(字符串)、POSITION(位置/坐标)。成员带类型是 PML2 相对 PML1 的实质性收益之一:它让"这个成员里装的是元素引用还是名字字符串"变成显式约定。需要声明更多类型时,请到 SCRM 的Object Type Details与Summary of Objects, Members and Methods两节查找本环境支持的类型名。
Q3:方法体里的!this到底指谁?和!!CE是什么关系?!this指"调用这个方法的那个对象实例",而!!ce是全局的当前元素,属于环境状态、不是对象成员。官方示例里二者的配合正是对象化的典型用法:!this.element = !!ce把"此刻的当前元素"存进对象实例,形成一份快照——之后当前元素怎么变,这份快照都不受影响。!this.name与!!ce.name写法上都是点号,但前者是读你自己的成员、后者是读元素的属性,这是最容易混淆的一处,务必分清。
Q4:一个对象定义好之后,我怎么知道它有哪些成员和方法?难道要回去翻源码?
不必翻源码,PML2 提供自省:官方材料给出!exampleObject.methods()与!exampleObject.attributes()两个写法,前者列出可用方法、后者列出成员属性。要查"某个对象类型本来该有哪些成员和方法",权威来源是 SCRM(Software Customisation),它的相关章节为 Summary of Objects, Members and Methods、Methods Available to All Objects、Forms and Menus Objects、Summary of Gadget-Specific Methods、Gadget Syntax Graphs、Object Type Details。另注意:客观输出格式官方材料没有给出,请以本机实际输出为准并记录下来。
Q5:对象定义要放在什么文件里?.pmlobj是必须的吗?
按官方培训材料口径,PML 的三类文件是.pmlfnc(函数)、.pmlfrm(表单)、.pmlobj(对象),它们依赖环境变量PMLLIB来定位。所以对象应放在PMLLIB覆盖的目录下、以.pmlobj为扩展名,并建议"对象名即文件名"。还有一条容易忽略的约束:对象名处在全局命名空间里,与既有对象、全局变量(PML2 里窗体就是全局变量)重名会互相覆盖,所以命名前先确认没有冲突。
Q6:我已经会用 PML1 了,为什么还要学 PML2 对象?什么时候用它更划算?
因为官方对二者的定位不同:PML2 是面向对象的扩展,专为定制 AVEVA 产品的 Forms and Menus 设计,而官方同时承认**“某些任务用 PML1 更高效”**。划算的边界大致是这样:需要"一份有形状的数据"(多个属性绑成一条记录、需要跨步骤携带)时用 PML2 对象,比如把"元素引用 + 名字 + 类型 + 位置"绑成一个快照对象;需要做表单界面时必须用 PML2,因为窗体本身就是用全局变量表示的对象;而纯粹的一次性批量操作、单纯的数值计算,用 PML1 宏或.pmlfnc函数更轻。判断口诀:要一份带形状的数据 → PML2 对象;只跑一段过程 → PML1。