PDMS-AVEVA-E3D二次开发教程(07):表单与交互——PML Forms & Menus 从零到可用
版本与事实声明
- 本篇的对象模型、约束、回调与生命周期机制全部取自官方 Software Customisation Guide(help.aveva.com)的 Form Concepts、Form and Gadget Callbacks、Forms、Gadget Set、Form Layout、FMSYS Object 各章,术语保留官方原文。
- 表单定义的语言级语法(
Defining a Form、Form Definition File、Gadget Definition Commands等章节的内容)本篇不给臆造写法:给出的是"结构规格表 + 官方章节对照",请按你所用版本的手册填写。所有代码块均为回调函数库(define function语法已在官方 IFC 帮助文档中出现,可运行、不臆造)。- 官方明确:表单与 gadget 的全部内置成员与方法列在Software Customisation Reference Manual中——本节凡涉及具体成员名,均指向该手册而非自行拟定。
- 示例位号、数值为示例性数据,不代表任何标准规定。
一句话结论:表单是一类对象、用全局变量命名(如!!EntryForm),gadget 是表单的成员并用点号访问(如!!EntryForm.TextField、取值!!EntryForm.TextField.val);gadget 不支持用户自定义成员变量与方法,因此业务状态只能放在表单变量或函数局部变量里,界面层必须与逻辑层分离。
〇、认知问题
- Q1:"表单(form)"和"gadget"在 PML 里到底是什么?是控件还是对象?这个区别为什么重要?
- Q2:为什么表单的名字必须是一个全局变量名?为什么它不能与其它对象、全局变量或另一个表单重名?
- Q3:官方那句"gadget 不支持用户自定义成员变量或用户自定义 gadget 方法"意味着什么?它会怎样改变你的代码结构?
- Q4:回调(callback)到底是什么?为什么官方说"回调决定了表单的智能"?
- Q5:表单的生命周期里有哪些必设的回调?为什么长耗时操作必须配合进度与中断机制?
一、机制解析
1.1 表单不是控件,是对象
官方 Form Concepts 一节的表述非常明确,值得逐字拆:
- PML 2 中,表单是一种对象,由一个全局变量表示——也就是表单的名字(例如
!!EntryForm); - 表单对象拥有一组预定义成员变量与内置方法;在此之外,用户可以定义自己的成员(表单变量与表单 gadget)以及自己的表单方法;
- 表单成员一律用点号记法访问(原文 “Form members are always accessed using the dot notation”),例如
!!EntryForm.TextField。
为什么这对你重要:如果你带着"表单就是一堆控件"的直觉来写代码,你会本能地想去"操纵控件";但 PML 的模型是"操作一个对象及其成员"。这个心智差异直接解释了后面所有别扭之处——比如为什么 gadget 值要通过!!EntryForm.TextField.val这样"成员的点号路径"访问,而不是像很多 GUI 框架那样有独立的状态句柄。
1.2 命名约束:一处约束,三个后果
官方原文:表单不能与任何其它对象类型、任何其它全局变量或任何其它表单同名。
推导出三条工程后果:
- 表单名必须进命名规范:表单名占的是全局命名空间(第 06 篇讲过属性名与类型名共享全局命名空间,表单名又与全局变量共享)。团队里必须有统一前缀,否则一定会撞名——而撞名的表现往往是"表单显示不出来"或"打开的是另一个表单"。
- 表单不能"临时起名":既然名字是全局标识符,它就成了对外接口。别用
!!temp这类名字,因为你不知道何时会有别人也起这个名。 - 表单名是不可重入的标识:一个表单同时只能有一份(它就是一个全局变量)。所以表单不是"可以多开的窗口"。需要"多份并存"的时候,要另想数据结构(例如把多份数据放在表单变量的数组里)。
1.3 那条决定性硬约束:gadget 没有自定义成员
官方原文:“Note that gadgets do not support user-defined member variables or user-defined gadget methods.”
这是 PML Forms & Menus 里最容易被忽视、后果最严重的一条限制。它意味着:
| 你不能做的事 | 后果 | 正确做法 |
|---|---|---|
| 给某个 gadget 挂一个"它自己的状态变量" | 无法在界面上保存"这个控件的额外信息" | 状态放在表单变量(官方 “Form Variables: PML Variables within a Form” 一节)或函数局部变量 |
| 给某个 gadget 加一个辅助方法(如"校验我的输入") | 不能把校验逻辑绑在控件上 | 校验写成表单方法或 PML 函数,由回调调用 |
| 期望"每个控件自治" | 控件之间无法互相协调 | 由一个"控制器"(表单方法或回调函数)统一编排 |
架构结论(本篇的核心主张):
┌──────────────────────────────────────────────────────┐ │ 界面层(Form + Gadgets) │ │ · 只做两件事:取值、显示 │ │ · 状态放在表单变量里 │ │ · 没有任何业务逻辑 │ ├──────────────────────────────────────────────────────┤ │ 适配层(回调函数库 / 表单方法) │ │ · 把 gadget 的原始值翻译成业务参数 │ │ · 调业务层、把结果写回显示 gadget │ │ · 处理进度、中断、异常 │ ├──────────────────────────────────────────────────────┤ │ 业务层(纯 PML 函数,无界面依赖) │ │ · 第 06 篇的校验器、第 05 篇的遍历、第 11 篇的检查 │ │ · 输入是元素引用与规则表,输出是文本契约 │ └──────────────────────────────────────────────────────┘为什么必须这样分层:因为 gadget 不能自治,界面代码注定是"哑的";而哑的界面代码无法被复用。把逻辑推到业务层,好处是——同一套业务函数既能被表单调用,也能被批处理调用(第 09 篇),也能被命令行调用。反过来,把逻辑写在回调里,就等于把工具锁死在"必须有人点按钮"这一种用法上。
1.4 回调:文本字符串才是"智能"所在
官方原文:回调(callbacks)是为表单及其 gadget 指定的用户自定义动作,在操作者与表单交互时执行;回调以文本字符串形式提供,可以是任何合法的 PML 表达式、PML 函数或方法调用,包括任何表单、gadget 或自定义对象的方法。并且官方下了一个很重的判断:“Effectively, these callbacks determine the intelligence of the form.”
三件事要抓住:
- 回调是"文本":这意味着回调可以被代码生成。这是做"配置驱动的界面"的技术前提(例如把回调函数名放进配置文件)。
- 回调可以是表达式,也可以是函数/方法调用:官方为此分了 “Callbacks: Expressions” 与 “Callbacks: Form Methods / PML Functions” 两节。最佳实践:回调里只写"一行调用",把逻辑放在被调用的函数里——理由是回调作为文本字符串,不可断点、不好调试。
- 回调决定表单的智能:所以设计表单时的真正工作是"设计回调图"(哪个动作触发哪个函数),而不是"摆控件"。
回调和"开放回调"还有一层:官方另有 “PML Open Callbacks” 与 “Objects That Can Have Open Callbacks” 两节,说明某些对象本身可以挂开放回调——即"不是被点击触发,而是被某个事件触发"。第 11 篇的规则执行、第 15 篇的数据对接都可能落在这类机制上(具体可挂载的对象类型以官方SOFTCG15.15.07一节为准)。另外官方还专门有 “Undo/Redo Support for Callbacks” 一节——说明回调里的操作需要考虑撤销/重做语义,这是有副作用界面的必备功课(与铁律 4 呼应)。
1.5 生命周期:五个必须知道的时点
官方 Forms 章节给出了一串表单级回调,按生命周期排序:
| 时点 | 官方回调 | 典型用途 |
|---|---|---|
| 初始化 | Form Initialisation Callback(另有 FIRSTSHOWN callback) | 填默认值、按当前范围预填、禁用不该用的按钮 |
| 确认 / 取消 | Form OK and CANCEL Callbacks | 校验输入、提交或丢弃 |
| 退出 / 关闭 | Quit/Close Callback | 释放资源、还原现场(与!!ce保护呼应) |
| 销毁 | KILLING callback | 清理临时文件、写审计日志 |
配套的显示与消失控制官方也有专节:Loading, Showing, and Hiding Forms、Hiding Forms、Killing Forms、NOQUIT Form Status、Position of Forms on the Screen、Form Opacity。
最佳实践:把"还原现场"挂在 Quit/Close 与 KILLING 上。理由:用户点 X 关闭表单是最容易绕过业务代码的路径;如果现场保护只写在"运行"按钮的回调里,用户"打开表单 → 手动改了点东西 → 直接关闭"就会留下脏状态。
1.6 长耗时操作:进度与中断
官方 FMSYS Object 章节里有两项直接决定工具体验的能力:Progress and Interrupt Methods(进度与中断方法)与Writing to the AppWindow’s Status Field(写应用窗口状态栏)。
为什么这是刚需:第 06 篇的校验器、第 11 篇的碰撞检查、第 17 篇的批量检查,在真实项目上都会跑几十秒到几分钟。没有进度反馈的界面会被人当成"卡死了"——然后用户强杀进程,你的脚本就死在半路(第 09 篇的幂等重跑就是为这种情况准备的)。
官方 FMSYS 目录里还有两项值得注意:Checking References to Other Forms(检查对其他表单的引用)——这是判断"我的表单依赖的另一个表单还在不在"的官方手段;LoadForm() Method、CurrentDocument() Method、Setting the Default Format Object for Text Fields(给文本字段设默认格式对象,对数值输入尤其重要)。
1.7 Unicode 与中文界面
官方明确:F&M 默认的"系统字体"是Arial Unicode MS,它覆盖世界上大量字母表;内部使用完整 Unicode,但只能显示当前系统字体可访问的字符;并且可以在 F&M 的文本字段中复制粘贴 Unicode 字符。
工程结论:做中文界面表单是官方支持的,但有两条注意:
- 不要在界面上用生僻字/自造符号:内部是 Unicode,但显示受系统字体限制;
- 界面文案的编码:表单定义文件本身的编码要与产品读取方式匹配(官方 “PMLs Character Format” 一节专讲字符格式)。这是我见过的"表单里中文变乱码"的头号原因——不是 Unicode 问题,是文件编码问题。
1.8 范围选择器:官方有现成 gadget 类型
第 05、06 篇反复强调"范围必须先裁剪到管理单元"。手工填元素名很痛苦——官方 Gadget Set 章节里有一类Database Selector Gadgets(数据库选择器 gadget),正是为"选择数据库元素"设计的。
另外,官方还给出了一批实用 gadget 相关机制,写表单时按需查:
| 需要的能力 | 官方章节 |
|---|---|
| gadget 的通用成员与方法 | Some Generic Gadget Members and Methods(SOFTCG20.21.03) |
| 置灰/不可用 | De-activating Gadgets: Greying Out(SOFTCG20.21.07) |
| 显示/隐藏 | Making Gadgets Visible and Invisible(SOFTCG20.21.09) |
| 刷新 | Refreshing Gadgets(SOFTCG20.21.12) |
| 文本框的编辑控制与输入校验 | Controlling Text Gadgets’ Editing / Validating Input to Text Fields(SOFTCG21.22.38、21.22.42) |
| 快速填充列表 / 选择器 / 文本区 | Fast Access to Lists, Selectors and Textpanes using DO Loops(SOFTCG21.22.46) |
| 表单被引用的检查 | Checking References to Other Forms(SOFTCG23.24.08) |
经验法则:先查官方 gadget 目录,再决定要不要自己造。PML 的 gadget 种类相当齐全(按钮、开关、单选、下拉、滑块、数值输入、列表、多列表、文本、文本区、视图、帧、容器…),绝大多数"我想要个某某控件"的需求官方都已经有了。
二、完整代码与逐行剖析
以下三段都是回调函数库(业务逻辑层与适配层的实现),语法与第 03~06 篇一致,可运行。表单定义本身请按官方 “Form Definition File” / “Gadget Definition Commands” 两节与现实项目模板填写。
代码 7-1:业务层——与界面完全解耦的校验内核(PML)
-- 文件:lint_core.pmlf(业务层:不 import 任何表单/gadget,可被表单、命令行、批处理共用) -- 用途:在第 06 篇 attr_lint 的基础上,把"范围"与"规则"变成显式参数 -- 设计要点:界面层只负责把 gadget 值翻译成这两个参数 define function !!LintCore( !scopeRef is DBREF, !ruleSetName is STRING ) is STRING !start = !!ce -- 输入契约校验:范围必须是有效引用,规则集名必须非空 if ( BADREF( !scopeRef ) ) then return |Failure: scope DBREF is invalid| endif if ( !ruleSetName eq '' ) then return |Failure: rule set name is empty| endif -- 范围描述(用于审计,见第 06 篇 1.5 节第 3 条) !!ce = !scopeRef !scopeName = name !scopeType = type !nTotal = 0 !nOK = 0 !nPartial = 0 !nFail = 0 handle any !!ce = !start return |Failure: unexpected error in LintCore| endhandle var !items coll all for $!it do !it values !items !!ce = !it.dbref() !nTotal = !nTotal + 1 -- 此处调用第 06 篇的属性检查逻辑(示例中简化为一条:名称不得为空) !nm = name if ( !nm eq '' ) then !nFail = !nFail + 1 write( |FAIL | & !it.ATTRIBUTE('REFNO').String() & | : name is empty| ) else !nOK = !nOK + 1 endif enddo !!ce = !start return |Success: ruleset=| & !ruleSetName & | scope=| & !scopeName & |(| & !scopeType & |)| & | total=| & !nTotal.String() & | ok=| & !nOK.String() & | partial=| & !nPartial.String() & | fail=| & !nFail.String() endfunction逐行剖析:
- 函数签名不含任何界面概念:两个参数分别是"元素引用"和"规则集名"。这就是 1.3 节三层架构里"业务层"的形态——它不知道表单存在,所以能同时服务于表单、命令行和批处理。
!scopeRef is DBREF:把范围显式化(第 06 篇 3-4 号报错的解法)。界面层从 Database Selector Gadget 取到用户选中的元素后,只需把它的 DBREF 传进来。!ruleSetName:规则集名是"给人看"的标识,会出现在返回文本里。经验法则:任何"可配置的东西"都应该有一个名字并出现在结果里——这样同一份报告能说清"是按哪套规则跑的"。!scopeName = name与!scopeType = type:把范围描述进返回文本。审计需求在这里落地。!it.ATTRIBUTE('REFNO'):在日志里用引用号标识问题元素。为什么不用名称?因为名称可能为空(本函数检查的正是这个)——用一个可能为空的字段去标识"这个字段为空"的元素,日志会变成一堆空字符串。这是一个非常真实的坑:选标识字段时,永远选"业务上允许为空字段之外"的那个。handle any与两个出口的现场还原:与第 05、06 篇一致。
代码 7-2:适配层——从 gadget 取值、调业务层、回写显示(PML)
-- 文件:lint_adapter.pmlf(适配层:把界面与业务层粘起来) -- 设计要点: -- * 回调里只写一行"调用本函数",逻辑全在这里(回调作为文本字符串不好调试) -- * 所有 gadget 取值集中在第一段:便于对照表单规格表核对 -- * 结果写回显示 gadget 前先判断 gadget 是否可用 define function !!OnRunLint() is STRING -- 1) 取范围:来自数据库选择器 gadget(官方 Database Selector Gadgets 一节) -- gadget 成员用点号访问;表单是全局对象,形如 !!PipingLintForm !scope = !!PipingLintForm.DbSelector.val -- 2) 取其他输入:文本字段的取值写法为 <表单>.<gadget>.val !ruleSet = !!PipingLintForm.RuleSetField.val -- 3) 参数契约:界面不做业务判断,只做"必填"这种最低限度检查 if ( !ruleSet eq '' ) then -- 输入提示用官方 Alert 对象(官方 Alert Objects 一章) !!alert.message( |Rule set is required| ) return |Failure: rule set empty| endif -- 4) 调用业务层(唯一的业务入口) !result = !!LintCore( !scope, !ruleSet ) -- 5) 回写显示:文本区 gadget 显示结果;先确保结果非空 !!PipingLintForm.ResultPane.val = !result -- 6) 用状态栏反馈(官方 FMSYS: Writing to the AppWindow's Status Field) -- 具体调用写法以官方 FMSYS Object 一章为准 return !result endfunction逐行剖析:
!!PipingLintForm.DbSelector.val:这里同时演示了官方两条机制——表单是全局变量(!!PipingLintForm)、gadget 是表单成员用点号访问、取值用.val(官方原文给出的文本字段取值形态是!!EntryForm.TextField.val)。注意.DbSelector与.RuleSetField是你在表单定义里起的成员名,必须与表单定义文件里的名字逐字一致(这也是撞名/拼错故障的高发区)。!!alert.message( … ):官方 Alert 对象(官方 IFC 示例中真实使用过!!alert.message( !exporter.getExportStatus( ) ))。它是最省事的用户反馈手段——比"改一个标签的文字"更直接。- 参数契约只做"必填"检查:
!ruleSet eq ''。界面层的检查要克制——界面只拦"根本没法开始"的情况(比如必填为空),业务规则一律交给业务层。这是分层架构的边界纪律:界面层判断越少越好,否则同一条规则会在两处各写一遍,且迟早不一致。 !!PipingLintForm.ResultPane.val = !result:把结果写回文本区。最佳实践:显示结果用"文本区(TextPane)“而不是"标签”,因为结果是多行文本;官方还有 “Fixed Width Font” 一节,做等宽显示时用得上(结果里对齐的表格用等宽字体才好看)。- 返回值仍返回
!result:适配层也要返回文本契约——这样如果将来有人从命令行调!!OnRunLint()(不打开表单),行为是可预期的(当然此时 gadget 取值会失败,所以真正的生产代码应在开头加"表单是否已加载"的检查,官方 “Checking References to Other Forms” 与 “Querying Form Members” 两节是这条检查的官方依据)。
代码 7-3:表单结构规格(对照官方章节填空,非可运行代码)
这一段不是代码,而是一张"表单结构规格表"。写表单定义前先把这张表填完,比直接开写要快得多——因为 1.3 节的硬约束已经决定了"哪些东西不能放在 gadget 上",规格表正是用来强制分流这些内容的。
| # | 界面元素(成员名) | 官方章节 | 作用 | 回调函数 | 状态放哪 |
|---|---|---|---|---|---|
| 1 | DbSelector | Database Selector Gadgets(SOFTCG21.22.36) | 选范围元素 | 无(取值即可) | 表单变量 |
| 2 | RuleSetField | Text Gadgets / Validating Input to Text Fields(SOFTCG21.22.37/42) | 填规则集名 | 无 | 表单变量 |
| 3 | RunBtn | Button Gadgets(SOFTCG21.22.16) | 触发校验 | !!OnRunLint() | — |
| 4 | ResultPane | TextPane Gadgets(SOFTCG21.22.44) | 显示结果 | 无 | — |
| 5 | ProgressInfo | FMSYS Progress and Interrupt Methods(SOFTCG23.24.05) | 长任务进度 | 由运行回调驱动 | 表单变量 |
| 6 | 表单本体!!PipingLintForm | Defining a Form / Form Attributes(SOFTCG16.16.05/06) | 容器与生命周期 | 初始化 / OK / CANCEL / Quit / KILLING | 表单变量集中于此 |
逐项剖析:
- 第 1、2、5 行的"状态放哪"列全填"表单变量":这就是 1.3 节硬约束的直接落地——gadget 上没有地方放状态,只能放在表单变量里(官方 “Form Variables: PML Variables within a Form” 一节)。
- 第 3 行的回调只填一个函数名:体现 1.4 节"回调里只写一行调用"。不要在回调里写多行逻辑。
- 第 5 行标注"由运行回调驱动":进度不是控件自己的行为,而是运行回调在循环里更新的。这正是"gadget 不自治"的又一体现。
- 第 6 行的生命周期回调列了五个时点:对应 1.5 节的表。其中 Quit/Close 与 KILLING 两个时点必须挂"还原现场 + 写审计日志",理由是用户点 X 关窗是最容易绕过业务代码的路径。
- 注意成员名的拼写必须与代码逐字一致:官方强调表单成员用点号访问;PML 大小写无关(第 03 篇),但成员名在表单定义与代码里不一致时会直接找不到成员。团队规范里应当规定"成员名一经确定不得改名"(改名等于改接口)。
三、常见报错与排查
报错 3-1:表单里的中文显示为乱码或方框。
现象:界面文字异常。根因有两类(按概率排序):①表单定义文件的编码问题——官方有 “PMLs Character Format” 与 “Textual File Handling” 两节专讲字符格式;F&M 内部使用完整 Unicode,但显示只能呈现当前系统字体(默认 Arial Unicode MS)可访问的字符;② 使用了系统中不存在的生僻字或自造符号。解法:先确认文件编码与产品读取方式匹配(这是绝大多数"乱码"的真因),再把界面文案换成常用字。
报错 3-2:想给某个 gadget 加一个自己的校验方法或状态变量,写不出来。
现象:无法在 gadget 上扩展。根因:这不是你的问题,是官方设计——“gadgets do not support user-defined member variables or user-defined gadget methods”。解法:把状态放到表单变量,把校验写成表单方法或独立 PML 函数,由回调调用。并据此调整架构(1.3 节的三层图),而不是试图绕过限制。
报错 3-3:回调里的代码报错,但完全看不出错在哪一行。
现象:点击按钮弹出一条看不懂的错误。根因:回调是文本字符串,可读性差、无法断点调试;此外回调的执行上下文(当前元素等)可能不是你以为的状态。解法:回调里只保留一行函数调用;全部逻辑写进函数文件,在函数里做参数契约校验与错误处理(handle any+return |Failure: …|),并把失败原因用!!alert或结果 TextPane 显示出来。这是在 PML 界面开发里唯一可行的调试纪律。
报错 3-4:表单打开时提示找不到某个成员 / 取值返回空。
现象:!!表单名.成员名取不到值。根因(按概率排序):① 成员名在表单定义文件与代码里不一致(拼错或改名后未同步);② 表单尚未加载/已被销毁(官方 “Loading, Showing, and Hiding Forms”、“Killing Forms” 两节);③ 表单名与其它对象/全局变量撞名(官方明确表单不能与任何其它对象类型、全局变量或表单同名)。解法:核对成员名逐字一致;用官方 “Querying Form Members” 一节的方法查询表单实际成员清单;检查是否有另一个同名表单存在或被误加载。
报错 3-5:长任务执行时界面像卡死,用户强杀进程。
现象:脚本中途死亡,留下半完成状态。根因:没有挂进度与中断机制。解法:接官方 FMSYS 的 Progress and Interrupt Methods(SOFTCG23.24.05)与状态栏写入(SOFTCG23.24.06);同时把任务设计成幂等可重跑(第 09 篇)——因为无论进度做得再好,总会有用户强杀。
四、动手练习
- 练习 1(规格先行):把代码 7-3 的规格表复制成你自己的版本,为"给班组用的一个工具"填满六行(可替换成你的实际工具)。判定标准:“状态放哪"列中,除结果显示类 gadget 外全部填"表单变量”;回调函数列每行都必须是函数名,不能是一段逻辑描述;每行的官方章节编号都真实可打开。
- 练习 2(分层验证):把代码 7-1 的
!!LintCore放在不打开任何表单的情况下从命令行直接调用(传入一个范围内元素的 DBREF 与一个规则集名)。判定标准:调用成功并返回结构化文本——这证明业务层确实与界面解耦;若调用失败,说明你的业务层里混进了 gadget 取值,需要剥离。 - 练习 3(回调纪律):为
RunBtn绑定回调,回调文本只允许是!!OnRunLint()一行。判定标准:点击按钮后结果出现在结果文本区;把业务层故意制造一个错误(例如传一个非法范围),观察界面显示的是可读的失败原因文本而不是产品级错误弹窗。 - 练习 4(现场保护挂钩):在表单的 Quit/Close 与 KILLING 回调里挂上"还原现场 + 写一行审计日志"。判定标准:做完以下动作后
!!ce与打开表单前一致——① 选中一个范围、点运行、然后点 X 关窗;② 选中一个范围、什么都不点直接关窗;③ 运行中途按中断、再关窗。三次都要验证(用第 05 篇代码 5-3 的自查序列)。 - 思考题(无标准答案):官方为什么选择"gadget 不支持自定义成员与方法"这种设计,而不是让每个控件都能自治?验证要点:① 这种限制把状态强制收拢到哪里,带来了什么好处(提示:调试与序列化);② 它对"表单能否被代码生成"有何影响(回调是文本字符串这一点可以一起想);③ 你的团队如果要写第二个表单,能复用第一个表单的多少代码——如果复用率很低,说明分层没做到位。
五、小结与下一篇预告
表单是对象、用全局变量命名、成员用点号访问;gadget 是表单成员、取值形如!!EntryForm.TextField.val;gadget 不支持自定义成员变量与方法——这条硬约束决定了 PML 界面开发必须分层:界面层只取值与显示、适配层(回调函数)做翻译与编排、业务层(纯函数)做逻辑。回调是文本字符串,所以回调里只写一行调用,逻辑放函数里。生命周期上,Quit/Close 与 KILLING 必须挂现场保护与审计。长任务必须接 FMSYS 的进度与中断机制,并且要假定"用户随时会强杀"。做中文界面是官方支持的,乱码的真因通常是文件编码而不是 Unicode。
下一篇《从脚本到产品功能:PML 函数库、命令注册与 Add-in》:把前三篇积累的函数与表单从"我的目录"升级成"团队的产品功能"——讲清函数文件的存放与加载、共享库的目录与命名约定、官方 Commands 机制(定义命令、注册、挂载到界面、销毁)与 PML Add-in(随模块加载、初始化、菜单与工具条挂载、会话间存数据)。
本篇认知问题回显(FAQ)
Q1:表单和 gadget 在 PML 里是什么?
A:两者都是对象。表单在 PML 2 中是一类对象,由一个全局变量表示(即表单名,如!!EntryForm),它拥有预定义成员变量与内置方法,用户还可定义自己的表单变量、gadget 与表单方法;gadget 是表单的用户自定义成员,通过点号记法访问(如!!EntryForm.TextField)。因此编程动作是"操作对象及其成员",而不是"操纵控件"。
Q2:表单名为什么必须是全局变量名且不能重名?
A:官方明确表单是一类由全局变量表示的对象,且表单不能与任何其它对象类型、任何其它全局变量或任何其它表单同名。三条后果:表单名占用全局命名空间所以团队必须有统一前缀;表单名是对外接口因此不能临时起名;一个表单同时只能存在一份,需要多份并存时应把多份数据放在表单变量里而不是复制表单。
Q3:"gadget 不支持用户自定义成员变量与方法"意味着什么?
A:意味着无法给控件挂自身状态或辅助方法。官方原文为 “gadgets do not support user-defined member variables or user-defined gadget methods”。因此业务状态只能放在表单变量或函数局部变量里,校验等逻辑必须写成表单方法或独立 PML 函数由回调调用,最终架构必然是界面层只取值与显示、适配层做编排、业务层做逻辑。
Q4:回调是什么?为什么说它决定表单的智能?
A:回调是为表单及其 gadget 指定的用户自定义动作,在操作者交互时执行;官方说明它以文本字符串形式提供,可以是任何合法的 PML 表达式、PML 函数或方法调用,并称"回调决定了表单的智能"。因为回调是文本,所以它可以被代码生成(便于做配置驱动界面);但也因为它是文本,不便调试,因此实践纪律是回调里只写一行函数调用,逻辑放进被调用的函数。
Q5:表单生命周期里哪些回调必设?
A:至少五处:初始化(Form Initialisation Callback,另有 FIRSTSHOWN)、确认与取消(Form OK and CANCEL Callbacks)、退出关闭(Quit/Close Callback)、销毁(KILLING callback)。其中 Quit/Close 与 KILLING 是必设的,因为用户直接点 X 关窗是最容易绕过业务代码的路径,应在此挂上"还原当前元素等会话状态"与写审计日志。长耗时操作还需配合官方 FMSYS 的进度与中断方法。