Widget 设计器
对照 UMG 设计器重建的 DreamUI Designer:按矩形拾取、缩放手柄与锚点徽章、框选与拖放重挂、调色板、撤销、源文件菜单、事件路由,以及对 .dui 支撑的类它能改什么、不能改什么。
打开一个 Dream Widget Blueprint,就进入 DreamUI Designer。它有两个模式:Designer(画布、调色板、层级、 细节面板、动画)和 Graph(Blueprint 事件图)。设计器编辑的是这个类的一个预览实例,改动直接写回类本身, 所以没有"应用"这一步,也没有每个实例各自的覆盖列表要对账。
设计器是被测试验证过的,不是被眼睛验证过的。 它的手势有自动化测试(Designer 预设),但没有人系统地
用肉眼走过每一处,预计会有粗糙的地方。遇到了请报告。
画布上的手势
设计器是对照 UMG 的 widget 设计器重新审过一遍的,这些是从上游之后加上的:
- 按矩形拾取。点击按 widget 的 rect 命中,而不是按渲染出来的三角形。只做布局的面板没有网格,射线永远 打不中,上游里面板因此点不中、也放不进东西——按矩形拾取解决的就是这个。
- 悬停反馈:指针下的 widget 会被描出来。
- 按轴的缩放手柄,以及锚点徽章(anchor medallion),在画布上显示选中 widget 的锚点。
- 框选:在空白处拖出一个框,选中框里的 widget。
- 在画布上拖动重挂:把 widget 拖到另一个容器里。
- 从内容浏览器拖放:把资产直接拖到设计画布上。
- 撤销:每个设计器手势是一步撤销,创建、粘贴和拖放都在撤销覆盖之内。
- 吸附与网格:工具栏的 Snap 开关和网格尺寸;按住拖动时有吸附参考线。
- 对齐与分布:Arrange 菜单把选中的兄弟 widget 按边或中心对齐(至少两个),或均分间距(至少三个)。
层级面板的右键菜单有:Wrap With...(用一个新容器包住选中项,它们保持原来的布局)、Unwrap(把一个 widget 从父子之间拿掉,子节点按原顺序落在它的位置)、Replace With...(换掉排列子节点的面板——和 UMG 不同,widget 本身的名字、位置、槽、组件和子节点都不动,只换布局容器)、Show / Hide in Designer、Lock / Unlock、 Find References、Focus Selection(等同于 F 键),以及 Reveal in VS Code。
调色板
调色板列出可以放进来的东西:基础 widget、布局容器、控件库、项目里的 widget Blueprint。
- 收藏:常用的条目可以收藏,收藏组排在最前面;
- 搜索:搜索框过滤条目,过滤时所有分组展开。
视口工具栏
| 项 | 作用 |
|---|---|
| Screen Size | 预览的设备分辨率;Flip Orientation 交换宽高,Show Resolution Guides 叠加常见分辨率 |
| Size Rule | Custom:画布是选定或输入的分辨率;Fill Screen:画布就是视口,一个设计单位一个像素;Desired:按根 widget 的面板测量出的尺寸设定画布,适合按自身大小制作一个 tooltip 尺寸的 widget |
| Zoom | Zoom to Fit 框住整个画布(F 框住选中项),Zoom 1:1 一个设计单位一个屏幕像素 |
| Rulers、Guides、Layout Debug | 标尺、吸附参考线、选中 widget 的测量 / 排列 / 槽 / 裁剪诊断 |
| Safe Zone | 画出平台的标题安全区(桌面平台需要 r.DebugSafeZone.TitleRatio 才会有) |
| Respect Locks | 关掉后可以选中、拖动锁定的 widget 而不解锁 |
| Designer Chrome | 关掉画布边界、选框、手柄、参考线等所有覆盖层,只看 widget 本身 |
| Preview DPI | 按项目的 DPI 曲线缩小画布,预览 Slate 实际会施加的应用缩放 |
| Preview Language | 用另一种文化解析预览里的文本,相当于 UMG 的本地化预览;关闭设计器时自动关掉 |
| Screen Space / Canvas Eye | 按运行时的方式把预览构建成屏幕空间叠加画布(声明的透视才会生效);把 3D 视图的相机放到画布自己的虚拟相机上 |
源文件:.dui 支撑的类
一个类的层级可以来自一个 .dui 文件。工具栏上的 DreamUI Source 下拉菜单管这件事:
| 菜单项 | 作用 |
|---|---|
| Create Source File... | 写一个起步用的 .dui(一个根加一个居中的标签),让类指向它并编译。只在类还没有源文件时出现 |
| Set Source File... | 选一个已有的 .dui 作为这个类的层级来源,然后重新编译 |
| Open in Default Editor | 用系统给 .dui 关联的程序打开 |
| Show in Explorer | 在文件浏览器里选中这个文件 |
| Reveal in VS Code | 在 VS Code 里打开并定位(层级面板的右键菜单里也有,定位到节点所在的行) |
能指定源文件的前提是类派生自 UDreamTextUserWidget,源文件就是它的类默认值 SourceFile。从脚本做同样的事
见脚本。
文本是唯一的真相
一个类一旦指向了 .dui,文件就是它层级的唯一真相,设计器是文件的图形前端。这样换来的是:一个地方读、一个
地方 diff、一个地方 review。代价是设计器不再什么都能做,而且它会在你动手之前就告诉你,而不是让改动存活到下次
编译再悄悄消失。
值会写回文件。 改一个属性,写回落在持有这个值的那一行上——不管节点的类型怎么写,在 @slot { … }
块里就写进那个块,没有 id 的节点用它生成的 id 定位。设计器从不覆盖一个在磁盘上被改过的文件(DUI7002)。在画布上
移动一个 widget 也是值的修改:它重算锚点,而 AnchorData.* 是会写回的。
结构归文件管。 创建、删除、在树里移动、重命名、复制、粘贴 widget,添加、删除、重排行为,替换面板——这些在
.dui 支撑的类上一律被拒绝,并弹出一条消息说出文件名:结构在文本里,改文件;设计器改值。旋转和缩放 gizmo
也不提供,因为写回只带 AnchorData.*,相对旋转和缩放在语言里没有写法。
写不回去的属性行是灰的,并说明原因:
| 原因 | 例子 |
|---|---|
| 文件用绑定写它 | Text <- GetTitle()——写一个字面量会删掉绑定 |
| 由循环展开而来 | for 生成的 N 个副本,没有逐个实例的值可写 |
| 在可写集合之外 | 写回没有地方放这个属性,改了也活不过编译 |
值在 .dui 里没有写法 | 实际上就是对象引用:字体、纹理、材质。在面板里换一个字体,预览变了,文件没变,下次编译就没了 |
写回时还会拒绝几种情况,"说出来而不是写进去":一个值没法写成语言读得回来的字面量(今天只有非有限的浮点数),
报 DUI7003;一个由 if 决定的可见性、一个无法承载这个改动的简写,报 DUI7004;
一个来自样式里 + Component 行的值,报 DUI7005——样式是共享的,写进去会移动所有用这个样式的节点。代码含义见
DUI7xxx。
自己在设计器里搭层级、不指向 .dui 的类,以上限制都不存在。
事件
细节面板的 Events 一节列出选中 widget 能发出的事件。点 +(Add Event Handler)会在事件图里创建一个签名正确的
自定义事件,以及一条指向它的路由——和 .dui 里的 OnClick -> HandleConfirm 是同一个东西。已经有处理函数时,
同一个按钮打开它。Remove Event Route 去掉路由。
路由名的是用户 widget(层级编译成的那个类)上的函数,UMG 也把事件处理放在那里。编译器把每条路由解析进
UDreamWidgetBlueprint::EventBindings,UDreamUserWidget::BindEventBindings 在初始化时接上。它能接到两种事件:
控件声明的 BlueprintAssignable 动态多播委托,和较老的 Interaction/ 行为声明的 FDreamUIEventDelegate 属性。
FDreamUIEventDelegate 自己的逐实例事件列表是遗留的、只读的:存在里面的绑定照常触发、也能从面板删掉,但不能
再新建。那种绑定是"用字面量参数调用任意对象上的函数",UMG 没有对应物,.dui 也没有语法。打开含有这种绑定的面板会
打一条警告点名它。不会自动改写——把"带这个值调用那个行为上的 Foo"改成"调用用户 widget 上的处理函数"会改变游戏的
行为——所以碰到时请重新写成路由。
动画编辑器
动画编辑器基于 Sequencer。.dui 里的 timeline 块编译成类里的一个 UDreamWidgetAnimation;因为文件拥有它,
每次编译都会重建,所以动画编辑器只读打开它,并在日志里说明原因,重命名它也会被拒绝("在 .dui 的 timeline
块里改名")。
要让 Sequencer 接管一个动画,把块改成 external——下面 Pulse 归文件,Celebrate 归资产:
class /Game/UI/WBP_Results
Widget Root {
Text Title { Text = "Victory" }
}
timeline Pulse {
duration = 0.6
loop = PingPong
Title.RenderScale : 0.0 = (1, 1, 1), 0.3 = (1.25, 1.25, 1) ease InOutQuad, 0.6 = (1, 1, 1)
}
timeline Celebrate externalexternal 不构建任何东西,它是一条清单:动画活在资产里,Sequencer 随便编辑,而文件仍然列着它——这让"这个类有哪些
动画"可以靠读文件回答。材质参数轨道、手调曲线这类 timeline 写不出来的东西,本来就该是 external。见
timeline。