2025 WinForms 日本語対応 中間一致サジェスト付きテキストボックスを自作する

WinFormsアプリケーション向けに中間一致サジェスト機能付きテキストボックスを自作しましたので、実装方法をまとめます。

:::note warn
2025年5月2日
名称入力からのサジェストに追加して、コード入力からのサジェストテキストボックスを追加しました。
:::

:::note
* 日本語IMEにも対応
* 中間一致（部分一致）検索
* 候補はListBoxで表示
* リストはユーザーコントロール外に動的追加してZオーダー問題を回避
* 最小化/復元にも自然対応
* 非常にシンプル＆安定
業務アプリにも安心して使えるレベルを目指しました✨
:::

## １．前提条件
* Visual studio 2022 Version 17.13.6
* .Net 9

:::note 
なお、すべてのソースコードを公開しています。
:::
https://github.com/masayahak/CustomCodeSuggest

## ２．目標 🎯

普通のテキストボックスに入力すると、その下にサジェスト候補が中間一致で表示され、選択できるUIを作ります。
* 入力確定は Enter キー or 候補リストクリック
* 上下キーでリスト内移動
* ESCキーでリストを閉じる
* リストはテキストボックスの下にポップアップ表示
* Zオーダー問題なし（親フォームに直追加）

## ３．画面イメージ

### ３．１ 名称入力（日本語入力）からサジェスト（ImeSuggestTextBox）

未入力状態(プレースホルダーが表示される)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/37d1821c-bcda-4462-b903-cce883991379.png)

中間一致サジェスト（ID）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/1b9fc1c3-f4a3-4ba8-91f1-b69b221598c8.png)

中間一致サジェスト（名称）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/4a5c7b66-a070-49df-9428-1eebb97b392c.png)

中間一致サジェスト（リストに表示されていない「かな」からも検索している）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/c8ec82da-63bc-4855-b1fa-25156f4ed2f2.png)

サジェストから選択済み（IDが設定される）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/0d309c52-e548-4be9-81f3-238b34b90d8e.png)

サジェスト選択内容からテキストを修正するとIDが自動でリセットされる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/2196cb08-0436-4d59-a4c0-ad6896913856.png)

デザイン時のプロパティ（プレースホルダやリストへの表示件数など指定可能）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/da13557f-4f46-4aa4-ae0b-6b9bff8d8868.png)

### ３．２ コード入力からサジェスト（CodeSuggestTextBox）

中間一致サジェスト（コード）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/72bd4074-1318-4105-9bd5-86cff0d146ec.png)

サジェストから選択済み（名称が設定される）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/611c207e-1edf-4a07-adfb-d8d0cef6d5af.png)


## ４．実装ポイント 📄
### A) リストボックスはユーザーコントロールの外に出す！
内部にListBoxを持つと、他のコントロールに隠れたり、最前面制御が難しいため、
親フォームに直接ListBoxを追加して管理しています。

```C#
_parentForm.Controls.Add(_listBox);
_parentForm.Controls.SetChildIndex(_listBox, 0);
```
これで、常に自然な前面表示ができます！

### B) 「↓」または「エンター」でサジェストを呼び出す

検索文字列が1文字ずつ変化するたびにサジェストすると邪魔。「↓」と「エンター」で呼び出す。
日本語入力中（変換中）に無理にサジェストすると、UXが悪化するため、IME確定後(Ime変換確定後イベント)にサジェストを開始します。

### C) 入力キーはすべてテキストボックス側で処理

ListBoxにフォーカスを取らせず、キーイベントは常にテキストボックス側で受け取り、自然な操作感を実現しています。

### D) リストのサイズは項目数と最大表示数で動的計算

表示件数が少ないときでも、リストサイズは動的に決まります。
```C#
int height = Math.Max(30, itemHeight * Math.Min(最大表示件数, マッチ.Count));
```
### E) 親フォームが閉じる時にリストボックスをDispose

親フォームが閉じた後にリストが残らないよう、
```C#
_parentForm.FormClosed += (s, e) => _listBox.Dispose();
```
で安全に破棄しています。


## ５．最終構成 📦

* ImeSuggestTextBox（ユーザーコントロール）
* ImeTextBox（カスタムテキストボックス、IME確定検出用）
* ListBox（親フォーム直下に動的配置）
* サジェスト候補クラス（サジェストアイテム）

## ６．ソースコード（抜粋）🔥
```C#
public override string Text
{
    get => _textBox.Text ?? string.Empty;
    set => _textBox.Text = value ?? string.Empty;
}

private void ShowPopupサジェスト()
{
    HidePopup();

    var マッチ = _候補リスト
        .Where(x => x.ID.Contains(入力値) || x.名称.Contains(入力値) || x.補助検索キー.Contains(入力値))
        .Take(最大表示件数)
        .ToList();

    if (マッチ.Count == 0) return;

    _listBox.DataSource = null;
    _listBox.Items.Clear();
    _listBox.DataSource = マッチ;
    _listBox.DisplayMember = nameof(サジェストアイテム.ToString);

    var screenPos = _textBox.PointToScreen(new Point(0, _textBox.Height));
    var clientPos = _parentForm!.PointToClient(screenPos);
    _listBox.Location = clientPos;

    int height = Math.Max(30, _listBox.ItemHeight * Math.Min(最大表示件数, マッチ.Count));
    _listBox.Size = new Size(this.Width, height);
    _listBox.Visible = true;
    _listBox.BringToFront();
}
```



## ７．最後に 📝

この構成は、
✅ シンプル
✅ 軽量
✅ 実務アプリでも安定
をすべて両立できることを目指しました。

中間一致サジェストは業務アプリで大活躍するので、
ぜひ参考にしてもらえたら嬉しいです🌸