【UE】Slate 编辑器工具开发01 - Slate语法 & 常用控件

Slate 框架

Slate 是 UE 自己实现的一套 C++ 声明式 UI 框架,编辑器几乎所有界面(关卡编辑器、内容浏览器、蓝图编辑器等)都是用 Slate 写的。

它的特点:

  1. 纯 C++,不依赖 UMG/UObject/反射系统;
  2. 使用大量宏(SNew/SAssignNew/SLATE_BEGIN_ARGS 等)实现 "声明式" 语法;
  3. 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 有了,现在需要通过ExtenderTabManager 来将其注入到编辑器菜单中

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 的可预览编辑的工具窗口

相关推荐
qq_170264751 小时前
unity的锁帧和垂直同步
unity·游戏引擎
unityのkiven12 小时前
Unity 共享材质修改问题与 MaterialPropertyBlock 解决方案
unity·游戏引擎·材质
丁小未13 小时前
Unity 极致高效的IM设计方案教程
unity·性能优化·游戏引擎·im
unityのkiven13 小时前
Unity UGUI 中 EventTrigger 导致 ScrollRect 鼠标滚轮失效的问题分析与解决
unity·计算机外设·游戏引擎
丁小未17 小时前
Unity动态图集方案
unity·游戏引擎
Soari18 小时前
【突发暴更】Claude Code v2.1.208 :无障碍辅助模式正式上线、Vim 连击热键出逃、大修内存泄漏与跨域 AWS SSO 登录崩溃!
编辑器·vim·aws·claude code
朗迹 - 张伟18 小时前
UE5.7 PCG从入门到精通系列课程【第五课:PCG沿一条样条线生成物体】
ue5·pcg
AA陈超20 小时前
003 XiYou 西游 — P0 阶段实施计划
c++·笔记·学习·ue5