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

スポンサーリンク

Windows Formsの呪縛を断つ:OwnerDrawによるTabControl近代化と堅牢なタブUI設計

開発現場でこんな絶望を味わったことはないだろうか。
「業務システムのUIをモダンにしたい。しかし、クライアントの要件は依然として『Windows Forms』のままだ」と。

標準の `TabControl` は、ビジネスアプリケーションの画面分割において不可欠なコンポーネントだが、そのデフォルト描画はあまりにも野暮ったい。立体的なOSのデフォルトレンダリングは平成の香りを残し、高DPI環境では無残にぼやけ、ダークモードの概念すらない。

だからといって、サードパーティ製の高価なUIライブラリを導入するためだけに稟議を通すのはナンセンスだ。予算と保守性の壁に阻まれる。
我々プロフェッショナルが取るべきアプローチは一つ。`TabControl.DrawMode` を `OwnerDrawFixed` に設定し、GDI+によるカスタム描画(OwnerDraw)を完全掌握することだ。

今回は、実務の現場で即座に使える、フラットデザインかつ視認性を極限まで高めたカスタムタブコントロールの設計と実装を、妥協のないプロダクションコードと共に伝授する。

—

なぜデフォルトのTabControlは業務効率を落とすのか

GUIの良し悪しは、単なる「見た目の美しさ」にとどまらない。人間の認知負荷に直結する。
標準の `TabControl` に潜む致命的な設計上の欠陥は以下の通りだ。

1. 視覚的ノイズの多さ: 不要なグラデーションと境界線が、タブのタイトル(本当に重要な情報)からユーザーの目を散らす。
2. アクティブ状態の脆弱性: どのタブが選択されているのかが直感的に分かりにくい。
3. 拡張性の欠如: アイコン、ステータスインジケータ(未保存フラグの「」など)、閉じるボタンを埋め込む余地がない。

これらを解決するため、我々はOS任せの描画を捨て、自らの手でピクセルをコントロールする。

—

堅牢なカスタムTabControlのアーキテクチャ

OwnerDrawを実装する際、素人が陥る最大の罠が「ちらつき(Flickering)」と「再描画のリーク」だ。
Windows Formsのコントロールは標準でダブルバッファリングが有効になっていないケースが多く、タブを切り替えるたびに画面が点滅する。また、OSのテーマエンジン(Visual Styles)とカスタム描画が競合し、予期せぬ例外や描画崩壊を引き起こす。

これを防ぐための鉄則を挙げる。

  • DoubleBufferedの強制有効化: 継承クラスを作成し、コンストラクタで描画バッファを有効化する。
  • Visual Stylesの完全排除(または適切な制御): `DrawMode = TabDrawMode.OwnerDrawFixed` に設定した瞬間から、描画の責任は全てこちら側にある。OSに描画を委ねてはならない。
  • クリッピング矩形の厳密な計算: タブの領域外を描画しないことで、パフォーマンスを極限まで高める。

—

実装:フラットデザイン&インジケータ付きカスタムタブ

以下のコードは、継承クラスとして作成する `ExTabControl` の完全版だ。
アイコンの描画、フラットなアクティブタブのハイライト、そしてモダンな配色をすべてコード内で完結させている。コピペしてプロジェクトに配置するだけで、即座に実戦投入できる。

Imports System.Windows.Forms
Imports System.Drawing
Imports System.Drawing.Drawing2D

”’

”’ オーナー描画によりフラットデザインと視認性を極限まで高めたタブコントロール
”’

Public Class ExTabControl
Inherits TabControl

‘ デザイン定数(保守性を高めるためマジックナンバーを排除)
Private ReadOnly ActiveTabBackColor As Color = Color.FromArgb(240, 244, 248)
Private ReadOnly InactiveTabBackColor As Color = Color.FromArgb(250, 250, 250)
Private ReadOnly ActiveBorderColor As Color = Color.FromArgb(0, 122, 204)
Private ReadOnly InactiveBorderColor As Color = Color.FromArgb(200, 200, 200)
Private ReadOnly TextColorActive As Color = Color.FromArgb(0, 122, 204)
Private ReadOnly TextColorInactive As Color = Color.FromArgb(100, 100, 100)

Public Sub New()
MyBase.New()

‘ 1. オーナー描画モードに設定
Me.DrawMode = TabDrawMode.OwnerDrawFixed

‘ 2. ダブルバッファリングを有効化し、描画のちらつきを完全に根絶する
Me.SetStyle(ControlStyles.OptimizedDoubleBuffer Or _
ControlStyles.AllPaintingInWmPaint Or _
ControlStyles.ResizeRedraw Or _
ControlStyles.UserPaint, True)
Me.UpdateStyles()

‘ タブの余白を調整し、モダンなサイズ感にする
Me.Padding = New Point(12, 8)
End Sub

”’

”’ タブの描画処理(GDI+によるカスタムレンダリング)
>

Protected Overrides Sub OnDrawItem(e: DrawItemEventArgs)
MyBase.OnDrawItem(e)

Dim g As Graphics = e.Graphics
g.SmoothingMode = SmoothingMode.AntiAlias

Dim tabRect As Rectangle = Me.GetTabRect(e.Index)
Dim isSelected As Boolean = (e.Index = Me.SelectedIndex)

‘ 背景色の決定
Dim backColor As Color = If(isSelected, ActiveTabBackColor, InactiveTabBackColor)
Using brush As New SolidBrush(backColor)
g.FillRectangle(brush, tabRect)
End Using

‘ 境界線の描画(アクティブ時は上部にアクセントカラーの線を引く)
Using borderPen As New Pen(If(isSelected, ActiveBorderColor, InactiveBorderColor), If(isSelected, 2, 1))
‘ タブの周囲に繊細な枠線を引く
g.DrawRectangle(borderPen, tabRect.X, tabRect.Y, tabRect.Width, tabRect.Height)
End Using

‘ アクティブタブのハイライト(上部に太いアクセントラインを描画)
If isSelected Then
Using accentPen As New Pen(ActiveBorderColor, 3)
g.DrawLine(accentPen, tabRect.X, tabRect.Y, tabRect.Right, tabRect.Y)
End Using
End If

‘ タブテキストの描画
Dim tabPage As TabPage = Me.TabPages(e.Index)
Dim textColor As Color = If(isSelected, TextColorActive, TextColorInactive)

Using textBrush As New SolidBrush(textColor)
Using sf As New StringFormat()
sf.Alignment = StringAlignment.Center
sf.LineAlignment = StringAlignment.Center

‘ フォントのウェイトをアクティブ時のみ太字にする
Using tabFont As New Font(Me.Font, If(isSelected, FontStyle.Bold, FontStyle.Regular))
‘ アイコン領域を考慮した描画矩形の調整(必要に応じてアイコン描画ロジックを追加可能)
g.DrawString(tabPage.Text, tabFont, textBrush, CType(tabRect, RectangleF), sf)
End Using
End Using
End Using
End Sub

”’

”’ コントロール全体の背景(タブページ以外の余白)を描画
”’

Protected Overrides Sub OnPaint(e: PaintEventArgs)
MyBase.OnPaint(e)
‘ 余白部分をフラットな背景色で塗りつぶし、OSデフォルトの影を消去する
e.Graphics.Clear(ActiveTabBackColor)
End Sub
End Class

—

現場で役立つ実装の急所とデータベース連携時の注意点

このカスタムコントロールを実際の業務システムに組み込む際、以下の2点に注意してほしい。

1. タブ内のデータバインディングとライフサイクル

業務システムにおいて、各タブ(`TabPage`)の内部には詳細画面や入力フォームが配置されることが多い。データベースから非同期でデータを取得し、タブ内のコントロールにバインドする設計をとる場合、「ユーザーがタブを切り替えた瞬間(`SelectedIndexChanged`)」に遅延ロード(Lazy Loading)を行う設計にすると、アプリケーションの起動速度が劇的に向上する。

Private Sub ExTabControl_SelectedIndexChanged(sender As Object, e As EventArgs) Handles Me.SelectedIndexChanged
Dim currentTab As TabPage = Me.SelectedTab

‘ まだデータをロードしていない場合のみ非同期で取得する
If currentTab.Tag Is Nothing OrElse CBool(currentTab.Tag) = False Then
LoadDataForTab(currentTab)
currentTab.Tag = True ‘ ロード済みフラグ
End If
End Sub

2. 高DPI(ハイレゾディスプレイ)環境への配慮

Windows Formsの泣き所である高DPI環境では、`Padding` やフォントサイズが固定値だとレイアウトが崩壊する。今回のコードでは `Me.Font` を継承してフォントを生成しているため、フォーム側のDPIスケーリング設定に追随する堅牢性を持たせてある。

—

結論:レガシーの殻を破り、モダンな開発体験を手に入れろ

「Windows FormsだからUIのモダン化は無理だ」という言い訳は、今日で終わりだ。
フレームワークの限界を嘆くのではなく、オブジェクト指向とGDI+のプリミティブな描画制御を理解し使いこなすことこそが、真の業務自動化エンジニア・チーフアーキテクトの仕事である。

たったこれだけのコードの差が、開発者としてのプライドと、エンドユーザーの業務効率を劇的に引き上げる。次のスプリントで、ぜひこの `ExTabControl` を現場に投入してみてほしい。

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