.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 / iOS | Windows(WinUI3) |
|---|---|---|
| Picker の内部実装 | OS ネイティブの選択 UI | ComboBox(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 化して再利用すると保守性が上がります。

コメント