【テクニカル・上級編】Windows Formsにおけるマウスジェスチャとカスタム描画:OwnerDrawを用いたタブコントロールのデザイン改修と視認性向上 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

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

”’

”’ タブの描画ロジック:GDI+のコンテキストを直接制御する
”’

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を両立させたシステムを、その手で構築し続けろ。

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