1. 从UMG和Slate说起为什么UE的UI系统值得深挖如果你用过虚幻引擎做项目大概率经历过这样的场景美术在UMG编辑器里拖拖拽拽搭好了一套界面运行起来发现某个按钮点不动或者列表滚动卡得不行又或者打包之后字体全糊了。你打开代码想排查发现底层全是Slate的SCompoundWidget、TAttribute、FSlateBrush这些东西一时间不知道从哪下手。这就是UMG和Slate的关系——UMG是给美术和策划用的可视化UI搭建工具Slate是给程序员用的底层UI框架。UMG的所有控件本质上都是对Slate控件的封装。你在UMG里拖一个Button底层就是一个SButton你设置一个Image的Brush底层就是一个FSlateBrush。理解这层关系是排查UE UI问题的起点。这篇内容适合三类人看一是刚接触UE UI系统的开发者想搞清楚UMG和Slate到底怎么回事二是有一定经验但遇到性能瓶颈、交互异常等问题的从业者想深入底层找原因三是需要做编辑器扩展或自定义控件的程序员必须直接跟Slate打交道。我会从架构设计、核心机制、实操要点、问题排查几个维度展开尽量把我在实际项目中踩过的坑和总结的经验都写出来。2. UMG与Slate的架构关系拆解2.1 两层架构的设计逻辑UE的UI系统采用了一种双层架构Slate是底层纯C实现负责所有渲染、布局、输入事件的底层处理UMG是上层建立在Slate之上提供了蓝图可访问的UWidget体系并且有一套可视化编辑器。为什么要这么设计核心原因是面向不同角色。Slate的API对程序员友好但对美术和策划来说门槛太高——你不可能让一个美术去写SNew(SButton).OnClicked(...)这样的代码。UMG的出现就是为了解决这个问题它把Slate的能力包装成蓝图可操作的UUserWidget让非程序员也能参与UI制作。但这个封装不是没有代价的。UMG的每一层封装都会带来额外的开销UWidget需要维护自己的属性系统、需要跟Slate层做同步、需要在RebuildWidget时创建对应的Slate控件。这就是为什么大量使用UMG的界面在性能上往往不如纯Slate实现。2.2 UWidget与SWidget的对应关系每一个UMG控件UWidget的子类在运行时都会创建一个对应的Slate控件SWidget的子类。这个创建过程发生在UWidget::RebuildWidget()里。比如UButton的RebuildWidget会创建一个SButtonUTextBlock会创建一个STextBlock。这个对应关系是理解UMG性能问题的关键。当你修改一个UMG控件的属性时如果这个属性需要同步到Slate层就会触发SynchronizeProperties()某些情况下还会触发RebuildWidget()——而RebuildWidget()是相对昂贵的操作因为它涉及到旧控件的销毁和新控件的创建。注意在UMG中频繁修改那些会触发RebuildWidget的属性比如UWidget的Slot相关属性是导致UI卡顿的常见原因之一。能通过SynchronizeProperties更新的属性就不要让它走到RebuildWidget。2.3 什么时候该用UMG什么时候该用Slate这个问题在实际项目中很常见。我的判断标准是这样的场景推荐方案原因游戏内HUD、菜单、设置界面UMG迭代快美术可参与蓝图可控制编辑器扩展、工具面板Slate需要精细控制性能要求高不需要美术参与高频更新的数据展示如血条、帧率Slate或UMG优化UMG需要做额外优化避免每帧重建复杂列表如背包、排行榜UMGListView用ListView做虚拟化避免全量创建自定义绘制如图表、曲线Slate需要重写OnPaintUMG做不了简单说面向玩家的界面优先UMG面向开发者的工具优先Slate。但这也不是绝对的有些项目为了性能会把核心HUD用Slate重写这需要权衡开发效率和运行效率。3. Widget系统的核心机制与实操要点3.1 Widget树的构建与渲染流程UE的UI渲染走的是Retained Mode保留模式这跟很多前端框架的Immediate Mode不同。什么意思呢就是说Widget树一旦构建好每帧渲染时不会重新创建控件只会根据属性变化做增量更新。整个流程大致是这样的UUserWidget在初始化时通过RebuildWidget构建出对应的Slate Widget树这棵树会被加入到Slate的渲染队列中。每帧渲染时Slate会遍历这棵树计算布局Prepass阶段然后执行绘制OnPaint阶段。如果某个Widget标记了Invalidate它会在下一帧的Prepass中重新计算布局。这个机制的好处是性能稳定不会因为每帧重建控件而抖动。但坏处是如果你不小心让某个高频更新的Widget触发了Invalidate它会导致整个子树重新计算布局开销可能比你想象的大得多。3.2 布局系统Slot与Panel的关系UMG的布局核心是Panel和Slot的组合。Panel负责管理子控件的排列方式比如CanvasPanel是自由定位VerticalBox是垂直排列GridPanel是网格排列Slot负责描述每个子控件在Panel中的具体位置和尺寸规则。这里有个容易混淆的点Slot是挂在子控件上的不是挂在Panel上的。也就是说同一个Widget放到不同的Panel下它的Slot类型是不同的。比如你把一个Button放到CanvasPanel下它的Slot是UCanvasPanelSlot你可以设置Anchor和Offset放到VerticalBox下Slot变成UVerticalBoxSlot你只能设置Padding、Alignment和Size。这个设计的好处是灵活但坏处是当你把一个Widget从一个Panel移到另一个Panel时原来的Slot信息会丢失。我在项目中遇到过好几次因为动态改变父级Panel导致布局错乱的问题排查半天才发现是Slot类型不匹配。3.3 输入事件的传递与处理UE的UI输入事件传递遵循从后往前、从子到父的规则。具体来说Slate会按照Widget树的绘制顺序后绘制的在上面反向遍历找到第一个命中测试通过的Widget然后把事件传给它。如果这个Widget不处理事件会继续往上传递给它的父级。这个机制导致了一个常见问题如果你在UMG里放了一个全屏的Image作为背景它会挡住下面所有控件的点击。解决办法是把背景Image的Visibility设置为Self Hit Test Invisible或者Hit Test Invisible这样它就不参与命中测试了。实操心得Visibility的四个选项要搞清楚——Visible是可见且可交互Hit Test Invisible是可见但自身不参与命中测试子控件仍可交互Self Hit Test Invisible是可见但自身和子控件都不参与命中测试Collapsed是不可见且不占布局空间。选错了就会出现按钮点不动或者布局莫名其妙多出一块空白的问题。3.4 数据绑定与属性同步UMG提供了属性绑定功能可以在编辑器里把一个控件的属性绑定到一个函数上每帧自动调用这个函数来更新属性值。这个功能很方便但性能陷阱也很多。属性绑定的函数是每帧都会调用的如果你在绑定函数里做了复杂计算比如遍历数组、查表、字符串拼接帧率会被拖垮。我见过一个项目策划在血条上绑了一个计算护甲减伤的复杂函数结果同屏20个敌人时帧率直接掉了15帧。正确的做法是能用事件驱动更新的就不要用属性绑定。比如血量变化时主动调用SetPercent而不是每帧去绑定一个函数返回当前血量百分比。如果确实需要绑定也要确保绑定函数足够轻量只做简单的取值操作。4. 实操过程与核心环节实现4.1 从零搭建一个自定义UMG控件假设我们要做一个自定义的血条控件带延迟掉血效果就是受到伤害后白色条先掉红色条延迟一段时间再跟上。这个需求在实际项目中很常见我用它来演示完整的自定义UMG控件流程。第一步创建C类继承UUserWidgetUCLASS() class MYGAME_API UDelayedHealthBar : public UUserWidget { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category HealthBar) void SetHealth(float NewHealth, float MaxHealth); protected: virtual void NativeConstruct() override; virtual void NativeTick(const FGeometry MyGeometry, float InDeltaTime) override; UPROPERTY(meta (BindWidget)) class UProgressBar* HealthBar; UPROPERTY(meta (BindWidget)) class UProgressBar* DelayedBar; private: float TargetPercent 1.0f; float CurrentPercent 1.0f; float DelayedPercent 1.0f; float DelaySpeed 0.5f; float DelayTimer 0.0f; bool bDelayActive false; };这里的关键是meta (BindWidget)它让C变量自动绑定到蓝图里同名的控件上。命名必须完全一致否则编译时会报错。第二步实现逻辑void UDelayedHealthBar::SetHealth(float NewHealth, float MaxHealth) { TargetPercent FMath::Clamp(NewHealth / MaxHealth, 0.0f, 1.0f); if (TargetPercent CurrentPercent) { bDelayActive true; DelayTimer 0.0f; } CurrentPercent TargetPercent; if (HealthBar) { HealthBar-SetPercent(CurrentPercent); } } void UDelayedHealthBar::NativeTick(const FGeometry MyGeometry, float InDeltaTime) { Super::NativeTick(MyGeometry, InDeltaTime); if (bDelayActive) { DelayTimer InDeltaTime; if (DelayTimer 0.3f) { DelayedPercent FMath::FInterpTo(DelayedPercent, CurrentPercent, InDeltaTime, DelaySpeed * 10.0f); if (FMath::IsNearlyEqual(DelayedPercent, CurrentPercent, 0.001f)) { DelayedPercent CurrentPercent; bDelayActive false; } if (DelayedBar) { DelayedBar-SetPercent(DelayedPercent); } } } }第三步在蓝图中创建基于UDelayedHealthBar的WidgetBlueprint放入两个ProgressBar分别命名为HealthBar和DelayedBar注意层级关系——DelayedBar在下面先绘制HealthBar在上面。这个实现有几个细节值得注意NativeTick默认是不开启的需要在UUserWidget的构造函数里设置bCanEverTick true或者在蓝图里勾选Tick选项。另外FInterpTo的速度参数需要根据实际效果调整太快了看不出延迟效果太慢了显得拖沓。4.2 Slate自定义控件的绘制流程有些效果UMG做不了比如自定义的曲线图、雷达图、特殊形状的进度条这时候就需要直接写Slate控件。Slate控件的核心是重写OnPaint函数。class SRadarChart : public SLeafWidget { public: SLATE_BEGIN_ARGS(SRadarChart) {} SLATE_ATTRIBUTE(TArrayfloat, Values) SLATE_ARGUMENT(int32, AxisCount) SLATE_END_ARGS() void Construct(const FArguments InArgs) { Values InArgs._Values; AxisCount InArgs._AxisCount; } virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D(200.0f, 200.0f); } virtual int32 OnPaint( const FPaintArgs Args, const FGeometry AllottedGeometry, const FSlateRect MyCullingRect, FSlateWindowElementList OutDrawElements, int32 LayerId, const FWidgetStyle InWidgetStyle, bool bParentEnabled) const override { const FVector2D Center AllottedGeometry.GetLocalSize() * 0.5f; const float Radius FMath::Min(Center.X, Center.Y) * 0.8f; const TArrayfloat Vals Values.Get(); const int32 Count FMath::Min(AxisCount, Vals.Num()); TArrayFVector2D Points; for (int32 i 0; i Count; i) { float Angle (2.0f * PI * i / Count) - PI * 0.5f; float R Radius * FMath::Clamp(Vals[i], 0.0f, 1.0f); Points.Add(Center FVector2D(FMath::Cos(Angle) * R, FMath::Sin(Angle) * R)); } if (Points.Num() 3) { FSlateDrawElement::MakeLines( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Points, ESlateDrawEffect::None, FLinearColor::Green, true, 2.0f ); } return LayerId 1; } private: TAttributeTArrayfloat Values; int32 AxisCount 5; };这个例子里有几个关键点SLeafWidget表示这是一个叶子控件没有子控件ComputeDesiredSize决定控件的期望尺寸OnPaint里通过FSlateDrawElement执行实际绘制。ToPaintGeometry()把局部坐标转换为绘制用的几何信息MakeLines画线段如果要填充区域可以用MakeCustomVerts。实操心得Slate的OnPaint是每帧调用的所以里面的计算要尽量轻量。如果数据不常变可以在数据变化时预先计算好顶点数组缓存起来OnPaint里只做绘制。另外FSlateDrawElement::MakeLines的最后一个参数是线宽在DPI缩放不同的屏幕上表现可能不一致需要做适配。4.3 UMG列表的性能优化实操列表是UMG里最容易出性能问题的地方。假设你要做一个1000个条目的排行榜如果用VerticalBoxScrollBox直接创建1000个EntryWidget打开界面的瞬间就会卡死。正确的做法是用UListView或者UTileView它们内置了虚拟化机制——只创建可见区域内的条目滚动时复用已有的Widget。使用UListView需要实现UUserListEntry接口或者继承UUserListEntry和UUserObjectListEntry。UCLASS() class ULeaderboardEntry : public UUserWidget, public IUserObjectListEntry { GENERATED_BODY() protected: virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override; UPROPERTY(meta (BindWidget)) class UTextBlock* RankText; UPROPERTY(meta (BindWidget)) class UTextBlock* NameText; UPROPERTY(meta (BindWidget)) class UTextBlock* ScoreText; }; void ULeaderboardEntry::NativeOnListItemObjectSet(UObject* ListItemObject) { ULeaderboardItem* Item CastULeaderboardItem(ListItemObject); if (!Item) return; RankText-SetText(FText::AsNumber(Item-Rank)); NameText-SetText(FText::FromString(Item-PlayerName)); ScoreText-SetText(FText::AsNumber(Item-Score)); }ULeaderboardItem是一个继承自UObject的数据类持有Rank、PlayerName、Score等字段。UListView会根据数据源自动管理EntryWidget的创建和回收滚动时把滚出视野的Widget重新绑定到新进入视野的数据上。这个方案的关键在于数据与视图分离。数据存在UObject里视图只负责展示。这样即使有10000条数据同时存在的Widget也只有可见的那十几个内存和性能都可控。4.4 半透明与景深相关的UI渲染设置热词里提到了ue半透明景深代码这涉及到UI与场景渲染的交互。在UE里UMG默认是渲染在场景之上的但如果你的UI需要跟场景有景深交互比如UI元素根据距离模糊就需要特殊处理。一种常见做法是把UI渲染到一个RenderTarget上然后在场景里用一个带景深材质的平面来显示这个RenderTarget。这样UI就会受到场景景深的影响。具体步骤是创建一个USceneCaptureComponent2D或者用DrawMaterialToRenderTarget把UMG画到RT上然后在场景里放一个Plane材质里采样这个RT并且把材质的BlendMode设为Translucent开启Allow Custom Depth Writes。不过这种做法性能开销不小因为多了一次RT绘制和一次场景绘制。如果只是想让UI有半透明效果直接在UMG里设置控件的Render Opacity就够了不需要走RT。景深交互只在特定场景下才需要比如VR项目里让UI有空间感。5. 常见问题与排查技巧实录5.1 UI不显示或显示异常的排查思路这是最高频的问题。我整理了一个排查顺序基本能覆盖90%的情况现象可能原因排查方法完全不显示Widget没添加到Viewport检查AddToViewport是否调用完全不显示Visibility设为Collapsed检查控件及其父级的Visibility完全不显示ZOrder被其他UI挡住检查ZOrder设置或临时把其他UI隐藏显示但位置不对Anchor和Offset设置错误在编辑器里切换不同分辨率预览显示但尺寸不对Slot的Size规则不对检查是Auto还是Fill显示但字体模糊字体纹理DPI不匹配检查字体资产的DPI设置显示但颜色不对材质或Brush的Tint不对检查Brush的TintColor和材质参数实操心得排查UI问题时先把问题控件之外的所有UI都隐藏只留这一个看它是否正常。如果正常说明是层级或遮挡问题如果不正常说明是这个控件本身的配置问题。这个隔离法能快速缩小排查范围。5.2 帧率低的常见原因与优化手段热词里ue排查帧率低的原因是个高频需求。UI导致的帧率低通常有以下几个原因属性绑定函数太重。前面说过绑定函数每帧调用里面不能做复杂计算。排查方法是打开Stat Slate或者Stat UMG看哪个Widget的Tick开销最大。Widget树太深。每多一层嵌套布局计算就多一层递归。优化方法是扁平化Widget树能用Panel解决的不要套多层Box。我见过一个项目一个简单的设置界面套了7层嵌套展开后帧率掉了8帧。Invalidation过多。某些属性变化会导致整个子树重新计算布局。比如修改一个SizeBox的WidthOverride会导致它下面所有子控件重新布局。优化方法是尽量用RenderTransform代替布局属性变化因为RenderTransform只影响绘制不影响布局。Overdraw严重。多个半透明UI元素叠加会导致像素被多次绘制。排查方法是打开Shader Complexity视图红色区域就是Overdraw严重的地方。优化方法是减少不必要的半透明叠加能用不透明就不用半透明。5.3 打包后UI表现不一致的问题编辑器里跑得好好的打包后UI就出问题这种情况太常见了。主要原因有几个字体资源没有正确打包。如果字体是通过代码动态加载的打包时可能没有被引用到导致打包后字体丢失。解决办法是在项目设置里把字体加入Additional Asset Directories to Cook或者用一个DataAsset显式引用。材质变体被裁剪。打包时UE会裁剪未使用的材质变体如果某个UI材质只在特定条件下使用可能被裁掉。解决办法是在项目设置的Packaging里调整材质变体裁剪策略或者用UMaterialInstanceDynamic时确保材质被正确引用。分辨率适配问题。编辑器里用的是固定分辨率预览打包后实际分辨率可能不同。解决办法是用DPI Scaling和Anchors做适配不要用硬编码的像素值。蓝图Nativization的副作用。如果开启了蓝图Nativization某些UMG的绑定逻辑可能表现不一致。排查方法是临时关闭Nativization重新打包对比。5.4 输入事件不响应的排查清单按钮点不动、拖拽没反应这类问题排查起来比较繁琐我列一个清单检查控件的Visibility是否包含Hit Test Visible。Hit Test Invisible和Self Hit Test Invisible都不参与命中测试。检查是否有上层控件挡住了。用Widget Reflector快捷键CtrlShiftW可以查看当前鼠标位置下的Widget层级。检查父级Panel的Visibility。如果父级是Hit Test Invisible子级即使设了Visible也不响应。检查是否被UUserWidget的bIsFocusable影响。某些情况下需要设置bIsFocusable true。检查输入模式。如果SetInputMode设置成了GameOnlyUI不会接收输入。检查是否有Overlay之类的控件设置了bConsumeInput。实操心得Widget Reflector是排查UI问题的神器强烈建议每个UE开发者都熟悉它。它能实时显示鼠标下的Widget层级、每个Widget的命中测试结果、以及当前聚焦的Widget。很多靠猜的问题用它一看就明白了。5.5 UMG与Slate混合使用的注意事项有时候需要在UMG里嵌入Slate控件或者反过来。UMG嵌入Slate用UWidget的RebuildWidget返回自定义Slate控件Slate嵌入UMG用SNew(SWidgetHost)配合UWidget::TakeWidget()。混合使用时要注意生命周期管理。Slate控件的生命周期由Slate的引用计数管理而UMG控件的生命周期由GC管理。如果Slate控件持有了UMG对象的裸指针GC时可能出问题。正确的做法是用TWeakObjectPtr或者TStrongObjectPtr来持有UMG对象的引用。另外混合使用时输入事件的传递链路会变长调试起来更麻烦。如果不是必要尽量保持UI层级的纯粹性——要么全UMG要么全Slate。6. 一些实战中总结的零碎经验关于UMG的动画系统UWidgetAnimation用起来很方便但它的性能开销比手动插值要大。如果只是简单的透明度变化或者位移用NativeTick里的FMath::Interp手动做性能会好很多。动画系统适合复杂的、多属性的、需要编辑器可视化编辑的场景。关于UUserWidget的NativeConstruct和NativeOnInitialized前者在每次添加到屏幕时调用后者只在第一次初始化时调用。如果你有只需要执行一次的初始化逻辑比如绑定委托放在NativeOnInitialized里如果是每次显示都需要重置的状态放在NativeConstruct里。关于字体UE的字体渲染在低分辨率下容易糊。解决办法是使用Font Face资产时设置合适的DPI并且在Font资产里开启Use Distance Field Rendering。距离场字体在缩放时表现更好但内存占用会大一些。关于UImage的Brush如果用的是Image类型而不是Material类型UE会走默认的UI材质性能更好。只有在需要特殊效果比如灰度、描边、模糊时才用Material类型的Brush。另外Brush的Draw As选项里Box比Image多了一次九宫格切割的计算非必要不用Box。关于UCanvasPanel它是最灵活的Panel但也是性能最差的。因为每个子控件的Slot都是独立的Anchor和Offset布局计算时没有批量优化的空间。如果界面元素是规则排列的优先用VerticalBox、HorizontalBox、GridPanel、UniformGridPanel这些有批量优化空间的Panel。关于UOverlay它适合做层叠布局但要注意它的子控件默认都是Fill的。如果你想让某个子控件不Fill需要设置它的Slot的HorizontalAlignment和VerticalAlignment。关于USizeBox它适合做固定尺寸的容器但不要滥用。每多一个SizeBox就多一层布局计算。如果只是想让一个控件有固定宽高直接在Slot上设置Size规则更高效。关于UScrollBox它的ScrollBarVisibility默认是Visible如果不需要滚动条可以设为Hidden省一点渲染开销。另外ScrollBox里的内容如果很多考虑用ListView替代。关于UEditableTextBox它在移动端上的输入法兼容性需要特别注意。不同平台的输入法行为不一致建议在目标平台上实测。另外UEditableTextBox的OnTextChanged事件触发频率很高里面不要做重操作。关于UComboBoxString它的下拉列表是独立于Widget树的不受父级布局影响。如果发现下拉列表位置不对检查一下ComboBox的MenuPlacement设置。关于UUserWidget的Tick默认是关闭的。如果确实需要Tick在构造函数里设bCanEverTick true但要注意Tick的开销。能用事件驱动的就不要用Tick。关于UWidgetSwitcher它适合做页面切换但所有页面都会被创建只是不显示而已。如果页面很多且复杂考虑用UUserWidget的动态创建和销毁来替代。关于UProgressBar的Percent它的更新会触发Invalidate如果每帧都更新比如做进度动画考虑用材质参数来控制进度条的填充这样只更新材质参数不触发布局重算。关于UButton的OnClicked和OnPressed前者是点击释放时触发后者是按下时触发。如果需要做长按检测用OnPressed配合定时器。关于UTextBlock的SetText如果文本内容频繁变化比如倒计时每次SetText都会触发文本重排。优化方法是把倒计时拆成多个TextBlock只更新变化的部分比如秒数或者用材质来做数字滚动效果。关于UImage的SetBrushFromTexture如果纹理是动态加载的要注意纹理的生命周期。纹理被GC后Image会显示空白。解决办法是用TStrongObjectPtr持有纹理引用或者在UImage的Brush里设置ResourceObject。关于UUserWidget的AddToViewport和AddToPlayerScreen前者是添加到整个视口后者是添加到玩家屏幕。在分屏模式下用AddToPlayerScreen可以确保UI只显示在对应玩家的屏幕上。关于UUserWidget的RemoveFromParent它只是从父级移除不会销毁Widget。如果需要销毁还要调用ConditionalBeginDestroy或者让GC回收。如果Widget被缓存了引用GC不会回收它。关于UWidgetTree它是UMG的Widget树容器每个UUserWidget都有一个。在运行时动态创建Widget时需要用WidgetTree-ConstructWidget来创建而不是直接NewObject否则Widget不会正确初始化。关于UWidgetBlueprintLibrary它提供了一些有用的静态函数比如Create动态创建Widget、GetInputMode、SetFocus等。在做UI交互逻辑时这个库里的函数能省不少事。关于UUMGSequencePlayer它是UMG动画的播放器每个动画播放实例都会创建一个。如果频繁播放动画注意及时停止和清理避免播放器堆积。关于UWidgetAnimation的BindToAnimationFinished它可以在动画结束时触发回调。但要注意如果动画被中途停止这个回调不会触发。需要在停止动画时手动处理后续逻辑。关于UUserWidget的NativeOnFocusReceived和NativeOnFocusLost它们处理焦点事件。如果UI需要键盘导航需要正确实现这些函数并且设置bIsFocusable true。关于UButton的IsFocusable默认是true。如果不需要键盘导航设为false可以省一点焦点管理的开销。关于UCheckBox的OnCheckStateChanged它的参数是ECheckBoxState枚举有Unchecked、Checked、Undetermined三种状态。如果只需要两种状态用IsChecked判断即可。关于USlider的OnValueChanged它的触发频率很高拖动时每帧都会触发。如果里面做了重操作考虑用OnMouseCaptureEnd来替代只在拖动结束时处理。关于USpinBox它的OnValueChanged和OnValueCommitted的区别是前者在每次值变化时触发后者在用户确认时触发。做数据校验时用后者更合适。关于UListView的OnItemSelectionChanged它提供选中项变化的事件。如果需要做多选用SetSelectionMode设置选择模式。关于UTileView的EntryHeight和EntryWidth它们决定了每个条目的尺寸。如果条目尺寸不一致用UListView更合适。关于UWrapBox它适合做自动换行的布局比如标签云。但它的布局计算比HorizontalBox复杂条目多时性能会下降。关于UUniformGridPanel它适合做等大小的网格布局比如背包格子。它的性能比GridPanel好因为所有格子尺寸相同布局计算可以批量处理。关于UWidgetSwitcher的SetActiveWidgetIndex切换时会触发Invalidate如果切换频繁考虑用Visibility来控制显示隐藏而不是用Switcher。关于UUserWidget的SetVisibility它会影响整个Widget树的可见性。如果只是想让某个子控件隐藏直接设置子控件的Visibility不要动父级的。关于UCanvasPanelSlot的SetPosition和SetSize它们会触发Invalidate。如果每帧都调用比如做拖拽考虑用RenderTransform的Translation来替代性能更好。关于UUserWidget的SetRenderOpacity它只影响绘制不影响布局性能比修改Visibility好。做淡入淡出效果时优先用它。关于UImage的SetColorAndOpacity它也是只影响绘制性能好。做颜色变化效果时用它。关于UUserWidget的SetRenderScale和SetRenderTransform它们都只影响绘制不影响布局。做缩放和旋转动画时用它们不要用SetDesiredSizeInViewport之类的布局属性。关于UUserWidget的SetPositionInViewport它会触发布局重算。如果只是做位移动画用RenderTransform的Translation。关于UUserWidget的SetDesiredSizeInViewport它会触发布局重算。如果只是做尺寸动画用RenderTransform的Scale。关于UUserWidget的SetAnchorsInViewport它会触发布局重算。如果只是做锚点变化考虑用CanvasPanelSlot的SetAnchors但同样会触发重算。最好的办法是避免频繁改变锚点。关于UUserWidget的SetAlignmentInViewport它也会触发布局重算。同样的用RenderTransform替代。关于UUserWidget的SetOffsets它也会触发布局重算。如果只是做位移动画用RenderTransform。关于UUserWidget的SetAutoSize它会触发布局重算。如果只是想让Widget自适应内容在编辑器里设置好就行不要在运行时频繁切换。关于UUserWidget的SetPadding它会触发布局重算。如果只是想让内容有间距在Slot上设置Padding不要在运行时频繁修改。关于UUserWidget的SetIsEnabled它会影响输入事件的响应但不会触发布局重算。做禁用状态切换时用它。关于UUserWidget的SetFocus它会把键盘焦点设置到指定Widget。做键盘导航时用它。关于UUserWidget的SetKeyboardFocus它和SetFocus的区别是前者只设置键盘焦点后者还会设置用户焦点。一般情况下用SetFocus就够了。关于UUserWidget的SetAllNavigationRules它设置导航规则。做复杂的键盘导航时用它。关于UUserWidget的SetNavigationRule它设置单个方向的导航规则。做精细的导航控制时用它。关于UUserWidget的SetIsVolatile它标记Widget为易变的每帧都会重绘。如果Widget内容每帧都变比如实时数据设置它为true可以避免缓存失效的判断开销。但不要滥用否则会失去缓存的好处。关于UUserWidget的SetIsEnabled它会影响输入事件的响应但不会触发布局重算。做禁用状态切换时用它。关于UUserWidget的Invalidate它标记Widget需要重绘。一般情况下不需要手动调用属性变化会自动触发。只有在特殊情况下比如自定义绘制的内容变了才需要手动调用。关于UUserWidget的ForceLayoutPrepass它强制立即执行布局计算。在某些需要立即获取布局结果的场景下用它但不要频繁调用因为布局计算是昂贵的。关于UUserWidget的GetCachedGeometry它获取缓存的几何信息。如果布局没有变化用它可以避免重新计算。关于UUserWidget的GetTickSpaceGeometry它获取Tick时的几何信息。在NativeTick里用它来获取当前Widget的尺寸和位置。关于UUserWidget的GetPaintSpaceGeometry它获取绘制时的几何信息。在NativePaint里用它。关于UUserWidget的NativePaint它允许在UMG的绘制流程中插入自定义绘制。如果需要用Slate的绘制API在UMG上画东西重写这个函数。关于UUserWidget的NativeOnPaint它和NativePaint的区别是前者在UMG的绘制之后调用后者在之前。根据绘制层级需求选择。关于UUserWidget的NativeOnMouseButtonDown等输入事件它们允许在UMG层面处理输入。如果Slate层的输入处理不够用可以重写这些函数。关于UUserWidget的NativeOnDragDetected它处理拖拽检测。做拖拽功能时用它。关于UUserWidget的NativeOnDrop它处理拖拽释放。做拖拽功能时用它。关于UUserWidget的NativeOnDragEnter和NativeOnDragLeave它们处理拖拽进入和离开。做拖拽高亮效果时用它们。关于UUserWidget的NativeOnDragOver它处理拖拽悬停。做拖拽预览时用它。关于UUserWidget的NativeOnMouseWheel它处理鼠标滚轮。做缩放或滚动时用它。关于UUserWidget的NativeOnKeyDown和NativeOnKeyUp它们处理键盘按键。做快捷键时用它们。关于UUserWidget的NativeOnAnalogValueChanged它处理模拟输入比如手柄摇杆。做手柄导航时用它。关于UUserWidget的NativeOnTouchGesture它处理触摸手势。做移动端手势操作时用它。关于UUserWidget的NativeOnTouchStarted、NativeOnTouchMoved、NativeOnTouchEnded它们处理触摸事件。做精细的触摸控制时用它们。关于UUserWidget的NativeOnMotionDetected它处理运动检测。做体感操作时用它。关于UUserWidget的NativeOnMouseEnter和NativeOnMouseLeave它们处理鼠标进入和离开。做悬停效果时用它们。关于UUserWidget的NativeOnRemovedFromFocusPath和NativeOnAddedToFocusPath它们处理焦点路径变化。做焦点高亮时用它们。关于UUserWidget的NativeOnFocusChanging它处理焦点变化。做焦点切换逻辑时用它。关于UUserWidget的NativeOnFocusReceived和NativeOnFocusLost它们处理焦点获得和失去。做焦点状态管理时用它们。关于UUserWidget的NativeOnKeyChar它处理字符输入。做文本输入时用它。关于UUserWidget的NativeOnPreviewKeyDown和NativeOnPreviewKeyUp它们在NativeOnKeyDown之前调用可以拦截按键事件。做全局快捷键时用它们。关于UUserWidget的NativeOnPreviewMouseButtonDown它在NativeOnMouseButtonDown之前调用可以拦截鼠标事件。做全局鼠标处理时用它。关于UUserWidget的NativeOnPreviewMouseWheel它在NativeOnMouseWheel之前调用可以拦截滚轮事件。关于UUserWidget的NativeOnPreviewTouchGesture它在NativeOnTouchGesture之前调用可以拦截触摸手势。关于UUserWidget的NativeOnPreviewTouchStarted、NativeOnPreviewTouchMoved、NativeOnPreviewTouchEnded它们在对应的非Preview版本之前调用可以拦截触摸事件。关于UUserWidget的NativeOnPreviewMotionDetected它在NativeOnMotionDetected之前调用可以拦截运动检测。关于UUserWidget的NativeOnPreviewMouseEnter和NativeOnPreviewMouseLeave它们在对应的非Preview版本之前调用可以拦截鼠标进入和离开。关于UUserWidget的NativeOnPreviewFocusChanging它在NativeOnFocusChanging之前调用可以拦截焦点变化。关于UUserWidget的NativeOnPreviewFocusReceived和NativeOnPreviewFocusLost它们在对应的非Preview版本之前调用可以拦截焦点获得和失去。关于UUserWidget的NativeOnPreviewKeyChar它在NativeOnKeyChar之前调用可以拦截字符输入。这些Preview函数在需要做全局拦截或者优先级处理时非常有用但要注意不要滥用否则会让输入事件的传递链路变得难以调试。最后说一个我个人的习惯在做任何UMG界面之前先在纸上画一下Widget树的层级结构标清楚每个Panel的用途和每个控件的Slot类型。这个习惯能避免很多后期的布局调整和性能问题。UI这东西结构设计好了后面就是填内容结构没设计好后面就是不停地打补丁。