Windows の .NET MAUI で Picker をクリックすると、Focused の直後に即座に Unfocused が発火し、そのままドロップダウンが開く――この“謎挙動”の正体は WinUI ComboBox のフォーカス移動仕様です。本記事では仕組みを平易に解説し、標準 API で不足する「開く直前」の検知を Windows 専用ハンドラで補う実装を、貼って動くサンプルと共に提示します。
.NET MAUI Picker で「クリック直後に Unfocused」が起きる理由
結論:Windows では Picker の実体が WinUI の ComboBox で、ドロップダウンは別ウィンドウ(ポップアップ)として描画されます。クリックでドロップダウンを開くと、そのポップアップにフォーカスが移るため、Picker 本体は Focused → Unfocused と連続発火します。Tab 移動ではドロップダウンは自動で開かないため、Unfocused は遅れて発火します。
| 操作 | Windows(.NET MAUI) | 理由 |
|---|---|---|
| マウスクリック | Focused → すぐ Unfocused、直後にドロップダウンが表示 | ポップアップ(ドロップダウン)にフォーカスが移る |
| Tab キーでフォーカス移動 | Focused のみ(この時点では未展開) | 自動で開かず本体にフォーカスが残る |
| Alt+↓ / F4 で展開 | 展開時点で Unfocused | 開いた瞬間にポップアップへフォーカス遷移 |
バグか仕様か ― 回答
- これは仕様です。Windows の
ComboBoxでは、ドロップダウンを別ポップアップで描画し、そのポップアップがフォーカスを取得します。そのためクリック直後にUnfocusedが起きます。 - プラットフォーム差があります。Android / iOS はネイティブ実装が異なり、通常この連続発火は観測されません(同じ XAML でも OS ごとに挙動が異なるのは MAUI の設計通り)。
「ドロップダウン直前」を取れるイベントはある?
標準の MAUI Picker には存在しません。「BeforeDropDown」のようなイベントは用意されていないため、Windows 固有のハンドラ(WinUI ComboBox)に降りてフックする必要があります。以降で、Windows 専用の安全な回避策を実装順に示します。
最小再現&観測:イベント順をログで確かめる
まずは現象を手元で確実に再現し、ログで確認します。
XAML(最小)
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="SampleApp.MainPage"
Padding="24">
<VerticalStackLayout Spacing="16">
<Picker x:Name="MyPicker"
Title="選択してください"
Focused="OnPickerFocused"
Unfocused="OnPickerUnfocused">
<Picker.Items>
<x:String>Alpha</x:String>
<x:String>Beta</x:String>
<x:String>Gamma</x:String>
</Picker.Items>
</Picker>
<Entry Placeholder="他コントロール(フォーカス移動テスト用)"/>
コードビハインド(ログ)
using System.Diagnostics;
namespace SampleApp;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
void OnPickerFocused(object sender, FocusEventArgs e)
=> Debug.WriteLine($"Focused: {DateTime.Now:HH:mm:ss.fff}");
void OnPickerUnfocused(object sender, FocusEventArgs e)
=> Debug.WriteLine($"Unfocused: {DateTime.Now:HH:mm:ss.fff}");
}
典型ログ(Windows)
// マウスクリック
Focused: 10:15:22.410
Unfocused: 10:15:22.430 // ほぼ直後に発火、その後ドロップダウン表示
// Tab でフォーカス移動 → Alt+↓ で展開
Focused: 10:16:04.120
Unfocused: 10:16:05.002 // Alt+↓ で開いた瞬間に発火
回避策1:Handler カスタマイズで WinUI ComboBox のイベントを購読
最も素直で安定するのは、Windows のときだけ ComboBox.DropDownOpened / DropDownClosed を購読し、展開・折りたたみの正確なタイミングを取得する方法です。必要に応じて「開く直前」の近似(マウス/キー操作)も合わせて取れます。
拡張 Picker(共通コード)
namespace SampleApp.Controls;
public class PickerEx : Picker
{
// 現在ドロップダウンが開いているか(Windowsで更新)
public static readonly BindableProperty IsDropDownOpenProperty =
BindableProperty.Create(nameof(IsDropDownOpen), typeof(bool), typeof(PickerEx), false);
public bool IsDropDownOpen
{
get => (bool)GetValue(IsDropDownOpenProperty);
internal set => SetValue(IsDropDownOpenProperty, value);
}
// 「開きそう(直前の兆候)」の近似フラグ(マウス/キー操作でON)
public static readonly BindableProperty IsAboutToOpenProperty =
BindableProperty.Create(nameof(IsAboutToOpen), typeof(bool), typeof(PickerEx), false);
public bool IsAboutToOpen
{
get => (bool)GetValue(IsAboutToOpenProperty);
internal set => SetValue(IsAboutToOpenProperty, value);
}
public event EventHandler? DropDownOpened;
public event EventHandler? DropDownClosed;
internal void RaiseOpened() => DropDownOpened?.Invoke(this, EventArgs.Empty);
internal void RaiseClosed() => DropDownClosed?.Invoke(this, EventArgs.Empty);
}
MauiProgram.cs(Windows 限定のマッピング追加)
using Microsoft.Maui.Handlers;
#if WINDOWS
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.System;
#endif
namespace SampleApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.ConfigureMauiHandlers(handlers =>
{
#if WINDOWS
PickerHandler.Mapper.AppendToMapping("ComboBoxEvents", (handler, view) =>
{
if (handler.PlatformView is ComboBox cb && view is Controls.PickerEx ex)
{
// --- 正式な展開/折畳の通知(確実) ---
cb.DropDownOpened += (*, __) =>
{
ex.IsDropDownOpen = true;
ex.IsAboutToOpen = false; // 直前フラグは消す
ex.RaiseOpened();
};
cb.DropDownClosed += (*, __) =>
{
ex.IsDropDownOpen = false;
ex.IsAboutToOpen = false;
ex.RaiseClosed();
};
// --- 「開く直前」の近似(入力の兆候) ---
cb.PointerPressed += (_, __) => { ex.IsAboutToOpen = true; };
cb.KeyDown += (_, e) =>
{
// Alt+↓ または F4 を検知(Alt 状態は KeyStatus で判定)
if ((e.Key == VirtualKey.Down && e.KeyStatus.IsMenuKeyDown) ||
e.Key == VirtualKey.F4)
{
ex.IsAboutToOpen = true;
}
};
}
});
#endif
});
return builder.Build();
}
}
ポイント:
- Windows 以外では何も追加されません。
#if WINDOWSでガードします。 DropDownOpened/Closedは「後から確定通知」。IsAboutToOpenは「前兆」の近似です。- Hot Reload / ハンドラ再生成に伴う二重購読を避けるため、同一インスタンスに複数回追加しない設計(上の Mapper 方式)にしています。
XAML:使い方
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:SampleApp.Controls"
x:Class="SampleApp.MainPage"
Padding="24">
北海道
青森県
岩手県
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
<Label Text="IsDropDownOpen:" Grid.Row="0" Grid.Column="0"/>
<Label Text="{Binding Source={x:Reference ExPicker}, Path=IsDropDownOpen}" Grid.Row="0" Grid.Column="1"/>
<Label Text="IsAboutToOpen:" Grid.Row="1" Grid.Column="0"/>
<Label Text="{Binding Source={x:Reference ExPicker}, Path=IsAboutToOpen}" Grid.Row="1" Grid.Column="1"/>
</Grid>
コードビハインド:Unfocused の扱いを改善
ポップアップへのフォーカス奪取で発火した Unfocused を「無視」したい場合のガード例です。
void OnFocused(object sender, FocusEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Focused");
}
void OnUnfocused(object sender, FocusEventArgs e)
{
#if WINDOWS
if (ExPicker.IsDropDownOpen || ExPicker.IsAboutToOpen)
{
// ドロップダウン表示に伴う見かけ上の Unfocused はスルー
return;
}
#endif
System.Diagnostics.Debug.WriteLine("Unfocused (実フォーカス喪失)");
}
回避策2:サブクラスではなく Behavior でブリッジ(既存 XAML を崩さない)
既存画面を書き換えたくない場合は、添付ビヘイビアで同様のブリッジを行う方法もあります。コントロールの型を入れ替えずに、Picker のまま追加機能を得られます。
Behavior(共通コード)
using Microsoft.Maui.Controls;
using System;
namespace SampleApp.Behaviors;
public class PickerDropDownMonitorBehavior : Behavior
{
public static readonly BindableProperty IsDropDownOpenProperty =
BindableProperty.Create(nameof(IsDropDownOpen), typeof(bool),
typeof(PickerDropDownMonitorBehavior), false, BindingMode.OneWayToSource);
public bool IsDropDownOpen
{
get => (bool)GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
public static readonly BindableProperty IsAboutToOpenProperty =
BindableProperty.Create(nameof(IsAboutToOpen), typeof(bool),
typeof(PickerDropDownMonitorBehavior), false, BindingMode.OneWayToSource);
public bool IsAboutToOpen
{
get => (bool)GetValue(IsAboutToOpenProperty);
set => SetValue(IsAboutToOpenProperty, value);
}
#if WINDOWS
private Microsoft.UI.Xaml.Controls.ComboBox? _cb;
#endif
protected override void OnAttachedTo(Picker bindable)
{
base.OnAttachedTo(bindable);
#if WINDOWS
bindable.HandlerChanged += OnHandlerChanged;
if (bindable.Handler is Microsoft.Maui.Handlers.PickerHandler h)
Attach(h);
#endif
}
protected override void OnDetachingFrom(Picker bindable)
{
base.OnDetachingFrom(bindable);
#if WINDOWS
Detach();
bindable.HandlerChanged -= OnHandlerChanged;
#endif
}
#if WINDOWS
private void OnHandlerChanged(object? sender, EventArgs e)
{
if (sender is Picker p && p.Handler is Microsoft.Maui.Handlers.PickerHandler h)
{
Detach();
Attach(h);
}
}
private void Attach(Microsoft.Maui.Handlers.PickerHandler handler)
{
if (handler.PlatformView is Microsoft.UI.Xaml.Controls.ComboBox cb)
{
_cb = cb;
cb.DropDownOpened += OnOpened;
cb.DropDownClosed += OnClosed;
cb.PointerPressed += OnPointerPressed;
cb.KeyDown += OnKeyDown;
}
}
private void Detach()
{
if (_cb is null) return;
_cb.DropDownOpened -= OnOpened;
_cb.DropDownClosed -= OnClosed;
_cb.PointerPressed -= OnPointerPressed;
_cb.KeyDown -= OnKeyDown;
_cb = null;
}
private void OnOpened(object? s, object e) { IsDropDownOpen = true; IsAboutToOpen = false; }
private void OnClosed(object? s, object e) { IsDropDownOpen = false; IsAboutToOpen = false; }
private void OnPointerPressed(object? s, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
=> IsAboutToOpen = true;
private void OnKeyDown(object? s, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
{
if ((e.Key == Windows.System.VirtualKey.Down && e.KeyStatus.IsMenuKeyDown) ||
e.Key == Windows.System.VirtualKey.F4)
{
IsAboutToOpen = true;
}
}
#endif
}
XAML:Behavior の適用
<ContentPage ...
xmlns:behaviors="clr-namespace:SampleApp.Behaviors">
<VerticalStackLayout>
<Picker x:Name="CityPicker" Title="都市">
<Picker.Behaviors>
<behaviors:PickerDropDownMonitorBehavior
IsDropDownOpen="{Binding IsOpen, Mode=OneWayToSource}"
IsAboutToOpen="{Binding IsAboutToOpen, Mode=OneWayToSource}"/>
</Picker.Behaviors>
<Picker.Items>
<x:String>Tokyo</x:String>
<x:String>Osaka</x:String>
<x:String>Nagoya</x:String>
</Picker.Items>
</Picker>
<Label Text="{Binding IsOpen, StringFormat='Open={0}'}"/>
<Label Text="{Binding IsAboutToOpen, StringFormat='AboutToOpen={0}'}"/>
回避策3:アプリ側の UX で吸収(実務チューニング)
コア仕様は変えられないため、イベントの扱いを工夫して UX を整えます。
| 目的 | 実装アイデア | 注意点 |
|---|---|---|
| 「見かけの Unfocused」を無視 | Unfocused で IsDropDownOpen || IsAboutToOpen をチェック | 閉じた瞬間の処理は DropDownClosed で行う |
| 「開く直前」に検証ロジックを走らせる | IsAboutToOpen が true になったら検証開始、Opened で確定 | 厳密な「直前」に拘ると OS 差で壊れやすい。兆候+確定の二段構えが安全 |
| フォームの自動スクロール | 展開後に項目リストの高さでスクロール位置を調整 | フォーカスがポップアップにある前提で処理順を設計 |
プラットフォーム差の整理
| プラットフォーム | Picker 実体 | フォーカス遷移の特徴 | 「直前」取得の可否 |
|---|---|---|---|
| Windows | WinUI ComboBox | 別ポップアップにフォーカスが移る | 標準 MAUI では不可。ハンドラで代替 |
| Android | ネイティブ Spinner 等 | 挙動は端末/テーマ差あり。連続発火は稀 | 開閉の検知は別アプローチ(カスタムレンダラ/ハンドラ) |
| iOS | UIPickerView 等 | モーダル的表示が多い。Windows とフォーカスモデルが違う | 標準 MAUI での「直前」イベントは無し |
実装の落とし穴と対策
- 二重購読:Hot Reload やページ再生成でハンドラが作り直されることがあります。
Mapper 方式なら「一度だけ付与」になりやすいですが、Behavior/サブクラス方式では必ずデタッチ処理を入れてください。 - スレッド:UI スレッドでフラグを切り替えるのが安全です。必要に応じて
MainThread.BeginInvokeOnMainThreadを使います。 - アクセシビリティ:
Unfocusedを安易に握り潰すとスクリーンリーダーの読み上げ順に影響します。
「ドロップダウン由来の Unfocused だけ無視」するガード条件を保ちましょう。 - 将来変更:MAUI/WinUI の更新で公式 API が増える可能性があります。Windows 専用コードは
#if WINDOWSで明確に分離し、影響範囲を限定します。
「開いた後に○○したい」を正しく行うためのテンプレート
典型的なユースケース別に、どのイベントで処理するかの指針を用意しました。
| やりたいこと | 使うイベント | 理由 |
|---|---|---|
| 開いた瞬間にアニメーション | DropDownOpened | 確実に UI が表示されたタイミング |
| 開く前に入力を検証してブロック | ブロック不可(WinUI では展開は防げない) | 代替:IsAboutToOpen で警告表示し、選択後に再検証 |
| 閉じたタイミングで保存処理 | DropDownClosed | 選択が確定してフォーカスが戻る/移る直後 |
| フォーカス喪失でバリデーション | Unfocused(ただし IsDropDownOpen を確認) | ポップアップ由来の Unfocused は除外 |
デモ:イベントを見える化する簡易ログビュー
開発中の検証に役立つ簡易ログ表示(ページ内表示)です。Debug.WriteLine だけでなく、UI 上に履歴を並べて確認できます。
<ContentPage ...>
<Grid RowDefinitions="Auto,*" Padding="16">
<controls:PickerEx x:Name="ExPicker"
Title="サンプル"
Focused="OnFocused"
Unfocused="OnUnfocused"/>
<CollectionView Grid.Row="1" ItemsSource="{Binding Logs}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding}" FontFamily="Consolas" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
public ObservableCollection<string> Logs { get; } = new();
void Add(string msg) => MainThread.BeginInvokeOnMainThread(() => Logs.Insert(0, $"{DateTime.Now:HH:mm:ss.fff} {msg}"));
void OnFocused(object sender, FocusEventArgs e) => Add("Focused");
void OnUnfocused(object sender, FocusEventArgs e)
{
#if WINDOWS
if (ExPicker.IsDropDownOpen || ExPicker.IsAboutToOpen)
{
Add("Unfocused (drop-down related) - ignored");
return;
}
#endif
Add("Unfocused (real)");
}
public MainPage()
{
InitializeComponent();
ExPicker.DropDownOpened += (_, __) => Add("DropDownOpened");
ExPicker.DropDownClosed += (_, __) => Add("DropDownClosed");
ExPicker.SelectedIndexChanged += (_, __) => Add($"SelectedIndexChanged: {ExPicker.SelectedIndex}");
}
テスト観点(実務チェックリスト)
- マウス/タッチ/キーボード(Tab, Alt+↓, F4)で挙動が一貫しているか。
- ドロップダウン展開中の
Unfocusedをバリデーションで誤認していないか。 - 開閉の連打(高速クリック)でフラグ競合が起きないか。
- ナビゲーション(他ページ遷移)直前にポップアップが開いていた場合の状態クリア。
- アクセシビリティ(キーボード操作のみ/スクリーンリーダー)での読上げ順。
トラブルシュート
| 症状 | 原因の目安 | 対処 |
|---|---|---|
DropDownOpened が複数回走る | イベント二重購読 | Behavior なら Detaching を実装/Mapper ならキー名を固定して一度だけ追加 |
IsAboutToOpen が true のまま | 閉じイベントでのリセット漏れ | DropDownOpened/Closed 双方で false に戻す |
| Android/iOS でビルド警告/エラー | Windows API が混入 | #if WINDOWS で厳格に分岐し、共通プロジェクトに Windows 名前空間を流出させない |
まとめ
- Windows の
Pickerは WinUIComboBoxの仕様により、クリック直後にUnfocusedが発生します(ポップアップにフォーカスが移る)。 - 標準 MAUI には「開く直前」イベントはありません。ハンドラ拡張で
DropDownOpened/Closedを購読し、必要なら 入力の兆候(IsAboutToOpen)を近似取得します。 - 実務では:
Unfocusedをそのままバリデーションに使わず、IsDropDownOpen/IsAboutToOpenと組み合わせて誤発火を回避します。 - Windows 専用コードは
#if WINDOWSで切り分け、将来のフレームワーク更新にも備えます。
参考:サンプル一式のフォルダ構成(目安)
SampleApp/
- MauiProgram.cs // Mapper で ComboBox イベントを購読
- MainPage.xaml // PickerEx の使用例
- MainPage.xaml.cs // ログと Unfocused ガード
- Controls/
PickerEx.cs // フラグ/イベントを公開
- Behaviors/
PickerDropDownMonitorBehavior.cs // 既存 Picker を拡張する場合
よくある質問(実装の考え方)
- Q. 「ドロップダウンを開くのを禁止」できますか?
A. いいえ。WinUI のComboBoxは OS 標準の動作を前提としており、アプリ側から展開自体を止める公式 API はありません。代替として、IsAboutToOpenを検知してユーザーに警告を出す/DropDownClosed直後に値をロールバックする等が現実的です。 - Q. 「Unfocused の発生順」を OS 間で揃えられますか?
A. 完全には困難です。UI のフォーカスモデルが OS ごとに異なるため、プラットフォームに依存しないアプリロジック(開閉イベント+値確定で処理)に寄せるのが安全です。 - Q. 「DropDownOpened と SelectedIndexChanged の順序」は?
A. 典型的にはDropDownOpened→(ユーザー操作)→SelectedIndexChanged→DropDownClosedの順で、Unfocusedはポップアップ表示直後または閉じた後に別途発生します。ロジックは各イベントの目的を分離して設計してください。
以上で、クリック直後に Unfocused が発生する理由と、ドロップダウン直前を検知するための実装方針、そして実務での安全な扱い方が理解できるはずです。ハンドラ拡張で「確定(Opened/Closed)」と「兆候(AboutToOpen)」の二軸を押さえ、フォーカスイベントに振り回されない UX を組み立てましょう。

コメント