ComboBoxとAutoCompletionの極限最適化:数万件のマスターデータを瞬時に手なづけるインクリメンタルサーチ実装
レガシーシステムの維持、あるいは堅牢な社内ニッチシステムの構築において、Windows Forms(WinForms)がいまだに現役の主力である現場は少なくない。VB.NETによるデスクトップアプリケーション開発において、避けて通れない課題の一つが「大量マスターデータのUI統合」である。
社員マスタ、顧客マスタ、商品マスタ――。これらが数千件、あるいは数万件に達したとき、標準の `ComboBox` に `AutoCompleteSource.ListItems` を設定して安易に満足している開発者は、プロファイルを取ったことがあるだろうか。
文字を入力するたびにUIスレッドがロックされ、メモリ使用量が跳ね上がり、ユーザーが「使い物にならない」と絶叫する。この悲劇を引き起こす原因は、.NET Frameworkの抽象化層の甘さと、Windowsメッセージキューの特性を無視した実装にある。
今回は、VB.NETの極限のパフォーマンスを引き出し、数万件のレコードをミリ秒単位でインクリメンタルサーチするための高度な拡張実装と、その裏にあるメモリ・APIレベルの知見を徹底的に解説する。
—
1. なぜ標準のAutoCompletionでは破綻するのか?
標準の `AutoCompleteMode.SuggestAppend` は、内部でWin32の `CB_GETLBTEXT` やリストボックス全体の走査を行っている。データソースがバインドされた `ComboBox` において、ユーザーが1文字入力するたびに、WinFormsはマネージドとアンマネージドの境界を越えて文字列の比較を全件走査(あるいは非効率なインデックス検索)で実行する。
特に `DataTable` や `BindingSource` を直接 `DataSource` に割り当てた場合、データバインディングのオーバーヘッドが重畳し、O(N)の検索コストが直接UIスレッドを直撃する。
これを解決するためのアプローチは以下の3点に集約される。
1. データの非バインド化(OwnerDrawn または 仮想化の検討):全件をコントロールに抱え込ませない。
2. インメモリでの高速インデックス検索:VB.NETの配列(Array)と `String.IndexOf` の最適化。
3. Windows APIによる描画・メッセージの制御:不要な再描画の抑止(`SendMessage` による `WM_SETREDRAW`)。
—
2. 実装:超高速インクリメンタルサーチ・コンボボックス
以下に、数万件のデータであっても軽快に動作するカスタム `ComboBox` コレクションの骨子を示す。余計なデータバインディングの呪縛を断ち切り、内部配列に対して高速なバイナリサーチまたは前方一致フィルタリングを行う設計だ。
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
Public Class HighPerformanceComboBox
Inherits ComboBox
‘ Win32 API 宣言:再描画のロック・アンロックによる画面チラツキと負荷の軽減
Private Const WM_SETREDRAW As Integer = &HB
Private Shared Function SendMessage(hWnd As IntPtr, wMsg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
End Function
‘ マスターデータの保持(Key: 表示名, Value: 内部コード等)
Private _masterData As Dictionary(Of String, String)
Private _filteredKeys() As String
Private _isInternalUpdate As Boolean = False
Public Sub New()
MyBase.New()
‘ 標準のオートコンプリートは無効化し、自前で完全に制御する
Me.AutoCompleteMode = AutoCompleteMode.None
Me.DropDownStyle = ComboBoxStyle.DropDown
End Sub
”’
”’
Public Sub SetMasterData(dataSource As Dictionary(Of String, String))
‘ メモリ解放の配慮:既存参照の切断
If _masterData IsNot Nothing Then
_masterData.Clear()
End If
_masterData = dataSource
_filteredKeys = _masterData.Keys.ToArray()
RefreshList(String.Empty)
End Sub
”’
”’
Protected Overrides Sub OnTextUpdate(ByVal e As EventArgs)
If _isInternalUpdate Then Return
MyBase.OnTextUpdate(e)
Dim currentText As String = Me.Text
‘ 描画一時停止によるパフォーマンス最大化
SendMessage(Me.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
Try
‘ 候補の絞り込み(LINQまたはArray.FindAllによる高速フィルタリング)
If String.IsNullOrEmpty(currentText) Then
_filteredKeys = _masterData.Keys.ToArray()
Else
‘ 前方一致または部分一致の要件に合わせて変更(ここでは高速な前方・部分一致)
_filteredKeys = _masterData.Keys.Where(Function(k) k.Contains(currentText, StringComparison.OrdinalIgnoreCase)).ToArray()
End If
RefreshList(currentText)
‘ カーソル位置とテキストの復元
Me.Text = currentText
Me.SelectionStart = currentText.Length
Me.SelectionLength = 0
‘ ドロップダウンの自動展開
If Not Me.DroppedDown AndAlso _filteredKeys.Length > 0 AndAlso Not String.IsNullOrEmpty(currentText) Then
Me.DroppedDown = True
‘ カーソルが勝手に先頭に移動するのを防ぐWinFormsの挙動対策
Cursor.Current = Cursors.Default
End If
Finally
‘ 描画再開
SendMessage(Me.Handle, WM_SETREDRAW, New IntPtr(1), IntPtr.Zero)
Me.Invalidate()
End Try
End Sub
Private Sub RefreshList(filter As String)
_isInternalUpdate = True
Me.BeginUpdate()
Try
Me.Items.Clear()
If _filteredKeys IsNot Nothing AndAlso _filteredKeys.Length > 0 Then
‘ 配列サイズが大きい場合は上限を設ける(例:最大100件まで表示して描画負荷を抑制)
Dim displayLimit = Math.Min(_filteredKeys.Length, 100)
Dim displayArray(displayLimit – 1) As String
Array.Copy(_filteredKeys, displayArray, displayLimit)
Me.Items.AddRange(displayArray)
End If
Finally
Me.EndUpdate()
_isInternalUpdate = False
End Try
End Sub
”’
”’
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
If _masterData IsNot Nothing Then
_masterData.Clear()
_masterData = Nothing
End If
_filteredKeys = Nothing
End If
MyBase.Dispose(disposing)
End Sub
End Class
—
3. アーキテクチャの急所:なぜこのコードが「極限」なのか?
① `WM_SETREDRAW` による描画バーストの抑制
文字を入力するたびに `Items.Clear()` と `Items.AddRange()` を実行すると、コントロールは内部でウィンドウの再描画(Paint)を強制される。これが数万件のアイテム操作や高速タイピング時に画面のチラツキ(Flicker)と重い遅延を生む。
`SendMessage` で `WM_SETREDRAW` を `0`(False)に設定することで、リスト更新中のUI描画処理を完全にバイパスし、処理完了後に一括して `1`(True)に戻して再描画させる。このテクニックの有無で体感速度は桁違いになる。
② メモリのライフサイクル管理とGC(ガベージコレクション)対策
レガシーなVB.NETアプリケーションで最も恐ろしいのは、「何気なく生成したString配列やLINQの遅延評価オブジェクトによるGen 2 GCの肥大化」である。
数万件のデータを扱うUIコンポーネントでは、ユーザーがタイピングするたびに無数のオブジェクトがヒープにばら撒かれる。上記のコードでは、マスターデータを `Dictionary(Of String, String)` としてメモリ上に常駐させ、フィルタリング結果の配列だけを最小限アロケーションするように設計している。
また、コンポーネント破棄時(`Dispose`)には明示的に `Clear()` を呼び出し、マネージドヒープへの参照を断ち切ることでメモリリークを防いでいる。
③ 表示件数のキャップ(Capping)
たとえ内部で数万件持っていても、ドロップダウンリストに数万件すべてを表示するUI上の意味はない。ユーザーが視認できるのはせいぜい数十件だ。
`Math.Min(_filteredKeys.Length, 100)` により、UI描画対象を強制的に最大100件に制限することで、WinFormsのリストボックスコントロール自体の内部描画コストを極限まで低く抑え込んでいる。
—
4. レガシー環境・システム間連携における実務的注意点
1. 文字コードと大文字小文字の比較
社内マスタ等では、全角・半角の混在や、大文字小文字(ASCII/JIS)の揺れが必ず発生する。`.Contains(…, StringComparison.OrdinalIgnoreCase)` は.NET Core / .NET 5以降、あるいは `.NET Framework 4.7.2` 以降で有効だが、レガシーな `Framework 4.0` あたりで動かす場合は `CultureInfo.CurrentCulture` を意識した文字列比較に書き換える必要がある。必要であれば `Microsoft.VisualBasic.Strings.StrConv` を用いた正規化をマスターロード時に行うべきだ。
2. スレッドセーフティ
データベースや外部Web APIから非同期(`Async/Await`)でマスターデータを取得し、バックグラウンドスレッドから直接 `SetMasterData` を呼んではならない。必ず `If Me.InvokeRequired Then` によるマーシャリングを挟むこと。
—
5. 総括
「動けばいい」という妥協の産物であるスパゲッティコードのUIは、データが増えた瞬間にシステムを死に至らしめる。
Visual Basic、そしてWindows Formsという、一見レガシーに思える技術スタックであっても、Windowsのウィンドウメッセージの挙動、.NETのメモリ管理、そしてアルゴリズムの計算量を理解していれば、モダンなWebアプリケーションに匹敵する極限のレスポンスを実現できる。
アーキテクトたる者、フレームワークの背後で何が起きているのかを常に直視し、コードの1行1行にエンジニアリングの魂を宿らせなければならない。
