⌨️

Blazor Web App でリアルタイムキー入力したい

に公開

はじめに

ゲームっぽいことをしたい場合、リアルタイムキー入力が欲しいです。
取り敢えずそれらしいことができたので、記録を残します。

なお、MudBlazor 9.0.0で新しく導入されたMudHotKeyを使えば、同様のことが実現可能です。

環境

以下の環境で確認しました。

  • .NET 10
  • Windows版 Google Chrome

実装

この実装では、ゲームらしく、キートップの表記に関係なく絶対位置で取り扱うために、キーコード(e.code)を使用しています。
より一般的に、e.keyを使って、キーによって入力される文字を得る方法もあります。

javascript側でリピートさせて(e.repeatを返して)いますが、リピート時は送らず、updownだけを返してC#側で状態を管理した方が、ゲームらしいと思います。
upの分通信量は増えますが、押しっぱなしにした際は減らせますね。

実際には、ゲームそのものではなくゲーム開発用ツールなので、いろいろと端折っています。

wwwroot/js/KeyEventHandler.js
// 登録済みのハンドラー
const keyHandlers = new Map();

// キーイベントハンドラの登録
function initializeKeyEventHandler(dotNetHelper, event, name, ...nodes) {
    const handler = (e) => {
        if (nodes.includes(document.activeElement.nodeName)) {
            const classes = Array.from(document.activeElement.classList);
            dotNetHelper.invokeMethodAsync(name, e.code, e.repeat, e.ctrlKey, e.shiftKey, e.altKey, e.metaKey, document.activeElement.nodeName, classes);
        }
    };
    window.addEventListener(event, handler);
    const id = self.crypto.randomUUID();
    keyHandlers.set(id, [event, handler]);
    return id;
}

// キーイベントハンドラの破棄
function disposeKeyEventHandler(id) {
    const handler = keyHandlers.get(id);
    if (handler) {
        window.removeEventListener(handler[0], handler[1]);
        keyHandlers.delete(id);
    }
}

ハンドルを登録する際に、内部でidを生成して返します。
破棄の際は、そのidを指定します。

キーが押されるたびに、登録したハンドラが呼ばれます。
ハンドラは、キーコードの他、キーリピートとモディファイアキーの状態、イベントを受け取ったノードとそのCSSクラスを受け取り、イベントにどのように応答するか(しないか)を決定・処理します。

GetKeyEvent.razor
@implements IAsyncDisposable
@inject IJSRuntime JSRuntime

@code {
    /// <summary>遅延初期化</summary>
    protected override async Task OnAfterRenderAsync (bool firstRender) {
        await base.OnAfterRenderAsync (firstRender);
        if (firstRender) {
            // キーハンドラ登録
            _keyHandlerReference = DotNetObjectReference.Create (this);
            _keyHandlerId = await JSRuntime.InvokeAsync<string> ("initializeKeyEventHandler", _keyHandlerReference, "keydown", nameof (OnKeydown), "BODY", "DIV");
        }
    }

    /// <summary>キーハンドラ参照</summary>
    protected DotNetObjectReference<GetKeyEvent> _keyHandlerReference;

    /// <summary>キーハンドラー登録識別子</summary>
    protected string _keyHandlerId;

    /// <summary>キーイベントハンドラ</summary>
    /// <param name="keyCode">キーコード</param>
    /// <param name="onRepeat">リピート中</param>
    /// <param name="onCtrl">コントロール</param>
    /// <param name="onShift">シフト</param>
    /// <param name="onAlt">Alt/Option</param>
    /// <param name="onMeta">Win/Command</param>
    /// <param name="target">ターゲットエレメント名</param>
    /// <param name="classes">ターゲットエレメントクラス名一覧</param>
    [JSInvokable] public async Task OnKeydown (string keyCode, bool onRepeat, bool onCtrl, bool onShift, bool onAlt, bool onMeta, string target, string[] classes) {
        if (target == "BODY") {
            // ~
        } else if (target == "DIV" && classes.Contains ("fixed")) {
            // ~
        }
    }

    /// <summary>破棄</summary>
    public async ValueTask DisposeAsync () {
        // キーハンドラ
        if (!string.IsNullOrEmpty (_keyHandlerId)) {
            try {
                await JSRuntime.InvokeVoidAsync ("disposeKeyEventHandler", _keyHandlerId);
            }
            catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine ($"DisposeJSAudioAsync catch exception: {ex.Message}\n{ex.StackTrace}");
            }
        }
        _keyHandlerReference?.Dispose ();
    }
}

MudButton を押したあとフォーカスが残る

ボタンが押されると、その後のキー入力のターゲットエレメントがBUTTONになります。
BUTTONをターゲットに加えることもできますが、今回は、以下のような対応をしてフォーカスを外しました。

wwwroot/js/KeyEventHandler.js
// フォーカスを外す
function activeElementBlur() {
    document.activeElement.blur();
}
GetKeyEvent.razor
<MudIconButton OnClick="ChangeVolume" Icon="@(Volume? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)" />

@code {
    /// <summary>効果音 ON/OFF</summary>
    protected bool Volume { get; set; }

    /// <summary>効果音切り替え</summary>
    protected async Task ChangeVolume () {
        Volume = !Volume;
        await ActiveElementBlur ();
    }

    /// <summary>着目中のエレメントからフォーカスを外す</summary>
    protected async ValueTask ActiveElementBlur () {
        await JSRuntime.InvokeVoidAsync ("activeElementBlur");
    }
}

資料

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0

https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent

おわりに

dotnet側で標準的な仕組みが用意されて、C#だけでリアルタイムキー入力ができるようになり、この記事が不要になる未来を夢見ています。

最後までお読みいただきありがとうございました。
お気づきの点があればお知らせいただけると助かります。

Discussion