UnrealC++でエディタ上の入力をプログラムから行う


はじめに

あけましておめでとうございます!
2021年初のUE4記事を投稿しようと思って日付が変わった瞬間に投稿しましたが、果たして初記事になれたのでしょうか...w

それはさておき、今回はプログラムからエディタ上での入力を行う方法をご紹介したいと思います。
どういうことかというと、コピペする際にCtrl+Cを押したりすると思いますが、その入力をプログラムから行います。絶妙に使い道が無さそうな内容ですが、お雑煮でも啜りながら読んでいただければと思います。

こちらのプラグインでコピペをプログラムからする必要があったため、こちらのプラグインのコードで説明していきます。

つくりかた

まず、今回の主役であるFSlateApplicationを使用するためにSlateモジュールとApplicationCoreモジュールを追加します。

GraphPrinter.Build.cs

GraphPrinter.Build.cs
PrivateDependencyModuleNames.AddRange(
    new string[]
    {
        "CoreUObject",
        "Engine",
        "UMG",
        "Slate",        // これ
        "SlateCore",
        "RenderCore",
        "UnrealEd",
        "GraphEditor",
        "MainFrame",
        "EditorStyle",
        "ApplicationCore",  // これ
        "DesktopPlatform",
        "ImageWriteQueue",
        "AssetManagerEditor",
         }
     );

さっそく本題に入り、入力を呼び出す関数を見ていきましょう。
GraphPrinterCore.cpp

GraphPrinterCore.cpp
bool FGraphPrinterCore::ExportGraphToPngFile(const FString& FilePath, TSharedPtr<SGraphEditor> GraphEditor)
{
    if (!GraphEditor.IsValid())
    {
        return false;
    }

    FPngTextChunkWriter Writer(FilePath);

    // Make a shortcut key event for copy operation.
    FKeyEvent KeyEvent;
    if (!GetKeyEventFromUICommandInfo(FGenericCommands::Get().Copy, KeyEvent))
    {
        return false;
    }

    // Since the clipboard is used, the current data is temporarily saved.
    FString CurrentClipboard;
    FPlatformApplicationMisc::ClipboardPaste(CurrentClipboard);

    // Get information about the selected node via the clipboard.
    bool bWasSucceedKeyDown = FSlateApplication::Get().ProcessKeyDownEvent(KeyEvent);

    FString ExportedText;
    FPlatformApplicationMisc::ClipboardPaste(ExportedText);

    // Restore the saved clipboard data.
    FPlatformApplicationMisc::ClipboardCopy(*CurrentClipboard);

    if (!bWasSucceedKeyDown || !FEdGraphUtilities::CanImportNodesFromText(GraphEditor->GetCurrentGraph(), ExportedText))
    {
        return false;
    }

    return Writer.WriteTextChunk(GraphPrinterCoreDefine::PngTextChunkKey, ExportedText);
}

入力の呼び出しにはFSlateApplication::ProcessKeyDownEvent関数を使用します。引数ではFKeyEventで入力するキーなどの情報を受け渡します。
こちらの関数ではGetKeyEventFromUICommandInfo関数でエディタのコピーのショートカットキーの情報のFKeyEventを作成しクリップボードを経由して現在開いているグラフエディタのテキスト情報を取得しています。
コメントにも書いてありますが、クリップボードを経由するためクリップボードの内容を保管して、作業後に元に戻しています。

続いてGetKeyEventFromUICommandInfo関数の中身でFKeyEventをどうやって作成するのかを見ていきましょう。
今回のようにエディタのショートカットキーを使いたい場合など、この方法でやるのが良いかと思います。
GraphPrinterCore.cpp

GraphPrinterCore.cpp
bool FGraphPrinterCore::GetKeyEventFromUICommandInfo(const TSharedPtr<FUICommandInfo>& UICommandInfo, FKeyEvent& OutKeyEvent)
{
    if (!UICommandInfo.IsValid())
    {
        return false;
    }
    const TSharedRef<const FInputChord>& Chord = UICommandInfo->GetFirstValidChord();

    FModifierKeysState ModifierKeys(
        Chord->bShift, Chord->bShift,
        Chord->bCtrl, Chord->bCtrl,
        Chord->bAlt, Chord->bAlt,
        Chord->bCmd, Chord->bCmd,
        false
    );
    const uint32* CharacterCodePtr;
    const uint32* KeyCodePtr;
    FInputKeyManager::Get().GetCodesFromKey(Chord->Key, CharacterCodePtr, KeyCodePtr);
    uint32 CharacterCode = (CharacterCodePtr != nullptr ? *CharacterCodePtr : 0);
    uint32 KeyCode = (KeyCodePtr != nullptr ? *KeyCodePtr : 0);
    FKeyEvent KeyEvent(Chord->Key, ModifierKeys, FSlateApplication::Get().GetUserIndexForKeyboard(), false, CharacterCode, KeyCode);
    OutKeyEvent = KeyEvent;

    return true;
}

FKeyEventコンストラクタは以下のようになっていて、それぞれの引数が以下のようになっています。

FKeyEvent
(
    const FKey InKey,
    const FModifierKeysState & InModifierKeys,
    const uint32 InUserIndex,
    const bool bInIsRepeat,
    const uint32 InCharacterCode,
    const uint32 InKeyCode
)

InKeyはBPでもよく見かけるFKey型で何のキーかを指定します。
InModifierKeysFModifierKeysStateでモディファイアキー、つまりSHIFTキー、CTRLキー、ALTキー、CMDキーの状態を指定します。
InUserIndexGetPlayerControllerなどでよく見るPlayerIndexと同じようなものだと思います。
bInIsRepeatは名前の通りTrueの場合自動繰り返し入力になります。
InCharacterCodeInKeyで指定したキーの文字コードを指定します。
InKeyCodeInKeyで指定したキーのキーコードを指定します。

GetKeyEventFromUICommandInfo関数ではまず、FUICommandInfoから有効な入力情報をFInputChord型で取得します。
FUICommandInfo::GetFirstValidChord関数を使用すると、ショートカットキーのプライマリとセカンダリから有効な方を取得できます。

続いて、FInputChordからそれぞれのモディファイアキーの状態を取得してFModifierKeysStateの変数を用意します。

さらに続いて、FInputKeyManager::GetCodesFromKey関数を使ってFKeyで指定したキーの文字コードとキーコードを取得します。取得する際にuint32のポインタでの受け取りとなる点に注意してください。

これで素材は揃ったので、FKeyEventを作成して戻り値としています。
InUserIndexの指定にFSlateApplication::GetUserIndexForKeyboard関数を指定していますが、プログラムからの入力なので適当に0などにしてもよいかと思います。

おわりに

タイトルではエディタ上の入力と書きましたが、今回使用したモジュールはすべてランタイムモジュールなのでインゲームでも使用できるかもしれません(試していないためとりあえずエディタ上と書いておきました...)。
また、今回ご紹介したFSlateApplication::ProcessKeyDownEvent関数ではキーの押した時のイベントを取れますが、以下の関数を使用すればマウスやキーを離した時などのイベントも呼び出せそうでした。

Source\Runtime\Slate\Public\Framework\Application\SlateApplication.h
public:

  /**
     * Called by the native application in response to a mouse move. Routs the event to Slate Widgets.
     *
     * @param  InMouseEvent  Mouse event
     * @param  bIsSynthetic  True when the even is synthesized by slate.
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessMouseMoveEvent( const FPointerEvent& MouseEvent, bool bIsSynthetic = false );

  /**
     * Called by the native application in response to a mouse button press. Routs the event to Slate Widgets.
     *
     * @param  PlatformWindow  The platform window the event originated from, used to set focus at the platform level. 
     *                         If Invalid the Mouse event will work but there will be no effect on the platform.
     * @param  InMouseEvent    Mouse event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessMouseButtonDownEvent(const TSharedPtr< FGenericWindow >& PlatformWindow, const FPointerEvent& InMouseEvent);

  /**
     * Called by the native application in response to a mouse button release. Routs the event to Slate Widgets.
     *
     * @param  InMouseEvent  Mouse event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessMouseButtonUpEvent( const FPointerEvent& MouseEvent );

  /**
     * Called by the native application in response to a mouse release. Routs the event to Slate Widgets.
     *
     * @param  InMouseEvent  Mouse event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessMouseButtonDoubleClickEvent( const TSharedPtr< FGenericWindow >& PlatformWindow, const FPointerEvent& InMouseEvent );

  /**
     * Called by the native application in response to a mouse wheel spin or a touch gesture. Routs the event to Slate Widgets.
     *
     * @param  InWheelEvent    Mouse wheel event details
     * @param  InGestureEvent  Optional gesture event details
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessMouseWheelOrGestureEvent( const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent );

  /**
     * Called when a character is entered
     *
     * @param  InCharacterEvent  Character event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessKeyCharEvent( const FCharacterEvent& InCharacterEvent );

  /**
     * Called when a key is pressed
     *
     * @param  InKeyEvent  Keyb event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessKeyDownEvent( const FKeyEvent& InKeyEvent );

  /**
     * Called when a key is released
     *
     * @param  InKeyEvent  Key event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessKeyUpEvent( const FKeyEvent& InKeyEvent );

  /**
     * Called when a analog input values change
     *
     * @param  InAnalogInputEvent Analog input event
     * @return  Was this event handled by the Slate application?
     */
    bool ProcessAnalogInputEvent(const FAnalogInputEvent& InAnalogInputEvent);

  /**
     * Called when a drag from an external (non-slate) source enters a window
     *
     * @param WindowEntered  The window that was entered by the drag and drop
     * @param DragDropEvent  Describes the mouse state (position, pressed buttons, etc) and associated payload
     * @return true if the drag enter was handled and can be processed by some widget in this window; false otherwise
     */
    bool ProcessDragEnterEvent( TSharedRef<SWindow> WindowEntered, const FDragDropEvent& DragDropEvent );

  /**
     * Called when a touchpad touch is started (finger down) when polling game device state
     * 
     * @param ControllerEvent   The touch event generated
     */
    void ProcessTouchStartedEvent( const TSharedPtr< FGenericWindow >& PlatformWindow, const FPointerEvent& InTouchEvent );

  /**
     * Called when a touchpad touch is moved  (finger moved) when polling game device state
     * 
     * @param ControllerEvent   The touch event generated
     */
    void ProcessTouchMovedEvent( const FPointerEvent& InTouchEvent );

  /**
     * Called when a touchpad touch is ended (finger lifted) when polling game device state
     * 
     * @param ControllerEvent   The touch event generated
     */
    void ProcessTouchEndedEvent( const FPointerEvent& InTouchEvent );

  /**
     * Called when motion is detected (controller or device) when polling game device state
     * 
     * @param MotionEvent       The motion event generated
     */
    void ProcessMotionDetectedEvent( const FMotionEvent& InMotionEvent );