You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Slate UI C++的SComboButton创建下拉菜单时出现UI堆叠异常

Slate UI自定义SCompoundWidget下拉菜单堆叠问题解决

问题描述

在C++中基于Slate UI开发自定义SCompoundWidget下拉菜单,项目编译运行正常,初始下拉菜单显示无异常,但点击下拉选项后,会出现新下拉菜单堆叠在旧菜单上方的视觉问题。

示例下拉菜单选项:

Example
Test
p1

默认选中首个选项“Example”,下拉菜单采用AutoWidth()适配最长的“Example”宽度。点击下拉菜单显示正常,但选择“Test”后,因“Test”宽度更短,视觉上新菜单堆叠在旧菜单上;选择更短的“p1”时,会出现三层堆叠的假象。

使用Unreal Engine的Widget Inspector工具查看,实际仅存在一个SComboButton,视觉上的旧菜单无法交互。

已尝试的方案:

  • 每次选择选项时调用SAssignNew重新创建SComboButton,方法不够优雅
  • 尝试重绘SComboButton,未解决问题

问题代码

SDropDown.h

#pragma once

#include "CoreMinimal.h"

// 下拉菜单中的单个条目
struct FMenuEntry
{
    FMenuEntry(FText Label, TFunction<void()> Action = (), FText Tooltip = FText::GetEmpty(), FSlateIcon Icon = FSlateIcon())
    {
        this->Label = Label;
        this->Action = Action;
        this->Tooltip = Tooltip;
        this->Icon = Icon;
    }

    // 下拉条目显示文本
    FText Label;

    // 选中时触发的回调函数
    TFunction<void()> Action;

    // 悬停提示文本
    FText Tooltip;

    // 条目左侧图标
    FSlateIcon Icon;
};

class MYPROJ_API SDropDown : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SDropDown) :
        _MenuEntries(TArray<FMenuEntry>())
        { }

        SLATE_ARGUMENT(TArray<FMenuEntry>, MenuEntries)

    SLATE_END_ARGS()

    void Construct(const FArguments& InArgs);
    virtual bool SupportsKeyboardFocus() const override { return true; }

private:
    // 下拉菜单所有条目
    TArray<FMenuEntry> MenuEntries;

    // 下拉按钮显示的文本
    FText FeaturedItem;

    // 下拉按钮控件
    TSharedPtr<SComboButton> ComboButton;

    // 绑定下拉按钮文本到FeaturedItem
    FText GetFeaturedItem() const;
};

SDropDown.cpp

#include "SDropDown.h"

void SDropDown::Construct(const FArguments& InArgs)
{
    // 绑定传入的菜单条目
    this->MenuEntries = InArgs._MenuEntries;

    if(!MenuEntries.Num())
    {
        UE_LOG(LogSlate, Fatal, TEXT("下拉菜单未添加任何条目,请至少添加一个..."));
        return;
    }

    // 默认选中第一个条目
    FeaturedItem = MenuEntries[0].Label;

    FMenuBuilder Menu(true, nullptr);
    for(const FMenuEntry Entry : MenuEntries)
    {
        // 为每个条目添加菜单选项
        Menu.AddMenuEntry(Entry.Label, Entry.Tooltip, Entry.Icon, FUIAction(FExecuteAction::CreateLambda( [this, Entry] ()
            {
                // 更新下拉按钮显示的文本
                if(ComboButton.IsValid())
                {
                    FeaturedItem = Entry.Label;
                }

                // 执行条目绑定的回调
                if(Entry.Action)
                {
                    Entry.Action();
                }
            }
        )));
    }

    ChildSlot
    [
        SAssignNew(ComboButton, SComboButton)
        .ButtonContent()
        [
            SNew(STextBlock)
            .Text(this, &SDropDown::GetFeaturedItem) // 绑定文本到FeaturedItem
        ]
        .MenuContent()
        [
            Menu.MakeWidget()
        ]
    ];
}

FText SDropDown::GetFeaturedItem() const
{
    return FeaturedItem;
}

解决方案

这个问题的核心是SComboButton的按钮宽度在文本变化时未重新计算,导致渲染残留。Slate的AutoWidth()默认仅在初始化时计算一次宽度,后续文本变更不会自动触发宽度重算,因此视觉上会保留旧宽度的渲染痕迹。

修复方案1:触发布局强制刷新

使用Slate的FCachedValue绑定文本,同时在文本变更后强制刷新控件布局:

  1. 修改SDropDown.h中的FeaturedItem类型:
// 替换原FText FeaturedItem;
FCachedValue<FText> FeaturedItem;
  1. 修改SDropDown.cpp中的初始化和回调逻辑:
void SDropDown::Construct(const FArguments& InArgs)
{
    // ... 其他代码 ...
    // 初始化CachedValue,绑定当前Widget以通知更新
    FeaturedItem = FCachedValue<FText>(this, MenuEntries[0].Label);
    // ... 其他代码 ...

    for(const FMenuEntry Entry : MenuEntries)
    {
        Menu.AddMenuEntry(Entry.Label, Entry.Tooltip, Entry.Icon, FUIAction(FExecuteAction::CreateLambda( [this, Entry] ()
            {
                if(ComboButton.IsValid())
                {
                    // 更新CachedValue,自动通知Widget值变更
                    FeaturedItem.Set(Entry.Label);
                    // 强制刷新按钮布局,重新计算宽度
                    ComboButton->InvalidateLayoutAndVolatility();
                }

                if(Entry.Action)
                {
                    Entry.Action();
                }
            }
        )));
    }
    // ... 其他代码 ...
}

FText SDropDown::GetFeaturedItem() const
{
    return FeaturedItem.Get();
}

修复方案2:固定按钮为最大宽度

如果希望按钮始终保持所有选项中的最大宽度,避免宽度变化,可以提前计算所有条目的文本宽度,固定按钮宽度:

void SDropDown::Construct(const FArguments& InArgs)
{
    // ... 其他代码 ...

    // 计算所有条目的最大文本宽度
    const FSlateFontInfo FontInfo = FCoreStyle::Get().GetFontStyle("NormalFont");
    const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
    float MaxWidth = 0.0f;
    for(const FMenuEntry& Entry : MenuEntries)
    {
        const FVector2D TextSize = FontMeasure->Measure(Entry.Label, FontInfo);
        MaxWidth = FMath::Max(MaxWidth, TextSize.X);
    }

    // 添加按钮时设置固定宽度
    ChildSlot
    [
        SAssignNew(ComboButton, SComboButton)
        .ButtonContent()
        [
            SNew(STextBlock)
            .Text(this, &SDropDown::GetFeaturedItem)
        ]
        .MenuContent()
        [
            Menu.MakeWidget()
        ]
        .ButtonStyle(&FCoreStyle::Get().GetWidgetStyle<FButtonStyle>("ComboButton"))
        .WidthOverride(MaxWidth + 30) // 加上按钮图标和边距的额外宽度
    ];
}

内容的提问来源于stack exchange,提问作者Septimis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 09:14:56