UWP/WinUIのKeyDownでEnterキーのフォーカス移動がCOMException(0x8000FFFF)になる原因と回避策

UWP/WinUIで「Enterキーを押したら次の入力欄へフォーカスを移動したい」とKeyDown内でFocusManager.TryMoveFocusを呼ぶと、0x8000FFFFのCOMExceptionが出ることがあります。本記事では原因の考え方と、FindNextElementOptions+SearchRootで安全に回避する実装を整理します。

目次

発生する症状:KeyDownでフォーカス移動すると例外になる

フォーム入力の画面を作っていると、Enterキーで次の入力欄へ進む動き(Windowsの業務アプリでは定番)を実装したくなります。UWP(Windows.UI.Xaml)でもWinUI(Microsoft.UI.Xaml)でも、フォーカス移動の中心は FocusManager です。

ところが、次のようにKeyDownイベントの中で FocusManager.TryMoveFocus を呼ぶと、環境によっては実行時に例外が飛びます。

private void Obj_KeyDown(object sender, KeyRoutedEventArgs e)
{
    if (e.Key.ToString() == "Enter")
        FocusManager.TryMoveFocus(FocusNavigationDirection.Down);
}

代表的な例外メッセージは次のとおりです。

  • System.Runtime.InteropServices.COMException: 'Catastrophic failure (0x8000FFFF (E_UNEXPECTED))'

この例外は「COMが絡む内部処理が想定外の状態で失敗した」という意味合いで、呼び出し側のコードだけ見ても原因が特定しにくいのが厄介な点です。ですが、フォーカス探索の範囲(SearchRoot)を明示することで回避できるケースが多くあります。

なぜ起きる:TryMoveFocusの「探索ルート」が曖昧なまま動くことがある

TryMoveFocus は「現在のフォーカス要素」から、指定方向(Down/Nextなど)に従って次のフォーカス可能要素を探し、移動までまとめて行います。このとき内部では、次のような情報が関係します。

  • フォーカス探索を始める起点(現在フォーカスを持っている要素)
  • どの方向で探すか(FocusNavigationDirection)
  • どの範囲(ツリー)を探索対象にするか(SearchRoot)
  • フォーカススコープ、Tab順(TabIndex/IsTabStop)、可視状態、Enabledなど

通常は「ウィンドウのルート」を暗黙に使って探索できるのですが、次のような状況では暗黙のルート解決がうまくいかず、内部でE_UNEXPECTEDが返り、結果としてCOMExceptionが表面化することがあります。

発生しやすい状況なぜ起きやすいか(イメージ)
WinUI 3 / Windows App SDKで、複数のXamlRoot(ウィンドウ、Xaml Island、ポップアップ)が絡む「どのXAMLツリーで次要素を探すべきか」が曖昧になり、内部探索が失敗することがある
ContentDialog/Popup/TeachingTipなど、別レイヤーの要素内でKeyDownが発生探索範囲がウィンドウ全体に広がったり、逆に探索対象から外れたりして不整合が出ることがある
Loaded前/表示切替直後など、フォーカスツリーが安定していないタイミング探索対象の要素がまだツリーに乗っていない/レイアウトが確定していない
方向指定(Down)で「物理方向の探索」が成立しないレイアウト方向探索は座標やナビゲーション設定の影響が大きく、想定外の探索経路になりやすい

ポイントは、例外そのものが「Enterキー」や「KeyDown」だけが原因ではなく、フォーカス探索の「範囲」と「タイミング」が噛み合っていないときに起きやすい、という点です。

解決策:FindNextElementOptionsでSearchRootを明示してTryMoveFocusする

回避策として有効なのが、FindNextElementOptions を使い、SearchRoot(探索のルート)を明示してからフォーカス移動を実行する方法です。

さらに、フォーム入力で「次の項目に進む」目的なら、方向は Down より Next が自然です(Tabキー相当の動き)。

private void Obj_KeyDown(object sender, KeyRoutedEventArgs e)
{
    if (e.Key == Windows.System.VirtualKey.Enter)
    {
        // this: Page や UserControl など、XamlRoot を持つ要素を想定
        var options = new FindNextElementOptions
        {
            SearchRoot = this.XamlRoot.Content
        };

        FocusManager.TryMoveFocus(FocusNavigationDirection.Next, options);

        // Enterの既定動作と二重になりそうなら有効化
        // e.Handled = true;
    }
}

これで「次のフォーカス対象の探索範囲」が明確になり、TryMoveFocusが内部で迷子になりにくくなります。

tbという変数は「画面のルート」を指すものに置き換える

ネット上の回答例で tb のような変数名が出てくることがありますが、重要なのは変数名ではなく、SearchRootに渡す実体です。基本方針は次のとおりです。

コードを書いている場所SearchRootに指定しやすいもの補足
Pageのコードビハインドthis.XamlRoot.Contentウィンドウ配下のルートに対して探索できる
UserControl内this.XamlRoot.Content またはコントロール自身のルート要素「画面全体」か「この部品内」か、要件で決める
ContentDialog / Popup内ダイアログ内のルートコンテナ、または XamlRoot.Content背後の画面にフォーカスが飛ばないよう範囲を絞ることも検討

「Enterで次の入力欄へ」の体験を安定させるなら、まずは 画面全体のルートをSearchRootに しておくと動作が読みやすくなります。

NextとDownの違い:Enter移動はNextがハマりやすい

フォーカス移動の方向指定には複数ありますが、Enterでフォーム入力を進めたい場合は FocusNavigationDirection.Next がハマりやすいです。

方向動きの意味向いているケース注意点
NextTab順で次へ(論理順)入力フォーム、設定画面、リスト形式の入力TabIndex/IsTabStopの設計が重要
PreviousTab順で前へShift+Enterで戻る、などの拡張誤操作防止のUI設計も必要
Down/Up/Left/Right物理方向で探索ゲームパッド操作、TV向けUI、十字キー操作レイアウト・座標依存が強く、フォーム用途では不安定になりやすい

もし「縦に並んでいるからDownでいいはず」と思っていても、実際にはGridの列やパネル構造、ScrollViewerの内外などで座標計算が絡み、Down探索が期待どおりに動かないケースがあります。フォーム用途はNextが無難です。

実務で使える:安全寄りのEnter移動ハンドラー(コピペ可)

現場では「XamlRootがまだ取得できない」「ダイアログ内の入力」「特定の入力欄だけEnterを無効化したい」など、細かい要件が出がちです。まずは例外回避を最優先し、次のようにガード節を入れておくと事故が減ります。

private void MoveFocusNextOnEnter(object sender, KeyRoutedEventArgs e)
{
    if (e.Key != Windows.System.VirtualKey.Enter)
        return;

    // 複数行入力(AcceptsReturn=true)など、Enterを改行として使うUIならここで除外する
    // if (sender is TextBox tb && tb.AcceptsReturn) return;

    // SearchRootを安全に取得
    // sender基準でXamlRootを取る方が、部品化したときに崩れにくい
    if (sender is FrameworkElement fe && fe.XamlRoot != null)
    {
        var options = new FindNextElementOptions
        {
            SearchRoot = fe.XamlRoot.Content
        };

        FocusManager.TryMoveFocus(FocusNavigationDirection.Next, options);

        // Enterの既定動作(ボタンのクリックなど)を抑止したい場合はtrue
        // e.Handled = true;
    }
}

このパターンの良いところは、Page内でもUserControl内でも同じハンドラーを使い回しやすい点です。this固定でSearchRootを取るより、sender から XamlRoot を辿るほうが、部品の再利用やダイアログ表示で破綻しにくくなります。

Tab順(TabIndex/IsTabStop)を整えると“Next”が期待どおりに動く

Next はTab順を見て移動します。つまり、Enter移動を「気持ちよく」するには、Tab順の設計が本体だと考えると上達が早いです。

プロパティ役割よくある設定例
TabIndexフォーカス移動の順番上から 0,1,2… と明示してフォーム順を固定する
IsTabStopTab/Nextで止まるかラベルや装飾用コントロールはfalseにして“引っかかり”を減らす
IsEnabled/Visibilityフォーカス可能かに影響無効・非表示の要素は自動的にスキップされる

「Enterで次へ」が不自然に飛ぶ、戻る、あるいは止まる場合は、まずTabIndexが重複していないか、IsTabStopが意図どおりかを点検すると解決が早いです。

複数の入力欄にまとめて適用する方法

Enter移動を実装する対象は、TextBoxだけでなくPasswordBoxやNumberBox(WinUI)など複数になりがちです。代表的な適用方法を比較します。

適用方法メリットデメリット向いている規模
XAMLで各コントロールにKeyDownを指定分かりやすい、挙動が追いやすい数が多いと記述が増える小〜中
Loadedでツリーを走査して一括でハンドラー付与画面追加や入力欄増減に強い走査コストや例外時の切り分けが必要中〜大
Behavior/AttachedPropertyでMVVM寄りに実装再利用性が高い、ViewModelに寄せやすい導入の学習コスト、パッケージ依存が増える中〜大

XAMLで素直に付ける(最短)

まず動かしたいなら、XAMLでイベントを付けるのが最短です。

<TextBox Header="氏名" KeyDown="MoveFocusNextOnEnter" />
<TextBox Header="メール" KeyDown="MoveFocusNextOnEnter" />
<PasswordBox Header="パスワード" KeyDown="MoveFocusNextOnEnter" />

「Enterで次へ」を入れたい入力欄だけに限定できるので、複数行入力や検索ボックスなどEnterを別用途で使うUIと混在していても管理しやすいのが利点です。

コードで一括付与する(入力フォームが巨大な場合)

入力欄が何十個もある画面だと、XAMLで逐一KeyDownを書くのがつらくなります。その場合は、ルートコンテナ配下のTextBox等を走査してまとめてイベントを付与します。

private void Page_Loaded(object sender, RoutedEventArgs e)
{
    AttachEnterMoveFocusHandlers(this);
}

private void AttachEnterMoveFocusHandlers(DependencyObject root)
{
    int count = VisualTreeHelper.GetChildrenCount(root);
    for (int i = 0; i < count; i++)
    {
        var child = VisualTreeHelper.GetChild(root, i);

        if (child is TextBox tb)
        {
            tb.KeyDown += MoveFocusNextOnEnter;
        }
        else if (child is PasswordBox pb)
        {
            pb.KeyDown += MoveFocusNextOnEnter;
        }

        AttachEnterMoveFocusHandlers(child);
    }
}

この方法は便利ですが、次の点は意識しておくと安全です。

  • 不要なコントロール(検索ボックス、メモ欄など)に付けないよう、NameやTagで除外条件を用意する
  • ページ遷移や再表示がある場合、二重にイベントを付けないように注意する(Loadedの多重実行)

二重動作を防ぐ:e.Handledを使うべき場面

Enterキーはコントロールによって既定動作が異なります。たとえば、ボタンが既定ボタン(DefaultButton)的に扱われる状況、AutoSuggestBoxの確定、TextBoxの改行など、Enterが別機能として消費されるケースがあります。

「Enterは常に次へ」に寄せるなら、ハンドラー内で e.Handled = true; を立てて、既定動作を抑止するのが有効です。ただし、既定動作を残したいUIでは逆効果になり得ます。

UI/状況e.Handled=trueが向いている?理由
業務フォーム(Enter=次へを徹底)向いている確定や改行よりも入力移動を優先したい
複数行TextBox(メモ入力)向いていないEnterは改行として必要
検索ボックス(Enterで検索実行)要件次第検索実行を優先するならHandledしない

それでも不安定なときの追加策:タイミングをずらす

SearchRootを指定しても、画面切替直後やダイアログ表示直後など、フォーカスツリーが不安定なタイミングでEnterが押されると、期待どおりに移動しないことがあります。その場合は「KeyDownの中で即座に動かす」のではなく、UIスレッドの次のタイミングに回すと安定することがあります。

例えばDispatcher(WinUI 3ならDispatcherQueue、UWPならCoreDispatcher)で、フォーカス移動を次のキューに積みます。

private void MoveFocusNextOnEnter_Defer(object sender, KeyRoutedEventArgs e)
{
    if (e.Key != Windows.System.VirtualKey.Enter)
        return;

    if (sender is FrameworkElement fe && fe.XamlRoot != null)
    {
        var root = fe.XamlRoot.Content;

        // WinUI 3: fe.DispatcherQueue
        fe.DispatcherQueue.TryEnqueue(() =>
        {
            var options = new FindNextElementOptions { SearchRoot = root };
            FocusManager.TryMoveFocus(FocusNavigationDirection.Next, options);
        });

        // e.Handled = true; // 必要なら
    }
}

「例外回避」というより、フォーカス移動が取りこぼされる場合の保険として覚えておくと便利です。

デバッグのコツ:次にフォーカスされる要素を可視化する

フォーカスが思ったところへ行かないときは、TryMoveFocusをいきなり呼ぶより、まず「次に候補になる要素」を取得してログに出すと原因が掴みやすくなります。UWP/WinUIには次候補を探すAPIもあります。

private void DebugNextFocus(FrameworkElement fe)
{
    if (fe.XamlRoot == null) return;

    var options = new FindNextElementOptions
    {
        SearchRoot = fe.XamlRoot.Content
    };

    var next = FocusManager.FindNextFocusableElement(FocusNavigationDirection.Next, options);

    // next が null なら、Tab順の末尾 or 探索範囲が不適切
    System.Diagnostics.Debug.WriteLine(next?.ToString() ?? "next is null");
}

next が null になる場合は、次の観点をチェックすると改善しやすいです。

  • 探索範囲(SearchRoot)が狭すぎないか(入力フォーム全体を含んでいるか)
  • 次の入力欄が IsEnabled / Visibility / IsTabStop でフォーカス可能になっているか
  • TabIndexが重複していないか、意図した順序になっているか

よくあるQ&A

KeyDownでEnter判定するならToString比較でもいい?

動くこともありますが、推奨はしません。キー名の文字列比較は環境差や意図しない変換で事故りやすく、読み手にも意図が伝わりにくいです。e.Key == Windows.System.VirtualKey.Enter のように列挙体で比較するのが安全です。

Enter移動を入れると、ボタンのクリックや確定動作が効かなくなった

e.Handled = true; を入れている場合は、それが原因です。Enterの既定動作を残したいUIではHandledを立てない、または「特定のコントロールではHandledしない」条件分岐を入れます。

SearchRootは必ずXamlRoot.Contentにすべき?

「画面全体で次を探す」なら分かりやすい選択です。一方で、ダイアログ内の入力欄だけを巡回したいなど、範囲を絞りたい要件もあります。その場合は、ダイアログのルートGridなど、目的の範囲を覆う要素をSearchRootに指定します。

Downでやりたい(縦方向に並んでいる)けど安定しない

フォーム用途ではNextが基本です。どうしてもDownが必要なら、方向ナビゲーション(XYFocus)の設定やレイアウトの見直しが必要になります。まずはNextで要件を満たせないか検討すると、保守性が上がります。

まとめ:Enterでのフォーカス移動は「範囲(SearchRoot)」を明示して安定させる

UWP/WinUIのKeyDown内でEnterを押した瞬間にフォーカスを動かす実装は、状況によっては 0x8000FFFF (E_UNEXPECTED) のCOMExceptionに繋がります。回避の要点は次の3つです。

  • FindNextElementOptions を使い、SearchRootを明示して探索範囲を固定する
  • フォーム入力の「次へ」なら FocusNavigationDirection.Next を優先する
  • 必要に応じて e.Handled やDispatcherで二重動作・タイミング問題を抑える

この3点を押さえておけば、Enterキーによるフォーカス移動はかなり安定し、UWP/WinUIの入力フォームが「業務アプリらしい操作感」に近づきます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次