2.3 事件、委托与属性绑定

委托的三种写法

SNew(SButton)
// ① 无返回值的用 OnClicked_Lambda / OnPressed_Lambda
.OnClicked_Lambda([]() { DoSomething(); return FReply::Handled(); })

// ② 成员函数
.OnClicked(this, &FMyTool::OnButtonClicked)                    // 原生 C++ 类
.OnClicked_Raw(MyUObject, &UMyObject::HandleClicked)           // UObject

// ③ 弱引用绑定(推荐:对象销毁后安全)
SNew(SButton).OnClicked(this, &FMyTool::OnClicked)             // TSharedPtr 持有者安全

最常出的事故:OnClicked_Raw 绑了一个 UObject 指针,对象被 GC/销毁后按钮事件调到野指针 → 崩溃。规则:

  • 绑 UObject 成员函数一律用 SPROPERTY 的安全绑定或检查对象有效性
  • 绑原生 C++ 对象用 OnClicked(this, ...)(持有 TSharedPtr 的场景)或确保生命周期长于 UI
  • lambda 捕获 this 前,确认 UI 与对象的生命周期关系

常用事件

控件 事件
SButton OnClicked / OnPressed / OnHovered / OnReleased
SCheckBox OnCheckStateChanged
SEditableTextBox OnTextChanged / OnTextCommitted(回车/失焦)
SComboBox OnSelectionChanged
SListView OnSelectionChanged / OnMouseButtonDoubleClick / OnGenerateRow
SSlider OnValueChanged

ListView 的完整事件链

SListView 是编辑器工具里最复杂也最有用的控件,关键回调是 OnGenerateRow——为每个列表项生成一个行控件:

SNew(SListView<TSharedPtr<FListItem>>)
.ItemHeight(24.0f)
.ListItemsSource(&Items)                 // 数据源:TArray<TSharedPtr<Item>>
.OnGenerateRow_Lambda([](TSharedPtr<FListItem> Item, const TSharedRef<STableViewBase>& OwnerTable)
{
    return SNew(STableRow<TSharedPtr<FListItem>>, OwnerTable)
    [
        SNew(STextBlock).Text(Item->DisplayName)
    ];
})
.OnSelectionChanged_Lambda([](TSharedPtr<FListItem> Item, ESelectInfo::Type)
{
    if (Item.IsValid()) ShowDetails(Item);
})

要点:

  • 数据源是外部 TArray 的指针,ListView 不持有数据——数据变化时调 ListView->RequestListRefresh() 通知重绘
  • Item 用 TSharedPtr<FStruct> 而不是 UObject 指针——列表项频繁创建销毁,避免 GC 压力
  • 大量数据(>100)必须确认开启了虚拟化(SListView 默认按需生成行)

属性绑定(TAttribute)

属性绑定的回归函数在每次 Tick/重绘时调用,所以:

// 对:轻量函数
SNew(STextBlock).Text_Lambda([](){ return GetCountText(); })

// 错:重计算(每帧扫资产)
SNew(STextBlock).Text_Lambda([](){ return CountAllAssetsSlowly(); })   // 卡

绑定函数必须快(O(1) 级别)。重活做成:数据变化时缓存结果 → 绑定读缓存。

双向同步:控件值 ↔ 数据

想「用户改输入框 → 更新数据;数据变 → 输入框刷新」,需要:

// 输入框
SNew(SEditableTextBox)
.Text_Lambda([]() { return DataToText(CachedValue); })          // 数据→UI
.OnTextChanged_Lambda([](const FText& T) { CachedValue = TextToData(T); })  // UI→数据

这套「双向绑定」得自己写,Slate 没有现成的 MVVM(UE5 有 MVVM 插件,但工具 UI 一般不值得引入)。UMG 的「属性绑定」蓝图等价于这里的 TAttribute。

Tick 事件

SWidget 默认不 Tick。需要逐帧逻辑时用 SNew(SWidget).Tick_Lambda(...) 或从 SCompoundWidget 派生重写 Tick()。工具 UI 的 Tick 是卡顿来源——能用事件驱动(OnTextChanged 等)就别 Tick。

心得:UI 代码崩溃 80% 来自「事件绑定的对象已销毁」。Slate 工具上线前必做的一步:故意在工具运行中删除资产/关闭关卡/热重载,看事件还有没有触发崩溃。能熬过这套「破坏测试」的工具,线上基本不炸。