SRichTextBlock 에서 하이퍼링크에 FOnGetTooltipText 를 사용시 툴팁이 표시되지 않음

이 포스팅은 엔진 버전 5.3을 기준으로 작성되었습니다.

증상

  1. SRichTextBlock 사용시
  2. FHyperlinkDecorator::Create()FSlateHyperlinkRun::FOnGetTooltipText 를 사용하여 툴팁의 텍스트를 지정하려고 하면
  3. 툴팁이 표시되지 않습니다.

자세한 사연은 이랬습니다.

SRichTextBlock 를 생성해서, 그 안의 하이퍼링크에 마우스를 가져가면 간단한 툴팁이 나오게 하고 싶었습니다.

1
2
3
4
5
6
7
8
9
SNew(SRichTextBlock)
.DecoratorStyleSet(&FAppStyle::Get())
.Text(INVTEXT("SomeText5 <a id=\"browser\" href=\"https://hyaniner.com/\"> SomeLink </>"))
+ FHyperlinkDecorator::Create(
    TEXT("browser")
    , FSlateHyperlinkRun::FOnClick::CreateStatic(&OnBrowserLinkClicked)
    , FSlateHyperlinkRun::FOnGetTooltipText::CreateStatic(&SomeFunctionName)
    //InToolTipDelegate 는 디폴트값이 존재하므로 생략.
);

SomeFunctionName 는 이렇게 동작하게 했습니다.

1
2
3
4
static FText SomeFunctionName(const FSlateHyperlinkRun::FMetadata& Metadata)
{
    return SomeText;
}

하지만 아무것도 안 나오더라구요. 그래서 디버깅을 시작했지요.

참고로, FHyperlinkDecorator::Create()는 이렇게 생겼습니다.

(홈페이지에서 보기 편하게 하고 싶어서, 제가 인위적으로 줄바꿈을 조금 변경했습니다.)

1
2
3
4
5
6
static SLATE_API TSharedRef< FHyperlinkDecorator > Create(
    FString Id
    , const FSlateHyperlinkRun::FOnClick& NavigateDelegate
    , const FSlateHyperlinkRun::FOnGetTooltipText& InToolTipTextDelegate = FSlateHyperlinkRun::FOnGetTooltipText()
    , const FSlateHyperlinkRun::FOnGenerateTooltip& InToolTipDelegate = FSlateHyperlinkRun::FOnGenerateTooltip() 
);

원인

참고

참고로, 아래에서부터는,

  • 노출되는 엔진 코드 길이를 줄이기 위해 일부 내용을 생략하습니다. 정확한 코드는 GitHub 링크로 이동해서 보시기 바랍니다.
  • 이해를 돕기 위해, 제가 임의로 주석을 추가한 부분이 있습니다.

SWidget 의 사정

void SWidget::SWidgetConstruct(const FSlateBaseNamedArgs& Args) 에는 아래와 같은 코드가 있습니다.

GitHub 링크
1
2
3
4
5
6
7
8
if (Args._ToolTip.IsSet())
{
    SetToolTip(Args._ToolTip);
}
else if (Args._ToolTipText.IsSet())
{
    SetToolTipText(Args._ToolTipText);
}

ToolTip 이 주어졌다면, ToolTipText 는 무시됩니다.

FSlateHyperlinkRun 의 사정

FHyperlinkDecorator::Create() 가 만드는 FHyperlinkDecorator 객체는 SRichTextBlock 에게 전달됩니다.

SRichTextBlock 는 첫 Prepass에서 이를 기반으로 FSlateHyperlinkRun::CreateBlock()를 호출하고, 결과물을 TSharedRef< ILayoutBlock > 의 형식으로 생성합니다.

이 때의 콜스택은 아래와 같습니다.

FSlateHyperlinkRun::CreateBlock(int, int, TVector2<…>, const FLayoutBlockTextContext &, const TSharedPtr<…> &) SlateHyperlinkRun.cpp:66
FTextLayout::FRunModel::CreateBlock(const FTextLayout::FBlockDefinition &, float, const FLayoutBlockTextContext &) TextLayout.cpp:2715
FTextLayout::CreateLineViewBlocks(int, const int, const float, const TOptional<…> &, int &, int &, int &, TArray<…> &) TextLayout.cpp:330
FTextLayout::FlowLineLayout(const int, const float, TArray<…> &) TextLayout.cpp:588
FTextLayout::FlowLayout() TextLayout.cpp:543
FTextLayout::UpdateLayout() TextLayout.cpp:1208
FTextLayout::UpdateIfNeeded() TextLayout.cpp:1191
FSlateTextBlockLayout::ComputeDesiredSize(const FSlateTextBlockLayout::FWidgetDesiredSizeArgs &, const float) SlateTextBlockLayout.cpp:124
[inline] FSlateTextBlockLayout::ComputeDesiredSize(const FSlateTextBlockLayout::FWidgetDesiredSizeArgs &, const float, const FTextBlockStyle &) SlateTextBlockLayout.cpp:132
SRichTextBlock::ComputeDesiredSize(float) SRichTextBlock.cpp:90

이 때, FSlateHyperlinkRun::CreateBlock() 의 코드는 아래와 같습니다. GitHub 링크

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
FText ToolTipText;//여기에 중단점을 찍어두면 디버깅하기 편합니다.
TSharedPtr<IToolTip> ToolTip;//nullptr로 생성됨

if(TooltipDelegate.IsBound())//디폴트값인 FSlateHyperlinkRun::FOnGenerateTooltip() 이 전달되면 여기서 false.
{
    ToolTip = TooltipDelegate.Execute(RunInfo.MetaData);
}
else
{
...
/* ToolTipText 이 결정되는 부분. 자세한 내용은 GitHub 에서 보세요. */
...
}

TSharedRef< SWidget > Widget = SNew( SRichTextHyperlink, ViewModel )
    .Style( &Style )
    .Text( FText::FromString( FString( EndIndex - StartIndex, **Text + StartIndex ) ) )
    .ToolTip( ToolTip )//nullptr가 값으로서 설정되어 TAttribute<> 형식으로 전달됨.
    .ToolTipText( ToolTipText )
    .OnNavigate( this, &FSlateHyperlinkRun::OnNavigate )
    .TextShapingMethod( TextContext.TextShapingMethod );

TSharedPtr<IToolTip> ToolTipTAttribute<> 가 아니라는 게 문제입니다.

  1. TSharedPtr<IToolTip>ToolTip 이 지역변수로 만들어집니다. nullptr 이죠.
  2. 위의 SRichTextHyperlink 생성 부분에서 .ToolTip( ToolTip ) 에 nullptr 인 TSharedPtr<IToolTip> ToolTip 이 전달됩니다.
  3. 이 설정된 값을 기반으로 TAttribute<TSharedPtr<IToolTip>>이 만들어지고, SWidget에게 Args._ToolTip 로 전달됩니다.
  4. SWidget 에서 Args._ToolTip.IsSet() 는 true 가 됩니다. 그 내용물이 nullptr 이지만, 값이 설정되기는 했습니다.
  5. 그래서 SetToolTip(Args._ToolTip);부분의 코드가 실행되고, SetToolTipText(Args._ToolTipText);부분은 건너 뛰게 됩니다.
  6. 하지만 내용물이 nullptr 이니까 아무것도 나오지 않습니다.

해결안

const FSlateHyperlinkRun::FOnGetTooltipText& InToolTipTextDelegate 는 엔진을 수정하지 않는 한 사용이 불가능합니다.

그래서 그냥 const FSlateHyperlinkRun::FOnGenerateTooltip& InToolTipDelegate를 쓰기로 했습니다.

일단 SRichTextBlock 생성시 델리게이트 생성 부분을 아래와 같이 바꿨습니다.

1
2
3
4
5
6
7
8
9
SNew(SRichTextBlock)
.DecoratorStyleSet(&FAppStyle::Get())
.Text(INVTEXT("SomeText5 <a id=\"browser\" href=\"https://hyaniner.com/\"> SomeLink </>"))
+ FHyperlinkDecorator::Create(
    TEXT("browser")
    , FSlateHyperlinkRun::FOnClick::CreateStatic(&OnBrowserLinkClicked)
    , FSlateHyperlinkRun::FOnGetTooltipText()
    , FSlateHyperlinkRun::FOnGenerateTooltip::CreateStatic(&OnGenerateTooltip)
);

그리고 델리게이트가 호출하는 함수는 아래처럼 했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TSharedRef<IToolTip> SomeClassName::OnGenerateTooltip(const FSlateHyperlinkRun::FMetadata& Metadata)
{
    FText Result = /*여기에 툴팁내용*/;

    return SNew( SToolTip )
    .Text(Result)
    .IsInteractive(false)
    .BorderImage( FCoreStyle::Get().GetBrush("ToolTip.BrightBackground") )
    .TextMargin(FMargin(11.0f))
    ;
}

잘 나왔습니다.

앞으로 조심하고 싶은 점

SCompoundWidget 기반으로 Slate 위젯 클래스를 만들어 사용할 때, 여러 가지 경우의 수를 고려해야 하는 상황이어서 혼동이 발생할 가능성이 높다면, 그리고 TAttribute<> 관련 파라메터들를 전달할 필요가 있다면, 관련된 항목은 TAttribute<> 타입을 유지하는 것이 안전하지 않을까 하는 생각이 들었습니다. 저도 분명 유사한 실수를 할 것 같아서요.

태그