Blazor Web App でリアルタイムキー入力したい
はじめに
ゲームっぽいことをしたい場合、リアルタイムキー入力が欲しいです。
取り敢えずそれらしいことができたので、記録を残します。
なお、MudBlazor 9.0.0で新しく導入されたMudHotKeyを使えば、同様のことが実現可能です。
環境
以下の環境で確認しました。
- .NET 10
- Windows版 Google Chrome
実装
この実装では、ゲームらしく、キートップの表記に関係なく絶対位置で取り扱うために、キーコード(e.code)を使用しています。
より一般的に、e.keyを使って、キーによって入力される文字を得る方法もあります。
javascript側でリピートさせて(e.repeatを返して)いますが、リピート時は送らず、upとdownだけを返してC#側で状態を管理した方が、ゲームらしいと思います。
upの分通信量は増えますが、押しっぱなしにした際は減らせますね。
実際には、ゲームそのものではなくゲーム開発用ツールなので、いろいろと端折っています。
// 登録済みのハンドラー
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クラスを受け取り、イベントにどのように応答するか(しないか)を決定・処理します。
@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をターゲットに加えることもできますが、今回は、以下のような対応をしてフォーカスを外しました。
// フォーカスを外す
function activeElementBlur() {
document.activeElement.blur();
}
<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");
}
}
資料
おわりに
dotnet側で標準的な仕組みが用意されて、C#だけでリアルタイムキー入力ができるようになり、この記事が不要になる未来を夢見ています。
最後までお読みいただきありがとうございました。
お気づきの点があればお知らせいただけると助かります。
Discussion