2012年3月3日土曜日

wpf : テキスト編集可のComboBoxでポップアップのクリックを検出

wpfでテキスト編集可のComboBoxを使っていて次の2通りのタイミングで処理がしたくなりました。

  • ComboBoxについているTextBoxでエンターキーが押された。
  • ComboBoxについているポップアップメニューのクリックで項目が選択された。

エンターキーはKeyBindingで検出するとして、問題はポップアップメニューの方です。 最初はSelectionChangedイベントを使えばいいのかと思ってたんですが、このイベント、けっこう色んなタイミングで発行されるんですよね。 具体的には次のタイミングで発行されるのを確認しました。

  • TextBoxの内容がComboBoxItemの内容と一致。
  • TextBoxの内容とComboBoxItemの内容とが一致した状態からTextBoxが書き換えられる。
  • TextBoxにフォーカスをあわせ上下キーでComboBoxItemを選択。
  • ポップアップメニューでComboBoxItemを選択。

TextBoxの書き換えのときはスルーして、エンターキーとポップアップメニュークリックにだけ反応させたかったのですが、そのようなイベントは用意されていませんでした。 自分で実装です。

ポップアップメニュークリックの検出について、具体的にどうするかと言うと、まずはComboBoxについているPopupを探します。 (注:ComboBoxには色んなPopupを付けられるようですが、今回はデフォルトのポップアップメニューで試しました。) こんなコードで見つかりました。

// 最初に見つかったコントロールを返す。
private DependencyObject FindControl(DependencyObject obj, Type controlType)
{
    if (obj == null)
        return null;
    if (obj.GetType() == controlType)
        return obj;

    int childrenCount = VisualTreeHelper.GetChildrenCount(obj);
    for (int i = 0; i < childrenCount; i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(obj, i);
        DependencyObject descendant = FindControl(child, controlType);
        if (descendant != null && descendant.GetType() == controlType)
        {
            return descendant;
        }
    }

    return null;
}

ComboBoxのLoadedイベントで

Popup popup = (Popup)FindControl(コンボボックス, typeof(Popup))
_comboBoxPopup = popup // ←メンバー変数に参照を記憶

と書けばOK。 ちなみに、引数に渡すタイプをTextBoxにすればTextBoxが見つかります。

そしてComboBoxのSelectionChangedイベントでPopupが開いているかチェックします。 開いているかのチェックはPopup.IsOpenで行います。 開いているときにSelectionChangedイベントが発行されたということは、Popupから選択されたということを表しています。

private void OnComboBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (_comboBoxPopup == null || !_comboBoxPopup.IsOpen)
        return;

    MessageBox.Show("コンボボックスのポップアップがクリックされた");
}

これで、色んなタイミングで発行されるSelectionChangedイベントの中からポップアップメニューのクリックだけを検出で切るようになりました。 目的達成です。 意外と時間が取られたような、でも短い時間で済んだような...

以下、サンプルコード全体を載せておきます。 まずはMainWindow.xaml

<Window x:Class="ComboBoxPopupClickTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:ComboBoxPopupClickTest"
        Title="MainWindow" Height="350" Width="525">
    <ComboBox
            Name="testComboBox"
            Loaded="OnComboBoxLoaded"
            SelectionChanged="OnComboBoxSelectionChanged"
            IsEditable="True"
            Height="30"
            VerticalAlignment="Top"
    >
        <ComboBox.CommandBindings>
            <CommandBinding Command="{x:Static app:MainWindow.EnterKeyDownCommand}" Executed="OnEnterKeyDown"/>
        </ComboBox.CommandBindings>
        <ComboBox.InputBindings>
            <KeyBinding Key="Enter" Command="{x:Static app:MainWindow.EnterKeyDownCommand}"/>
        </ComboBox.InputBindings>
        <ComboBoxItem>10</ComboBoxItem>
        <ComboBoxItem>20</ComboBoxItem>
        <ComboBoxItem>30</ComboBoxItem>
        <ComboBoxItem>40</ComboBoxItem>
        <ComboBoxItem>50</ComboBoxItem>
    </ComboBox>
</Window>

そしてMainWindow.xaml.csです。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace ComboBoxPopupClickTest
{
    public partial class MainWindow : Window
    {
        public readonly static RoutedCommand EnterKeyDownCommand = new RoutedCommand();

        private Popup _comboBoxPopup;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void OnComboBoxLoaded(object sender, RoutedEventArgs e)
        {
            _comboBoxPopup = (Popup)FindControl(testComboBox, typeof(Popup));
        }

        private void OnEnterKeyDown(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("コンボボックスでエンターキーが押された");
        }

        private void OnComboBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (_comboBoxPopup == null || !_comboBoxPopup.IsOpen)
                return;

            MessageBox.Show("コンボボックスのポップアップがクリックされた");
        }

        // 最初に見つかったコントロールを返す。
        private DependencyObject FindControl(DependencyObject obj, Type controlType)
        {
            if (obj == null)
                return null;
            if (obj.GetType() == controlType)
                return obj;

            int childrenCount = VisualTreeHelper.GetChildrenCount(obj);
            for (int i = 0; i < childrenCount; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                DependencyObject descendant = FindControl(child, controlType);
                if (descendant != null && descendant.GetType() == controlType)
                {
                    return descendant;
                }
            }

            return null;
        }
    }
}