#はじめに
RichTextを少し拡張してStyleSetのデータテーブルIDからデフォルトのStyleSetを持ってくるようにしてみた。#UE4Study pic.twitter.com/DEJYNFyZia
— Naotsun (@Naotsun_UE) June 20, 2020
最近、RichTextBlockを触っていていくつか発見があったのでまとめてみようと思います。
また、標準のRichTextBlockを使っていて不便だと思った部分を拡張してみます。
こちらの記事で紹介するプロジェクトのUE4のバージョンは4.25です。
RichTextの基本的な使い方を理解している前提で話が進みますので以下の記事に目を通してから読み進めて頂ければと思います。
Rich Text Block を使ったテキストの高度なスタイル設定
-
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のみをサポートするようにします。
#実装方法の紹介
色々と機能を追加しましたが、全部説明すると長くなるので一部掻い摘んで紹介します。
-
Default以外のTextStyleSetを初期値として設定する機能
CustomRichTextBlock.h
CustomRichTextBlock.cpp
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();
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::OnDataTableChanged
とUObject::PostEditChangeProperty
のタイミングでOnChageDefaultStyleSetID
関数を呼んでいます。
OnChageDefaultStyleSetID
関数ではTextStyleSetのデータテーブルの指定のRowをDefaultTextStyleとして設定します。URichTextBlock::SetDefaultTextStyle
を使用するとDefaultTextStyleOverride
に設定されます。なので、拡張版RichTextBlockではDefaultTextStyleOverride
を封印します。
- TextStyleSetのデータテーブルに
Default
のRowがない時にコンパイルエラーを出す機能 - デフォルトのRichTextBlockImageDecoratorを使用できないようにする
CustomRichTextBlock.h
CustomRichTextBlock.cpp
// UWidget interface.
#if WITH_EDITOR
virtual void ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const override;
#endif
// End of UWidget interface.
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
を使用します。
-
DefaultTextStyleOverrideを封印
CustomRichTextBlock.h
CustomRichTextBlock.cpp
virtual bool CanEditChange(const FProperty* InProperty) const override;
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
関数を使用することにより子クラスから親クラスの変数の変更の可否を切り替えることができます。
今回はbOverrideDefaultStyle
とDefaultTextStyleOverride
を使えないようにしたいため、この二つのプロパティの時だけfalseを返すようにしています。
ちなみに、プロパティはFProperty::GetFName
とGET_MEMBER_NAME_CHECKED(対象のクラス, 対象のプロパティ)
を比較することで該当のプロパティかを判定できます。(4.24以前ではUProperty::GetFName
)
-
4.20以前のRichTextBlockで使えたインライン装飾タグを復活させる
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に対応するため変更しました。
元のコードではDefaultFont
とDefaultColor
はURichTextBlock::GetDefaultTextStyle
から設定していましたが、こちらの関数ではDefaultTexrStyleOverride
の値は取得できず、インライン装飾タグを使用するとDefaultTexrStyleOverride
の設定が無視されてしまうためURichTextBlock::GetCurrentDefaultTextStyle
を使用します。
-
ImageDecoratorで挿入するテクスチャのサイズをDetailsから変更できるようにする
CustomRichTextBlock.h
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 挿入される画像サイズ.
UPROPERTY(EditAnywhere, Category = "Appearance")
int32 InlineImageSize;
public:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 挿入される画像サイズを取得.
UFUNCTION(BlueprintPure, Category = "Widget")
int32 GetInlineImageSize() const { return InlineImageSize; }
まずはUCustomRichTextBlock
側にテクスチャのサイズを指定する変数とそのゲッターを作成します。
CustomRichTextBlockImageDecorator.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
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
を挿み、SCanvas
のPosition
で位置をずらすことができます。
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