命名って難しい

変数、関数、クラスなどなど実装より命名に毎回悩むタイプの人間による技術についてのメモ。

検索ボックス入力中の文字列を取得してリアルタイム絞り込みを行う

弊社のアプリを進化させるため、よくWebであるやつをWindowsFormsで再現してみました。

画面1

こんな感じの画面でテキストボックスに入力すると・・・

絞り込み

変換途中の文字でこんな感じで絞り込める、そんな感じのを実現します。

原理

実現するために、以下の2つの要素が必要です。

  • 入力中の文字列の取得
  • 絞り込み

入力中の文字列の取得

入力中の文字列の取得には 入力メソッドマネージャー(Input Method Manager) を利用します。

入力メソッド マネージャー - Win32 apps | Microsoft Docs

IMMを利用する上で、コンテキストの取得と開放が必須になります。

コンテキストの取得 ImmGetContext

ImmGetContext function (imm.h) - Win32 apps | Microsoft Docs

An application should routinely use this function to retrieve the current input context before attempting to access information in the context. The application must call ImmReleaseContext when it is finished with the input context.

コンテキストの開放 ImmReleaseContext

こちらはコンテキストの開放です。

先程の関数で取得したコンテキストを開放し忘れないよう、try-catch-finally を利用することになります。

ImmReleaseContext function (imm.h) - Win32 apps | Microsoft Docs

入力中の文字列の取得 ImmGetCompositionStringW

ユニコード版を使います。 ImmGetContext function (immdev.h) - Win32 apps | Microsoft Docs

絞り込み(力技)

検索するための文字情報をStrConvを使い、すべて ひらがな全角大文字 に変換します。 変換したそれらを "|" で連結し、検索用キーワードを作ります。

検索用キーワードが検索ワードを含むか、 Containsメソッド を使ってマッチを確認します。

力技です。

ソースコード

DataGridViewのデータについて

DataGridViewに表示するデータは東証一部上場企業のデータを使いました。

www.kabu-data.info

テーブルの構築

以下のようなクラスを用意しました。

    public class ListedCompany
    {
        public string 銘柄名 { get; set; }
        public string 証券コード { get; set; }
        public string 上場区分 { get; set; }
        public string 読み { get; set; }
        private readonly string 検索用キーワード;
        public ListedCompany(string 銘柄名, string 証券コード, string 上場区分, string 読み)
        {
            this.銘柄名 = 銘柄名;
            this.証券コード = 証券コード;
            this.上場区分 = 上場区分;
            this.読み = 読み;
            this.検索用キーワード = string.Join(
                "|",
                new string[] { 銘柄名, 証券コード, 上場区分, 読み }
                    .Select(x => Strings.StrConv(x, VbStrConv.Hiragana | VbStrConv.Wide | VbStrConv.Uppercase)));
        }

        public bool IsMatch(string keyword)
        {
            return this.検索用キーワード.Contains(keyword);
        }
    }

データはpublic static classに定数として定義しています。

    public static class ListedCompanies
    {
        public static List<ListedCompany> Data { get; set; } = new List<ListedCompany>() {
            new ListedCompany("アーク","7873","東証1部 その他製品","あーく"),
            new ListedCompany("アークス","9948","東証1部 小売業","あーくす"),
            new ListedCompany("アークランドサカモト","9842","東証1部 小売業","あーくらんどさかもと"),
            new ListedCompany("アース製薬","4985","東証1部 化学","あーすせいやく"),
            new ListedCompany("アーネストワン","8895","東証1部 不動産業","あーねすとわん"),
            /* 以下略 */
        }
    }

IMM用クラス

IMMの関数をDllImportして定義するstatic classを定義しました。

    public static class Imm
    {
        /// <summary>
        /// IMM(Imput Method Manager)のコンテキストの取得。
        /// コンテキストは必ず ImmReleaseContext で開放すること。
        /// </summary>
        /// <param name="hWnd"></param>
        /// <returns></returns>
        [DllImport("Imm32.dll")]
        public static extern IntPtr ImmGetContext(IntPtr hWnd);

        /// <summary>
        /// IMMコンテキストの開放
        /// </summary>
        /// <param name="hWnd"></param>
        /// <param name="hIMC"></param>
        /// <returns></returns>
        [DllImport("Imm32.dll")]
        public static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);

        /// <summary>
        /// 現在入力中の文字列の取得
        /// </summary>
        /// <param name="hIMC"></param>
        /// <param name="dwIndex"></param>
        /// <param name="lpBuf"></param>
        /// <param name="dwBufLen"></param>
        /// <returns></returns>
        [DllImport("Imm32.dll", CharSet = CharSet.Unicode)]
        public static extern int ImmGetCompositionStringW(IntPtr hIMC, int dwIndex, byte[] lpBuf, int dwBufLen);

        public const int GCS_COMPSTR = 8;

    }

KeyDownイベント

ここからが本題ですね。

        private void SearchTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            var context = Imm.ImmGetContext((sender as TextBox).Handle);
            // 取得できない場合は終了
            if (context == IntPtr.Zero)
            {
                return;
            }
            try
            {
                // IME入力値の取得
                var buf = new byte[1024];
                var length = Imm.ImmGetCompositionStringW(context, Imm.GCS_COMPSTR, buf, buf.Length);
                if (length == 0)
                {
                    // 入力中でなかった場合は何もしない
                }
                else if (length >= 0)
                {
                    var composition = Encoding.Unicode.GetString(buf, 0, length);
                    // 現在の入力内容
                    var searchExpression =
                        this.uiSearchWordTextBox.Text.Substring(0, this.uiSearchWordTextBox.SelectionStart)
                        + composition
                        + this.uiSearchWordTextBox.Text.Substring(this.uiSearchWordTextBox.SelectionStart + this.uiSearchWordTextBox.SelectionLength);

                    // 行の表示・非表示
                    this.FilterBySearchWord(searchExpression);
                }
                else
                {
                    // 0未満はエラー
                }
            }
            finally
            {
                Imm.ImmReleaseContext(this.uiSearchWordTextBox.Handle, context);
            }
        }

フィルタ処理

データは DataTableクラス を利用して、 DataGridViewクラス に表示しています。

そのため、 DataTableクラスの DefaultView プロパティの RowFilter を使ってフィルタ処理を行います。

今回は以下のような関数を使ってフィルタをしています。

        private void FilterBySearchWord(string searchWord)
        {
            // 検索ワードにマッチする企業の証券コードを取得する。
            var upperWideSearchWord = Strings.StrConv(searchWord, VbStrConv.Hiragana | VbStrConv.Wide | VbStrConv.Uppercase);
            var codes = ListedCompanies.Data.Where(company => company.IsMatch(upperWideSearchWord)).Select(company => $"'{company.証券コード}'");
            this.dataTable.DefaultView.RowFilter = codes.Any() ? $"[証券コード] IN ({string.Join(",", codes)}) " : null;
            Debug.WriteLine(this.dataTable.DefaultView.RowFilter);
        }

サンプルプロジェクト

とここまで説明したものをGitHubで共有しました。 少し手直ししてからアップしているので本記事と一部記載がずれがあるかもしれません。

実際に動かしてためしてみてください。

github.com

以上