PDMS-AVEVA-E3D二次开发教程(07):表单与交互——PML Forms Menus 从零到可用
PDMS-AVEVA-E3D二次开发教程07表单与交互——PML Forms Menus 从零到可用版本与事实声明本篇的对象模型、约束、回调与生命周期机制全部取自官方 Software Customisation Guidehelp.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中——本节凡涉及具体成员名均指向该手册而非自行拟定。示例位号、数值为示例性数据不代表任何标准规定。一句话结论表单是一类对象、用全局变量命名如!!EntryFormgadget 是表单的成员并用点号访问如!!EntryForm.TextField、取值!!EntryForm.TextField.valgadget 不支持用户自定义成员变量与方法因此业务状态只能放在表单变量或函数局部变量里界面层必须与逻辑层分离。〇、认知问题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 与中文界面官方明确FM 默认的系统字体是Arial Unicode MS它覆盖世界上大量字母表内部使用完整 Unicode但只能显示当前系统字体可访问的字符并且可以在 FM 的文本字段中复制粘贴 Unicode 字符。工程结论做中文界面表单是官方支持的但有两条注意不要在界面上用生僻字/自造符号内部是 Unicode但显示受系统字体限制界面文案的编码表单定义文件本身的编码要与产品读取方式匹配官方 “PMLs Character Format” 一节专讲字符格式。这是我见过的表单里中文变乱码的头号原因——不是 Unicode 问题是文件编码问题。1.8 范围选择器官方有现成 gadget 类型第 05、06 篇反复强调范围必须先裁剪到管理单元。手工填元素名很痛苦——官方 Gadget Set 章节里有一类Database Selector Gadgets数据库选择器 gadget正是为选择数据库元素设计的。另外官方还给出了一批实用 gadget 相关机制写表单时按需查需要的能力官方章节gadget 的通用成员与方法Some Generic Gadget Members and MethodsSOFTCG20.21.03置灰/不可用De-activating Gadgets: Greying OutSOFTCG20.21.07显示/隐藏Making Gadgets Visible and InvisibleSOFTCG20.21.09刷新Refreshing GadgetsSOFTCG20.21.12文本框的编辑控制与输入校验Controlling Text Gadgets’ Editing / Validating Input to Text FieldsSOFTCG21.22.38、21.22.42快速填充列表 / 选择器 / 文本区Fast Access to Lists, Selectors and Textpanes using DO LoopsSOFTCG21.22.46表单被引用的检查Checking References to Other FormsSOFTCG23.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 AppWindows 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 上规格表正是用来强制分流这些内容的。#界面元素成员名官方章节作用回调函数状态放哪1DbSelectorDatabase Selector GadgetsSOFTCG21.22.36选范围元素无取值即可表单变量2RuleSetFieldText Gadgets / Validating Input to Text FieldsSOFTCG21.22.37/42填规则集名无表单变量3RunBtnButton GadgetsSOFTCG21.22.16触发校验!!OnRunLint()—4ResultPaneTextPane GadgetsSOFTCG21.22.44显示结果无—5ProgressInfoFMSYS Progress and Interrupt MethodsSOFTCG23.24.05长任务进度由运行回调驱动表单变量6表单本体!!PipingLintFormDefining a Form / Form AttributesSOFTCG16.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” 两节专讲字符格式FM 内部使用完整 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 anyreturn |Failure: …|并把失败原因用!!alert或结果 TextPane 显示出来。这是在 PML 界面开发里唯一可行的调试纪律。报错 3-4表单打开时提示找不到某个成员 / 取值返回空。现象!!表单名.成员名取不到值。根因按概率排序① 成员名在表单定义文件与代码里不一致拼错或改名后未同步② 表单尚未加载/已被销毁官方 “Loading, Showing, and Hiding Forms”、“Killing Forms” 两节③ 表单名与其它对象/全局变量撞名官方明确表单不能与任何其它对象类型、全局变量或表单同名。解法核对成员名逐字一致用官方 “Querying Form Members” 一节的方法查询表单实际成员清单检查是否有另一个同名表单存在或被误加载。报错 3-5长任务执行时界面像卡死用户强杀进程。现象脚本中途死亡留下半完成状态。根因没有挂进度与中断机制。解法接官方 FMSYS 的 Progress and Interrupt MethodsSOFTCG23.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.valgadget 不支持自定义成员变量与方法——这条硬约束决定了 PML 界面开发必须分层界面层只取值与显示、适配层回调函数做翻译与编排、业务层纯函数做逻辑。回调是文本字符串所以回调里只写一行调用逻辑放函数里。生命周期上Quit/Close 与 KILLING 必须挂现场保护与审计。长任务必须接 FMSYS 的进度与中断机制并且要假定用户随时会强杀。做中文界面是官方支持的乱码的真因通常是文件编码而不是 Unicode。下一篇《从脚本到产品功能PML 函数库、命令注册与 Add-in》把前三篇积累的函数与表单从我的目录升级成团队的产品功能——讲清函数文件的存放与加载、共享库的目录与命名约定、官方 Commands 机制定义命令、注册、挂载到界面、销毁与 PML Add-in随模块加载、初始化、菜单与工具条挂载、会话间存数据。本篇认知问题回显FAQQ1表单和 gadget 在 PML 里是什么A两者都是对象。表单在 PML 2 中是一类对象由一个全局变量表示即表单名如!!EntryForm它拥有预定义成员变量与内置方法用户还可定义自己的表单变量、gadget 与表单方法gadget 是表单的用户自定义成员通过点号记法访问如!!EntryForm.TextField。因此编程动作是操作对象及其成员而不是操纵控件。Q2表单名为什么必须是全局变量名且不能重名A官方明确表单是一类由全局变量表示的对象且表单不能与任何其它对象类型、任何其它全局变量或任何其它表单同名。三条后果表单名占用全局命名空间所以团队必须有统一前缀表单名是对外接口因此不能临时起名一个表单同时只能存在一份需要多份并存时应把多份数据放在表单变量里而不是复制表单。Q3gadget 不支持用户自定义成员变量与方法意味着什么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 的进度与中断方法。