【実務・中級編】ListViewコントロールの高速大量描画:BeginUpdateとEndUpdateを用いたチラつきのない明細画面の構築手法 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

ListView高速大量描画の極意:数千件のレコードを「一瞬」で描画し、UIの硬直を完全駆逐する技術

業務アプリケーションの開発現場において、数千件から数万件におよぶログデータやファイルリストをUIに表示する要件は枚挙にいとまがありません。ここで多くの開発者が陥る罠があります。

「`For`ループを回して `ListView.Items.Add` を連打する」

この実装を行った瞬間、アプリケーションは激しい画面のチラつき(フリッカー)を起こし、最悪の場合はUIスレッドが完全にブロックされて「応答なし」の烙印を押されます。なぜなら、Windows Formsの標準コントロールである `ListView` は、1件アイテムが追加されるたびにウィンドウメッセージ(WM_PAINT等)を処理し、レイアウトと描画を再計算しようとするからです。

今回は、VB.NETのWindows Forms開発において、`BeginUpdate` と `EndUpdate` を駆使し、圧倒的な描画速度と滑らかなスクロール性を両立させるプロフェッショナルな設計手法を伝授します。

—

1. なぜ「1件ずつの追加」は悪なのか?(パフォーマンスのメカニズム)

GUIフレームワークの根幹には「描画スレッド(UIスレッド)」が存在します。
ループ内で `ListView.Items.Add` を呼び出すと、以下のコストが追加のたびに発生します。

1. DOM(アイテムコレクション)のメモリ再割り当て
2. ハンドル(HWND)の更新通知
3. OSレベルでの再描画(Invalidate / Update)の強制

数千回このサイクルを回せば、CPUは不要な描画計算にリソースを奪われ、ユーザーは画面が固まるようなストレスを味わうことになります。

解決の鍵:描画の「トランザクション化」

データベース操作におけるトランザクションを思い出してください。大量のINSERTは、都度コミットするのではなく、トランザクションで囲んで最後に一括コミットすることで爆発的な速度向上を得られます。

UI描画も全く同じです。「これから大量のデータを更新するから、終わるまで一切画面を再描画するな」とOSに命令すればいいのです。それが `BeginUpdate` と `EndUpdate` です。

—

2. 堅牢な設計と実装パターン(VB.NET)

実際の業務アプリケーションでそのまま使える、堅牢で保守性の高いコードを提示します。
ここでは、UIスレッドをブロックせず、数万件のダミーデータ(またはDB/ファイル群)を安全に流し込む実装を行います。

Imports System.Windows.Forms

Public Class MainForm

Private Sub BtnLoadData_Click(sender As Object, e As EventArgs) Handles BtnLoadData.Click
‘ 1. ユーザー操作のブロック(二重押下防止とパフォーマンス保護)
Me.Cursor = Cursors.WaitCursor
BtnLoadData.Enabled = False

Try
‘ 2. 高速描画メソッドの呼び出し
LoadDataToListView()
Catch ex As Exception
MessageBox.Show($”データ読み込み中にエラーが発生しました: {ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
‘ 3. 必ずカーソルとコントロールの状態を復元
BtnLoadData.Enabled = True
Me.Cursor = Cursors.Default
End Try
End Sub

”’

”’ BeginUpdate / EndUpdate を用いた高速リスト構築
”’

Private Sub LoadDataToListView()
‘ ListViewの初期設定(デザイナーで行っていない場合はここで行う)
With ListViewLogs
.View = View.Details
.FullRowSelect = True
.GridLines = True

‘ 【超重要】データ追加前のクリア
.Items.Clear()
End With

‘ 大量のダミーデータを生成(実務ではDBやファイルI/Oに置き換えます)
Dim rawDataList As List(Of LogEntity) = FetchDataFromSource(20000)

‘ ==========================================
‘ ここからが高速描画のコアテクニック
‘ ==========================================

‘ 描画の完全停止(メッセージキューへの描画指示をシャットオフ)
ListViewLogs.BeginUpdate()

Try
‘ コレクションの事前アロケーション効果を高めるため、配列やリストで一括追加を準備
‘ ListView.ListViewItemCollection.AddRange を使うのが最も効率的
Dim items As New List(Of ListViewItem)(rawDataList.Count)

For Each data As LogEntity In rawDataList
‘ サブアイテムを持つListViewItemの構築
Dim lvi As New ListViewItem(data.Id.ToString())
lvi.SubItems.Add(data.Timestamp.ToString(“yyyy-MM-dd HH:mm:ss”))
lvi.SubItems.Add(data.LogLevel)
lvi.SubItems.Add(data.Message)

items.Add(lvi)
Next

‘ 一括追加(内部配列を一度に流し込む)
ListViewLogs.Items.AddRange(items.ToArray())

Finally
‘ 【絶対必須】例外が発生しても必ず描画を再開させるため、Finally句で呼ぶ
ListViewLogs.EndUpdate()
End Try
‘ ==========================================
End Sub

”’

”’ モック用データ生成メソッド
”’

Private Function FetchDataFromSource(count As Integer) As List(Of LogEntity)
Dim list As New List(Of LogEntity)(count)
Dim rnd As New Random()
Dim levels() As String = {“INFO”, “WARN”, “ERROR”, “DEBUG”}

For i As 1 To count
list.Add(New LogEntity With {
.Id = i,
.Timestamp = DateTime.Now.AddSeconds(-rnd.Next(100000)),
.LogLevel = levels(rnd.Next(levels.Length)),
.Message = $”システムログエントリ番号 {i}: 処理が正常に実行されました。”
})
Next

return list
End Function

End Class

‘ データの型定義
Public Class LogEntity
Public Property Id As Integer
Public Property Timestamp As DateTime
Public Property LogLevel As String
Public Property Message As String
End Class

—

3. プロジェクトリーダーが教える「実装上の急所と注意点」

このコードを現場に導入するにあたり、シニアエンジニアとして特に注意してほしいポイントを解説します。

① `BeginUpdate` と `EndUpdate` は必ず `Try…Finally` で囲め

初心者がやりがちなバグとして、`BeginUpdate` を呼んだ後に予期せぬ例外が発生し、`EndUpdate` がスキップされてしまうケースがあります。これが発生すると、アプリケーションが二度と再描画されなくなります(画面が真っ白、または固まったままになる)。
必ず上記のサンプルコードのように `Try…Finally` 構造を厳守してください。

② `Items.Add` ではなく `Items.AddRange` を使え

ループ内で1件ずつ `.Add` するのではなく、`ListViewItem` の配列(または `ICollection`)を生成し、最後に `.AddRange()` で一括挿入してください。内部のメモリコピー処理の回数が劇的に減り、CPU負荷が最小化されます。

③ 仮想モード(VirtualMode)というさらなる高み

もし数万件ではなく、数百万件のデータを扱う必要がある場合は、今回紹介した `AddRange` すらメモリ不足の原因になります。その場合は、`ListView.VirtualMode = True` に設定し、自前でデータキャッシュを持つ「仮想ListView(Virtual ListView)」を設計してください。OSから要求されたインデックスのデータだけを動的に返す仕組みにすることで、メモリ消費量をほぼゼロに抑えることができます。

—

まとめ:道具の特性を知り、プロフェッショナルなコードを書け

Visual Basic / VB.NET は、その歴史の長さゆえに「古い書き方」がネット上に蔓延しています。しかし、フレームワークの内部挙動(メッセージループや描画制御)を正しく理解していれば、C# に全く引けを取らない高速で堅牢な業務アプリケーションを構築することが可能です。

今回紹介した `BeginUpdate` / `EndUpdate` によるトランザクション描画は、今日からあなたのコードベースに組み込める最もコスト対効果の高い最適化手法です。
「動くだけのコード」から「スケーラブルで美しいコード」へ。ワンランク上のエンジニアを目指していきましょう。

タイトルとURLをコピーしました