Slate 框架
Slate 是 UE 自己实现的一套 C++ 声明式 UI 框架,编辑器几乎所有界面(关卡编辑器、内容浏览器、蓝图编辑器等)都是用 Slate 写的。
它的特点:
- 纯 C++,不依赖 UMG/UObject/反射系统;
- 使用大量宏(SNew/SAssignNew/SLATE_BEGIN_ARGS 等)实现 "声明式" 语法;
- Widget 均以 S 开头(Slate Widget),通过 TSharedPtr / TSharedRef 管理生命周期。
Slate 声明式语法
┌────────────────────────────────────────────────────────────┐
│ SNew(SXxx) │ 创建一个新 Widget,类型SXxx,返回可链式的对象
│ .PropA(value) │ 设置属性 A
│ .PropB(value) │ 设置属性 B
│ [ Child ] │ 设置子 Widget([] 相当于 SetContent)
│ │
│ 容器 (如 SVerticalBox / SHorizontalBox / SScrollBox) │ 可以放多个子 Widget
│ + SVerticalBox::Slot() │ 用 + Slot() 语法追加子槽位
│ .AutoHeight().Padding(...).VAlign(...) │ 设置槽位属性
│ [ SomeWidget ] │ 槽位的内容
│ │
│ SAssignNew(MemberPtr, SXxx) │ = SNew + 把返回值赋给成员指针供后续访问
│ │
│ .OnClicked(this, &Class::Fn) / .OnClicked_Lambda(...) │ 绑定事件(后者可就地写 Lambda)
│ .Text_Lambda([this]() -> FText { ... }) │ 绑定属性字段(每帧调用,实现动态 UI)
└────────────────────────────────────────────────────────────┘
示例
SNew(SBorder) //创建容器
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) //设置背景
.Padding(8.f) //内边距8
[
SNew(SScrollBox) //滑动列表
+ SScrollBox::Slot() //增加一个滑动列表插槽
.Padding(0, 0, 0, 8) //下边距8 (左、上、右、下)
[
SNew(STextBlock) //创建文本块
.Text(FText::FromString("xxx")) //设置文本
.Font(FAppStyle::GetFontStyle("HeadingLarge")) //设置字体
.ColorAndOpacity(FLinearColor::Yellow) //设置颜色和不透明度
]
+ SScrollBox::Slot() //增加一个滑动列表插槽
[
SNew(SSeparator) //创建分界线
]
]
常用控件头文件
#include "Widgets/SBoxPanel.h" // SVerticalBox / SHorizontalBox
#include "Widgets/Layout/SBorder.h" // 有背景的容器
#include "Widgets/Layout/SBox.h" // 固定尺寸 / 占位容器
#include "Widgets/Layout/SScrollBox.h" // 可滚动容器
#include "Widgets/Layout/SExpandableArea.h" // 可折叠区域
#include "Widgets/Layout/SSeparator.h" // 分隔线
#include "Widgets/Layout/SUniformGridPanel.h" // 等分网格容器
#include "Widgets/Text/STextBlock.h" // 只读文本
#include "Widgets/Input/SButton.h" // 按钮
#include "Widgets/Input/SEditableTextBox.h" // 单行输入框
#include "Widgets/Input/SMultiLineEditableTextBox.h" // 多行文本框
#include "Widgets/Input/SCheckBox.h" // 复选框
#include "Widgets/Input/SSlider.h" // 滑动条
#include "Widgets/Input/SSpinBox.h" // 数字微调框
#include "Widgets/Input/SComboBox.h" // 下拉框
#include "Widgets/Notifications/SProgressBar.h" // 进度条
#include "Widgets/Views/SListView.h" // 列表视图
#include "Widgets/Images/SImage.h" // 图片
#include "Styling/AppStyle.h" // 编辑器统一样式(图标、字体、画刷)
常用控件
现在来创建一个工具窗口,使用到常用控件
首先编写Widget代码
MyWidgetTool.h
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
class SMyWidgetTool : public SCompoundWidget
{
public:
// 这两个宏会展开出一个内嵌类型 FArguments,用来承载 SNew() 时通过
// 链式调用传入的参数(例如 .Text(...), .OnClicked(...) 等)
// 如果需要外部传参,可在中间加入:
// SLATE_ARGUMENT(FText, InitialText) // 普通值参数
// SLATE_ATTRIBUTE(FText, DynamicText) // TAttribute 可绑定参数
// SLATE_EVENT(FSimpleDelegate, OnSomething) // 委托事件
SLATE_BEGIN_ARGS(SMyWidgetTool) {}
SLATE_END_ARGS()
// 构造Widget
void Construct(const FArguments& InArgs);
// 向 LogTextBox 添加日志
void AddToLog(const FString& Message);
private:
// 滑动条值
float SliderValue = 0;
// 下拉框选项
TArray<TSharedPtr<FString>> ComboOption;
// 当前下拉框选中
FString SelectedComboOption;
// 列表选项
TArray<TSharedPtr<FString>> ListItems;
// 日志框
TSharedPtr<class SMultiLineEditableTextBox> LogTextBox;
};
MyWidgetTool.cpp
#include "MyWidgetTool.h"
#include "Widgets/Input/SSpinBox.h"
#include "Widgets/Input/SSlider.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Layout/SUniformGridPanel.h"
#include "Widgets/Notifications/SProgressBar.h"
void SMyWidgetTool::Construct(const FArguments& InArgs)
{
// 设置滑动条默认值
SliderValue = 0.5f;
// 添加下拉框选项
ComboOption.Add(MakeShared<FString>(TEXT("Option 1")));
ComboOption.Add(MakeShared<FString>(TEXT("Option 2")));
ComboOption.Add(MakeShared<FString>(TEXT("Option 3")));
SelectedComboOption = *ComboOption[0];
// 添加列表选项
for (int i = 0; i < 6; i++)
{
ListItems.Add(MakeShared<FString>(FString::Printf(TEXT("List Item %d"), i)));
}
// SCompoundWidget 默认子插槽
ChildSlot
[
// 最外层背景容器
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
.Padding(8.f)
[
// 滑动容器
SNew(SScrollBox)
+ SScrollBox::Slot()
.Padding(0, 0, 0, 8)
[
// 文本块
SNew(STextBlock)
.Text(FText::FromString("Test My Widget Tool"))
.Font(FAppStyle::GetFontStyle("HeadingLarge"))
.ColorAndOpacity(FLinearColor::Yellow)
]
+ SScrollBox::Slot()
[
// 分界线
SNew(SSeparator)
]
+ SScrollBox::Slot()
.Padding(0, 8)
[
// 水平布局容器
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.AutoWidth()
[
SNew(STextBlock).Text(FText::FromString("InputField: "))
]
+ SHorizontalBox::Slot()
[
// 输入框
SNew(SEditableTextBox)
.HintText(FText::FromString("Input something..."))
.OnTextCommitted(FOnTextCommitted::CreateLambda([this](const FText& NewText, ETextCommit::Type CommitType)
{
if (CommitType == ETextCommit::Type::OnEnter)
{
this->AddToLog(FString::Printf(TEXT("Input: %s"), *NewText.ToString()));
}
}))
]
]
+ SScrollBox::Slot()
.Padding(0, 8)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(0, 0, 4, 0)
[
// 勾选框
SNew(SCheckBox)
.IsChecked(false)
.OnCheckStateChanged(FOnCheckStateChanged::CreateLambda([this](ECheckBoxState NewState)
{
this->AddToLog(FString::Printf(TEXT("CheckBox: %s"), NewState == ECheckBoxState::Checked ? TEXT("Checked") : TEXT("Unchecked")));
}))
[
SNew(STextBlock).Text(FText::FromString("CheckBox"))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(SBox).WidthOverride(120.f)
[
// 数字微调框
SNew(SSpinBox<int32>)
.MinValue(0).MaxValue(10)
.Value(2)
.OnValueChanged(FOnInt32ValueChanged::CreateLambda([this](int32 NewValue)
{
this->AddToLog(FString::Printf(TEXT("SpinBox value changed: %d"), NewValue));
}))
]
]
]
+ SScrollBox::Slot()
.Padding(0, 8)
[
// 垂直布局容器
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 0, 0, 8)
[
// 滑动条
SNew(SSlider)
.Value(SliderValue)
.OnValueChanged_Lambda([this](float NewValue)
{
this->SliderValue = NewValue;
})
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 0, 0, 8)
[
// 进度条
SNew(SProgressBar)
.Percent_Lambda([this]() -> TOptional<float>
{
return this->SliderValue;
})
]
]
+ SScrollBox::Slot()
.Padding(0, 8)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock).Text(FText::FromString("ComboOption"))
]
+ SHorizontalBox::Slot()
[
// 下拉框
SNew(SComboBox<TSharedPtr<FString>>)
.OptionsSource(&ComboOption)
// 下拉框选项样式
.OnGenerateWidget(SComboBox<TSharedPtr<FString>>::FOnGenerateWidget::CreateLambda([](TSharedPtr<FString> Item)->TSharedRef<SWidget>
{
return SNew(STextBlock).Text(FText::FromString(*Item));
}))
.OnSelectionChanged(SComboBox<TSharedPtr<FString>>::FOnSelectionChanged::CreateLambda([this](TSharedPtr<FString> InItem, ESelectInfo::Type SelectType)
{
if (InItem.IsValid())
{
SelectedComboOption = *InItem;
this->AddToLog(FString::Printf(TEXT("SelectItem: %s"), **InItem));
}
}))
.InitiallySelectedItem(ComboOption[0])
[
// 下拉框选中显示
SNew(STextBlock).Text_Lambda([this]()
{
return FText::FromString(SelectedComboOption);
})
]
]
]
+ SScrollBox::Slot()
.Padding(0, 8)
[
// 可展开收起容器
SNew(SExpandableArea)
.AreaTitle(FText::FromString("ExpandableArea"))
.InitiallyCollapsed(false)
.BodyContent()
[
SNew(SBox)
.HeightOverride(160.f)
[
// 列表容器
SNew(SListView<TSharedPtr<FString>>)
.ListItemsSource(&ListItems)
.SelectionMode(ESelectionMode::Single)
.OnGenerateRow(SListView<TSharedPtr<FString>>::FOnGenerateRow::CreateLambda([](TSharedPtr<FString> InItem, const TSharedRef<STableViewBase>& OwnerTable) -> TSharedRef<ITableRow>
{
return SNew(STableRow<TSharedPtr<FString>>, OwnerTable)
.Padding(FMargin(4.f))
[
SNew(STextBlock).Text(FText::FromString(*InItem))
];
}))
]
]
]
+ SScrollBox::Slot()
.Padding(0, 8)
[
// 网格布局容器
SNew(SUniformGridPanel)
.SlotPadding(FMargin(4.f))
+ SUniformGridPanel::Slot(0, 0)[SNew(SButton).Text(FText::FromString("Button1"))]
+ SUniformGridPanel::Slot(1, 0)[SNew(SButton).Text(FText::FromString("Button2"))]
+ SUniformGridPanel::Slot(2, 0)[SNew(SButton).Text(FText::FromString("Button3"))]
+ SUniformGridPanel::Slot(0, 1)[SNew(SButton).Text(FText::FromString("Button4"))]
+ SUniformGridPanel::Slot(1, 1)[SNew(SButton).Text(FText::FromString("Button5"))]
+ SUniformGridPanel::Slot(2, 1)[SNew(SButton).Text(FText::FromString("Button6"))]
]
+ SScrollBox::Slot()
[
SNew(SSeparator)
]
+ SScrollBox::Slot()
[
SNew(SBox)
.HeightOverride(140.f)
[
// 多行文本框
SAssignNew(LogTextBox, SMultiLineEditableTextBox)
.AlwaysShowScrollbars(true)
.IsReadOnly(true)
.Text(FText::FromString("=== Log ===\n"))
]
]
]
];
}
void SMyWidgetTool::AddToLog(const FString& Message)
{
LogTextBox->SetText(FText::FromString(LogTextBox->GetText().ToString() + Message + TEXT("\n")));
UE_LOG(LogTemp, Warning, TEXT("%s"), *Message);
}
Widget 有了,现在需要通过Extender 和TabManager 来将其注入到编辑器菜单中
MyTestVI.cpp (Game Module入口)
#include "MyTestVI.h"
#include "Modules/ModuleManager.h"
#include "LevelEditor.h"
#include "Framework/Docking/TabManager.h"
#include "Widget/MyWidgetTool.h"
static const FName MyWidgetToolTabName("MyWidgetTool");
static void RegistryMyWidgetTool()
{
// 加载 LevelEditor 模块
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
// 创建Extender
TSharedPtr<FExtender> Extender = MakeShared<FExtender>();
Extender->AddMenuExtension(
"LevelEditor",
EExtensionHook::After,
nullptr,
FMenuExtensionDelegate::CreateLambda([](FMenuBuilder& MenuBuilder)
{
// Extender行为,也可通过参数 const TSharedPtr< FUICommandList >& CommandList 实现
MenuBuilder.AddMenuEntry(
FText::FromString("MyWidgetTool"),
FText::FromString("Open MyWidgetTool"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateLambda([]() {
// 通过 FGlobalTabmanager 拉起窗口
FGlobalTabmanager::Get()->TryInvokeTab(MyWidgetToolTabName);
})
));
}));
// 注册Extender
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(Extender);
// 注册 MyWidgetToolTabName 和其行为到 FGlobalTabmanager
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
MyWidgetToolTabName,
FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args) -> TSharedRef<SDockTab>
{
return SNew(SDockTab).TabRole(ETabRole::NomadTab)
[
SNew(SMyWidgetTool)
];
}))
.SetMenuType(ETabSpawnerMenuType::Hidden)
.SetDisplayName(FText::FromName(MyWidgetToolTabName));
}
class FMyTestVIModule : public FDefaultGameModuleImpl
{
virtual void StartupModule() override
{
#if WITH_EDITOR
if (GIsEditor)
{
RegistryMyWidgetTool();
}
#endif
}
virtual void ShutdownModule() override
{
#if WITH_EDITOR
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(MyWidgetToolTabName);
#endif
}
};
IMPLEMENT_PRIMARY_GAME_MODULE(FMyTestVIModule, MyTestVI, "MyTestVI");
Build模块加入Slate相关依赖
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
重新生成项目,编译运行


关于Extender扩展点
在 AddMenuExtension时传入了LevelEditor 这个扩展点,表示了我们的菜单选项插入的位置
我们可以在Preference中开启一个选项来显示扩展点


下一篇将实现一个类似 StaticMesh Editor 的可预览编辑的工具窗口