绑定与路由
三种箭头把树接到类的代码上——绑定驱动属性、路由调用处理函数、双向绑定镜像变量——以及每个 widget 都有的 Shown。
三种箭头把树接到类的代码上:
| 写法 | 方向 | 例子 |
|---|---|---|
Prop <- Expr | 类 → 树:属性跟着表达式走 | Text <- GetTitle() |
Event -> Handler | 树 → 类:事件调用类的一个函数 | OnClicked -> HandleConfirm |
Prop <-> Var | 两个方向:属性和变量互相镜像 | Value <-> Volume |
这里的“类”就是这棵树编译进去的那个 user widget——UMG 也把事件处理放在那里。函数和变量可以写在 C++ 里,也可以写在 Blueprint 的图表里。
<- 驱动属性
class /Game/UI/WBP_Inventory
Widget Root {
+ VerticalBox { Spacing = 8 }
Text Title { Text <- GetTitle() }
Image Glow { RenderOpacity <- Fade() * 0.8 }
Widget Badge { Shown <- Count() > 0 && !IsLocked() }
}右边是一个表达式,随着类的运行重新求值。表达式里可以有:
- 类的函数调用,
Func()或Func(a, b); - 类的变量,
Volume——包括props里的条目; - 字面值:数字、字符串、
true、false,或@Resource; - 运算符,从最松到最紧:
||;&&;==!=;<<=>>=;+-;*%;以及前缀!和-。括号分组。
没有 /——它属于路径和注释——也没有 ? :、下标 Items[0]、数组或字典字面值。表达式里每加一个运算符,都得能降成 Blueprint 图、
教给 VS Code 扩展的扫描器、写进三处文档,所以除法和二选一写成类上的函数,下标写成一个带参数的函数;对 widget 来说,二选一就是一个
if。
一个裸的 Func() 直接绑定那个函数;更复杂的表达式会被编译成一个生成的函数。对目标属性的要求:
- 属性要有 setter(DUI5005)——组件的 prop 例外,见编写组件。
- 只能绑定 widget、它的视觉或某个行为上的整个属性(DUI5008):不能绑槽位属性、
容器属性,也不能绑结构体里的某个字段(
AnchorData.SizeDelta <- …不行)。
在 VS Code 里,编辑器运行时 <- 后面的补全会去问编辑器这个类到底声明了什么,函数调用有参数提示,悬停能看到函数和变量的类型。
-> 路由事件
class /Game/UI/WBP_Dialog
Widget Root {
+ HorizontalBox { Spacing = 16 }
Native.Button Confirm { OnClicked -> HandleConfirm }
Widget Cancel {
+ UIButton { OnClick -> HandleCancel }
}
}左边是 widget、它的视觉或某个行为上的一个事件:BlueprintAssignable 委托(Controls/ 那一族控件声明的),或者
FDreamUIEventDelegate 属性(较老的 Interaction/ 行为声明的)。每个 widget 都有 OnVisibilityChanged、OnFocusReceived、
OnFocusLost。右边是类的一个函数(不带括号),或者 emit 一个本类声明的事件(见编写组件):
class /Game/UI/Components/WBP_Row
events {
Picked(Number Index)
}
Widget Root {
+ UIButton { OnClick -> emit Picked(0) }
}编译器把每条路由解析进 UDreamWidgetBlueprint::EventBindings,UDreamUserWidget::BindEventBindings 在 Initialize 时把它们接上。
在设计器里,同一件事就是细节面板的 Events 一节:那里的 + 会建一个签名正确的自定义事件,并写出指向它的路由。
FDreamUIEventDelegate 自带的逐实例事件列表是遗留功能,只读。存在里面的绑定照样会触发,也还能从面板上删掉,但不能再新建:
那种绑定是在任意对象上调一个函数、带一个字面参数,UMG 里没有对应物,.dui 里也没有写法。打开带有这种绑定的面板会记一条警告点名它。
不会自动改写——把“在那个行为上用这个值调 Foo”改成“调 user widget 上的一个处理函数”会改变游戏的行为——所以下次碰到时把它们重写成路由。
<-> 双向镜像
class /Game/UI/WBP_Audio
Widget Root {
+ VerticalBox { Spacing = 8 }
Native.Slider Volume { Value <-> MasterVolume }
Native.Toggle Mute { bIsOn <-> bMuted }
}属性跟着变量走,控件自己产生的变化再写回变量。变量必须是这个类的 FieldNotify 变量。
<-> 是两半拼起来的:正向是一个生成的 getter,和任何绑定函数一样;反向是一条生成的路由
OnValueChangedBP -> 生成的 setter。所有值类控件都通过 OnValueChangedBP 这个约定的名字广播变化,没有这个事件的目标会报
DUI5010。<-> 右边的名字不是这个类的变量(以上一次编译为准)是 DUI5011。
VS Code 里 <-> 后面的补全列出类的变量,FieldNotify 的排在前面。
Shown
每个 widget 都有 Shown:true 是 Visible,false 是 Collapsed。它自己不存任何东西——读它就是读 Visibility,写它就是写
Visibility——条件绑定的就是它:
class /Game/UI/WBP_Detail
Widget Root {
+ VerticalBox { }
Widget Detail {
Shown <- HasDetail()
}
Text Empty {
Text = "Nothing selected"
Shown = false
}
}一个 Hidden 或命中测试不可见的 widget 也算“显示”,Shown 为 true 时它保持原来的状态,不会被改成 Visible。if 给每个分支里的 widget 绑的也是 Shown,见
if 与 for。Shown 和 Visibility 是同一个值,设计器写回时只比较文件写出来的那一面。