【テクニカル・上級編】Windows Formsのカスタムレイアウトエンジン自作:LayoutEngineを継承して独自の整列ロジックを持つパネルコントロールを開発する – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsの極限最適化:LayoutEngine継承による「完全カスタム配置パネル」の自作

レガシーシステムの保全、あるいは泥臭い現場の業務アプリケーションにおいて、Windows Forms(WinForms)がいまだに現役の主力である現場は少なくない。`FlowLayoutPanel` や `TableLayoutPanel`。これらは一見便利だが、現場の歪な業務要件――例えば「画面解像度の変動に完全追従しつつ、特定の黄金比率を維持してタイル状に崩壊せず並ぶグリッド」や「数千件の動的コントロール描画におけるカクつきの排除」――の前には無力だ。

標準のレイアウトコンテナの挙動にイライラさせられるのは今日で終わりにしよう。
今回は、WinFormsのレンダリングパイプラインの深部へメスを入れ、`System.Windows.Forms.Layout.LayoutEngine` を直接継承して「独自の整列ロジックを持つカスタムパネルコントロール」をVB.NETで実装する極限の知見を授ける。

—

1. WinFormsレイアウト機構の暗黒面と真実

多くの開発者は、コントロールのサイズが変わるたびに `Resize` イベントをフックし、その中で子コントロールの `Location` や `Size` を泥臭く計算して書き換えている。

今すぐそのコードを捨てろ。

`Resize` イベントでの動的再配置は、レイアウト計算の多重発火(レイアウトループ)、無駄な `Invalidate()` による画面のちらつき(フリッカ)、そしてマネージドヒープへの過剰な負荷を引き起こす元凶である。

WinFormsの真のアーキテクチャでは、レイアウトはコントロール自身ではなく、専用の `LayoutEngine` クラス群が非同期かつ一元的にハンドリングするべきものとして設計されている。ここに独自ロジックを差し込むことで、親のサイズ変更、子コントロールの追加・削除、プロパティ変更といった一連のライフサイクルイベントを、パフォーマンスを極限まで高めた状態で制御できる。

—

2. 実装:黄金比と余白を自動計算する「FibonacciLayoutEngine」

今回は実用例として、配置される子コントロールの数に応じて、あたかも螺旋を描くように、あるいは動的なグリッドとして美しく自動整列を行うカスタムレイアウトエンジンを実装する。

以下のコードは、余計なオーバーヘッドを排除し、直接Win32 APIの描画抑制すらも視野に入れたVB.NETのプロダクションコードである。

Imports System
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.Layout

Namespace CustomUI.Layouts

‘ =================================================================0
‘ 1. カスタムレイアウトエンジン本体
‘ =================================================================0
Public Class GoldenRatioLayoutEngine
Inherits LayoutEngine

‘ シングルトンパターンでインスタンスを共有し、無駄なGC発生を阻止する
Private Shared _instance As GoldenRatioLayoutEngine
Public Shared ReadOnly Property Instance As GoldenRatioLayoutEngine
Get
If _instance Is Nothing Then
_instance = New GoldenRatioLayoutEngine()
End If
Return _instance
End Get
End Property

Private Sub New()
End Sub

‘ レイアウト処理の核心:親コンテナのサイズ変更や子コントロール増減時に必ず呼ばれる
Public Overrides Function Layout(container As Object, e As LayoutEventArgs) As Boolean
Dim parent As Control = TryCast(container, Control)
If parent Is Nothing OrElse parent.Controls.Count = 0 Then
Return MyBase.Layout(container, e)
End If

‘ 描画のチラつき(フリッカ)を防止するため、親のレイアウトロジックを一時停止
‘ (実運用ではControlStyles.OptimizedDoubleBufferと併用すること)
Dim clientWidth As Integer = parent.ClientSize.Width
Dim clientHeight As Integer = parent.ClientSize.Height
Dim padding As Padding = parent.Padding

‘ 有効描画領域の算出
Dim availableWidth As Integer = clientWidth – padding.Horizontal
Dim availableHeight As Integer = clientHeight – padding.Vertical

If availableWidth <= 0 OrElse availableHeight <= 0 Then Return True Dim visibleControls = parent.Controls.Cast(Of Control)().Where(Function(c) c.Visible).ToList() Dim count As Integer = visibleControls.Count If count = 0 Then Return True ' --- ここに独自の極限整列アルゴリズムを記述 --- ' 例:子コントロールをタイル状に黄金比(1.618...)を意識して自動分配配置する Dim columns As Integer = Math.Max(1, CInt(Math.Ceiling(Math.Sqrt(count 1.618)))) Dim rows As Integer = CInt(Math.Ceiling(count / CDbl(columns))) Dim cellWidth As Integer = availableWidth / columns Dim cellHeight As Integer = availableHeight / rows Dim index As Integer = 0 For r As Integer = 0 To rows - 1 For c As Integer = 0 To columns - 1 If index >= count Then Exit For

Dim ctrl As Control = visibleControls(index)

‘ 座標計算(余白を考慮し、コントロールをセル内に美しく収める)
Dim targetX As Integer = padding.Left + (c cellWidth) + 4
Dim targetY As Integer = padding.Top + (r cellHeight) + 4
Dim targetW As Integer = Math.Max(10, cellWidth – 8)
Dim targetH As Integer = Math.Max(10, cellHeight – 8)

‘ 無駄なプロパティ設定によるレイアウト連鎖を防ぐため、変化がある場合のみ代入
If ctrl.Left <> targetX orelse ctrl.Top <> targetY orelse ctrl.Width <> targetW orelse ctrl.Height <> targetH Then
ctrl.SetBounds(targetX, targetY, targetW, targetH, BoundsSpecified.All)
End If

index += 1
Next
Next

‘ 戻り値 True は「レイアウトがこのメソッド内で完全に処理された」ことを意味する
Return True
End Function
End Class

‘ =================================================================0
‘ 2. 専用レイアウトエンジンを使用するカスタムパネル
‘ =================================================================0
Public Class GoldenPanel
Inherits Panel

Public Sub New()
MyBase.New()
‘ ダブルバッファリングを強制有効化し、描画パフォーマンスを極限まで引き上げる
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
ControlStyles.UserPaint Or
ControlStyles.OptimizedDoubleBuffer Or
ControlStyles.ResizeRedraw, True)
Me.UpdateStyles()
End Sub

‘ 親コントロールがレイアウトエンジンを要求した際に、自作エンジンを返す
Public Overrides ReadOnly Property LayoutEngine As LayoutEngine
Get
Return GoldenRatioLayoutEngine.Instance
End Get
End Property
End Class

End Namespace

—

3. シニアエンジニアが押さえるべき「メモリ最適化とWindows APIの活用」

業務システムにおいて、数百万レコードを扱う画面や、数百個の動的ボタンを配置するパネルでは、GC(ガベージコレクション)の発生頻度がボトルネックになる。

① `SuspendLayout` と `ResumeLayout` の徹底

子コントロールを動的に一括追加する際、追加のたびに `LayoutEngine` が走り、CPUが破滅的な高負荷に陥る。必ず以下のようにブロック化せよ。

myGoldenPanel.SuspendLayout()
Try
For i As Integer = 1 To 500
Dim btn As New Button()
btn.Text = “Item ” & i.ToString()
myGoldenPanel.Controls.Add(btn)
Next
Finally
‘ 一括追加後に初めてレイアウトエンジンを1回だけ発火させる
myGoldenPanel.ResumeLayout(True)
End Try

② Windows APIによる描画完全凍結 (`WM_SETREDRAW`)

もし数千個単位のコントロールを同時に破棄・再構築するようなモンスター級の処理を行う場合、マネージド層の制御だけでは画面のチラつきや描画の遅延を防ぎきれない。その場合は、Win32 APIを直接叩いてウィンドウの描画を物理的にシャットダウンする。

Imports System.Runtime.InteropServices

Public Module Win32API

Public Function SendMessage(hWnd As IntPtr, wMsg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
End Function

Public Const WM_SETREDRAW As Integer = &HB

‘ 描画の凍結
Public Sub SuspendDrawing(ctrl As Control)
SendMessage(ctrl.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
End Sub

‘ 描画の再開と強制再描画
Public Sub ResumeDrawing(ctrl As Control)
SendMessage(ctrl.Handle, WM_SETREDRAW, New IntPtr(1), IntPtr.Zero)
ctrl.Refresh()
End Sub
End Module

これを活用することで、レガシーなWindows環境であっても、ブラウザアプリ並みにスムーズなレイアウト再構築を実現できる。

—

4. 総括

標準コントロールのラッパーをこねくり回す時代は終わった。
`LayoutEngine` の継承構造を理解し、WinFormsの描画ライフサイクルを手中に収めれば、VB.NETの表現力とパフォーマンスは極限まで高められる。

「古い技術だから仕方ない」と諦める前に、アーキテクチャの根幹にメスを入れろ。
真のプロフェッショナルであれば、使い物にならないフレームワークは自分の手で調教し、システムを意のままに操るべきだ。

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