文章

设置界面

设置界面

LyraSettingScreen(设置主界面)

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;
};

输入操作

InputBar

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. P_ButtonActionBar

通过勾选默认回退操作

蓝图中勾选即可. 注意该固定栏的容器必须要实例化出来.并在容器中指定按钮的类. 蓝图->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)));
}

ULyraBoundActionButton

用于监听输入方法改变时切换按钮样式

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,可以轻松添加新的设置类型。 灵活性高:支持过滤无效设置项,适配不同平台或游戏状态。

顶部侧边栏联动设置具体面板

蓝图逻辑: RegisterTab

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;
}

TabButton样式的选择

TabButtonDebugStyle TabButtonStyle

TabButtonGroup

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 蓝图收束: ButtonToNav

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);
    }
}

这种设计的核心是解耦设置的 “变更发生” 与 “变更管理”:注册表只负责通知变更,而追踪器负责记录和处理这些变更,使代码职责更清晰,扩展性更强。

本文由作者按照 CC BY 4.0 进行授权