4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unreal Engine (UE)Advent Calendar 2021

Day 15

Slateのテキストに無理やり(?)アクセスする方法

Last updated at Posted at 2021-12-14

#はじめに

こちらの翻訳プラグインで実装した機能の中からエディタ上のテキストに無理やり(?)アクセスする方法をご紹介します。
こちらのプラグインでは、マウスポインタ直下にあるテキストを翻訳する機能や、編集中のテキストを翻訳して置き換える機能、ツールチップのテキストを翻訳して置き換える機能があり、それらの翻訳部分を除いた部分の実装方法を見ていきます。

今回の記事で使用するプロジェクトはUE4.27ですが、UE5でも同様に動作します。

#やり方

###Slateのウィジェットクラスのキャスト

まずこれらの機能を作るにあたって去年のアドカレで書いたこちらの記事の内容を使います。
UnrealC++でSlateのクラスをキャストする

今回のサンプルプロジェクトでは専用のヘッダーを用意しました。
CastSlateWidget.h

CastSlateWidget.h
namespace SamplePlugin
{
	namespace Private
	{
		/**
		 * Cast function for classes that inherit from SWidget.
		 */
		template<class To, class From>
		TSharedPtr<To> CastSlateWidget(TSharedPtr<From> FromPtr, const FName& ToClassName)
		{
			static_assert(TIsDerivedFrom<From, SWidget>::IsDerived, "This implementation wasn't tested for a filter that isn't a child of SWidget.");
			static_assert(TIsDerivedFrom<To, SWidget>::IsDerived, "This implementation wasn't tested for a filter that isn't a child of SWidget.");

			if (FromPtr.IsValid())
			{
				if (FromPtr->GetType() == ToClassName)
				{
					return StaticCastSharedPtr<To>(FromPtr);
				}
			}

			return nullptr;
		}
	}
}

#define CAST_SLATE_WIDGET(ToClass, FromPtr) SamplePlugin::Private::CastSlateWidget<ToClass>(FromPtr, #ToClass)

###テキストにアクセスする仕組み

今回、読み取り専用のテキストと編集可能なテキスト、ツールチップのテキストのアクセス方法についてご紹介しますが、いずれも仕組みは同じでアクセスに使用するインターフェースで定義されている関数が違うだけです。
読み取り専用のテキストを例としてテキストにアクセスする仕組みをご紹介します。

ReadOnlyTextAccessor.h

ReadOnlyTextAccessor.h
	/**
	 * Accessor interface for read-only text widgets.
	 */
	class SAMPLEPLUGIN_API IReadOnlyTextAccessor
	{
	public:
		// Destructor.
		virtual ~IReadOnlyTextAccessor() = default;
		
		// Returns the string set in the widget.
		virtual FText GetText() const = 0;

		// Returns the wrapped widget.
		virtual TSharedPtr<SWidget> AsWidget() const = 0;
	};

こちらが読み取り専用のテキストにアクセスするためのインターフェースで、テキストと対象のウィジェットを取得する関数のみが定義されています。

ReadOnlyTextAccessor.h

ReadOnlyTextAccessor.h
	// The process of converting the passed widget to IReadOnlyTextAccessor.
	DECLARE_DELEGATE_RetVal_OneParam(TSharedPtr<IReadOnlyTextAccessor>, FOnGetReadOnlyTextAccessor, TSharedPtr<SWidget> /* InWidget */);

	/**
	 * Factory class for read-only text widgets accessor.
	 */
	class SAMPLEPLUGIN_API FReadOnlyTextAccessorFactory
	{
	public:
		// Register the widget type name and accessor generation function.
		static void RegisterAccessor(const FName& WidgetTypeName, const FOnGetReadOnlyTextAccessor& Generator);

		// Unregister the widget type name and accessor generation function.
		static void UnregisterAccessor(const FName& WidgetTypeName);

		// Returns whether the specified widget type is registered.
		static bool IsAccessorRegistered(const FName& WidgetTypeName);

		// Create a accessor according to the registered widget type.
		static TSharedPtr<IReadOnlyTextAccessor> CreateAccessor(TSharedPtr<SWidget> InWidget);

	private:
		// Registry of widget type name and accessor generation function.
		static TMap<FName, FOnGetReadOnlyTextAccessor> ReadOnlyTextAccessorRegistry;
	};

次にこちらがウィジェットのクラス名と上のインターフェースを実装した各ウィジェット用のクラスの生成関数を登録するクラスです。

ReadOnlyTextAccessor.cpp

ReadOnlyTextAccessor.cpp
	TSharedPtr<IReadOnlyTextAccessor> FReadOnlyTextAccessorFactory::CreateAccessor(TSharedPtr<SWidget> InWidget)
	{
		if (InWidget.IsValid())
		{
			if (const FOnGetReadOnlyTextAccessor* Generator = ReadOnlyTextAccessorRegistry.Find(InWidget->GetType()))
			{
				return Generator->Execute(InWidget);
			}
		}

		return nullptr;
	}

CreateAccessorの引数で渡されたウィジェットの型が登録されていたら生成関数を実行しIReadOnlyTextAccessorを生成します。(登録されてなかったらnullptrを返す)

ReadOnlyTextAccessor.h

ReadOnlyTextAccessor.h
	/**
	 * Template class used when registering widgets in FReadOnlyTextAccessorFactory.
	 * Only classes with functions that must be overridden in
	 * IReadOnlyTextAccessor can be specified in TWidgetClass.
	 */
	template<class TWidgetClass>
	class TReadOnlyText : public IReadOnlyTextAccessor
	{
	public:
		// Constructor.
		TReadOnlyText(TSharedPtr<TWidgetClass> InReadOnlyText)
			: IReadOnlyTextAccessor()
			, ReadOnlyText(InReadOnlyText)
		{
		}
	
		// IReadOnlyTextWrapper interface.
		virtual FText GetText() const override
		{
			if (ReadOnlyText.IsValid())
			{
				return ReadOnlyText->GetText();
			}

			return FText();
		}

		virtual TSharedPtr<SWidget> AsWidget() const override
		{
			return ReadOnlyText;
		}
		// End of IReadOnlyTextWrapper interface.

	private:
		// An instance of the actual widget.
		TSharedPtr<TWidgetClass> ReadOnlyText;
	};

こちらはIReadOnlyTextAccessorを実装するクラスの基本的な形です。
特別な実装をする場合以外はこのクラステンプレートを使用する感じになります。

DefaultSlateTextAccessor.cpp

DefaultSlateTextAccessor.cpp
		FReadOnlyTextAccessorFactory::RegisterAccessor(
			GET_CLASS_NAME(STextBlock),
			FOnGetReadOnlyTextAccessor::CreateLambda(
			[](TSharedPtr<SWidget> InWidget) -> TSharedPtr<IReadOnlyTextAccessor>
			{
				if (TSharedPtr<STextBlock> TextBlock = CAST_SLATE_WIDGET(STextBlock, InWidget))
				{
					return MakeShared<TReadOnlyText<STextBlock>>(TextBlock);
				}

				return nullptr;
			})
		);

実際の使用例としてSTextBlockを登録するコードです。
STextBlockは先程のクラステンプレートで問題ないので引数で渡されてきたウィジェットのクラスをSTextBlockにキャストしてTReadOnlyTextのコンストラクタで渡し、TReadOnlyTextを返してあげます。

DefaultSlateTextAccessor.cpp

DefaultSlateTextAccessor.cpp
		class FHyperlinkImpl : public IReadOnlyTextAccessor
		{
		public:
			// Constructor.
			FHyperlinkImpl(TSharedPtr<SHyperlink> InHyperlink)
				: IReadOnlyTextAccessor()
				, Hyperlink(InHyperlink)
			{
			}
	
			// IReadOnlyTextAccessor interface.
			virtual FText GetText() const override
			{
				if (Hyperlink.IsValid())
				{
					return Hyperlink->GetAccessibleText();
				}

				return FText();
			}

			virtual TSharedPtr<SWidget> AsWidget() const override
			{
				return Hyperlink;
			}
			// End of IReadOnlyTextAccessor interface.

		private:
			TSharedPtr<SHyperlink> Hyperlink;
		};

SHyperlinkの様にTReadOnlyTextが使用できない場合はこのようにそのウィジェットクラス用のIReadOnlyTextAccessorを実装したクラスを用意する必要があります。

ざっくり要約すると、Factoryクラスにウィジェットの型名とその型用の共通のインターフェースを実装したクラスを生成する関数を登録して、生成関数にTSharedPtr<SWidget>を渡した時に登録されている型とマッチするのであればその型用のクラスを生成して返すという感じです。
インターフェースからテキストの取得やその他の操作を行うことができます。

一通りの実装の流れを紹介しましたが、後述する編集可能テキストやツールチップのテキストも同様の方法で実装されていますので、以降この辺りの実装についての紹介は省きます。

###マウスポインタ直下にあるテキストにアクセスしてみる

では、ここまでで確認したIReadOnlyTextAccessorを使用して本題であるマウスポインタ直下にあるテキストへのアクセスを行う部分を見ていきましょう。

SamplePluginSlateHelpers.cpp

SamplePluginSlateHelpers.cpp
	TSharedPtr<SWidget> FSamplePluginSlateHelpers::GetWidgetUnderMouseCursor()
	{
		FSlateApplication& SlateApplication = FSlateApplication::Get();
		const FWidgetPath WidgetsUnderCursor = SlateApplication.LocateWindowUnderMouse(
			SlateApplication.GetCursorPos(), SlateApplication.GetInteractiveTopLevelWindows()
		);
 
		if (WidgetsUnderCursor.IsValid())
		{
			const FArrangedChildren& Widgets = WidgetsUnderCursor.Widgets;
			if (Widgets.Num() > 0)
			{
				return Widgets.Last().Widget;
			}
		}
	
		return nullptr;
	}

まずはマウスポインタ直下にあるウィジェットを取得する関数を見ていきましょう。
FSlateApplicationLocateWindowUnderMouseというマウスポインタ直下にあるウィジェットパス(使用中のウィンドウから最下層のウィジェットまでのウィジェット)を返す関数があるので、それを使用して最下層のウィジェットを返すようにしています。
こちらの関数はウィジェット関連のデバッグなどでも便利なので覚えておくといつか助かるかもしれません。

SamplePluginActions.cpp

SamplePluginActions.cpp
	void FSamplePluginActions::TestReadOnlyTextAccessor()
	{
		// Find the read only text to process.
		TSharedPtr<IReadOnlyTextAccessor> ReadOnlyText = nullptr;
		{
			const TSharedPtr<SWidget> Widget = FSamplePluginSlateHelpers::GetWidgetUnderMouseCursor();
			if (Widget.IsValid())
			{
				ReadOnlyText = FReadOnlyTextAccessorFactory::CreateAccessor(Widget);
			}
		}

		if (ReadOnlyText.IsValid())
		{
			const FText& Text = ReadOnlyText->GetText();
			if (!Text.IsEmpty())
			{
				UE_LOG(LogSamplePlugin, Log, TEXT("%s"), *Text.ToString());
			}
		}
	}

続いて取得したウィジェットからテキストを取り出す部分ですが、上記の関数でマウスポインタ直下にあるウィジェットを取得し、取得したウィジェットをFReadOnlyTextAccessorFactory::CreateAccessorに渡してIReadOnlyTextAccessorを生成します。
ここで取得したウィジェットがFReadOnlyTextAccessorFactoryに登録されていたらIReadOnlyTextAccessorを通してテキストを取得しログ出力します。

サンプルプロジェクトではShift+Ctrl+Zでマウスポインタ直下にあるテキストをログ出力する機能を確認することができます。

また、冒頭でご紹介した翻訳プラグインではマウスポインタ直下にあるテキストを翻訳し、その結果をポップアップウィンドウで表示する機能で使用されています。

###編集中のテキストにアクセスしてみる

EditableTextAccessor.h

EditableTextAccessor.h
	/**
	 * Accessor interface for editable text widgets.
	 */
	class SAMPLEPLUGIN_API IEditableTextAccessor
	{
	public:
		// Destructor.
		virtual ~IEditableTextAccessor() = default;
		
		// Returns the string set in the widget.
		virtual FText GetText() const = 0;

		// Returns the string selected in the widget.
		virtual FText GetSelectedText() const = 0;
			
		// Sets the text of the widget.
		virtual void SetText(const FText& Text) = 0;

		// Query to see if any text is selected within the widget.
		virtual bool AnyTextSelected() const = 0;
			
		// Returns the wrapped widget.
		virtual TSharedPtr<SWidget> AsWidget() const = 0;
	};

こちらが編集中のテキストにアクセスするインターフェースです。
読み取り専用のテキストのインターフェースであった機能に加えて選択しているテキストのみを取得する関数や、テキストを設定する関数が追加されています。

EditableTextHandle.h

EditableTextHandle.h
	/**
	 * A utility structure for replacing only selected parts.
	 */
	class SAMPLEPLUGIN_API FEditableTextHandle
	{
	public:
		// Constructor.
		FEditableTextHandle(const TSharedPtr<IEditableTextAccessor>& InEditableText);

		// If the text is selected, only the selected part is returned.
		FString GetText() const;
		
		// If the text is selected, replace only that part.
		void SetText(const FString& InText);

		// Returns whether the editable text on which this data is based is valid.
		bool IsValid() const;
		
		// Returns editable text from which this data is based.
		TSharedPtr<IEditableTextAccessor> GetEditableTextAccessor() const;

		// Returns text at the time this handle was generated.
		FString GetCachedText() const;

		// Returns whether any character was selected.
		bool IsSelectedText() const;
		
		// Returns selected text at the time this handle was generated.
		FString GetCachedSelectedText() const;

	private:
		// The editable text from which this data is based.
		TSharedPtr<IEditableTextAccessor> EditableTextAccessor;

		// The text at the time this handle was generated.
		FString CachedText;

		// Whether any character was selected.
		bool bIsSelectedText;
		
		// The selected text at the time this handle was generated.
		FString CachedSelectedText;

		// Owner of EditableText.
		TSharedPtr<SInlineEditableTextBlock> InlineEditableTextBlock;
	};

上記のインターフェースだけだと少し使い勝手が悪い(例えばインターフェースを生成してから非同期で処理が入り、その後選択していた部分だけを置き換える、など...)ので、FEditableTextHandleを使います。
子のクラスを生成した時点のテキストを保持していたり、選択中のテキストのみを置き換えたりできるクラスです。

2.PNG
また、このように常に編集モードのテキストならインターフェースを使ってウィジェットにテキストを設定すればテキストの置き換えを行えますが、
4.PNG
変数名や関数名のように編集モードと読み取り専用モードがあるテキストの場合は少し工夫をする必要があります。

EditableTextHandle.cpp

EditableTextHandle.cpp
		TSharedPtr<SInlineEditableTextBlock> FindInlineEditableTextBlock(const TSharedPtr<SWidget>& SearchTarget)
		{
			if (!SearchTarget.IsValid())
			{
				return nullptr;
			}

			if (!SearchTarget->IsParentValid())
			{
				return nullptr;
			}

			const TSharedPtr<SWidget> Parent = SearchTarget->GetParentWidget();
			const TSharedPtr<SInlineEditableTextBlock> InlineEditableTextBlock = CAST_SLATE_WIDGET(SInlineEditableTextBlock, Parent);
			if (InlineEditableTextBlock.IsValid())
			{
				return InlineEditableTextBlock;
			}

			return FindInlineEditableTextBlock(Parent);
		}

EditableTextHandle.cpp

EditableTextHandle.cpp
	FEditableTextHandle::FEditableTextHandle(const TSharedPtr<IEditableTextAccessor>& InEditableText)
		: EditableTextAccessor(InEditableText)
		, bIsSelectedText(false)
	{
		// ~~~

		// When the edit mode and display mode are switched like a comment node or function name,
		// it is necessary to switch the mode from this handle, so the it is cached.
		InlineEditableTextBlock = EditableTextHandleInternal::FindInlineEditableTextBlock(
			EditableTextAccessor->AsWidget()
		);
	}

まず、このタイプのテキストブロックはSInlineEditableTextBlockが生成した編集用のテキストボックスで読み取り専用モードになると削除されてしまいます。
そのため、親のウィジェットのSInlineEditableTextBlockを再帰的に探す関数を使って、コンストラクタでキャッシュしておきます。

EditableTextHandle.cpp

EditableTextHandle.cpp
	void FEditableTextHandle::SetText(const FString& InText)
	{
		if (!EditableTextAccessor.IsValid())
		{
			return;
		}

		// Switch to edit mode if there is an InlineEditableTextBlock.
		if (InlineEditableTextBlock.IsValid())
		{
			const TSharedPtr<FSlateUser> SlateUser = FSamplePluginSlateHelpers::GetLocalSlateUser();
			if (SlateUser.IsValid())
			{
				SlateUser->ReleaseAllCapture();
				InlineEditableTextBlock->EnterEditingMode();
			}
		}
		
		if (bIsSelectedText)
		{
			const FString ReplacedString = CachedText.Replace(*CachedSelectedText, *InText);
			EditableTextAccessor->SetText(FText::FromString(ReplacedString));
		}
		else
		{
			EditableTextAccessor->SetText(FText::FromString(InText));
		}
	}

SInlineEditableTextBlockがある場合は編集モードにしてからテキストの設定を行います。
また、テキストが選択されている場合は選択している部分のみを置き換えて設定しています。

SamplePluginSlateHelpers.cpp

SamplePluginSlateHelpers.cpp
	TSharedPtr<FSlateUser> FSamplePluginSlateHelpers::GetLocalSlateUser()
	{
		return FSlateApplication::Get().GetCursorUser();
	}

	// ~~~

	TSharedPtr<SWidget> FSamplePluginSlateHelpers::GetEditingWidget()
	{
		const TSharedPtr<FSlateUser> SlateUser = GetLocalSlateUser();
		if (SlateUser.IsValid())
		{
			return SlateUser->GetFocusedWidget();
		}
	
		return nullptr;
	}

続いて編集中のウィジェットの取得の仕方です。
こちらはそこまで難しくなく、FSlateUser(GameFrameworkで言うPlayerController的なやつ)からフォーカス中のウィジェットを取得するだけです。

SamplePluginActions.cpp

SamplePluginActions.cpp
	void FSamplePluginActions::TestEditableTextAccessor()
	{
		// Find the editable text to process.
		TSharedPtr<IEditableTextAccessor> EditableText = nullptr;
		{
			const TSharedPtr<SWidget> Widget = FSamplePluginSlateHelpers::GetEditingWidget();
			if (Widget.IsValid())
			{
				EditableText = FEditableTextAccessorFactory::CreateAccessor(Widget);
			}
		}
	
		if (!EditableText.IsValid())
		{
			return;
		}

		if (EditableText->GetText().IsEmpty() || !EditableText->AnyTextSelected())
		{
			return;
		}

		const TSharedPtr<FEditableTextHandle> EditableTextHandle = MakeShared<FEditableTextHandle>(EditableText);
		check(EditableTextHandle.IsValid());
		
		EditableTextHandle->SetText(TEXT("Test Editable Text Accessor"));
	}

こちらが編集中のテキストの選択している部分をTest Editable Text Accessorに置き換える部分です。
基本的な流れはマウスポインタ直下にあるテキストにアクセスする場合と同じですが、テキストを設定するためにFEditableTextHandleを使用しています。

5.PNG
6.PNG
7.PNG

サンプルプロジェクトではShift+Ctrl+Xで編集中のテキストの選択している部分を置き換える機能を確認することができます。

また、冒頭でご紹介した翻訳プラグインでは編集中のテキストの選択している部分を翻訳結果に置き換える機能で使用されています。

###ツールチップのテキストにアクセスしてみる

TooltipTextAccessor.h

TooltipTextAccessor.h
	/**
	 * Accessor interface for tooltip text widgets.
	 */
	class SAMPLEPLUGIN_API ITooltipTextAccessor
	{
	public:
		// Destructor.
		virtual ~ITooltipTextAccessor() = default;
		
		// Returns the tooltip text set in the widget.
		virtual FText GetTextTooltip() const = 0;

		// Returns the wrapped widget.
		virtual TSharedPtr<SWidget> AsWidget() const = 0;
	};

インターフェースは読み取り専用テキストと同じです。

TooltipTextHandle.h

TooltipTextHandle.h
	/**
	 * A utility structure for replace widget tooltips until mouse is moved.
	 */
	class SAMPLEPLUGIN_API FTooltipTextHandle
	{
	public:
		// Create this handle and update active instance.
		static TSharedPtr<FTooltipTextHandle> CreateTooltipTextHandle();
		
		// Replace the tooltip of the owner's widget with this handle.
		void SetText(const FString& InText);

		// Returns false if the mouse is already moved or the owner's widget is incorrect.
		bool IsValid() const;

	private:
		// Constructor.
		FTooltipTextHandle();
		
		// Called when the mouse cursor is moved from the widget of the owner of this handle.
		void HandleOnMouseLeave(const FPointerEvent& InEvent);
		
	private:
		// The tooltip owner widget indicated by this handle.
		TSharedPtr<SWidget> TooltipOwner;
		
		// The original tooltip that is returned when the mouse cursor is moved.
		TSharedPtr<IToolTip> OriginalTooltip;
		
		// Whether the mouse cursor has already been moved.
		bool bHasMouseCursorLeft;
		
		// An instance of the currently active handle.
		static TSharedPtr<FTooltipTextHandle> ActiveInstance;
	};

インターフェースだけだとテキストの取得しかできないため、ツールチップが消えるまで内容を上書きするためにFTooltipTextHandleを使います。

TooltipTextHandle.cpp

TooltipTextHandle.cpp
	FTooltipTextHandle::FTooltipTextHandle()
		: bHasMouseCursorLeft(false)
	{
		TooltipOwner = FSamplePluginSlateHelpers::GetWidgetUnderMouseCursor();
		if (!TooltipOwner.IsValid())
		{
			bHasMouseCursorLeft = true;
			return;
		}
		
		TooltipOwner->SetOnMouseLeave(FSimpleNoReplyPointerEventHandler::CreateRaw(this, &FTooltipTextHandle::HandleOnMouseLeave));
	}

	void FTooltipTextHandle::HandleOnMouseLeave(const FPointerEvent& InEvent)
	{
		if (IsValid())
		{
			TooltipOwner->SetToolTip(OriginalTooltip);
		}
		
		bHasMouseCursorLeft = true;
		TooltipOwner->SetOnMouseLeave(FSimpleNoReplyPointerEventHandler());
		ActiveInstance.Reset();
	}

仕組みとしては簡単で、コンストラクタでマウスポインタ直下にあウィジェットをキャッシュし、そのウィジェットからマウスポインタが離れた時のイベントをバインドしていて、マウスポインタが離れた時にキャッシュしておいたオリジナルのツールチップに戻す感じです。

TooltipTextHandle.cpp

TooltipTextHandle.cpp
	void FTooltipTextHandle::SetText(const FString& InText)
	{
		if (!IsValid())
		{
			return;
		}
		
		auto& SlateApplication = FSlateApplication::Get();
		const TSharedPtr<IToolTip> NewTooltip = SlateApplication.MakeToolTip(FText::FromString(InText));
		if (NewTooltip.IsValid())
		{
			OriginalTooltip = TooltipOwner->GetToolTip();
			TooltipOwner->SetToolTip(NewTooltip);
			SlateApplication.UpdateToolTip(true);
		}
	}

ツールチップはIToolTipを継承したウィジェットをSWidgetが持っているので、置き換えたいテキストから生成したツールチップを対象のSWidgetに設定しています。

SamplePluginSlateHelpers.cpp

SamplePluginSlateHelpers.cpp
	TSharedPtr<SWidget> FSamplePluginSlateHelpers::GetTooltipWidget()
	{
		// Looking to tooltip window from all visible windows,
		// because we can't get active tooltip window from outside.
		TArray<TSharedRef<SWindow>> VisibleWindows;
		FSlateApplication::Get().GetAllVisibleWindowsOrdered(VisibleWindows);

		for (const auto& VisibleWindow : VisibleWindows)
		{
			// The tooltip window should be the top window with no title.
			const bool bHasNoTitle = VisibleWindow->GetTitle().IsEmpty();
			const bool bIsTopmostWindow = VisibleWindow->IsTopmostWindow();
			if (!bHasNoTitle || !bIsTopmostWindow)
			{
				continue;
			}
				
			const FChildren* Children = VisibleWindow->GetChildren();
			if (Children == nullptr)
			{
				continue;
			}

			for (int32 Index = 0; Index < Children->Num(); Index++)
			{
				const TSharedRef<const SWidget> ChildWidget = Children->GetChildAt(Index);
				const TSharedPtr<SWidget> ChildWidgetPtr = ConstCastSharedRef<SWidget>(ChildWidget);
				const TSharedPtr<SWeakWidget> WeakWidget = CAST_SLATE_WIDGET(SWeakWidget, ChildWidgetPtr);
				if (WeakWidget.IsValid())
				{
					return WeakWidget;
				}
			}
		}
		
		return nullptr;
	}

意外にも(?)現在表示しているツールチップを取得する関数がない(FSlateApplicationのprivate関数はある)ので、全ウィンドウからツールチップで使用しているウィンドウのウィジェットを探す関数を作りました。

SamplePluginActions.cpp

SamplePluginActions.cpp
	void FSamplePluginActions::TestTooltipTextAccessor()
	{
		const TSharedPtr<SWidget> TooltipWidget = FSamplePluginSlateHelpers::GetTooltipWidget();

		// Find the SToolTip contained in the tooltip widget and get the text.
		TSharedPtr<ITooltipTextAccessor> TooltipText = nullptr;
		{
			TArray<TSharedPtr<SWidget>> ChildWidgets;
			FSamplePluginSlateHelpers::GetAllChildWidgets(TooltipWidget, ChildWidgets);
			for (const auto& ChildWidget : ChildWidgets)
			{
				TooltipText = FTooltipTextAccessorFactory::CreateAccessor(ChildWidget);
				if (TooltipText.IsValid())
				{
					break;
				}
			}
		}

		if (TooltipText.IsValid())
		{
			const FText& TextTooltip = TooltipText->GetTextTooltip();
			if (!TextTooltip.IsEmpty())
			{
				UE_LOG(LogSamplePlugin, Log, TEXT("%s"), *TextTooltip.ToString());
				
				const TSharedPtr<FTooltipTextHandle> TooltipTextHandle = FTooltipTextHandle::CreateTooltipTextHandle();
				check(TooltipTextHandle.IsValid());
				TooltipTextHandle->SetText(TEXT("Test Tooltip Text Accessor"));
			}
		}
	}

先程の関数でツールチップのウィンドウウィジェットを取得し、その子ウィジェットを走査してツールチップ本体を見つけています。
FTooltipTextHandleFEditableTextHandleと同様に作成からテキストの設定まで非同期の処理を挟むことができます。

8.PNG
9.PNG

サンプルプロジェクトではShift+Ctrl+Cでツールチップのテキストをログ出力してから置き換える機能を確認することができます。

また、冒頭でご紹介した翻訳プラグインではツールチップのテキストを翻訳結果に置き換える機能で使用されています。

#おわりに
翻訳プラグインの中でも作るのに苦労した部分だったので記事にしようと思ったのですが、思いのほか説明するのが難しくコードを見せながら少し説明する急ぎ足な記事になってしまいました。
そもそもエディタ上のテキストにアクセスする需要がないのでは?という話ですが、珍しいケースではあると思うので、こんなやり方もあるんだなぁくらいに思って貰えたら助かります。(Slateクラスをキャストできればなんでもできるよ!)

この記事で紹介したプロジェクトは以下でダウンロードできます。
https://github.com/Naotsun19B/SlateTextAccessor

明日は、@nana_321321さんの「BlenderのAutoRigを使ってUE4に持っていくまでのお話」です!

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?