Windows Formsの呪縛を断つ:OwnerDrawとAPI制覇によるTabControl近代化計画
レガシーシステムの延命とモダナイゼーションの狭間で、我々Windows Formsエンジニアが最も絶望するのは、デフォルトの `TabControl` が放つ圧倒的な「平成初期の空気感」だ。灰色の平坦なタブ、OSのテーマエンジンに依存した無機質な描画、そして高DPI環境で無残に崩れるスケーリング。
「VB.NETだからUIのモダン化は諦めるしかない」などと言い訳をするシニアエンジニアに、私は問いたい。
本当にGDI+の描画パイプラインとWindowsメッセージを掌握しているか?と。
今回は、`DrawMode.OwnerDrawFixed` を用いたタブの完全カスタム描画と、GDI+リソースの枯渇(メモリリーク)を完全に防ぐオブジェクトライフサイクルの管理、そしてマウスジェスチャ的な視覚フィードバックを統合した、極限のUI改修ノウハウを公開する。
—
1. オーナー描画(OwnerDraw)の罠とGDI+のメモリ最適化
Windows Formsで `TabControl` の見た目を変えようと `DrawItem` イベントを安易にフックする者がいるが、そこにこそGDI+の罠が潜んでいる。`Brush` や `Font`、`Pen` をイベントハンドラ内で都度 `New` して解放(`Dispose`)し忘れると、GC(ガベージコレクション)が追いつかずにあっという間にGDIハンドルが枯渇し、アプリケーションはクラッシュする。
極限のパフォーマンスが求められる環境では、描画に使用するリソースは静的にキャッシュし、フォームのライフサイクルと同期して明示的に破棄しなければならない。
以下のコードは、ちらつき(Flickering)を完全に排除する `DoubleBuffered` なカスタムTabControlの骨組みだ。
.Net
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
”’
”’
Public Class ModernTabControl
Inherits TabControl
‘ GDI+リソースのキャッシュ(毎回のインスタンス化によるメモリ肥大化を防ぐ)
Private ReadOnly _bgBrush As New SolidBrush(Color.FromArgb(30, 30, 30)) ‘ ダークモード風背景
Private ReadOnly _activeTabBrush As New SolidBrush(Color.FromArgb(0, 122, 204)) ‘ アクティブタブ
Private ReadOnly _inactiveTabBrush As New SolidBrush(Color.FromArgb(45, 45, 48)) ‘ 非アクティブタブ
Private ReadOnly _textActiveBrush As New SolidBrush(Color.White)
Private ReadOnly _textInactiveBrush As New SolidBrush(Color.FromArgb(150, 150, 150))
Private ReadOnly _accentPen As New Pen(Color.FromArgb(0, 122, 204), 2)
Public Sub New()
MyBase.New()
‘ オーナー描画を有効化
Me.DrawMode = TabDrawMode.OwnerDrawFixed
Me.SizeMode = TabSizeMode.Fixed
Me.ItemSize = New Size(120, 35) ‘ タブのサイズを近代的な比率に固定
‘ 描画時のちらつき(Flickering)を殺すためのスタイル設定
Me.SetStyle(ControlStyles.UserPaint Or _
ControlStyles.AllPaintingInWmPaint Or _
ControlStyles.OptimizedDoubleBuffer Or _
ControlStyles.ResizeRedraw, True)
Me.UpdateStyles()
End Sub
”’
”’
Protected Overrides Sub OnDrawItem(e As DrawItemEventArgs)
MyBase.OnDrawItem(e)
Dim g As Graphics = e.Graphics
g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
Dim isSelected As Boolean = (e.State = DrawItemState.Selected)
Dim tabRect As Rectangle = Me.GetTabRect(e.Index)
‘ タブ背景の描画
Dim currentBrush As SolidBrush = If(isSelected, _activeTabBrush, _inactiveTabBrush)
g.FillRectangle(currentBrush, tabRect)
‘ アクティブタブに対するアクセントライン(フラットデザイン特有のインジケータ)
If isSelected Then
Dim lineRect As New Rectangle(tabRect.X, tabRect.Y, tabRect.Width, 3)
g.FillRectangle(_accentPen.Brush, lineRect)
End If
‘ テキストの描画(GDI+のStringFormatで中央配置)
Dim textBrush As SolidBrush = If(isSelected, _textActiveBrush, _textInactiveBrush)
Dim sf As New StringFormat() With {
.Alignment = StringAlignment.Center,
.LineAlignment = StringAlignment.Center
}
‘ パディングを考慮した描画領域の調整
Dim textRect As Rectangle = tabRect
textRect.Y += 2
g.DrawString(Me.TabPages(e.Index).Text, Me.Font, textBrush, textRect, sf)
End Sub
”’
”’
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
_bgBrush.Dispose()
_activeTabBrush.Dispose()
_inactiveTabBrush.Dispose()
_textActiveBrush.Dispose()
_textInactiveBrush.Dispose()
_accentPen.Dispose()
End If
MyBase.Dispose(disposing)
End Sub
End Class
—
2. Windows API連携:WM_PAINTメッセージの乗っ取りとちらつきの根絶
`TabControl` は標準のままだと、タブ領域以外の余白(TabStripの背景部分)がOSのデフォルトテーマで塗りつぶされ、カスタマイズしたタブとの間に強烈な違和感を生む。これを根本から解決するには、Windows メッセージ (`WndProc`) をオーバーライドし、OSの描画命令をバイパスする必要がある。
.Net
Const WM_PAINT As Integer = &HF
Private Shared Function GetWindowDC(hWnd As IntPtr) As IntPtr
End Function
Private Shared Function ReleaseDC(hWnd As IntPtr, hDC As IntPtr) As Integer
End Function
”’
”’
Protected Overrides Sub WndProc(ByRef m As Message)
MyBase.WndProc(m)
If m.Msg = WM_PAINT Then
Dim hDC As IntPtr = GetWindowDC(Me.Handle)
Try
Using g As Graphics = Graphics.FromHdc(hDC)
‘ タブコントロール全体の背景色をフラットなダークグレーで統一
g.Clear(Color.FromArgb(30, 30, 30))
End Using
Finally
ReleaseDC(Me.Handle, hDC)
End Try
End If
End Sub
(注: `DllForm` は `DllImport` のタイポだが、VB.NETのシグネチャとしては正しく `DllImport(“user32.dll”)` を使用すること)
—
3. 視認性向上:マウスオーバーとアイコン合成の高度な実装
フラットデザインにおけるUIの直感性は、「ホバー時のフィードバック」と「視覚的アンカー(アイコン)」で決まる。タブの上にマウスカーソルが乗った際の色変化(HotTracking)をオーナー描画内で動的に制御するためには、マウス座標と `HitTest` を組み合わせる必要がある。
.Net
Private _hoverIndex As Integer = -1
Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
MyBase.OnMouseMove(e)
Dim hitTestInfo As Point = e.Location
Dim foundIndex As Integer = -1
For i As Integer = 0 To Me.TabCount – 1
If Me.GetTabRect(i).Contains(hitTestInfo) Then
foundIndex = i
Exit For
End If
Next
If _hoverIndex <> foundIndex Then
_hoverIndex = foundIndex
Me.Invalidate() ‘ 再描画を要求
End If
End Sub
Protected Overrides Sub OnMouseLeave(e As EventArgs)
MyBase.OnMouseLeave(e)
_hoverIndex = -1
Me.Invalidate()
End Sub
`OnDrawItem` 内で `_hoverIndex` と `e.Index` が一致する場合に背景色を微かに明るくする(例: `Color.FromArgb(60, 60, 60)`)ことで、Webアプリケーション並みに滑らかなインタラクションをWindows Forms上で再現できる。
—
チーフアーキテクトからの提言
レガシーなWindows Formsアプリケーションにおいて、UIの近代化は単なる「見た目の化粧直し」ではない。それは、肥大化したコードベースの中で失われかけていた、パフォーマンスとメモリ管理に対するエンジニアの矜持を取り戻す作業に他ならない。
GDI+のライフサイクルを完全に掌握し、不要なOS描画をフックして制御下置くこと。このアプローチを習得した者にとって、もはやVB.NETに「古い」という言い訳は通用しない。圧倒的なパフォーマンスとモダンなUIを両立させたシステムを、その手で構築し続けろ。
