AssetTagsEditor 模块全面系统分析
源文档:https://gitee.com/chouchouxia/share-ue5
目录
1 模块概述
1.1 基本信息
| 属性 | 值 |
|---|---|
| 模块名称 | AssetTagsEditor |
| 类型 | Editor(编辑器模块) |
| 位置 | Engine/Source/Editor/AssetTagsEditor |
| 描述 | 提供内容浏览器中资产标签(Asset Tags)的 Slate UI 控件,包括标签项的显示、勾选、重命名、计数和工具提示功能 |
整个模块只有 5 个文件,代码量非常小,但它在 UE5 编辑器的内容浏览器中扮演着不可忽视的角色------我们每天在内容浏览器左侧看到的收藏夹(Collections)树、标签筛选面板,其每个条目都是由这个模块提供的控件渲染出来的。
模块的核心由一个 Slate 复合控件 SAssetTagItem 构成,辅以 SAssetTagItemTableRow 表格行容器、SAssetTagItemToolTip 自定义工具提示和 AssetTagItemTypes.h 中的类型定义。依赖关系非常简洁,只依赖了 Core、CoreUObject、Slate、SlateCore 四个引擎基础模块。
2 模块整体架构解析
2.1 架构图
这个模块的架构相当扁平,没有复杂的继承层次,也没有子系统之间的耦合。它本质上就是一个"UI 控件 + 类型定义"的工具包:
┌─────────────────────────────────────────────────────────────────────┐
│ 内容浏览器 (Content Browser) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ SAssetTagItemTableRow<ItemType> (模板表格行) │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ SAssetTagItem (核心控件) │ │ │
│ │ │ ┌──────────┐ ┌──────────────┐ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │ SCheckBox │ │ SInlineEdit- │ │ SImage │ │ SBorder│ │ │ │
│ │ │ │ (勾选框) │ │ ableTextBlock│ │(警告) │ │(计数) │ │ │ │
│ │ │ └──────────┘ │ (名称/重命名)│ └────────┘ └────────┘ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ SAssetTagItemToolTip (自定义工具提示) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AssetTagItemTypes.h (类型定义层) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ EAssetTagItemViewMode : Standard / Compact │ │
│ │ FOnBuildAssetTagItemToolTipInfoEntry : TFunctionRef │ │
│ │ FOnBuildAssetTagItemToolTipInfo : DECLARE_DELEGATE_OneParam │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
2.2 模块间依赖关系
AssetTagsEditor 模块
↓ 依赖
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Core │ CoreUObject │ Slate │ SlateCore │
│ (基础类型) │ (UObject系统) │ (UI框架) │ (UI核心) │
└──────────────┴──────────────┴──────────────┴──────────────┘
仅依赖四个引擎基础模块,没有任何外部插件依赖。这意味着 AssetTagsEditor 是一个自包含的 UI 工具包------任何需要展示资产标签列表的编辑器功能都可以直接引用它。
2.3 模块划分
1. 类型定义层 --- AssetTagItemTypes.h
职责:
- 定义
EAssetTagItemViewMode枚举,区分Standard和Compact两种展示模式 - 定义
FOnBuildAssetTagItemToolTipInfo委托类型,用于外部向工具提示中注入自定义信息行
特点:
- 极简设计,只定义必要的类型别名
- 使用
TFunctionRef作为回调,避免不必要的拷贝开销
2. UI 控件层 --- SAssetTagItem
职责:
- 渲染单个资产标签条目的完整 UI(勾选框 + 名称 + 警告图标 + 计数徽章)
- 支持内联重命名(通过
SInlineEditableTextBlock) - 支持选中高亮和拖放目标状态
- 根据
ViewMode自动切换 Standard / Compact 样式
特点:
- 使用 Slate 声明式语法构建 UI
- 所有外观属性通过
TAttribute绑定,支持动态更新 - 可选功能(勾选框、重命名、计数、警告)通过参数是否绑定的方式自动启用/禁用
3. 表格行容器 --- SAssetTagItemTableRow
职责:
- 将
SAssetTagItem包装为STableRow,使其可在SListView/STreeView中使用 - 提供统一的选中边框和拖放目标边框样式
特点:
- 模板类,可适配任意
ItemType - 自动处理选中边框的外边距,确保视觉一致性
4. 工具提示 --- SAssetTagItemToolTip
职责:
- 在鼠标悬停时显示标签名称和详细信息
- 通过
FOnBuildAssetTagItemToolTipInfo委托让外部注入自定义信息行
特点:
- 继承自
SToolTip,重写OnOpening延迟构建内容 - 布局与内容浏览器瓦片视图的工具提示风格一致
2.4 数据流走向
标准渲染流程:
1. 外部代码(如内容浏览器)创建 SAssetTagItemTableRow
↓
2. SAssetTagItemTableRow 内部创建 SAssetTagItem 作为内容
↓
3. SAssetTagItem::Construct() 被调用
├─ 创建 SAssetTagItemToolTip 作为工具提示
├─ 根据 ViewMode 选择样式后缀 (Standard / Compact)
├─ 根据 OnNameCommitted 是否绑定,决定使用 SInlineEditableTextBlock 还是 STextBlock
├─ 根据 IsChecked / OnCheckStateChanged 是否绑定,决定是否启用 SCheckBox
├─ 根据 WarningText 是否设置,决定是否显示警告图标
└─ 根据 CountText 是否设置,决定是否显示计数徽章
↓
4. 渲染完成,显示在内容浏览器中
重命名流程:
1. 用户右键点击标签 → 外部代码调用 SAssetTagItem::RequestRename()
↓
2. InlineRenameWidget->EnterEditingMode() 被调用
↓
3. SInlineEditableTextBlock 进入编辑模式
↓
4. 用户输入新名称 → OnVerifyName 验证 → OnNameCommitted 提交
↓
5. 外部代码处理重命名逻辑(如更新 Collection 名称)
工具提示显示流程:
1. 用户鼠标悬停在 SAssetTagItem 上
↓
2. SToolTip::OnOpening() 被触发
↓
3. SAssetTagItemToolTip::CreateToolTipWidget() 构建内容
├─ 顶部区域:显示标签名称
└─ 底部区域:调用 OnBuildToolTipInfo 委托获取详细信息
↓
4. 工具提示显示
2.5 核心技术栈
| 技术/类 | 用途 |
|---|---|
SCompoundWidget |
SAssetTagItem 的基类,Slate 复合控件 |
STableRow |
SAssetTagItemTableRow 的基类,表格行 |
SToolTip |
SAssetTagItemToolTip 的基类,工具提示 |
SCheckBox |
勾选框,用于选中/取消选中标签 |
SInlineEditableTextBlock |
内联可编辑文本,用于标签重命名 |
STextBlock |
静态文本,用于显示标签名称和计数 |
SImage |
警告图标 |
SBorder |
计数徽章容器 |
SBox |
计数徽章尺寸控制 |
TAttribute |
属性绑定,实现响应式 UI 更新 |
TFunctionRef |
轻量级函数引用,用于工具提示构建回调 |
FAppStyle |
编辑器样式系统,获取统一的样式参数 |
3 类级代码注释详解
3.1 AssetTagItemTypes.h --- 类型定义
3.1.1 概述
AssetTagItemTypes.h 是整个模块的类型定义文件,虽然代码量极少,但它是模块的"约定层"------定义了所有控件之间通信的枚举和委托类型。
核心设计理念:
- 极简:只定义必要的类型,不引入任何不必要的抽象
- 约定优于配置:通过统一的委托类型,使外部代码遵循一致的接口约定
3.1.2 EAssetTagItemViewMode 枚举
cpp
enum class EAssetTagItemViewMode : uint8
{
Standard,
Compact,
};
功能分析:
两个枚举值控制 SAssetTagItem 的视觉样式:
Standard(标准模式):适用于内容浏览器的默认视图,标签条目有正常的间距和字体大小Compact(紧凑模式):适用于空间受限的场景(如侧边栏),标签条目使用更紧凑的间距和更小的字体
实现原理:
在 SAssetTagItem::Construct() 中,该枚举值会被转换为一个样式后缀字符串指针:
cpp
const ANSICHAR* StyleSpecifier = nullptr;
if (InArgs._ViewMode == EAssetTagItemViewMode::Compact)
{
StyleSpecifier = ".Compact";
}
然后通过 FAppStyle::GetMargin("ContentBrowser.AssetTagNamePadding", StyleSpecifier) 来获取对应的样式参数。当 StyleSpecifier 为 nullptr 时,FAppStyle 会使用默认键名;当为 ".Compact" 时,会在键名后追加 .Compact 后缀去查找对应的紧凑样式。
3.1.3 委托类型定义
cpp
typedef TFunctionRef<void(const FText&, const FText&)> FOnBuildAssetTagItemToolTipInfoEntry;
DECLARE_DELEGATE_OneParam(FOnBuildAssetTagItemToolTipInfo, const FOnBuildAssetTagItemToolTipInfoEntry&);
功能分析:
这两个类型构成了工具提示内容构建的回调链路:
FOnBuildAssetTagItemToolTipInfoEntry:一个TFunctionRef,由工具提示内部调用,用来往信息框中添加一行(Key-Value 对)。使用TFunctionRef而非TFunction意味着它不持有函数对象的所有权,避免了不必要的堆分配FOnBuildAssetTagItemToolTipInfo:由外部代码绑定的委托。当工具提示需要构建信息内容时,会将一个FOnBuildAssetTagItemToolTipInfoEntry实例传递给这个委托,外部代码调用这个实例来逐行添加信息
使用模式:
cpp
// 外部代码绑定委托
OnBuildToolTipInfo.BindLambda([](const FOnBuildAssetTagItemToolTipInfoEntry& AddEntry)
{
AddEntry(LOCTEXT("Key1", "类型"), LOCTEXT("Value1", "收藏夹"));
AddEntry(LOCTEXT("Key2", "包含资产数"), FText::AsNumber(42));
});
这种设计巧妙地将"如何构建 UI"(由 SAssetTagItemToolTip 负责)和"要显示什么数据"(由外部代码负责)解耦开来了。
3.2 SAssetTagItem --- 核心标签条目控件
3.2.1 概述
SAssetTagItem 是整个模块的核心,它继承自 SCompoundWidget,是一条资产标签在内容浏览器中的完整 UI 呈现。它不是一个抽象基类,而是一个可以直接使用的具体控件------外部代码通过 SLATE_BEGIN_ARGS/SLATE_END_ARGS 宏定义的参数系统来定制它的外观和行为。
核心设计理念:
- 可选功能自动启用:勾选框、重命名、警告图标、计数徽章都是可选的,通过是否绑定对应参数来决定是否启用
- 属性绑定驱动 :所有外观属性(颜色、文本、可见性)都通过
TAttribute绑定,支持外部动态更新 - 声明式 UI 构建:使用 Slate 的声明式语法,UI 结构一目了然
3.2.2 SLATE_BEGIN_ARGS 参数系统
SAssetTagItem 的参数系统设计得非常完善,我们逐个来看:
| 参数 | 类型 | 必须 | 作用 |
|---|---|---|---|
ViewMode |
EAssetTagItemViewMode |
否 | Standard 或 Compact 样式 |
BaseColor |
TAttribute<FLinearColor> |
否 | 标签的基础颜色,派生明亮/暗淡/禁用三种状态 |
DisplayName |
TAttribute<FText> |
是 | 标签的显示名称 |
CountText |
TAttribute<FText> |
否 | 计数文本,不设置则隐藏计数徽章 |
WarningText |
TAttribute<FText> |
否 | 警告文本,不设置则隐藏警告图标 |
HighlightText |
TAttribute<FText> |
否 | 搜索高亮文本 |
IsNameReadOnly |
TAttribute<bool> |
否 | 是否禁止重命名 |
IsCheckBoxEnabled |
TAttribute<bool> |
否 | 是否启用勾选框 |
IsChecked |
TAttribute<ECheckBoxState> |
否 | 勾选框状态,不设置则禁用勾选框 |
OnCheckStateChanged |
FOnCheckStateChanged |
否 | 勾选状态变化回调,不绑定则禁用勾选框 |
OnBeginNameEdit |
FOnBeginTextEdit |
否 | 开始编辑名称回调 |
OnNameCommitted |
FOnTextCommitted |
否 | 名称提交回调,不绑定则使用只读文本 |
OnVerifyName |
FOnVerifyTextChanged |
否 | 名称验证回调 |
IsSelected |
FIsSelected |
否 | 选中状态回调 |
OnBuildToolTipInfo |
FOnBuildAssetTagItemToolTipInfo |
否 | 构建工具提示信息回调 |
3.2.3 Construct() --- 构建函数
这是整个模块最核心的函数,我们来逐段拆解它的构建逻辑。
第一步:基础属性存储
cpp
BaseColor = InArgs._BaseColor;
WarningText = InArgs._WarningText;
IsSelectedCallback = InArgs._IsSelected;
checkf(InArgs._DisplayName.IsSet(), TEXT("SAssetTagItem DisplayName must be set!"));
DisplayName 是唯一必须设置的参数,因为一个标签条目不能没有名字。checkf 确保在开发阶段就能发现这个错误。
第二步:创建工具提示
cpp
TSharedRef<SAssetTagItemToolTip> AssetTagToolTip = SNew(SAssetTagItemToolTip)
.DisplayName(InArgs._DisplayName)
.OnBuildToolTipInfo(InArgs._OnBuildToolTipInfo);
工具提示在 Construct 阶段就创建好了,但内容(特别是详细信息部分)是延迟构建的------在 OnOpening 时才调用 CreateToolTipWidget()。这避免了每次属性变化都重建工具提示的开销。
第三步:确定样式后缀
cpp
const ANSICHAR* StyleSpecifier = nullptr;
if (InArgs._ViewMode == EAssetTagItemViewMode::Compact)
{
StyleSpecifier = ".Compact";
}
这个设计很巧妙------通过一个简单的字符串后缀来切换样式。FAppStyle::GetMargin() 和其他样式获取方法都支持这种后缀拼接,意味着不需要为每种视图模式定义一套全新的样式键。
第四步:智能选择名称控件
cpp
TSharedPtr<SWidget> NameWidget;
if (InArgs._OnNameCommitted.IsBound())
{
NameWidget = SAssignNew(InlineRenameWidget, SInlineEditableTextBlock)
.Text(InArgs._DisplayName)
.HighlightText(InArgs._HighlightText)
.OnBeginTextEdit(InArgs._OnBeginNameEdit)
.OnTextCommitted(InArgs._OnNameCommitted)
.OnVerifyTextChanged(InArgs._OnVerifyName)
.IsSelected(IsSelectedCallback)
.IsReadOnly(InArgs._IsNameReadOnly);
}
else
{
NameWidget = SNew(STextBlock)
.Text(InArgs._DisplayName)
.HighlightText(InArgs._HighlightText);
}
这是 SAssetTagItem 最精妙的设计之一:根据外部是否提供了重命名回调,自动选择使用可编辑还是只读文本控件 。如果 OnNameCommitted 没有绑定,标签就是只读的(比如大多数内置标签);如果绑定了,用户就可以双击或按 F2 来重命名(比如自定义收藏夹)。
第五步:构建主布局
cpp
ChildSlot
[
SAssignNew(HBox, SHorizontalBox)
.ToolTip(AssetTagToolTip)
+SHorizontalBox::Slot()
.AutoWidth()
[
SAssignNew(CheckBox, SCheckBox)
.IsEnabled(IsCheckBoxEnabled)
.IsChecked(InArgs._IsChecked)
.OnCheckStateChanged(InArgs._OnCheckStateChanged)
.ToolTipText(this, &SAssetTagItem::GetCheckBoxTooltipText)
]
+SHorizontalBox::Slot()
.Padding(FAppStyle::GetMargin("ContentBrowser.AssetTagNamePadding", StyleSpecifier))
.VAlign(VAlign_Center)
[
NameWidget.ToSharedRef()
]
];
主布局是一个水平盒子,包含勾选框和名称控件。注意勾选框的 IsEnabled 属性在代码前面已经做了处理:如果 IsChecked 或 OnCheckStateChanged 没有设置,IsCheckBoxEnabled 会被强制设为 false。
第六步:条件性添加警告图标
cpp
if (WarningText.IsSet())
{
HBox->AddSlot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SImage)
.Visibility(this, &SAssetTagItem::GetWarningIconVisibility)
.Image(FAppStyle::Get().GetBrush("Icons.Warning"))
.ColorAndOpacity(FAppStyle::Get().GetSlateColor("Colors.AccentYellow"))
.ToolTipText(WarningText)
];
}
警告图标只有在 WarningText 属性被设置了才会添加。而且即使设置了,它的可见性还取决于 GetWarningIconVisibility() 的返回值------如果警告文本为空字符串,图标会折叠隐藏。这提供了双重保护。
第七步:条件性添加计数徽章
cpp
if (InArgs._CountText.IsSet())
{
HBox->AddSlot()
.AutoWidth()
.Padding(FAppStyle::GetMargin("ContentBrowser.AssetTagCountPadding", StyleSpecifier))
[
SNew(SBorder)
.Padding(0.0f)
.VAlign(VAlign_Center)
.BorderBackgroundColor(this, &SAssetTagItem::GetCountBackgroundColor)
.BorderImage(FAppStyle::GetBrush("ContentBrowser.AssetTagBackground"))
[
SNew(SBox)
.VAlign(VAlign_Center)
.MinDesiredWidth(30.0f)
[
SNew(STextBlock)
.Text(InArgs._CountText)
.Justification(ETextJustify::Center)
]
]
];
}
计数徽章是一个带圆角背景的文本块,最小宽度 30px,文本居中。背景色通过 GetCountBackgroundColor() 动态获取,与标签的基础颜色联动。
3.2.4 颜色派生方法
SAssetTagItem 提供了三个颜色派生方法,从 BaseColor 派生出不同透明度:
cpp
FLinearColor GetAssetTagBrightColor() const
{
return BaseColor.Get().CopyWithNewOpacity(0.3f);
}
FLinearColor GetAssetTagDullColor() const
{
return BaseColor.Get().CopyWithNewOpacity(0.1f);
}
FLinearColor GetAssetTagDisabledColor() const
{
return BaseColor.Get().CopyWithNewOpacity(0.04f);
}
设计意图:
这三个方法的设计体现了 UE5 编辑器 UI 中一个常见的模式------通过透明度来传达状态。不是重新定义一堆颜色常量,而是从一个基础颜色出发,用不同透明度来表示不同状态:
Bright(0.3):正常/激活状态,如计数徽章背景Dull(0.1):次要/非激活状态Disabled(0.04):禁用状态
这种方式的优势在于:如果外部改变了标签的颜色(比如给不同类型的标签设置不同的颜色标记),所有派生状态自动适配,不需要手动调整多套颜色。
3.2.5 勾选框工具提示
cpp
FText GetCheckBoxTooltipText() const
{
return CheckBox && CheckBox->IsEnabled()
? CheckBox->IsChecked()
? NSLOCTEXT("AssetTagsEditor", "RemoveAssetSelectionFromAssetTagItem", "Untag the current asset selection")
: NSLOCTEXT("AssetTagsEditor", "AddAssetSelectionToAssetTagItem", "Tag the current asset selection")
: FText::GetEmpty();
}
勾选框的工具提示会根据当前状态动态切换文案:
- 已勾选时:提示"取消当前选中资产的标签"
- 未勾选时:提示"将当前选中资产打上标签"
这种动态文案让用户一眼就知道点击会发生什么,是很贴心的 UX 细节。
3.2.6 RequestRename() --- 请求重命名
cpp
void RequestRename()
{
if (InlineRenameWidget)
{
InlineRenameWidget->EnterEditingMode();
}
}
这是一个简洁的公开方法,让外部代码可以程序化地触发重命名(比如右键菜单中的"重命名"选项)。如果标签是不可重命名的(InlineRenameWidget 为空),调用也没副作用。
3.3 SAssetTagItemTableRow --- 表格行容器
3.3.1 概述
SAssetTagItemTableRow 是一个模板类,继承自 STableRow<ItemType>,用于将 SAssetTagItem 包装为可在列表/树视图中使用的行。
模板参数:
ItemType:列表项的数据类型,透传给STableRow
3.3.2 Construct() --- 构建函数
cpp
void Construct(const typename STableRow<ItemType>::FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView)
{
this->ConstructInternal(InArgs, InOwnerTableView);
this->ConstructChildren(
InOwnerTableView->TableViewMode,
ApplySelectionBorderOuterPadding(InArgs._Padding),
WrapContentInSelectionBorder(InArgs._Content.Widget)
);
}
构建过程分三步:
ConstructInternal:初始化STableRow内部状态ApplySelectionBorderOuterPadding:在原始 Padding 基础上增加底部 1px 的额外间距(避免相邻行的选中边框重叠)WrapContentInSelectionBorder:用SBorder包裹内容,边框笔刷绑定到GetBorder()方法
3.3.3 SetIsDropTarget() --- 设置拖放目标状态
cpp
void SetIsDropTarget(const TAttribute<bool>& InIsDropTarget)
{
IsDropTarget = InIsDropTarget;
}
当该行成为拖放目标时,GetBorder() 会返回 InactiveHoveredBrush(高亮边框),给用户视觉反馈。
3.3.4 GetBorder() --- 边框笔刷选择
cpp
virtual const FSlateBrush* GetBorder() const override
{
if (IsDropTarget.Get(false))
{
return &this->Style->InactiveHoveredBrush;
}
return STableRow<ItemType>::GetBorder();
}
拖放目标状态优先于默认选中状态------如果当前行既是拖放目标又被选中,显示拖放目标的高亮边框。
3.4 SAssetTagItemToolTip --- 自定义工具提示
3.4.1 概述
SAssetTagItemToolTip 继承自 SToolTip,为资产标签条目提供自定义工具提示。它重写了 OnOpening() 来延迟构建内容,这意味着工具提示内容只在真正需要显示时才构建,而不是在控件创建时就构建。
3.4.2 Construct() --- 构建函数
cpp
void Construct(const FArguments& InArgs)
{
DisplayName = InArgs._DisplayName;
OnBuildToolTipInfo = InArgs._OnBuildToolTipInfo;
SToolTip::Construct(
SToolTip::FArguments()
.TextMargin(1.0f)
.BorderImage(FAppStyle::GetBrush("ContentBrowser.TileViewTooltip.ToolTipBorder"))
);
}
注意这里只设置了工具提示的外框样式,没有设置内容 。内容在 OnOpening() 中延迟构建。
3.4.3 IsEmpty() --- 空状态检查
cpp
virtual bool IsEmpty() const override
{
return false;
}
始终返回 false,确保即使外部没有提供详细信息,工具提示也会显示(至少显示标签名称)。
3.4.4 OnOpening() --- 延迟构建
cpp
virtual void OnOpening() override
{
SetContentWidget(CreateToolTipWidget());
}
在工具提示即将显示时调用 CreateToolTipWidget() 构建内容。这种延迟构建的优势在于:
- 如果工具提示从未显示,就不会浪费资源构建内容
- 每次显示时都重新构建,确保内容是最新的
3.4.5 CreateToolTipWidget() --- 内容构建
工具提示的内容分为上下两个区域:
顶部区域:标签名称
cpp
SNew(SBorder)
.Padding(6)
.BorderImage(FAppStyle::GetBrush("ContentBrowser.TileViewTooltip.ContentBorder"))
[
SNew(STextBlock)
.Text(DisplayName)
.Font(FAppStyle::GetFontStyle("ContentBrowser.TileViewTooltip.NameFont"))
]
底部区域:详细信息(如果有)
cpp
if (InfoBox->NumSlots() > 0)
{
OverallTooltipVBox->AddSlot()
.AutoHeight()
.Padding(0, 4, 0, 0)
[
SNew(SBorder)
.Padding(6)
.BorderImage(FAppStyle::GetBrush("ContentBrowser.TileViewTooltip.ContentBorder"))
[
InfoBox
]
];
}
底部区域只有在有实际内容时才显示,避免出现空的灰色区块。
3.4.6 AddToToolTipInfoBox() --- 信息行添加
cpp
static void AddToToolTipInfoBox(const TSharedRef<SVerticalBox>& InfoBox, const FText& Key, const FText& Value)
{
InfoBox->AddSlot()
.AutoHeight()
.Padding(0, 1)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(0, 0, 4, 0)
[
SNew(STextBlock)
.Text(FText::Format(LOCTEXT("AssetTagTooltipKeyFormat", "{0}:"), Key))
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
]
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.Text(Value)
.ColorAndOpacity(FSlateColor::UseForeground())
.WrapTextAt(700.0f)
]
];
}
每一行信息是一个水平盒子,包含:
- 左侧:Key 文本(如"类型:"),使用
SubduedForeground颜色(灰色) - 右侧:Value 文本,使用
Foreground颜色(白色),支持 700px 处换行
4 功能使用示例编写
示例1:创建基础的资产标签条目
这是最基础的用法------创建一个只读的标签条目,显示名称和包含的资产数量。
cpp
// 在某个 Slate 面板中
TSharedRef<SAssetTagItem> CreateBasicTagItem()
{
return SNew(SAssetTagItem)
.DisplayName(FText::FromString(TEXT("武器模型")))
.CountText(FText::AsNumber(42))
.BaseColor(FLinearColor(0.2f, 0.6f, 1.0f)); // 蓝色基调
}
渲染结果是一个带有蓝色计数徽章的"武器模型 42"标签条目。
示例2:创建支持勾选的标签条目
在内容浏览器中,选中一些资产后,我们可能需要给它们批量打标签。勾选框就是用来控制这个的。
cpp
class FMyTagBrowser
{
public:
TSharedRef<SAssetTagItem> CreateCheckableTagItem(const FString& TagName, int32 AssetCount)
{
return SNew(SAssetTagItem)
.DisplayName(FText::FromString(TagName))
.CountText(FText::AsNumber(AssetCount))
.IsChecked(this, &FMyTagBrowser::IsTagChecked, TagName)
.OnCheckStateChanged(this, &FMyTagBrowser::OnTagCheckStateChanged, TagName)
.BaseColor(FLinearColor::Green);
}
private:
ECheckBoxState IsTagChecked(FString TagName) const
{
// 检查当前选中资产是否都已经打上了这个标签
return CheckedTags.Contains(TagName)
? ECheckBoxState::Checked
: ECheckBoxState::Unchecked;
}
void OnTagCheckStateChanged(ECheckBoxState NewState, FString TagName)
{
if (NewState == ECheckBoxState::Checked)
{
// 给当前选中的资产打上这个标签
AddTagToSelectedAssets(TagName);
}
else
{
// 从当前选中的资产上移除这个标签
RemoveTagFromSelectedAssets(TagName);
}
}
TSet<FString> CheckedTags;
};
示例3:创建支持重命名的标签条目
对于用户自定义的收藏夹,需要支持重命名功能。
cpp
TSharedRef<SAssetTagItem> CreateRenamableTagItem(const FString& InitialName)
{
return SNew(SAssetTagItem)
.DisplayName(FText::FromString(InitialName))
.OnBeginNameEdit(this, &FMyPanel::OnTagBeginRename)
.OnNameCommitted(this, &FMyPanel::OnTagNameCommitted)
.OnVerifyName(this, &FMyPanel::OnTagVerifyName)
.IsSelected(this, &FMyPanel::IsTagSelected);
}
bool FMyPanel::OnTagVerifyName(const FText& NewName, FText& OutError) const
{
FString NewNameStr = NewName.ToString().TrimStartAndEnd();
if (NewNameStr.IsEmpty())
{
OutError = LOCTEXT("EmptyNameError", "名称不能为空");
return false;
}
if (ExistingTagNames.Contains(NewNameStr))
{
OutError = LOCTEXT("DuplicateNameError", "该名称已被使用");
return false;
}
return true;
}
void FMyPanel::OnTagNameCommitted(const FText& NewName, ETextCommit::Type CommitType)
{
if (CommitType == ETextCommit::OnEnter || CommitType == ETextCommit::OnUserMovedFocus)
{
FString NewNameStr = NewName.ToString().TrimStartAndEnd();
if (!NewNameStr.IsEmpty())
{
RenameTag(CurrentRenamingTag, NewNameStr);
}
}
}
示例4:在列表视图中使用 SAssetTagItemTableRow
将 SAssetTagItem 嵌入到 SListView 中:
cpp
// 数据模型
struct FTagItemData
{
FString Name;
int32 Count;
FLinearColor Color;
};
// 行生成回调
TSharedRef<ITableRow> FMyTagBrowser::OnGenerateTagRow(
TSharedPtr<FTagItemData> Item,
const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(SAssetTagItemTableRow<TSharedPtr<FTagItemData>>, OwnerTable)
.Padding(FMargin(0, 1))
[
SNew(SAssetTagItem)
.DisplayName(FText::FromString(Item->Name))
.CountText(FText::AsNumber(Item->Count))
.BaseColor(Item->Color)
.IsSelected_Lambda([this, Item]()
{
return SelectedItem == Item;
})
];
}
// 创建列表视图
TagListView = SNew(SListView<TSharedPtr<FTagItemData>>)
.ListItemsSource(&TagItems)
.OnGenerateRow(this, &FMyTagBrowser::OnGenerateTagRow);
示例5:使用工具提示信息委托
给标签条目的工具提示添加自定义信息:
cpp
TSharedRef<SAssetTagItem> CreateTagWithRichTooltip(const FTagItemData& Data)
{
return SNew(SAssetTagItem)
.DisplayName(FText::FromString(Data.Name))
.CountText(FText::AsNumber(Data.Count))
.BaseColor(Data.Color)
.OnBuildToolTipInfo_Lambda([&Data](const FOnBuildAssetTagItemToolTipInfoEntry& AddEntry)
{
AddEntry(LOCTEXT("Type", "类型"),
Data.bIsCollection
? LOCTEXT("Collection", "收藏夹")
: LOCTEXT("Tag", "标签"));
AddEntry(LOCTEXT("AssetCount", "包含资产"), FText::AsNumber(Data.Count));
AddEntry(LOCTEXT("LastModified", "最后修改"), FText::AsDate(Data.LastModified));
});
}
示例6:带警告状态的标签条目
当标签引用的资产路径有损坏时,显示警告图标:
cpp
TSharedRef<SAssetTagItem> CreateTagWithWarning(const FTagItemData& Data)
{
FText WarningMsg = Data.HasBrokenReferences
? LOCTEXT("BrokenRefWarning", "此标签引用了部分已删除的资产")
: FText::GetEmpty();
TAttribute<FText> WarningText;
if (Data.HasBrokenReferences)
{
WarningText = TAttribute<FText>(WarningMsg);
}
return SNew(SAssetTagItem)
.DisplayName(FText::FromString(Data.Name))
.CountText(FText::AsNumber(Data.Count))
.BaseColor(FLinearColor::Yellow)
.WarningText(WarningText);
}
5 总结与最佳实践
5.1 核心要点
-
AssetTagsEditor 是一个 UI 控件工具包,不是业务逻辑模块
- 它只负责"如何渲染",不负责"渲染什么数据"
- 所有数据通过
TAttribute和委托从外部注入 - 这种设计使其可以被内容浏览器、资产选择器、标签管理器等多个场景复用
-
可选功能自动启用的设计模式
SAssetTagItem根据参数是否绑定,自动决定启用哪些功能- 勾选框:
IsChecked和OnCheckStateChanged同时设置才启用 - 重命名:
OnNameCommitted绑定才启用 - 警告图标:
WarningText设置才显示 - 计数徽章:
CountText设置才显示
-
延迟构建工具提示
SAssetTagItemToolTip在OnOpening()时才构建内容- 避免为从未显示的标签预建工具提示
-
颜色派生模式
- 从单一的
BaseColor派生出不同透明度来区分状态 - 外部改动颜色后,所有派生状态自动适配
- 从单一的
-
样式后缀切换
- 通过
StyleSpecifier字符串后缀(nullptr/".Compact")来切换样式 - 利用
FAppStyle的键名拼接机制,无需为每种视图模式定义独立的样式键
- 通过
5.2 值得学习的设计技巧
-
TAttribute驱动的响应式 UI- 所有外观属性都用
TAttribute绑定,而非在构造时写入固定值 - 外部数据变化时,UI 自动更新,无需手动调用刷新
- 所有外观属性都用
-
TFunctionRef用于轻量回调FOnBuildAssetTagItemToolTipInfoEntry使用TFunctionRef而非TFunction- 在不需要持有回调所有权的场景下,避免了堆分配开销
-
checkf保障必须参数DisplayName是唯一必须的参数,Construct开头就用checkf验证- 在开发阶段尽早暴露配置错误
-
模板 + 组合的控件复用
SAssetTagItemTableRow是模板类,可适配任意ItemType- 内部组合
SAssetTagItem,而非继承它,保持了职责分离
5.3 潜在改进点
-
自定义颜色状态数量有限
- 目前只有三种透明度派生(Bright / Dull / Disabled),如果将来需要更多状态(如 Hovered、Pressed),需要修改源码
- 可以考虑将颜色派生逻辑开放为可配置的策略
-
工具提示样式硬编码
- 工具提示的样式键(如
"ContentBrowser.TileViewTooltip.ToolTipBorder")是硬编码的 - 如果要在非内容浏览器场景下使用,样式可能不完全匹配,建议将样式键也开放为参数
- 工具提示的样式键(如
-
缺少蓝图支持
SAssetTagItem是纯 C++ Slate 控件,没有对应的UWidget包装- 如果需要在 UMG 中使用,需要额外封装一层
-
紧凑模式与标准模式的切换粒度
- 目前只有两种视图模式,且切换的是整个样式后缀
- 如果只想调整某个特定属性(如只改间距但不改字体),目前无法实现
附录:文件结构参考
AssetTagsEditor/
├── AssetTagsEditor.Build.cs
├── Private/
│ ├── AssetTagsEditorModule.cpp
│ └── SAssetTagItem.cpp
└── Public/
├── AssetTagItemTypes.h
└── SAssetTagItem.h