【WPF】部分一致絞り込みが可能なComboBoxの作り方

## 項目数が多いComboBoxでは部分一致絞り込みが欲しくなる

WPFに限らずですが、あらかじめ決められた項目の中からユーザーに1つ選択させるためのコントロールとして、ComboBoxというものが標準で用意されています。「プルダウン」とも呼ばれたりすることがあるやるです。
しかし、選択肢が多くなればなるほど、多数の中から目的の項目を探し出す必要が生じるため、ただのComboBoxは使い勝手が悪くなります。

そのようなときにあると便利なものが、**入力した文字列と部分一致する項目のみに自動的に絞り込んでくれるComboBox**だったりします。

## 大まかな作成手順

ざっくり纏めると、下記の手順となります。

1. ComboBoxを継承したコントロール（Xamlとコードビハインド）を作成する。
2. 作成したコントロールをComboBoxと同じような感覚で使用する。

**以上。**

## 作成手順の詳細

例として、空のWPFプロジェクト（プロジェクト名はTestBenchWPFとする）に作成していきます。

##### 1. 自作のコントロール用のフォルダを作成する。
プロジェクトを右クリック →「追加」→「新しいフォルダー」で作成します。
フォルダ名は「Controls」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140394/13b45658-012c-23ce-c673-261e556c7696.png)

##### 2. Controlsフォルダ内に、新規のユーザーコントロールを作成する。
Controlsフォルダを右クリック →「追加」→「ユーザーコントロール」で作成します。
名前は「PartialSearchComboBox」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140394/5c62163f-b5cd-1736-ef7d-c83b3a6536bd.png)

##### 3. PartialSearchComboBox.xamlを修正する。

```xml:BEFORE
<UserControl x:Class="TestBenchWPF.Controls.PartialSearchComboBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:TestBenchWPF.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
            
    </Grid>
</UserControl>
```
```xml:AFTER
<ComboBox x:Class="TestBenchWPF.Controls.PartialSearchComboBox"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          IsEditable="True">
    <!-- ↑任意の文字列を入力可能にするため、IsEditableをTrueにすることは必須。 -->
</ComboBox>
```

##### 4. PartialSearchComboBox.xaml.csを修正する。
以下のように実装します。

```csharp:PartialSearchComboBox.xaml.cs
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace TestBenchWPF.Controls
{
    /// <summary>
    /// PartialSearchComboBox.xaml の相互作用ロジック
    /// </summary>
    public partial class PartialSearchComboBox : ComboBox
    {
        private TextBox _textBox = null;
        private Popup _popUp = null;

        public PartialSearchComboBox()
        {
            InitializeComponent();

            this.Loaded += delegate
            {
                _textBox = this.Template.FindName("PART_EditableTextBox", this) as TextBox;
                _popUp = this.Template.FindName("PART_Popup", this) as Popup;

                if (_textBox != null)
                {
                    _textBox.TextChanged += delegate
                    {
                        if (!_popUp.IsOpen && string.IsNullOrEmpty(_textBox.Text))
                        {
                            // プログラムの処理によってコンボボックス内のテキストが空に上書きされた場合、ここに入る。
                            // この処理がないと、コンボボックス内のテキストを空に上書きするときにプルダウンが開いてしまう。
                            this.Items.Filter += obj =>
                            {
                                // プルダウン部分へ適用されているフィルターを解除する。
                                return true;
                            };

                            return;
                        }

                        // 入力がある都度、即時フィルターをかける。
                        this.Items.Filter += obj =>
                        {
                            if (!(obj is ComboBoxItem))
                            {
                                return true;
                            }

                            var item = obj as ComboBoxItem;
                            if (((string)item.Content).Contains(_textBox.Text))
                            {
                                //「選択肢の文字列の中に入力された文字列が含まれる場合」にフィルターを通過させる。
                                // フィルターを通過すると、展開された選択肢の中に表示される。
                                return true;
                            }

                            return false;
                        };

                        _popUp.IsOpen = true;
                    };

                    _textBox.GotFocus += delegate
                    {
                        if (!_popUp.IsOpen)
                        {
                            // コンボボックスの入力欄にフォーカスが当たったとき、プルダウンを展開する。
                            _popUp.IsOpen = true;
                        }
                    };
                }
            };
        }
    }
}

```

## 作成したComboBoxの使い方

上記手順で作成したPartialSearchComboBoxを実際に使ってみます。
簡単な例として、MainWindow.xamlの上に都道府県のコンボボックスとして設置してみます。

```xml:BEFORE
<Window x:Class="TestBenchWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestBenchWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
    </Grid>
</Window>
```

```xml:AFTER
<Window x:Class="TestBenchWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestBenchWPF"
        xmlns:cc="clr-namespace:TestBenchWPF.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <cc:PartialSearchComboBox HorizontalAlignment="Center" VerticalAlignment="Center"
                                  Height="32" Width="120">
            <ComboBoxItem Content="北海道"/>
            <ComboBoxItem Content="青森県"/>
            <!-- 中略 -->
            <ComboBoxItem Content="鹿児島県"/>
            <ComboBoxItem Content="沖縄県"/>
        </cc:PartialSearchComboBox>
    </Grid>
</Window>
```

## 動作のサンプル

これをビルド・実行してComboBox上に任意の文字列を入力すると、部分一致する都道府県のみに絞り込まれるようになることが分かります。

**例１ 『川』と入力した場合**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140394/cdfb4d14-f45b-51a1-1409-7e144be017b2.png)

**例２ 『京』と入力した場合**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140394/057b500b-640e-85e4-f35b-b7c750a5efe0.png)


## （追記）ItemsSourceに対して部分一致絞り込みを使いたい場合

ComboBoxのアイテムとして、上記のComboBoxItemではなく**ItemsSource**を使用する場合にどのようにすればよいかを記載します。

#### 1. サンプルとしてItemsSourceに使用するクラス
以下のクラスでできたListをItemsSourceに詰め込むものとします。

```csharp:Prefecture.cs
namespace TestBenchWPF.Models
{
    /// <summary>
    /// 都道府県を表すクラス
    /// </summary>
    public class Prefecture
    {
        /// <summary>
        /// 都道府県名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 県庁所在地
        /// </summary>
        public string PrefecturalCapitalCityName { get; set; }
    }
}
```

#### 2. ComboBoxを使用する側のXamlとそのコードビハインド
ComboBoxItemを使用していたときと同様に都道府県を一覧に表示する場合、MainWindow.xamlとそのコードビハインドは以下のようになります。

```xml:MainWindow.xaml
<Window x:Class="TestBenchWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestBenchWPF"
        xmlns:cc="clr-namespace:TestBenchWPF.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <cc:PartialSearchComboBox x:Name="PrefecturesComboBox"
                                  HorizontalAlignment="Center" VerticalAlignment="Center"
                                  Height="32" Width="120" TextSearch.TextPath="Name">
            <!-- 都道府県名の部分のみを表示するためのテンプレート -->
            <cc:PartialSearchComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}"/>
                    </StackPanel>
                </DataTemplate>
            </cc:PartialSearchComboBox.ItemTemplate>
        </cc:PartialSearchComboBox>
    </Grid>
</Window>
```

```csharp:MainWindow.xaml.cs
/*省略*/
        public MainWindow()
        {
            InitializeComponent();

            // コンストラクタ内でItemsSourceを積み込むとする。
            PrefecturesComboBox.ItemsSource = new List<Prefecture>()
            {
                new Prefecture(){ Name = "北海道", PrefecturalCapitalCityName = "札幌市"},
                new Prefecture(){ Name = "青森県", PrefecturalCapitalCityName = "青森市"},
                // 中略
                new Prefecture(){ Name = "鹿児島県", PrefecturalCapitalCityName = "鹿児島市"},
                new Prefecture(){ Name = "沖縄県", PrefecturalCapitalCityName = "那覇市"},
            };
        }
/*省略*/
```
MVVMで実装する場合、コードビハインドを使わずに、対応するViewModelから同様の要領でバインドすれば大丈夫です。

#### 3. PartialSearchComboBoxに対する修正

フィルターの対象をComboBoxItemクラスからPrefectureクラスに変更する必要があります。
独自のクラスをItemsSourceに使用する場合は、同様にその独自のクラスに変更すれば大丈夫です。

PrefectureのNameに対してフィルターをかけたいので、PartialSearchComboBoxのコードビハインドを以下のように変更します。

```csharp:PartialSearchComboBox.xaml.cs（BEFORE）
/*省略*/

                        // 入力がある都度、即時フィルターをかける。
                        this.Items.Filter += obj =>
                        {
                            if (!(obj is ComboBoxItem))
                            {
                                return true;
                            }

                            var item = obj as ComboBoxItem;
                            if (((string)item.Content).Contains(_textBox.Text))
                            {
                                //「選択肢の文字列の中に入力された文字列が含まれる場合」にフィルターを通過させる。
                                // フィルターを通過すると、展開された選択肢の中に表示される。
                                return true;
                            }

                            return false;
                        };

/*省略*/
```

```csharp:PartialSearchComboBox.xaml.cs（AFTER）
/*省略*/

                        // 入力がある都度、即時フィルターをかける。
                        this.Items.Filter += obj =>
                        {
                            if (!(obj is Prefecture))
                            {
                                return true;
                            }

                            var item = obj as Prefecture;
                            if (item.Name.Contains(_textBox.Text))
                            {
                                //「PrefectureのNameの中に入力された文字列が含まれる場合」にフィルターを通過させる。
                                // フィルターを通過すると、展開された選択肢の中に表示される。
                                return true;
                            }

                            return false;
                        };

/*省略*/
```

以上の手順で、ItemsSourceを使用する場合にも対応することができます。


#### 4. おまけ：特殊なフィルターをかけてみる

なかなか用途がないとは思いますが、これを応用すると、
「入力された文字列と県庁所在地名が部分一致する都道府県のみをフィルターする」
といったことができます。

フィルター部分を以下のように変えることで実現可能です。

```csharp:PartialSearchComboBox.xaml.cs
/*省略*/

                        // 入力がある都度、即時フィルターをかける。
                        this.Items.Filter += obj =>
                        {
                            if (!(obj is Prefecture))
                            {
                                return true;
                            }

                            var item = obj as Prefecture;

                            // ↓「item.Name」を「item.PrefecturalCapitalCityName」に変えただけ
                            if (item.PrefecturalCapitalCityName.Contains(_textBox.Text))
                            {
                                return true;
                            }

                            return false;
                        };

/*省略*/
```

この状態で部分一致絞り込みを使用すると、以下のような動作になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140394/da940ba5-1f67-2224-92c9-6c56225bf75f.png)
※県庁所在地として「津市」と「大津市」がヒット