WinFormsの標準ボタンを並べて、「業務アプリだから見た目は二の次でいい」と妥協していないか?
もし君が、ユーザーから「使いにくい」「古臭い」というフィードバックを受け、それをOSのせいにしているのなら、今すぐその考えを捨てろ。真のアーキテクトは、与えられたフレームワークの制約の中で、OSの描画エンジン(GDI+)を極限まで飼い慣らし、最高のユーザー体験を創出する。
今回は、Windows Formsにおいて最も頻繁に利用され、かつ最も「標準の限界」を感じやすい「ボタン」を、完全独自のモダンUIへと昇華させる極限の手法を伝授する。
—
1. なぜ標準コントロールのプロパティをいじるだけでは不十分なのか
多くの初級者は、ボタンの角を丸くするために `Region` プロパティをいじったり、背景色を変えて満足したりする。しかし、それでは「ジャギー(ギザギザ)」や「描画のちらつき(フリッカー)」、そして「状態変化のレスポンス不足」という壁にぶち当たる。
プロフェッショナルな設計において重要なのは、以下の3点だ。
1. リソースの厳格な管理: GDI+オブジェクト(BrushやPen)は、必ず `Using` ブロックで囲み、描画のたびに確実に破棄する。これを怠れば、数千回のクリック後にアプリはメモリリークで死ぬ。
2. 描画品質の最大化: `SmoothingMode.AntiAlias` を指定し、ピクセル単位での美しさを追求する。
3. カプセル化と保守性: 描画ロジックをコントロール内に封じ込め、呼び出し側はプロパティをセットするだけで済むように設計する。
—
2. プロダクション・グレードのカスタムボタン実装
それでは、実務でそのまま使える「モダン・グラデーション・ラウンドボタン」の実装コードを示す。このコードは、ただ描画するだけでなく、マウスの状態(通常・ホバー・クリック)を内部で管理し、動的に色を変化させる堅牢な作りになっている。
実装コード:ModernCustomButton.vb
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Windows.Forms
Imports System.ComponentModel
”’
”’
Public Class ModernCustomButton
Inherits Button
‘ — プロパティ変数 —
Private _borderRadius As Integer = 15
Private _gradientAngle As Single = 45.0F
Private _startColor As Color = Color.FromArgb(52, 152, 219) ‘ 美しいブルー
Private _endColor As Color = Color.FromArgb(41, 128, 185)
‘ — 状態管理 —
Private _isHovering As Boolean = False
Private _isPressed As Boolean = False
Public Property BorderRadius As Integer
Get
Return _borderRadius
End Get
Set(value As Integer)
_borderRadius = value
Me.Invalidate() ‘ 再描画を強制
End Set
End Property
‘ コンストラクタでちらつき防止(ダブルバッファリング)を有効化
Public Sub New()
Me.SetStyle(ControlStyles.UserPaint Or
ControlStyles.AllPaintingInWmPaint Or
ControlStyles.OptimizedDoubleBuffer Or
ControlStyles.ResizeRedraw Or
ControlStyles.SupportsTransparentBackColor, True)
Me.FlatStyle = FlatStyle.Flat
Me.FlatAppearance.BorderSize = 0
Me.BackColor = Color.Transparent
Me.Size = New Size(150, 40)
End Sub
‘ — マウスイベントによる状態遷移 —
Protected Overrides Sub OnMouseEnter(e As EventArgs)
_isHovering = True
Me.Invalidate()
MyBase.OnMouseEnter(e)
End Sub
Protected Overrides Sub OnMouseLeave(e As EventArgs)
_isHovering = False
_isPressed = False
Me.Invalidate()
MyBase.OnMouseLeave(e)
End Sub
Protected Overrides Sub OnMouseDown(mevent As MouseEventArgs)
_isPressed = True
Me.Invalidate()
MyBase.OnMouseDown(mevent)
End Sub
Protected Overrides Sub OnMouseUp(mevent As MouseEventArgs)
_isPressed = False
Me.Invalidate()
MyBase.OnMouseUp(mevent)
End Sub
‘ — コア描画ロジック —
Protected Overrides Sub OnPaint(pevent As PaintEventArgs)
Dim g = pevent.Graphics
‘ アンチエイリアスを有効化(これが無ければプロの仕事ではない)
g.SmoothingMode = SmoothingMode.AntiAlias
g.InterpolationMode = InterpolationMode.HighQualityBicubic
Dim rect = Me.ClientRectangle
‘ 描画領域を1ピクセル内側に絞る(境界の切れを防ぐ)
rect.Width -= 1
rect.Height -= 1
‘ 角丸パスの生成
Using path As GraphicsPath = GetRoundPath(rect, _borderRadius)
‘ ボタンの状態に応じた色の決定
Dim currentStartColor = _startColor
Dim currentEndColor = _endColor
If _isPressed Then
currentStartColor = ControlPaint.Dark(_startColor, 0.1)
currentEndColor = ControlPaint.Dark(_endColor, 0.1)
Else If _isHovering Then
currentStartColor = ControlPaint.Light(_startColor, 0.1)
currentEndColor = ControlPaint.Light(_endColor, 0.1)
End If
‘ グラデーションブラシの生成
Using brush As New LinearGradientBrush(rect, currentStartColor, currentEndColor, _gradientAngle)
g.FillPath(brush, path)
End Using
‘ テキストの描画
Dim flags = TextFormatFlags.HorizontalCenter Or TextFormatFlags.VerticalCenter Or TextFormatFlags.EndEllipsis
TextRenderer.DrawText(g, Me.Text, Me.Font, rect, Me.ForeColor, flags)
End Using
End Sub
”’
”’
Private Function GetRoundPath(rect As Rectangle, radius As Integer) As GraphicsPath
Dim path As New GraphicsPath()
Dim d = radius 2
If d <= 0 Then
path.AddRectangle(rect)
Return path
End If
path.StartFigure()
path.AddArc(rect.X, rect.Y, d, d, 180, 90) ' 左上
path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90) ' 右上
path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90) ' 右下
path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90) ' 左下
path.CloseFigure()
Return path
End Function
End Class
---
3. チーフアーキテクトによる設計の急所解説
① `Region` ではなく `GraphicsPath` で描く
多くの解説記事では `Me.Region = New Region(path)` と書いて角を切り抜く手法を紹介しているが、これはアンチエイリアスが効かないため、エッジがガタガタになる。
本コードでは `Region` はそのまま(四角いまま)で、内部に `FillPath` で角丸を描画している。背景色を `Transparent` に設定し、`DoubleBuffering` を効かせることで、滑らかで美しいエッジを実現している。
② GDI+ オブジェクトの「不文律」
`LinearGradientBrush` や `GraphicsPath` は、Win32 GDIリソースを消費する。描画のたびにこれらを生成して `Dispose` しないコードは、エンタープライズ開発では「一発アウト」だ。必ず `Using` を使え。
③ 業務ロジックとの連携における注意点
このボタンを、重いファイル処理やデータベース(DB)アクセスを伴うツールに配置する場合、「ボタンの連打防止」をUI層で保証しなければならない。
- 状態のロック: DB処理中は `Enabled = False` にするだけでなく、今回実装した `_isPressed` などの内部フラグを強制的にリセットし、見た目でも「処理中」であることがわかるようにカラープロパティを動的に変更する設計にせよ。
- 非同期実行: `Click` イベント内で `Await` を使い、UIスレッドをブロックするな。スレッドが固まれば、せっかく作り込んだ `OnPaint` の再描画すら走らなくなる。
—
4. 結論:細部に宿る品質がシステムの信頼性を決める
「たかがボタン」と侮る者は、いずれ大規模なシステム設計においても「たかが例外処理」「たかがログ出力」と手を抜くようになる。
今回紹介したカスタムペイントの手法は、WinFormsという枯れた技術の上で、モダンな操作感を提供するための第一歩だ。このコードをベースに、アニメーション(Timerを使った色の遷移)や、アイコンの配置ロジックを追加してみるがいい。
君が書く一行の描画コードが、業務で疲弊したユーザーの指先に心地よいフィードバックを与える。それこそが、我々エンジニアが提供できる「最高のUI」なのだ。
さあ、今すぐ標準ボタンを捨て、君だけの至高のコントロールをビルドしろ。
