5
5

More than 3 years have passed since last update.

UE4のRichTextBlockを拡張してみる

Last updated at Posted at 2020-06-28

はじめに

最近、RichTextBlockを触っていていくつか発見があったのでまとめてみようと思います。
また、標準のRichTextBlockを使っていて不便だと思った部分を拡張してみます。
こちらの記事で紹介するプロジェクトのUE4のバージョンは4.25です。
RichTextの基本的な使い方を理解している前提で話が進みますので以下の記事に目を通してから読み進めて頂ければと思います。

Rich Text Block を使ったテキストの高度なスタイル設定

拡張して実装する機能

1.PNG

  • Default以外のTextStyleSetを初期値として設定する機能
    標準のRichTextBlockではTextStyleSetで指定したデータテーブルのDefault以外のTextStyleSetを使用する場合、対象の箇所を<'RowName'></>で囲う必要があります。一部分だけならこれでいいのですが、全体を変えたい場合は全部をタグで囲うか個別のデータテーブルを作成するかの対応が必要です。

  • TextStyleSetのデータテーブルにDefaultのRowがない時にコンパイルエラーを出す機能
    標準のRichTextBlockにはDefaultTextStyleOverrideという項目があり、ここで指定した情報でTextStyleSetの情報を上書きできます。そのため、TextStyleSetのデータテーブルにDefaultのRowが無くても動作させることができます。エディタ上では問題なく動作しますが、パッケージ化するとRichTextBlockImageDecoratorを使用して挿入しているテクスチャが表示されなくなるため、データテーブルにDefaultのRowを追加しないとコンパイルが通らないようにします。

  • DefaultTextStyleOverrideを封印
    こちらの機能は便利ですが、上記のような不具合が生じたり、データテーブルでの管理から漏れるデータが出てきてしまいますし、拡張版RichTextBlockで内部的にDefaultTextStyleOverrideを使用するため封印します。

  • 4.20以前のRichTextBlockで使えたインライン装飾タグを復活させる
    Acren/RichTextBlockInlineDecorator
    こちらを参考にさせて頂き、
    [UE4] 部分的に色付きのテキストをUMGで作成する
    こちらの記事で紹介されているcolorやsizeなどのインライン装飾タグを使用できるようにします。

  • ImageDecoratorで挿入するテクスチャのサイズをDetailsから変更できるようにする
    標準のRichTextBlockImageDecoratorでは、挿入するテクスチャのサイズはwidthタグとheightタグを指定して変更できますが、テクスチャごとに毎回タグを付けるのも面倒なのでデフォルト値を設定できるようにします。

  • デフォルトのRichTextBlockImageDecoratorを使用できないようにする
    上記の機能を実装するために拡張版RichTextBlockでは、拡張版RichTextBlockImageDecoratorのみをサポートするようにします。

実装方法の紹介

色々と機能を追加しましたが、全部説明すると長くなるので一部掻い摘んで紹介します。


CustomRichTextBlock.h
protected:
    // デフォルトで使用するスタイルセットのデータテーブルID.
    UPROPERTY(EditAnywhere, Category = "Appearance")
    FName DefaultStyleSetID;

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

protected:
    // UObject Interface.
    virtual void PostLoad() override;
#if WITH_EDITOR
    virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
    virtual bool CanEditChange(const FProperty* InProperty) const override;
#endif
    // End of UObject Interface.

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    virtual void OnChageDefaultStyleSetID();
CustomRichTextBlock.cpp
void UCustomRichTextBlock::PostLoad()
{
    Super::PostLoad();

    OnChageDecoratorClasses();

    // データテーブル更新時に変更を反映させる.
    if (IsValid(TextStyleSet))
    {
        TextStyleSet->OnDataTableChanged().AddUObject(this, &UCustomRichTextBlock::OnChageDefaultStyleSetID);
    }

    OnChageInlineImageSize();
}

#if WITH_EDITOR
void UCustomRichTextBlock::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);

    // DecoratorClassesが変更されたらInlineDecoratorがあるかチェック.
    if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomRichTextBlock, DecoratorClasses))
    {
        OnChageDecoratorClasses();
    }

    // DefaultStyleSetIDの変更を適用.
    if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomRichTextBlock, DefaultStyleSetID) ||
        PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomRichTextBlock, DefaultTextStyleOverride))
    {
        OnChageDefaultStyleSetID();
    }

    // InlineImageSizeの変更を適用.
    if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomRichTextBlock, InlineImageSize))
    {
        OnChageInlineImageSize();
    }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

void UCustomRichTextBlock::OnChageDefaultStyleSetID()
{
    // データテーブルにあるIDなら変更を適用し、値をDefaultにする.
    if (auto StyleRow = TextStyleSet->FindRow<FRichTextStyleRow>(DefaultStyleSetID, StaticClass()->GetName()))
    {
        SetDefaultTextStyle(StyleRow->TextStyle);
    }
    else
    {
        DefaultStyleSetID = DefaultRowName;
    }
}

色々とやっていますが、流れ的にはUDataTable::OnDataTableChangedUObject::PostEditChangePropertyのタイミングでOnChageDefaultStyleSetID関数を呼んでいます。
OnChageDefaultStyleSetID関数ではTextStyleSetのデータテーブルの指定のRowをDefaultTextStyleとして設定します。URichTextBlock::SetDefaultTextStyleを使用するとDefaultTextStyleOverrideに設定されます。なので、拡張版RichTextBlockではDefaultTextStyleOverrideを封印します。


  • TextStyleSetのデータテーブルにDefaultのRowがない時にコンパイルエラーを出す機能
  • デフォルトのRichTextBlockImageDecoratorを使用できないようにする

2.PNG

CustomRichTextBlock.h
CustomRichTextBlock.cpp

CustomRichTextBlock.h
    // UWidget interface.
#if WITH_EDITOR
    virtual void ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const override;
#endif
    // End of UWidget interface.
CustomRichTextBlock.cpp
void UCustomRichTextBlock::ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const
{
    Super::ValidateCompiledDefaults(CompileLog);
    if (TextStyleSet)
    {
        // データテーブルにDefaultのRowがない.
        auto DefaultStyleRow = TextStyleSet->FindRow<FRichTextStyleRow>(DefaultRowName, StaticClass()->GetName());
        if (!DefaultStyleRow)
        {
            CompileLog.Error(
                FText::Format(LOCTEXT("RichTextBlock_NotFountDefaultRow", "{0} has no Row in it named {1}."),
                    FText::FromString(TextStyleSet->GetName()), FText::FromName(DefaultRowName)
                ));
        }
    }
    else
    {
        // データテーブルがセットされていない.
        CompileLog.Error(
            FText::Format(LOCTEXT("RichTextBlock_NotFountStyleSetDataTable", "The data table of Style Set is not set for {0}."),
                FText::FromString(GetName())
            ));
    }

    // 通常のRichTextBlockImageDecoratorはサポートしない.
    for (auto DecoratorClass : DecoratorClasses)
    {
        if (DecoratorClass->IsChildOf(URichTextBlockImageDecorator::StaticClass()) &&
            !DecoratorClass->IsChildOf(UCustomRichTextBlockImageDecorator::StaticClass()))
        {
            CompileLog.Error(
                FText::Format(LOCTEXT("RichTextBlock_NotSupportURichTextBlockImageDecorator", "{0} does not support decorators that inherit {1}. Use a class that inherits from {2}."),
                    FText::FromString(StaticClass()->GetName()), FText::FromString(URichTextBlockImageDecorator::StaticClass()->GetName()), FText::FromString(UCustomRichTextBlockImageDecorator::StaticClass()->GetName())
                ));
        }
    }
}

UWidget::ValidateCompiledDefaults関数はコンパイル時にデータが正常かを確認して、コンパイルエラーやワーニングを出すことができます。
今回はここで「データテーブルがセットされていないか」と「データテーブルにDefaultのRowがあるか」と「通常のRichTextBlockImageDecoratorが使われていないか」を確認し、問題があればコンパイルエラーになるようにしています。
ワーニングを出したい場合はCompileLog::ErrorではなくCompileLog::Warningを使用します。


CustomRichTextBlock.h
   virtual bool CanEditChange(const FProperty* InProperty) const override;
CustomRichTextBlock.cpp
bool UCustomRichTextBlock::CanEditChange(const FProperty* InProperty) const
{
    // bOverrideDefaultStyleとDefaultTextStyleOverrideの使用は禁止.
    bool bCanEdit = true;
    if (InProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomRichTextBlock, bOverrideDefaultStyle) ||
        InProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomRichTextBlock, DefaultTextStyleOverride))
    {
        bCanEdit = false;
    }

    return (Super::CanEditChange(InProperty) && bCanEdit);
}

UObject::CanEditChange関数はエディタ上でそのプロパティが変更できるかを設定できます。同様のことがUPROPERTYのメタ指定子EditConditionでもできますが、既に親クラスで定義されている変数については適用できません。(ベースクラスのコードを変える必要がある...)
UObject::CanEditChange関数を使用することにより子クラスから親クラスの変数の変更の可否を切り替えることができます。
今回はbOverrideDefaultStyleDefaultTextStyleOverrideを使えないようにしたいため、この二つのプロパティの時だけfalseを返すようにしています。
ちなみに、プロパティはFProperty::GetFNameGET_MEMBER_NAME_CHECKED(対象のクラス, 対象のプロパティ)を比較することで該当のプロパティかを判定できます。(4.24以前ではUProperty::GetFName


RichTextBlockInlineDecorator.cpp
TSharedPtr<ITextDecorator> URichTextBlockInlineDecorator::CreateDecorator(URichTextBlock* InOwner)
{
    FSlateFontInfo DefaultFont = InOwner->GetCurrentDefaultTextStyle().Font;
    FLinearColor DefaultColor = InOwner->GetCurrentDefaultTextStyle().ColorAndOpacity.GetSpecifiedColor();

    return MakeShareable(new FDefaultRichTextDecorator(this, DefaultFont, DefaultColor));
}

Acren/RichTextBlockInlineDecorator
基本的にはこちらのコードを使わせていただきましたが、一部拡張版RichTextBlockに対応するため変更しました。
元のコードではDefaultFontDefaultColorURichTextBlock::GetDefaultTextStyleから設定していましたが、こちらの関数ではDefaultTexrStyleOverrideの値は取得できず、インライン装飾タグを使用するとDefaultTexrStyleOverrideの設定が無視されてしまうためURichTextBlock::GetCurrentDefaultTextStyleを使用します。


  • ImageDecoratorで挿入するテクスチャのサイズをDetailsから変更できるようにする
    CustomRichTextBlock.h
CustomRichTextBlock.h
protected:
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // 挿入される画像サイズ.
    UPROPERTY(EditAnywhere, Category = "Appearance")
    int32 InlineImageSize;

public:
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // 挿入される画像サイズを取得.
    UFUNCTION(BlueprintPure, Category = "Widget")
    int32 GetInlineImageSize() const { return InlineImageSize; }

まずはUCustomRichTextBlock側にテクスチャのサイズを指定する変数とそのゲッターを作成します。

CustomRichTextBlockImageDecorator.cpp

CustomRichTextBlock.cpp
class FCustomRichInlineImage : public FRichTextDecorator
{
private:
    UCustomRichTextBlockImageDecorator* Decorator;
    UCustomRichTextBlock* CastedOwner;

public:
    FCustomRichInlineImage(URichTextBlock* InOwner, UCustomRichTextBlockImageDecorator* InDecorator)
        : FRichTextDecorator(InOwner)
        , Decorator(InDecorator)
    {
        CastedOwner = Cast<UCustomRichTextBlock>(InOwner);
    }

    virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override
    {
        // 拡張版RichTextBlockと使うことが前提.
        if (IsValid(CastedOwner) &&
            RunParseResult.Name == CustomRichTextBlockImageDecorator::TriggerWord &&
            RunParseResult.MetaData.Contains(CustomRichTextBlockImageDecorator::ImageMetaWord))
        {
            const FTextRange& IdRange = RunParseResult.MetaData[CustomRichTextBlockImageDecorator::ImageMetaWord];
            const FString TagId = Text.Mid(IdRange.BeginIndex, IdRange.EndIndex - IdRange.BeginIndex);

            const bool bWarnIfMissing = false;
            return Decorator->FindImageBrush(*TagId, bWarnIfMissing) != nullptr;
        }

        return false;
    }

protected:
    virtual TSharedPtr<SWidget> CreateDecoratorWidget(const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const override
    {
        const bool bWarnIfMissing = true;
        const FSlateBrush* Brush = Decorator->FindImageBrush(*RunInfo.MetaData[CustomRichTextBlockImageDecorator::ImageMetaWord], bWarnIfMissing);

        // 特に横幅の指定がなければ拡張版RichTextBlockのInlineImageSizeを適用.
        TOptional<int32> Width = CastedOwner->GetInlineImageSize();
        if (const FString* WidthString = RunInfo.MetaData.Find(CustomRichTextBlockImageDecorator::WidthMetaWord))
        {
            int32 WidthTemp;
            Width = FDefaultValueHelper::ParseInt(*WidthString, WidthTemp) ? WidthTemp : TOptional<int32>();
        }

        // 特に縦幅の指定がなければ拡張版RichTextBlockのInlineImageSizeを適用.
        TOptional<int32> Height = CastedOwner->GetInlineImageSize();
        if (const FString* HeightString = RunInfo.MetaData.Find(CustomRichTextBlockImageDecorator::HeightMetaWord))
        {
            int32 HeightTemp;
            Height = FDefaultValueHelper::ParseInt(*HeightString, HeightTemp) ? HeightTemp : TOptional<int32>();
        }

        EStretch::Type Stretch = EStretch::ScaleToFit;
        if (const FString* SstretchString = RunInfo.MetaData.Find(CustomRichTextBlockImageDecorator::StretchMetaWord))
        {
            static const UEnum* StretchEnum = StaticEnum<EStretch::Type>();
            int64 StretchValue = StretchEnum->GetValueByNameString(*SstretchString);
            if (StretchValue != INDEX_NONE)
            {
                Stretch = static_cast<EStretch::Type>(StretchValue);
            }
        }

        return SNew(SCustomRichInlineImage, Brush, TextStyle, Width, Height, Stretch);
    }
};

FCustomRichInlineImageのコンストラクタでOwnerをUCustomRichTextBlockにキャストして使用しているクラスが拡張版のRichTextBlockかを判定します。(デコレーターの性質上相互の依存関係があるのは如何なものか...と思いますが、今回は便利さ優先で行きます。インターフェースを用意すればこのあたりも改善できます。)
FCustomRichInlineImage::CreateDecoratorWidgetで各種タグの反映を行っているのでここに手を入れます。
今回はwidthタグやheightタグがない場合のみUCustomRichTextBlock::InlineImageSizeの値を設定するようにしました。
これでUCustomRichTextBlockのDetailsからサイズを変更できるようになりました。

番外編

ここまでで「拡張して実装する機能」で示した機能の実装方法は紹介しました。
ここで1つおまけで「挿入されるテクスチャの位置を変えたい」場合の対処方法を紹介します。

CustomRichTextBlockImageDecorator.cpp

CustomRichTextBlock.cpp
class SCustomRichInlineImage : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SCustomRichInlineImage)
    {}
    SLATE_END_ARGS()

public:
    void Construct(const FArguments& InArgs, const FSlateBrush* Brush, const FTextBlockStyle& TextStyle, TOptional<int32> Width, TOptional<int32> Height, EStretch::Type Stretch)
    {
        if (ensure(Brush))
        {
            const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
            float IconHeight = FMath::Min((float)FontMeasure->GetMaxCharacterHeight(TextStyle.Font, 1.0f), Brush->ImageSize.Y);
            float IconWidth = IconHeight;

            if (Width.IsSet())
            {
                IconWidth = Width.GetValue();
            }

            if (Height.IsSet())
            {
                IconHeight = Height.GetValue();
            }

            // ここでテクスチャの部分が作られる.
            ChildSlot
            [
                SNew(SBox)
                .HeightOverride(IconHeight)
                .WidthOverride(IconWidth)
                [
                    SNew(SScaleBox)
                    .Stretch(Stretch)
                    .StretchDirection(EStretchDirection::DownOnly)
                    .VAlign(VAlign_Center)
                    [
                        SNew(SImage)
                        .Image(Brush)
                    ]
                ]
            ];
        }
    }
};

上記コードのSCustomRichInlineImage::Construct内のChildSlotのSlateで挿入されるテクスチャの部分が組み立てられています。
そのため、以下のようにSScaleBoxの上にSCanvasを挿み、SCanvasPositionで位置をずらすことができます。

CustomRichTextBlock.cpp
ChildSlot
[
    SNew(SBox)
    .HeightOverride(IconHeight)
    .WidthOverride(IconWidth)
    [
        SNew(SCanvas)
        + SCanvas::Slot()
        .Size(FVector2D(IconWidth, IconHeight))
        .Position(FVector2D(/* ここでずらしたい分の座標を入れる */))
        [
            SNew(SScaleBox)
            .Stretch(Stretch)
            .StretchDirection(EStretchDirection::DownOnly)
            .VAlign(VAlign_Center)
            [
                SNew(SImage)
                .Image(Brush)
            ]
        ]
    ]
];

おわりに

少し長くなりましたが以上です。
どうでもいい話ですが、Slateを触っているとインデントが正常に入らないので結構ストレスを感じますね...w
RichTextBlockについてはあまり情報がなかったので、この記事が今後RichTextBlockを使う方の参考になれば幸いです。

この記事で紹介したプロジェクトは以下でダウンロードできます。(プラグインの形になっているので入れるだけでも動きます!)
https://github.com/Naotsun19B/CustomRichText

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5