.NET MAUIのPickerでクリック直後にUnfocusedが発火する理由とWinUI ComboBox仕様に基づく回避策まとめ

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 実体フォーカス遷移の特徴「直前」取得の可否
WindowsWinUI ComboBox別ポップアップにフォーカスが移る標準 MAUI では不可。ハンドラで代替
Androidネイティブ Spinner 等挙動は端末/テーマ差あり。連続発火は稀開閉の検知は別アプローチ(カスタムレンダラ/ハンドラ)
iOSUIPickerView 等モーダル的表示が多い。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&lt;string&gt; Logs { get; } = new();

void Add(string msg) =&gt; MainThread.BeginInvokeOnMainThread(() =&gt; Logs.Insert(0, $"{DateTime.Now:HH:mm:ss.fff}  {msg}"));

void OnFocused(object sender, FocusEventArgs e) =&gt; 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 += (_, __) =&gt; Add("DropDownOpened");
    ExPicker.DropDownClosed += (_, __) =&gt; Add("DropDownClosed");
    ExPicker.SelectedIndexChanged += (_, __) =&gt; 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 は WinUI ComboBox の仕様により、クリック直後に 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 を組み立てましょう。

この記事を書いた人

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

コメント

コメントする

目次