月間カレンダーの「限界」を突破する:MonthCalendarカスタム描画の深淵
多くのシニアエンジニアが一度は直面する壁がある。「標準の `MonthCalendar` コントロールに、社内カレンダーや稼働状況を反映させたい」という要求だ。
マイクロソフトの提供する `MonthCalendar` は、Windowsコモンコントロール(comctl32.dll)のラッパーに過ぎない。VB.NETでプロパティを弄る程度では、到底、ビジネスロジックと連動した「直感的なUI」など実現できない。
今日は、Win32 APIを直接叩き、GDI+の描画パイプラインに割り込むことで、このレガシーなコントロールを「最強の業務可視化エンジン」へと昇華させる極限の手法を伝授する。
—
1. なぜ標準機能では不十分なのか?
`MonthCalendar` には `AddBoldedDate` といった限定的なハイライト機能しかない。しかし、現場が求めるのは「繁忙期は赤」「休業日はグレー」「特定のイベントはホバー時にツールチップで詳細表示」といった多層的な表現だ。
これを実現するには、`OnPaint` イベントのオーバーライドや、Windowsメッセージ `WM_PAINT` へのフックが不可欠である。
2. 実装の要諦:OwnerDrawとメッセージの掌握
`MonthCalendar` はオーナー描画を直接サポートしていない。したがって、我々は以下のステップを踏む必要がある。
1. ネイティブウィンドウプロシージャのサブクラス化:メッセージループを横取りする。
2. GDI+による描画オーバーレイ:コントロールのDC(デバイスコンテキスト)を直接取得し、描画を上書きする。
3. メモリ管理の徹底:`Graphics` オブジェクトや `Pen`/`Brush` のリークは、長期間起動する業務アプリにおいて致命的なメモリ枯渇を招く。`IDisposable` を正しく実装せよ。
3. 実践コード:カレンダーの特定日付をハイライトする
以下のコードは、特定の配列にある日付を独自の背景色で塗りつぶすためのカスタムクラスの核心部分である。
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
”’
”’
Public Class AdvancedMonthCalendar
Inherits MonthCalendar
‘ 描画対象の日付リスト(外部データソースと連携)
Public Property HighlightDates As New List(Of Date)
Protected Overrides Sub OnPaint(e As PaintEventArgs)
‘ 基本描画を維持
MyBase.OnPaint(e)
‘ GDI+リソースの保護(usingステートメントによる即時解放)
Using brush As New SolidBrush(Color.FromArgb(128, Color.LightCoral))
For Each d As Date In HighlightDates
If d.Month = Me.SelectionRange.Start.Month Then
‘ ここでHitTest等を用い、対象日付の矩形を取得して描画する
‘ ※実際にはGetDayRectをAPI経由で呼び出すのが最も正確
Dim rect As Rectangle = GetDayRect(d)
e.Graphics.FillRectangle(brush, rect)
End If
Next
End Using
End Sub
‘ Win32 APIを使用して正確な矩形を取得する
‘ MCM_GETCALENDARRECT等のメッセージをSendMessageで送る必要がある
Private Function GetDayRect(targetDate As Date) As Rectangle
‘ メッセージ送信による矩形計算のロジックをここに記述
‘ 省略:MCM_GETCALENDARRECTをハンドルへ送信し、SYSTEMTIME構造体を変換する
Return New Rectangle(0, 0, 0, 0)
End Function
End Class
4. シニアエンジニアが守るべき3つの鉄則
① GDIリソースの徹底管理
`Graphics.FromHwnd` を使用する場合、必ず `Dispose` を呼ぶこと。VB.NETのガベージコレクタは「いつか」片付けてくれるが、業務アプリのUIスレッドでリソースが枯渇すれば、カレンダーは真っ白になる。`Using` ブロックは必須だ。
② Windows API呼び出しの最適化
`SendMessage` を多用する場合、P/Invokeのシグネチャを `Shared` で定義し、一度読み込んだモジュールをキャッシュせよ。頻繁に呼ばれる `MCM_` 系メッセージは、ボトルネックになりやすい。
③ システム間連携の疎結合
カレンダーのデータソースをハードコーディングしてはならない。`DataTable` や `IEnumerable(Of T)` を受け取り、バックグラウンドスレッドで非同期にデータをキャッシュする設計にせよ。UIの描画中にデータベースのI/Oを待機させるような設計は、現代の業務アプリでは「罪」である。
—
最後に:レガシーのその先へ
Visual Basicは、その歴史ゆえに「古い」と揶揄されることがある。しかし、Windowsの根幹を知り、APIの挙動を掌握した我々にとって、それは強力な武器に他ならない。
`MonthCalendar` をただの日付選択ツールで終わらせるか、あるいは業務のボトルネックを一目で看破できる「経営ダッシュボードの入り口」にするか。それは、コードを叩く諸君の設計思想ひとつにかかっている。
極限のパフォーマンスと、ユーザーが真に求めるUXを両立させること。それこそが、伝説と呼ばれるための唯一の道である。
