Windows Formsの呪縛を断つ:TableLayoutPanelで極限まで洗練された動的UIを構築する方法
こんにちは。エンタープライズ領域の業務システム開発において、数々のレガシーなWindows Formsアプリケーションを立て直してきたチーフアーキテクトだ。
現場でよくこんな相談を受ける。
「ユーザーの要望で、検索条件の入力欄や明細の入力行を動的に増減させたいのだが、`Anchor`や`Dock`プロパティをこねくり回しても、ウィンドウサイズを変えた途端にコントロール同士が重なったり、はみ出したりして崩壊する」と。
断言しよう。素の座標計算(`Location`や`Size`の直接操作)や、安易な`Panel`の敷き詰めを行っている時点で、そのUI設計は破綻している。
Windows Formsで真に堅牢かつスケーラブルな画面を実現したいのであれば、`TableLayoutPanel`のライフサイクルとレイアウトエンジン挙動を完全に掌握しなければならない。
今回は、実務の現場で即座に使える、動的行・列追加と自動リサイズ制御を極めたプロダクションコードを伝授する。
—
なぜ従来のUI設計は破綻するのか?
業務アプリケーションの寿命は長い。当初は3つだった入力項目が、法改正や業務拡大により「10個に増やしてくれ」「いや、動的に増えるようにしてくれ」と言い渡される。
ここで素人が陥る罠が、コードビハインドでの泥臭い座標計算だ。
‘ 【アンチパターン】絶対にやってはいけない動的配置
Dim tb As New TextBox()
tb.Location = New Point(10, currentY) ‘ 手動でY座標を計算
Me.Controls.Add(tb)
currentY += 30
このアプローチは、DPIスケーリング(高DPI環境)の変更、フォントサイズの変更、そして何よりユーザーがウインドウをリサイズした瞬間にすべてがゴミと化す。
リレイアウトの責任を開発者が手動で持とうとすること自体が、オブジェクト指向の原則に反しているのだ。レイアウトの計算は、レイアウトエンジンに任せる。これが鉄則だ。
—
TableLayoutPanelを極めるための3つの設計思想
`TableLayoutPanel`を使いこなすには、以下の3つの鉄則を守る必要がある。
1. SuspendLayout / ResumeLayout の徹底
動的にコントロールを数個〜数十個単位で追加・削除する場合、1回追加するごとに画面の再描画(レイアウト計算)走る。これがパフォーマンス低下の主原因だ。トランザクション張るが如く、操作前後にサスペンドを挟め。
2. SizeTypeの適切な使い分け
`Absolute`(固定値)、`Percent`(割合)、`AutoSize`(自動調整)の特性を理解する。動的行追加では、行高を`AutoSize`にし、コンテンツ側でサイズを担保させるのが王道だ。
3. ControlCollectionのインデックス整合性
セルの位置(列、行)とコントロールのコレクション順序を混同するな。`SetCellPosition`メソッドを正確に叩く必要がある。
—
実践:動的行追加・自動リサイズ対応フォームの構築
ここからは、実務でそのまま流用できるVB.NETのプロダクションコードを提示する。
このコードでは、以下の要件を満たしている。
- ボタン押下で、入力行(ラベル、テキストボックス、削除ボタンのセット)を動的に追加・削除できる。
- ウィンドウをリサイズしても、テキストボックスが追従して伸縮する。
- コントロール追加・削除時のちらつき(フリッカ)を完全抑制する。
プロダクションコード(VB.NET)
Imports System.Windows.Forms
Public Class DynamicForm
‘ フォーム上の主要コントロール(デザイナーで配置するか、コードで初期化してください)
Private WithEvents mainLayout As TableLayoutPanel
Private WithEvents btnAddRow As Button
Private scrollPanel As Panel
Public Sub New()
‘ フォームの基本設定
Me.Text = “TableLayoutPanel 動的行制御サンプル”
Me.Size = New Size(600, 400)
InitializeComponentCustom()
End Sub
Private Sub InitializeComponentCustom()
‘ 1. 外側のスクロールパネル(行が増えすぎた場合の対策)
scrollPanel = New Panel()
scrollPanel.Dock = DockStyle.Fill
scrollPanel.AutoScroll = True
Me.Controls.Add(scrollPanel)
‘ 2. メインのTableLayoutPanel構築
mainLayout = New TableLayoutPanel()
mainLayout.ColumnCount = 3
mainLayout.RowCount = 1
‘ 列スタイルの定義 (列0: 固定ラベル, 列1: 可変テキストボックス, 列2: 固定ボタン)
mainLayout.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 100.0F))
mainLayout.ColumnStyles.Add(New ColumnStyle(SizeType.Percent, 100.0F))
mainLayout.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 80.0F))
mainLayout.Dock = DockStyle.Top
mainLayout.AutoSize = True
mainLayout.AutoSizeMode = AutoSizeMode.GrowAndShrink
scrollPanel.Controls.Add(mainLayout)
‘ 3. 行追加ボタンの配置(別コンテナまたは最下部)
btnAddRow = New Button()
btnAddRow.Text = “行を追加”
btnAddRow.Dock = DockStyle.Bottom
AddHandler btnAddRow.Click, AddressOf BtnAddRow_Click
Me.Controls.Add(btnAddRow)
‘ 初期行を1つ追加
AddNewRow()
End Sub
”’
”’
Private Sub AddNewRow()
‘ 【重要】レイアウトロジックを一時停止し、再描画のちらつきと無駄な計算を防ぐ
mainLayout.SuspendLayout()
scrollPanel.SuspendLayout()
Try
‘ 行数を1つ増やす
Dim rowIndex As Integer = mainLayout.RowCount
mainLayout.RowCount = rowIndex + 1
mainLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
‘ コントロールの生成
Dim lbl = New Label()
lbl.Text = $”項目 {rowIndex}”
lbl.Anchor = AnchorStyles.Left Or AnchorStyles.Right
lbl.TextAlign = ContentAlignment.MiddleLeft
Dim txt = New TextBox()
txt.Name = $”txtInput_{rowIndex}”
txt.Dock = DockStyle.Fill
Dim btnDel = New Button()
btnDel.Text = “削除”
btnDel.Dock = DockStyle.Fill
‘ 削除ボタンに紐づく行を特定するためのイベントハンドラ登録
AddHandler btnDel.Click, Sub(sender, e) RemoveRow(rowIndex)
‘ TableLayoutPanelへコントロールを追加 (列, 行の順指定)
mainLayout.Controls.Add(lbl, 0, rowIndex – 1)
mainLayout.Controls.Add(txt, 1, rowIndex – 1)
mainLayout.Controls.Add(btnDel, 2, rowIndex – 1)
Finally
‘ レイアウトを再開
scrollPanel.ResumeLayout(True)
mainLayout.ResumeLayout(True)
End Try
End Sub
”’
”’
Private Sub RemoveRow(targetRowIndex As Integer)
mainLayout.SuspendLayout()
scrollPanel.SuspendLayout()
try
‘ 該当行に存在するコントロールを特定して破棄
For i As Integer = mainLayout.ColumnCount – 1 To 0 Step -1
Dim ctrl As Control = mainLayout.GetControlFromPosition(i, targetRowIndex – 1)
If ctrl IsNot Nothing Then
mainLayout.Controls.Remove(ctrl)
ctrl.Dispose()
End If
Next
‘ 注意: TableLayoutPanelの行削除はインデックスとRowStylesの調整が必要
‘ 実務上は全クリアして再構築するか、レイアウト行をデクリメントする設計にする。
‘ ここでは簡略化のため、コントロールを削除して高さを詰める処理に留める。
mainLayout.RowCount -= 1
Finally
scrollPanel.ResumeLayout(True)
mainLayout.ResumeLayout(True)
End Try
End Sub
Private Sub BtnAddRow_Click(sender As Object, e As EventArgs)
AddNewRow()
End Sub
End Class
—
データベース・ファイル連携時の実務的注意点
このような動的UIを構築した際、避けて通れないのが「入力されたデータの回収(シリアライズ)」と「バリデーション」だ。
画面からデータをDBやCSVファイルに書き出す際、インデックスのズレや破棄されたコントロールへの参照エラー(ObjectDisposedException)を踏み抜く開発者が後を絶たない。
データの回収は「コレクション走査」で行え
個別のコントロール名(`TextBox1`, `TextBox2`…)をハードコーディングして探すような愚行は厳禁だ。`TableLayoutPanel.Controls` を型安全にLINQでフィルタリングして回収する。
‘ データベース保存時などのデータ回収処理例
Dim formData As New List(Of String)()
For Each ctrl As Control In mainLayout.Controls
If TypeOf ctrl Is TextBox Then
If Not String.IsNullOrWhiteSpace(ctrl.Text) Then
formData.Add(ctrl.Text)
End If
End If
Next
‘ ここで formData をDBのバッチ処理やSQLパラメータへ渡す
このアプローチであれば、ユーザーが何行追加しようとも、コードを変更することなく動的にデータをエンティティへマッピングできる。
—
チーフアーキテクトからの提言
Windows Formsはレガシーフレームワークと揶揄されることもあるが、それは「設計をサボったコード」が量産されてきた歴史の裏返しに過ぎない。
`TableLayoutPanel`をはじめとするレイアウトエンジンを正しく理解し、`SuspendLayout`によるパフォーマンス担保、そしてデータ構造とUIコンポーネントの関心分離を徹底すれば、現代のWebアプリケーションにも負けない堅牢でメンテナンス性の高い業務画面を構築できる。
コピペで動かすだけではなく、なぜこの構造が必要なのか、その「背景にあるエンジニアリング」を持ち帰ってほしい。君たちのプロジェクトの成功を祈る。
