从 UMG 过来:名字是一样的
结构不同,词汇刻意相同——Resources/UMGParity 里每个 UMG 类一张对照表,adopt / map / reject 三种结论,自动化测试从两个方向拿它对照 UMG 的反射,以及由反射打印出来的 Docs/Reference。
DreamGUI 和 UMG 的结构不同(盒子优先、锚点加枢轴、widget 是 UObject,见Widget 与层级),
但词汇刻意保持一致。一个控件用的是它 UMG 对应物的名字,含义也是那边的含义:
ScrollWidgetIntoView接受目标位置和边距;- 滚动框有
bFrontPadScrolling和WheelScrollMultiplier; - 数值框有
MinSliderValue和ClearMaxValue; - 面板槽有
Nudge和bForceNewLine; - 每个 widget 都有
SetIsEnabled、SetRenderShear和FlowDirectionPreference。
而且细节面板能拧的每一个旋钮,Blueprint 在运行时都能拧,通过一个会把变化推出去的 setter——而不是写一个 之后再没人读的字段。
对照表
名字保不住的地方,是写下来的,而不是留给人去发现。Resources/UMGParity/ 里每个 UMG 类一张 JSON 表,
共 58 张,覆盖 960 个 Blueprint 可见的成员。每一行只说三件事之一:
| 结论 | 含义 | 行数 |
|---|---|---|
adopt | 同名、同义 | 605 |
map | 在这里换了个名字,或者在另一个类型上,并写明是哪个 | 301 |
reject | 刻意没有,附上理由 | 54 |
reject 的理由都是结构上的,比如:这里没有即时模式的绘制上下文可以往里画;一个程序化矩形没有自己的
材质实例。下面是 Border.json 里的一行(note 有删节):
{"umg": "SetBrushFromMaterial", "kind": "function", "status": "reject", "dream": "",
"note": "A procedural rect has no material of its own to set. ..."}一张表的形状——ListView.json 的表头和其中一行:
{
"umgClass": "UListView",
"umgHeader": "Components/ListView.h",
"dreamClass": "UDreamListView",
"members": [
{"umg": "EntrySpacing", "kind": "property", "status": "map", "dream": "FDreamListStyle::RowSpacing", "note": "The gap between entries is appearance, so it lives in the list's style rather than on the control: FDreamListStyle::RowSpacing, along the list's main axis (between rows of a vertical list, between columns of a horizontal one)."}
]
}| 字段 | 含义 |
|---|---|
umgClass、umgHeader | UMG 那边的类和它的头文件 |
dreamClass | 这边对应的类型 |
members[].umg | UMG 成员名 |
members[].kind | property 或 function |
members[].status | adopt / map / reject |
members[].dream | 在这边的名字;adopt 且同名时为空,map 时必须写 |
members[].note | 说明;reject 时就是理由 |
对应关系不全是一对一的。几个值得知道的:
| UMG | 这里 |
|---|---|
UCheckBox | UDreamToggle |
UComboBoxString | UDreamDropdown |
UEditableTextBox、UMultiLineEditableTextBox | UDreamTextInput |
UCircularThrobber、UThrobber | UDreamThrobber |
UListView、UListViewBase | UDreamListView |
每一种 …Slot | UDreamPanelSlot |
UPanelWidget | UDreamPanelLayoutBase |
UWidget | UDreamWidget |
UWidgetBlueprintLibrary | UDreamUIWidgetLibrary |
UWidgetLayoutLibrary | UDreamUILayoutLibrary |
USlateBlueprintLibrary | UDreamUIWidgetGeometryLibrary |
UCanvasPanel、UVerticalBox 等面板 | UDreamLayoutContainer… 同名容器 |
文件名以下划线开头的是给测试用的数据而不是对照表:_BlueprintWritableExceptions.json 列出刻意只在构造时
生效的旋钮,每个附理由(比如 UDreamUIControl::Template:树只在初始化时实例化一次,已经持有部件的控件没法
换一棵树而不扔掉接在旧树上的所有行为和绑定)。
测试从两个方向守着它
自动化测试套件(DreamGUI.UMGParity.*)把这些表和 UMG 自己的反射对照:
- 从表到 UMG:
adopt和map的目标必须在dreamClass上解析得到;map必须写目标;reject必须给出 一个别人能反驳的理由;没有结论的行是错误。这一半抓的是这边改了名——表里一行指向的东西已经不存在了。 - 从 UMG 到表:反射列出 UMG 类自己的 Blueprint 可见成员,表里没提到的每一个都是错误。这一半抓的是引擎升级—— UMG 多了一个成员,表里就多出一行"不存在的行"。
TheTablesShipWithThePlugin:表目录存在且非空。逐表的测试是枚举目录生成的,空目录会让它们什么都不跑就通过, 这是这类守卫唯一一种没人察觉的失败方式。AKnobThePanelCanTurnIsOneABlueprintCanTurn:细节面板能编辑的属性,Blueprint 必须能通过 setter 改; 例外只能来自_BlueprintWritableExceptions.json。原因是控件的字段只在它组装自己的时候读一次,直接写字段改的是 一个之后没人会再看的数。
测试套件本身见自动化测试。
两条事先值得知道的差别
新旋钮的默认值是控件原本的行为,不是 UMG 的默认值,这样既有内容不会动。比如滚动框的
ScrollWhenFocusChanges 在这里默认是 AnimatedScroll,UMG 是 NoScroll——这里的导航一直会把目标滚进视野,
改成 NoScroll 会让每一个手柄驱动的列表都不再跟随焦点。表里会记下每一个这样的情况:
{"umg": "ScrollWhenFocusChanges", "kind": "property", "status": "adopt", "dream": "", "note": "Default AnimatedScroll, not UMG's NoScroll: navigation here has always revealed its target, and NoScroll would stop every existing gamepad-driven list from following focus."}外观在样式结构体里,行为在控件上,所以有几个 UMG 属性在这里低了一层:EntrySpacing 是 Style.RowSpacing,
按钮的 WidgetStyle 是 Style(按 StyleSource 解析)。见样式与样式表。
Docs/Reference 是从反射打印出来的
插件的 Docs/Reference/ 是属性和函数参考,每个类一页,每页最后一节是这个类的 UMG 对照(读的就是上面那些表)。
本站的参考就是这些页面。它们不是手写的,而是由一个 commandlet 从反射打印出来,所以不会落后
于头文件:
UnrealEditor-Cmd.exe <project>.uproject -run=DreamGUIReferenceDocs
UnrealEditor-Cmd.exe <project>.uproject -run=DreamGUIReferenceDocs -Out=<directory>不带 -Out 时写进插件的 Docs/Reference/。
- 属性的描述就是头文件里它上面的注释——这也是为什么注释要写给用控件的人,而不是维护控件的人;
- 每个属性一行写明它是否出现在细节面板、Blueprint 怎么读写(getter / setter 的名字);
- 输出是确定性的:成员按声明顺序,其余排序。重新生成一页的 diff,就是 API 的 diff。