LyraSettingScreen(设置主界面)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| // 设置的最上层面板
// 1.响应输入 如回退,确认,取消
// 2.如果有变动就显示确认和取消的按钮
// 3.创建游戏设置注册器UGameSettingRegistry!!!极其重要.是我们游戏设置有多少选项的底层入口!
// 4.持有顶部标签栏,当我们点击或者注册Tab页面时,更新具体设置面板
// 5.持有底部侧边栏,用以显示底部按钮
UCLASS(Abstract, meta = (Category = "Settings", DisableNativeTick))
class ULyraSettingScreen : public UGameSettingScreen
{
GENERATED_BODY()
public:
protected:
// 绑定输入映射
virtual void NativeOnInitialized() override;
// 极其重要!!! 创建设置注册器 调用逻辑见父类
// 这里会去初始化我们底层有多少游戏设置.
virtual UGameSettingRegistry* CreateRegistry() override;
// 处理返回 尝试回退到上一次导航的位置(嵌套页面) 如果没有的话就应用变化 并退出
void HandleBackAction();
// 应用设置
void HandleApplyAction();
// 取消设置应用
void HandleCancelChangesAction();
// 设置变动时显示 应用和取消按钮
virtual void OnSettingsDirtyStateChanged_Implementation(bool bSettingsDirty) override;
protected:
// 绑定控件 用于切换显示的分类设置
UPROPERTY(BlueprintReadOnly, Category = Input, meta = (BindWidget, OptionalWidget = true, AllowPrivateAccess = true))
TObjectPtr<ULyraTabListWidgetBase> TopSettingsTabs;
// 返回的输入映射
UPROPERTY(EditDefaultsOnly)
FDataTableRowHandle BackInputActionData;
// 应用的输入映射
UPROPERTY(EditDefaultsOnly)
FDataTableRowHandle ApplyInputActionData;
// 取消的输入映射
UPROPERTY(EditDefaultsOnly)
FDataTableRowHandle CancelChangesInputActionData;
// 返回事件的句柄
FUIActionBindingHandle BackHandle;
// 应用设置的句柄
FUIActionBindingHandle ApplyHandle;
// 取消改变的句柄
FUIActionBindingHandle CancelChangesHandle;
};
|
输入操作
1
2
3
4
5
6
7
8
9
10
11
| void ULyraSettingScreen::NativeOnInitialized()
{
Super::NativeOnInitialized(); // 调用父类的初始化方法
// 注册"返回"输入动作绑定,当触发时调用HandleBackAction方法
BackHandle = RegisterUIActionBinding(FBindUIActionArgs(BackInputActionData, true, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleBackAction)));
// 注册"应用"输入动作绑定,当触发时调用HandleApplyAction方法
ApplyHandle = RegisterUIActionBinding(FBindUIActionArgs(ApplyInputActionData, true, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleApplyAction)));
// 注册"取消更改"输入动作绑定,当触发时调用HandleCancelChangesAction方法
CancelChangesHandle = RegisterUIActionBinding(FBindUIActionArgs(CancelChangesInputActionData, true, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleCancelChangesAction)));
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 当设置的脏状态(是否有未保存更改)发生变化时调用的实现方法
void ULyraSettingScreen::OnSettingsDirtyStateChanged_Implementation(bool bSettingsDirty)
{
// 如果设置有未保存的更改(脏状态为true)
if (bSettingsDirty)
{
// 如果"应用"动作绑定未被包含,则添加它
if (!GetActionBindings().Contains(ApplyHandle))
{
AddActionBinding(ApplyHandle);
}
// 如果"取消更改"动作绑定未被包含,则添加它
if (!GetActionBindings().Contains(CancelChangesHandle))
{
AddActionBinding(CancelChangesHandle);
}
}
else
{
// 如果设置没有未保存的更改(脏状态为false),则移除"应用"和"取消更改"的动作绑定
RemoveActionBinding(ApplyHandle);
RemoveActionBinding(CancelChangesHandle);
}
}
|
有一些操作需要以固定底部栏的方式进行实现. UCommonBoundActionBar作为容器. UCommonBoundActionButton作为按钮. W_BottomActionBar. W_BoundActionButton.
通过勾选默认回退操作
蓝图中勾选即可. 注意该固定栏的容器必须要实例化出来.并在容器中指定按钮的类. 蓝图->Defaults->Back: –Is Back Handler –Is Back Action Displayed in Action Bar.
通过注册时表明为底部按钮
1
2
3
4
5
6
7
8
9
10
11
| void ULyraSettingScreen::NativeOnInitialized()
{
Super::NativeOnInitialized();
BackHandle = RegisterUIActionBinding(FBindUIActionArgs(BackInputActionData,
true, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleBackAction)));
ApplyHandle = RegisterUIActionBinding(FBindUIActionArgs(ApplyInputActionData,
true, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleApplyAction)));
CancelChangesHandle = RegisterUIActionBinding(FBindUIActionArgs(CancelChangesInputActionData,
true, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleCancelChangesAction)));
}
|
用于监听输入方法改变时切换按钮样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| /**
* 底部按钮
* 用以切换按钮的样式,在输入方法改变时.
*/
UCLASS(MinimalAPI, Abstract, meta = (DisableNativeTick))
class ULyraBoundActionButton : public UCommonBoundActionButton
{
GENERATED_BODY()
protected:
UE_API virtual void NativeConstruct() override;
private:
void HandleInputMethodChanged(ECommonInputType NewInputMethod);
UPROPERTY(EditAnywhere, Category = "Styles")
TSubclassOf<UCommonButtonStyle> KeyboardStyle;
UPROPERTY(EditAnywhere, Category = "Styles")
TSubclassOf<UCommonButtonStyle> GamepadStyle;
UPROPERTY(EditAnywhere, Category = "Styles")
TSubclassOf<UCommonButtonStyle> TouchStyle;
};
|
创建游戏设置注册表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 创建设置注册表的方法
UGameSettingRegistry* ULyraSettingScreen::CreateRegistry()
{
// 实例化一个新的Lyra游戏设置注册表对象
ULyraGameSettingRegistry* NewRegistry = NewObject<ULyraGameSettingRegistry>();
// 获取当前拥有的本地玩家并进行类型检查,确保其为ULyraLocalPlayer类型
if (ULyraLocalPlayer* LocalPlayer = CastChecked<ULyraLocalPlayer>(GetOwningLocalPlayer()))
{
// 使用本地玩家初始化注册表
// 极其重要
NewRegistry->Initialize(LocalPlayer);
}
return NewRegistry;
}
|
GameSettingScreen(基类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| /**
* 游戏设置的主屏幕
* 1. 持有游戏设置注册器
* 2. 持有游戏设置变动追踪器
* 3. 提供若干蓝图接口,如创建游戏设置器,应用或取消游戏设置,导航等逻辑
*/
UCLASS(MinimalAPI, Abstract, meta = (Category = "Settings", DisableNativeTick))
class UGameSettingScreen : public UCommonActivatableWidget
{
GENERATED_BODY()
public:
protected:
// 无
UE_API virtual void NativeOnInitialized() override;
// 初始化游戏设置注册器的追踪
UE_API virtual void NativeOnActivated() override;
// 无
UE_API virtual void NativeOnDeactivated() override;
// 重写一下聚焦对象 如果蓝图设置就走蓝图的 如果蓝图没就走设置的细节面板
UE_API virtual UWidget* NativeGetDesiredFocusTarget() const override;
// 导航设置 由蓝图调用 接收由TabList在注册Tag或点击Tag时调用!
UFUNCTION(BlueprintCallable)
UE_API void NavigateToSetting(FName SettingDevName);
// 导航到对应的设置
UFUNCTION(BlueprintCallable)
UE_API void NavigateToSettings(const TArray<FName>& SettingDevNames);
// 设置变动触发的回调
UFUNCTION(BlueprintNativeEvent)
UE_API void OnSettingsDirtyStateChanged(bool bSettingsDirty);
virtual void OnSettingsDirtyStateChanged_Implementation(bool bSettingsDirty) { }
// 尝试弹出导航栈
UFUNCTION(BlueprintCallable)
UE_API bool AttemptToPopNavigation();
// 暴露给蓝图 获取对应的游戏设置集合!首次使用时创建,调用的位置是蓝图的注册Tab
// W_LyraSettingScreen->Construct->RegitterTopLevelTab->GetSettingCollection
UFUNCTION(BlueprintCallable)
UE_API UGameSettingCollection* GetSettingCollection(FName SettingDevName, bool& HasAnySettings);
protected:
// 创建游戏设置注册器
UE_API virtual UGameSettingRegistry* CreateRegistry() PURE_VIRTUAL(, return nullptr;);
// 获取游戏设置注册器
template <typename GameSettingRegistryT = UGameSettingRegistry>
GameSettingRegistryT* GetRegistry() const { return Cast<GameSettingRegistryT>(const_cast<UGameSettingScreen*>(this)->GetOrCreateRegistry()); }
// 取消变化
UFUNCTION(BlueprintCallable)
UE_API virtual void CancelChanges();
// 应用变化
UFUNCTION(BlueprintCallable)
UE_API virtual void ApplyChanges();
// 是否有设置变动
UFUNCTION(BlueprintCallable)
bool HaveSettingsBeenChanged() const { return ChangeTracker.HaveSettingsBeenChanged(); }
// 清理脏的状态
UE_API void ClearDirtyState();
// 在GetOrCreateRegistry中进行绑定
UE_API void HandleSettingChanged(UGameSetting* Setting, EGameSettingChangeReason Reason);
FGameSettingRegistryChangeTracker ChangeTracker;
private:
// 内部获取或创建游戏设置注册器
UE_API UGameSettingRegistry* GetOrCreateRegistry();
private:
// Bound Widgets
// 固定的控件
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
TObjectPtr<UGameSettingPanel> Settings_Panel;
// 由它持有 ok 没有问题
UPROPERTY(Transient)
mutable TObjectPtr<UGameSettingRegistry> Registry;
};
|
游戏设置追踪器的初始化
1
2
3
4
5
6
7
8
9
| // 这是 UI 组件激活时的回调(比如界面被打开时),用于初始化界面相关的逻辑。
void UGameSettingScreen::NativeOnActivated()
{
Super::NativeOnActivated(); // 调用父类的激活逻辑(如UI显示、基础初始化等)
ChangeTracker.WatchRegistry(Registry); // 让设置追踪器监视注册器
// 初始化时检查设置是否有变动,并触发状态变更事件(初始状态应为“无变动”)
OnSettingsDirtyStateChanged(HaveSettingsBeenChanged());
}
|
ChangeTracker.WatchRegistry(Registry): ChangeTracker 是 “设置变动追踪器”(用于记录设置是否被修改的工具)。 调用 WatchRegistry 让追踪器开始监视 Registry(设置注册表),当注册表中的设置项发生变化时,追踪器会记录这种变动(标记为 “脏状态”)。 OnSettingsDirtyStateChanged(…): 初始化时主动检查当前设置是否有变动(HaveSettingsBeenChanged()),并通过事件 OnSettingsDirtyStateChanged 通知外部(更新 “应用”“重置” 按钮的状态:如果无变动,按钮不显示)。
游戏设置注册表的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 获取或创建注册表
// GetOrCreateRegistry 采用 “懒加载” 模式,只有当 Registry 为空时才创建,避免重复初始化。
UGameSettingRegistry* UGameSettingScreen::GetOrCreateRegistry()
{
if (Registry == nullptr) // 如果还没有创建注册表,则执行创建逻辑
{
// 由子类实现具体的注册表创建(多态设计,让不同设置界面可以有不同的设置项)
UGameSettingRegistry* NewRegistry = this->CreateRegistry();
// 绑定设置项变更事件:当注册器中的任何设置被修改时,调用 HandleSettingChanged 处理
NewRegistry->OnSettingChangedEvent.AddUObject(this, &ThisClass::HandleSettingChanged);
// 让显示设置的面板(Settings_Panel)与注册器关联,面板会根据注册器加载并显示具体的设置项
Settings_Panel->SetRegistry(NewRegistry);
// 保存注册器引用,避免被垃圾回收(GC)
Registry = NewRegistry;
}
return Registry; // 返回已创建的注册器
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| protected:
// 暴露给蓝图 获取对应的游戏设置集合!首次使用时创建,调用的位置是蓝图的注册Tab
// (Lyra项目中通过此函数获取DisplayName)
// W_LyraSettingScreen->Construct->RegitterTopLevelTab->GetSettingCollection
UFUNCTION(BlueprintCallable)
UE_API UGameSettingCollection* GetSettingCollection(FName SettingDevName, bool& HasAnySettings);
// 获取游戏设置注册表
// 提供了类型安全的注册表获取方式。
// 例如,若子类实现了 UMyGameSettingRegistry,可以通过 GetRegistry<UMyGameSettingRegistry>() 直接获取特定类型的注册表,避免了手动类型转换的麻烦,同时确保类型正确。
template <typename GameSettingRegistryT = UGameSettingRegistry>
GameSettingRegistryT* GetRegistry() const { return Cast<GameSettingRegistryT>(const_cast<UGameSettingScreen*>(this)->GetOrCreateRegistry()); }
// 创建游戏设置注册表
// 这是一个纯虚函数(PURE_VIRTUAL),意味着必须由子类实现,通过“多态设计”实现不同的界面设计
UE_API virtual UGameSettingRegistry* CreateRegistry() PURE_VIRTUAL(, return nullptr;);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| UGameSettingCollection* UGameSettingScreen::GetSettingCollection(FName SettingDevName, bool& HasAnySettings)
{
HasAnySettings = false;
if (UGameSettingCollection* Collection = GetRegistry()->FindSettingByDevNameChecked<UGameSettingCollection>(SettingDevName))
{
TArray<UGameSetting*> InOutSettings;
FGameSettingFilterState FilterState;
Collection->GetSettingsForFilter(FilterState, InOutSettings);
// 检测 是否有游戏设置 如果是空项目 调用者应当知道是否可以创建空的面板按钮!
HasAnySettings = InOutSettings.Num() > 0;
return Collection;
}
return nullptr;
}
|
整体逻辑梳理
注册表(UGameSettingRegistry) 是游戏设置的 “总管理器”,由子类创建并填充具体的设置项(或设置集合)。 设置集合(UGameSettingCollection) 是设置项的 “分组容器”,用于将相关设置归类(如音频、视频、控制等)。 GetOrCreateRegistry 确保注册表在需要时初始化,并关联 UI 面板和变更事件;GetSettingCollection 则提供了从注册表中获取具体分组的能力,支持蓝图调用。
设计优点
职责分离:注册表管理数据,面板负责显示,变更事件处理状态更新。 扩展性强:通过子类重写 CreateRegistry,可以轻松添加新的设置类型。 灵活性高:支持过滤无效设置项,适配不同平台或游戏状态。
顶部侧边栏联动设置具体面板
蓝图逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| void UGameSettingScreen::NavigateToSetting(FName SettingDevName)
{
NavigateToSettings({SettingDevName});
}
void UGameSettingScreen::NavigateToSettings(const TArray<FName>& SettingDevNames)
{
FGameSettingFilterState FilterState;
// 创建好一个新的过滤器
for (const FName SettingDevName : SettingDevNames)
{
// 在游戏设置注册器里面寻找是否有该游戏设置
// 注意这里一般找的是UGameSettingCollection 它是GameSetting的子类
if (UGameSetting* Setting = GetRegistry()->FindSettingByDevNameChecked<UGameSetting>(SettingDevName))
{
FilterState.AddSettingToRootList(Setting);
}
}
// 使用该过滤器去过滤设置具体面板
Settings_Panel->SetFilterState(FilterState);
}
|
首先是从蓝图过来注册对象的Tab.此处的Tab是上层逻辑即蓝图逻辑,即控件逻辑.与游戏设置注册表无直接关联!!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| bool ULyraTabListWidgetBase::RegisterDynamicTab(const FLyraTabDescriptor& TabDescriptor)
{
// If it's hidden we just ignore it.
// 如果它是隐藏状态,我们就直接忽略它。
if (TabDescriptor.bHidden)
{
return true;
}
PendingTabLabelInfoMap.Add(TabDescriptor.TabId, TabDescriptor);
/**
* 会将一个与给定的控件实例相对应的新标签添加到列表中。如果该标签不在关联的切换器中,则会添加进去。
* @参数 TabID 用于跟踪此标签的名称标识符。尝试在已存在的重复标识符下注册标签将会失败。
* @参数 ButtonWidgetType 为该标签创建的控件类型
* @参数 ContentWidget 与注册的标签关联的控件
* @参数 TabIndex 决定将新标签插入到标签列表中的位置(-1 表示将标签添加到列表的末尾)
* @返回 如果新标签注册成功且不存在名称标识符冲突,则返回 True
*
*/
return RegisterTab(TabDescriptor.TabId, TabDescriptor.TabButtonType, TabDescriptor.CreatedTabContentWidget);
}
|
以下代码主要关注这两行
1
2
3
4
5
6
7
8
9
| // There is no PlayerController in Designer
UCommonButtonBase* const NewTabButton = TabButtonWidgetPool.GetOrCreateInstance<UCommonButtonBase>(ButtonWidgetType);
// ....
TabButtonGroup->AddWidget(NewTabButton);
HandleTabCreation(TabNameID, NewTabInfo.TabButton);
OnTabButtonCreation.Broadcast(TabNameID, NewTabInfo.TabButton);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
| bool UCommonTabListWidgetBase::RegisterTab(FName TabNameID, TSubclassOf<UCommonButtonBase> ButtonWidgetType, UWidget* ContentWidget, const int32 TabIndex /*= INDEX_NONE*/)
{
bool bAreParametersValid = true;
// Early out on redundant tab registration.
// 提前检查重复注册的标签页,如果已存在则标记参数无效
if (!ensure(!RegisteredTabsByID.Contains(TabNameID)))
{
bAreParametersValid = false;
}
// Early out on invalid tab button type.
// 提前检查标签按钮类型是否有效,无效则标记参数无效
if (!ensure(ButtonWidgetType))
{
bAreParametersValid = false;
}
// NOTE: Adding the button to the group may change it's selection, which raises an event we listen to,
// which can only properly be handled if we already know that this button is associated with a registered tab.
// 注意:将按钮添加到组中可能会改变其选择状态,这会触发我们监听的事件
// 只有当我们已经知道该按钮与已注册的标签页相关联时,才能正确处理该事件
// 检查标签按钮组是否有效,无效则标记参数无效
if (!ensure(TabButtonGroup))
{
bAreParametersValid = false;
}
// 如果参数无效,直接返回注册失败
if (!bAreParametersValid)
{
return false;
}
// There is no PlayerController in Designer
// 设计器模式下没有PlayerController,通过对象池创建或获取标签按钮实例
UCommonButtonBase* const NewTabButton = TabButtonWidgetPool.GetOrCreateInstance<UCommonButtonBase>(ButtonWidgetType);
// 确保按钮实例创建成功,失败则返回注册失败
if (!ensureMsgf(NewTabButton, TEXT("Failed to create tab button. Aborting tab registration.")))
{
return false;
}
// 获取当前已注册标签页数量
const int32 NumRegisteredTabs = RegisteredTabsByID.Num();
// 计算新标签页的索引:如果未指定索引则添加到末尾,否则限制在有效范围内
const int32 NewTabIndex = (TabIndex == INDEX_NONE) ? NumRegisteredTabs : FMath::Clamp(TabIndex, 0, NumRegisteredTabs);
// 如果新标签页插入到列表中间(非末尾),则需要重建标签列表
const bool bRequiresRebuild = (NewTabIndex < NumRegisteredTabs);
if (bRequiresRebuild)
{
// 遍历所有已注册标签页,将插入位置及之后的标签页索引+1(为新标签页腾出位置)
for (TPair<FName, FCommonRegisteredTabInfo>& Pair : RegisteredTabsByID)
{
if (NewTabIndex <= Pair.Value.TabIndex)
{
// Increment this tab's index as we are inserting the new tab before it.
// 由于新标签页插入到当前标签页之前,所以当前标签页索引+1
Pair.Value.TabIndex++;
}
}
}
// Tab book-keeping.
// 标签页信息记录:初始化新标签页的信息结构体
FCommonRegisteredTabInfo NewTabInfo;
NewTabInfo.TabIndex = NewTabIndex; // 标签页索引
NewTabInfo.TabButtonClass = ButtonWidgetType; // 标签按钮的类
NewTabInfo.TabButton = NewTabButton; // 标签按钮实例
NewTabInfo.ContentInstance = ContentWidget; // 标签页对应的内容部件
RegisteredTabsByID.Add(TabNameID, NewTabInfo); // 将新标签页信息存入映射表
// Enforce the "contract" that tab buttons require - single-selectability, but not toggleability.
// 强制设置标签按钮的交互特性:支持单选,但不支持切换(点击选中后再次点击不会取消)
NewTabButton->SetIsSelectable(true);
NewTabButton->SetIsToggleable(false);
// 将新创建的标签按钮添加到按钮组中
TabButtonGroup->AddWidget(NewTabButton);
// 处理标签页创建的自定义逻辑(子类可重写)
HandleTabCreation(TabNameID, NewTabInfo.TabButton);
// 广播标签按钮创建事件,供外部监听处理
OnTabButtonCreation.Broadcast(TabNameID, NewTabInfo.TabButton);
// 如果需要重建标签列表
if (bRequiresRebuild)
{
// 检查是否启用延迟重建
if (bDeferRebuildingTabList)
{
// 如果尚未有等待中的重建任务
if (!bPendingRebuild)
{
bPendingRebuild = true;
// 添加一个延迟任务到核心定时器,用于后续重建标签列表
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UCommonTabListWidgetBase::DeferredRebuildTabList));
}
}
else
{
// 立即重建标签列表
RebuildTabList();
}
}
// 标签页注册成功
return true;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| void UCommonTabListWidgetBase::NativeOnInitialized()
{
Super::NativeOnInitialized();
// Create the button group once up-front
// 预先创建按钮组(只创建一次)
TabButtonGroup = NewObject<UCommonButtonGroupBase>(this);
// 设置为必须有选中项(确保始终有一个标签按钮处于选中状态)
SetSelectionRequired(true);
// 绑定标签按钮选中状态变更的回调:当按钮组中选中的按钮发生变化时,调用HandleTabButtonSelected处理
TabButtonGroup->OnSelectedButtonBaseChanged.AddDynamic(this, &UCommonTabListWidgetBase::HandleTabButtonSelected);
}
|
触发的回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| void UCommonTabListWidgetBase::HandleTabButtonSelected(UCommonButtonBase* SelectedTabButton, int32 ButtonIndex)
{
// 遍历所有已注册的标签页
for (auto& TabPair : RegisteredTabsByID)
{
FCommonRegisteredTabInfo& TabInfo = TabPair.Value;
// 找到与选中按钮关联的标签页
if (TabInfo.TabButton == SelectedTabButton)
{
// 更新当前激活的标签页ID
ActiveTabID = TabPair.Key;
// 如果该标签页有内容部件,且已关联切换器(用于切换显示不同标签内容的控件)
if (TabInfo.ContentInstance && LinkedSwitcher.IsValid())
{
// There's already an instance of the widget to display, so go for it
// 直接显示该标签页对应的内容部件
LinkedSwitcher->SetActiveWidget(TabInfo.ContentInstance);
}
// 广播标签页选中事件(通知外部该标签页被选中)
OnTabSelected.Broadcast(TabPair.Key);
}
}
}
|
增加一个Tab按钮 如果是第一个就默认被选中
1
2
| // 将新创建的标签按钮添加到按钮组中
TabButtonGroup->AddWidget(NewTabButton);
|
1
2
3
4
5
6
7
8
9
| void UCommonWidgetGroupBase::AddWidget(UWidget* InWidget)
{
// 确保传入的部件有效,且类型符合当前组要求的部件类型
if (ensure(InWidget) && InWidget->IsA(GetWidgetType()))
{
// 调用部件添加的处理函数(由子类实现具体逻辑)
OnWidgetAdded(InWidget);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| void UCommonButtonGroupBase::OnWidgetAdded(UWidget* NewWidget)
{
// 将传入的部件转换为按钮类型(UCommonButtonBase)
if (UCommonButtonBase* Button = Cast<UCommonButtonBase>(NewWidget))
{
// 为按钮绑定各种状态变更的回调(选中、点击、双击、悬停、锁定等)
Button->OnSelectedChangedBase.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnSelectionStateChangedBase);
Button->OnButtonBaseClicked.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnHandleButtonBaseClicked);
Button->OnButtonBaseDoubleClicked.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnHandleButtonBaseDoubleClicked);
Button->OnButtonBaseHovered.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnButtonBaseHovered);
Button->OnButtonBaseUnhovered.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnButtonBaseUnhovered);
Button->OnButtonBaseLockClicked.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnHandleButtonBaseLockClicked);
Button->OnButtonBaseLockDoubleClicked.AddUniqueDynamic(this, &UCommonButtonGroupBase::OnHandleButtonBaseLockDoubleClicked);
// 将按钮添加到按钮组的列表中
Buttons.Emplace(Button);
// If selection in the group is required and this is the first button added, make sure it's selected (quietly)
// 如果按钮组要求必须有选中项,且当前是添加的第一个按钮,且该按钮未被选中
if (bSelectionRequired && Buttons.Num() == 1 && !Button->GetSelected())
{
// 静默选中该按钮(不播放音效、不广播事件)
Button->SetSelectedInternal(true, false);
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| void UCommonButtonBase::SetSelectedInternal(bool bInSelected, bool bAllowSound /*= true*/, bool bBroadcast /*= true*/)
{
// 判断选中状态是否发生了变化
bool bValueChanged = bInSelected != bSelected;
// 更新按钮的选中状态
bSelected = bInSelected;
// 根据选中状态更新按钮样式
SetButtonStyle();
if (bSelected)
{
// 执行选中状态的原生逻辑(可被子类重写)
NativeOnSelected(bBroadcast);
// 如果按钮不可切换(单选按钮特性)且当前可交互
if (!bToggleable && IsInteractable())
{
// If the button isn't toggleable, then disable interaction with the root button while selected
// The prevents us getting unnecessary click noises and events
// 不可切换的按钮在选中时禁用根按钮的交互(避免重复点击的音效和事件)
if (RootButton.IsValid())
{
RootButton->SetInteractionEnabled(bInteractableWhenSelected);
}
}
// 如果允许播放音效
if (bAllowSound)
{
// Selection was not triggered by a button click, so play the click sound
// 选中状态不是由点击触发时,播放点击音效
FSlateApplication::Get().PlaySound(NormalStyle.PressedSlateSound);
}
}
else
{
// Once deselected, restore the root button interactivity to the desired state
// 取消选中后,恢复根按钮的交互状态
if (RootButton.IsValid())
{
RootButton->SetInteractionEnabled(bInteractionEnabled);
}
// 执行取消选中状态的原生逻辑(可被子类重写)
NativeOnDeselected(bBroadcast);
}
// 更新输入动作相关部件的可见性
UpdateInputActionWidgetVisibility();
// 如果选中状态发生了变化
if (bValueChanged)
{
// 广播选中状态变更的事件
BroadcastBinaryPostStateChange(UWidgetSelectedStateRegistration::Bit, bSelected);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| void UCommonButtonBase::NativeOnSelected(bool bBroadcast)
{
// 调用蓝图中的选中回调
BP_OnSelected();
// 如果允许广播事件
if (bBroadcast)
{
// 广播选中状态变更(布尔值)
OnIsSelectedChanged().Broadcast(true);
// 广播选中状态变更(按钮实例+布尔值)
OnSelectedChangedBase.Broadcast(this, true);
// 广播按钮选中事件(按钮实例)
OnButtonBaseSelected.Broadcast(this);
}
// 更新当前文本样式(根据选中状态)
NativeOnCurrentTextStyleChanged();
}
|
OnSelectedChangedBase回调被触发. UCommonButtonBase->UCommonTabListWidgetBase::HandleTabButtonSelected->OnTabSelected->NaviagetToSetting 蓝图收束:
GameSettingRegistryChangeTracker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| /**
* FNoncopyable
* 用于不能被复制的类的实用模板。
* 按照此类进行派生,即可使您的类无法被复制(通过禁用拷贝构造和赋值运算符)
* 避免追踪状态在复制过程中出现混乱(例如两个追踪器同时修改同一份设置)。
*/
/**
* 这个类是游戏设置系统中的 “变更追踪器”,主要负责:
* 监视指定的设置注册表,记录哪些设置项被修改过(“脏状态”)。
* 提供对修改过的设置的操作(应用变更、恢复初始值、清除修改记录等)。
* 确保设置操作的安全性(如防止复原时的重复修改)。
*/
class FGameSettingRegistryChangeTracker : public FNoncopyable
{
public:
UE_API FGameSettingRegistryChangeTracker();
UE_API ~FGameSettingRegistryChangeTracker();
// 清除之前的绑定 并重新绑定的新的游戏设置注册器
UE_API void WatchRegistry(UGameSettingRegistry* InRegistry);
// 清除之前的绑定
UE_API void StopWatchingRegistry();
// 应用改变过的游戏设置变化
UE_API void ApplyChanges();
// 恢复改变过的游戏设置未初始值
UE_API void RestoreToInitial();
// 清除修改过的记录
UE_API void ClearDirtyState();
// 是否正在复原设置 避免同一时间 多次修改
bool IsRestoringSettings() const { return bRestoringSettings; }
// 是否有设置被修改过
bool HaveSettingsBeenChanged() const { return bSettingsChanged; }
private:
// 监听设置变化的事件 绑定在游戏设置注册器上
UE_API void HandleSettingChanged(UGameSetting* Setting, EGameSettingChangeReason Reason);
// 是否有设置变动过
bool bSettingsChanged = false;
// 是否正在复原设置
bool bRestoringSettings = false;
// 监听的游戏设置注册器
TWeakObjectPtr<UGameSettingRegistry> Registry;
// 被修改过的设置
TMap<FObjectKey, TWeakObjectPtr<UGameSetting>> DirtySettings;
};
|
绑定设置变动代理.
1
2
3
4
5
6
7
8
9
10
11
12
13
| void FGameSettingRegistryChangeTracker::WatchRegistry(UGameSettingRegistry* InRegistry)
{
ClearDirtyState(); // 清除之前的脏状态记录(重置追踪器)
StopWatchingRegistry(); // 停止之前的监视(解除旧注册表的事件绑定)
// 如果要监视的注册表与当前不同
if (Registry.Get() != InRegistry)
{
Registry = InRegistry; // 保存新的注册表弱引用
// 绑定新注册表的"设置变更事件"到当前追踪器的HandleSettingChanged函数
InRegistry->OnSettingChangedEvent.AddRaw(this, &FGameSettingRegistryChangeTracker::HandleSettingChanged);
}
}
|
这种设计的核心是解耦设置的 “变更发生” 与 “变更管理”:注册表只负责通知变更,而追踪器负责记录和处理这些变更,使代码职责更清晰,扩展性更强。