.NET MAUIでPicker.Focus()がWindows(WinUI3)で効かない原因と対策:ComboBoxのIsDropDownOpenでタップ開閉を実現

.NET MAUI で入力フォームを作っていると、「行全体(Grid/StackLayout)をタップしたら、その中の Picker を開きたい」という要望がよく出ます。ところが Windows(WinUI3)だけは Picker.Focus() を呼んでもドロップダウンが開かず、同じ実装が通用しません。この記事では、なぜ起きるのかを整理した上で、Windows でも確実に “タップで Picker を開く” 実装を、コピペしやすい形でまとめます。

目次

.NET MAUI の Picker.Focus() が Windows(WinUI3)で「効かない」ように見える理由

結論から言うと、Windows では フォーカスが当たること と ドロップダウンが開くこと が別の動作です。そのため picker.Focus() を呼んでも、「フォーカスは移る(ことがある)」が「リストが開かない」という現象が起きます。

特にレイアウト(Grid/StackLayout など)側のタップで Picker を開きたい場合、開きたい意図は「フォーカス」ではなく「ドロップダウンを開く」なので、Windows では追加の処理が必要になります。

観点Android / iOSWindows(WinUI3)
Picker の内部実装OS ネイティブの選択 UIComboBox(WinUI のコントロール)
Focus() の意味入力対象へフォーカス(結果として選択 UI が出るケースもある)キーボードフォーカスの移動が中心(開閉とは直結しない)
「開く」動作OS が提供する選択 UI を表示ComboBox.IsDropDownOpen = true が“開く”

Windows では「Focus とドロップダウン表示は別物」

.NET MAUI の Windows 版は WinUI3 をベースにしており、Picker は内部的に WinUI の ComboBox として描画されます。WinUI の ComboBox は、フォーカスが当たっただけではドロップダウンを開きません。これは UI 設計として、キーボード操作・タッチ操作・アクセシビリティを分離しやすくする狙いがあり、MAUI 側の Focus() だけで “開く” までを保証しない構造になっています。

そのため、Windows で「タップ → Picker を開く」を確実に実現するには、MAUI の抽象 API(Focus())ではなく、Windows ネイティブの ComboBox を直接操作するのが最短で安定します。

解決策:Handler から ComboBox を取得して IsDropDownOpen を true にする

Windows 向けは、Picker のハンドラーからネイティブコントロール(ComboBox)を取り出し、IsDropDownOpen を操作します。

#if WINDOWS
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Maui.ApplicationModel;
#endif

public static class PickerOpener
{
    public static void Open(Picker picker)
    {
        if (picker == null) return;

#if WINDOWS
        // UI スレッドで実行(タップイベントからなら不要なこともありますが、安定のために推奨)
        MainThread.BeginInvokeOnMainThread(() =>
        {
            // Handler からネイティブコントロール(ComboBox)を取得
            if (picker.Handler?.PlatformView is ComboBox combo)
            {
                // 先にフォーカスを入れておくと、環境によっては開閉が安定します
                combo.Focus(FocusState.Programmatic);
                combo.IsDropDownOpen = true;
            }
            else
            {
                // まだ Handler が無い場合のフォールバック
                picker.Focus();
            }
        });
#else
        // Windows 以外は通常の Focus で十分なケースが多い
        picker.Focus();
#endif
    }
}

ポイント

  • #if WINDOWS で囲むことで、Windows 以外のビルドで WinUI の型参照が混ざらないようにします。
  • picker.Handler?.PlatformView は、画面表示前だと null のことがあります(次の章で回避策を詳しく説明します)。
  • IsDropDownOpen = true が “開く” 本体です。Focus() は補助(安定化)として扱うと理解しやすいです。

実装例:Grid / StackLayout の「行タップ」で Picker を開く

よくある UI として、「ラベル+値」の行をタップしたら Picker を開きたいケースを想定します。Picker 自体は右側に置きつつ、行全体をタップ可能にします。

XAML(行全体に TapGestureRecognizer を付ける)

<Grid Padding="12" ColumnDefinitions="Auto,*" x:Name="PrefRow">
  <Label Text="都道府県" VerticalOptions="Center" />






C#(タップで開く)

void OnPrefRowTapped(object sender, EventArgs e)
{
    PickerOpener.Open(PrefPicker);
}

この形にしておくと、ユーザーは Picker の小さな領域を狙わなくて済み、モバイル・デスクトップの両方で操作性が上がります。特に Windows はマウス操作が中心になりやすいので、クリックターゲットが大きいほどフォームの離脱率が下がります。

つまずきポイント:Handler が null で ComboBox が取れない

Windows 向け実装で一番ハマりやすいのが picker.Handler がまだ作られていない(= PlatformView が取れない)タイミングで呼んでしまうことです。これは不具合というより、MAUI の描画ライフサイクル上「表示されて初めてハンドラーが確定する」場面があるために起きます。

呼び出す場所Handler が生成済み?おすすめ度メモ
ページのコンストラクタ直後未生成の可能性が高い低画面に載っていないので PlatformView が取れないことが多い
OnAppearing生成済みになりやすい中レイアウト状況により揺れることがある
Loaded(要素の Loaded)生成済みになりやすい高「表示後」を取りたいなら最有力
ユーザー操作(タップ/クリック)イベント内ほぼ生成済み最も高今回の「タップで開く」なら最も自然

今回の要件(レイアウトのタップで開く)であれば、タップイベント内で呼ぶのがシンプルで、ハンドラー未生成問題も回避しやすいです。

Loaded を使って「表示後に一度だけ」開けるようにする例

たとえば「初回表示時にガイドとして一度だけ開きたい」などの場合は、Loaded を使うと安全です。

public MainPage()
{
    InitializeComponent();

    PrefPicker.Loaded += (_, __) =>
    {
        // 表示後なので Handler が取れる確率が高い
        // PickerOpener.Open(PrefPicker);
    };
}

より堅牢にする:Windows 専用コードを Platforms/Windows に分離する

#if WINDOWS は手軽ですが、規模が大きくなると増殖しやすいのが弱点です。プロジェクトが育ってきたら、Windows 固有処理は Platforms/Windows に寄せておくと保守が楽になります。

ここでは「共通コードから Open() を呼ぶだけで、Windows ではネイティブに開く」という形にします。

共通側(Open を呼ぶ窓口)

public static partial class PickerPlatformOpener
{
    public static partial void OpenPlatform(Picker picker);


public static void Open(Picker picker)
{
    if (picker == null) return;

    // ここは共通。各プラットフォームの partial 実装に委譲
    OpenPlatform(picker);
}


}

Platforms/Windows/PickerPlatformOpener.Windows.cs(Windows 実装)

using Microsoft.Maui.ApplicationModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

public static partial class PickerPlatformOpener
{
public static partial void OpenPlatform(Picker picker)
{
MainThread.BeginInvokeOnMainThread(() =>
{
if (picker.Handler?.PlatformView is ComboBox combo)
{
combo.Focus(FocusState.Programmatic);
combo.IsDropDownOpen = true;
}
else
{
picker.Focus();
}
});
}
}

Platforms/Android / Platforms/iOS(必要なら)

他プラットフォームは Focus() で十分なら、こうしておくと挙動が揃いやすいです(必要に応じて OS ごとに最適化できます)。

public static partial class PickerPlatformOpener
{
    public static partial void OpenPlatform(Picker picker)
    {
        picker.Focus();
    }
}

この方式のメリットは、UI 側(XAML や ViewModel)が Windows 固有型(ComboBox)を一切知らなくてよくなり、将来の変更点(MAUI/WinUI の仕様差分)を Platforms/Windows に閉じ込められることです。

再利用性を上げる:Behavior で「タップ領域→対象 Picker を開く」を部品化する

「いろんな画面で同じ“行タップで Picker を開く”を使いたい」「コードビハインドを増やしたくない」という場合は、Behavior 化すると扱いやすくなります。ここでは、任意の Layout(Grid/StackLayout など)に付けられる Behavior を作ります。

Behavior(ターゲット Picker を開く)

public class TapToOpenPickerBehavior : Behavior<Layout>
{
    public Picker TargetPicker { get; set; }


TapGestureRecognizer _tap;

protected override void OnAttachedTo(Layout bindable)
{
    base.OnAttachedTo(bindable);

    _tap = new TapGestureRecognizer();
    _tap.Tapped += OnTapped;
    bindable.GestureRecognizers.Add(_tap);
}

protected override void OnDetachingFrom(Layout bindable)
{
    base.OnDetachingFrom(bindable);

    if (_tap != null)
    {
        _tap.Tapped -= OnTapped;
        bindable.GestureRecognizers.Remove(_tap);
        _tap = null;
    }
}

void OnTapped(object sender, EventArgs e)
{
    if (TargetPicker == null) return;

    // どちらでも OK。#if 方式なら PickerOpener.Open、partial 方式なら PickerPlatformOpener.Open
    PickerOpener.Open(TargetPicker);
}


}

XAML(x:Reference で対象 Picker を指定)

<Grid Padding="12" ColumnDefinitions="Auto,*">
  <Label Text="職種" VerticalOptions="Center" />






これで、画面側は「この行はこの Picker を開く」という宣言だけで済み、見通しが良くなります。フォーム項目が多い画面ほど効果が出ます。

実装の安定性を上げるチェックリスト(Windows で開かない時の確認ポイント)

「コードは合っているはずなのに開かない/一瞬開いて閉じる」場合は、次のチェックが効きます。

症状原因として多いもの対処
何も起きないHandler が未生成で PlatformView が取れないタップイベント内で呼ぶ/Loaded 後に呼ぶ/picker.Handler を null チェック
一瞬開いてすぐ閉じる他の要素にフォーカスが奪われる、タップが二重に処理されるcombo.Focus(...) を入れる/タップ領域の重複(透明要素など)を整理
開くが選べない行のレイアウトが上に被さってクリックを奪っている被せる要素を減らす/InputTransparent を見直す
開かない(特定条件のみ)Picker が IsEnabled=false、Items が空、Binding が未完了有効状態・ItemsSource を確認/データロード完了後に開く

UI/UX のコツ:行タップで開くときに「やっておくと親切」なこと

単に開くだけでなく、フォームとして使いやすくするための工夫も入れておくと、検索流入後の満足度が上がりやすいです。

  • タイトル(Title)を必ず設定:未選択時に「選択してください」などの状態が分かり、誤入力が減ります。
  • 選択値の見え方を揃える:ラベル側と Picker 側の余白・高さを揃えると、クリックターゲットが安定します。
  • キーボード操作も破壊しない:行タップを追加しても、Picker 自体の操作(Tab 移動、矢印キー等)を妨げないレイアウトにします。
  • エラー表示と相性が良い:必須項目なら、未選択時の赤枠やメッセージを同じ行に出す設計にすると、ユーザーが迷いません。

なぜ Windows だけネイティブ操作が必要なのか(設計の捉え方)

「Focus() を呼んだら開いてほしいのに」と感じるのは自然ですが、Windows の UI コンポーネントは “フォーカス=入力の準備” と “ドロップダウンを開く” を分離していることが多いです。これは、マウス・キーボード・スクリーンリーダーなど複数入力手段を前提にした設計が背景にあります。

MAUI はクロスプラットフォームを優先するため、抽象 API(Focus など)で共通化できる範囲は共通化しますが、今回の「開閉」という UX まで完全に統一するのは難しく、必要な場面では PlatformView を触るのが現実的な落としどころになります。

アプローチ比較:どの方法を採用すべきか

方法実装の簡単さ再利用性おすすめ度
タップイベント内で #if WINDOWS+IsDropDownOpen高中最短で直したいなら最適
Platforms/Windows に分離(partial)中高画面が増えるほど効く
Behavior 化して使い回す中高フォーム項目が多いアプリ向け

まとめ

  • Windows(WinUI3)では Picker.Focus() と「ドロップダウンを開く」は別動作になりやすく、Focus() だけでは開きません。
  • Windows では picker.Handler?.PlatformView から ComboBox を取り出し、IsDropDownOpen = true を設定すると確実に開けます。
  • Handler が null になり得るので、タップイベント内や Loaded 後など「表示後」のタイミングで呼ぶのが安定です。
  • 規模が大きいなら、Windows 固有処理は Platforms/Windows に寄せる(partial)か、Behavior 化して再利用すると保守性が上がります。

この記事を書いた人

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

コメント

コメントする

目次