【テクニカル・上級編】フォームのOnResizeイベントでコントロール配置を動的に最適化するレスポンシブUIの基礎 – Access VBA解析バイブル

スポンサーリンク

1. プロローグ:Access UIにおける「動的レイアウト」の不都合な真実

Microsoft Accessが提供する「フォーム」は、RAD(Rapid Application Development)ツールの先駆者として極めて優秀なビジュアルデザイナーを備えています。しかし、現代の多様なディスプレイ解像度(Full HDから4K、さらにはマルチモニター環境)において、Accessのデフォルトの描画挙動は牙を剥きます。

最大化されたウィンドウや、ユーザーが任意に変更したウィンドウサイズに対し、コントロールが左上に固定されたまま右下に広大な「空白の墓場」が広がる画面。これを解決しようと、安易にフォームの `OnResize` イベントに `Width` や `Height` の再計算ロジックを記述すると、今度は激しい画面のちらつき(フリッカー)や、最悪の場合は描画スタックのオーバーフローによるクラッシュに直面することになります。

Accessの描画エンジンは、Windowsのメッセージループと密結合しており、WebフロントエンドやWPFのような洗練されたレイアウトエンジン(GridやFlexbox)を持ちません。AccessでプロフェッショナルなレスポンシブUIを実現するには、「Windows APIによる描画制御」「Twip単位の正確な空間計算」、そして「イベントループの制御」を完全に掌握する必要があります。

本稿では、レガシーなAccessフォームをモダンで滑らかなレスポンシブUIへと昇華させる、極限の動的レイアウトエンジンを設計・実装します。

2. アーキテクチャ設計:レスポンシブ・エンジンの三原則

堅牢な動的配置処理を構築するためには、以下の3つの技術的原則を遵守しなければなりません。

原則1:描画メッセージ(WM_SETREDRAW)の完全な制御

コントロールの位置やサイズをVBAから1つずつ変更するたびに、AccessはOSに対して再描画要求(`WM_PAINT`)を発行します。コントロールが20個あれば、1回のサイズ変更で20回以上の再描画が走り、これがフリッカーの原因となります。
Windows APIを用いて、配置変更の開始前にフォームの画面更新を「凍結」し、すべての配置変更が完了した瞬間に「解除」して一括描画させます。

原則2:描画境界(Min/Max)の絶対防御

ユーザーがウィンドウを極端に縮小した場合、計算上のコントロールの幅(Width)や高さ(Height)が「ゼロ以下」または「マイナス」になる瞬間が発生します。Access VBAにおいて、コントロールのサイズに負の値を代入することは許されず、即座に 「実行時エラー 2100:領域が足りないか、無効な設定です」 を誘発します。
レイアウトエンジンには、フォームがどれだけ縮小されても破綻しない「最小許容サイズ(MinBounds)」のガードロジックが必須です。

原則3:Moveメソッドの一括適用

プロパティの変更は最小限に抑えるべきです。`Control.Left = X` と `Control.Width = W` を個別に設定すると、内部的には2回のプロパティセットが発生します。位置とサイズを同時に変更できる `Control.Move` メソッドを使用し、APIメッセージ送信回数を極限まで削減します。

3. 実装:フリッカーレス・レスポンシブレイアウト・エンジン

以下に、実務で最も要求される「上部ヘッダー(固定高・横幅伸縮)」「中央メインサブフォーム(縦横伸縮)」「下部フッター/ボタンエリア(下部固定・右寄せ)」を完璧に制御するコードを示します。

このコードは、Windows APIを安全に呼び出すための `PtrSafe` 宣言を含み、64bit/32bit両環境に対応しています。

標準モジュール: `mod_UI_Optimizer`

まず、描画制御を行うためのAPIおよびヘルパー関数を定義します。

Option Compare Database
Option Explicit

‘ —————————————————————————–
‘ Windows API 宣言 (32bit/64bit 互換)
‘ —————————————————————————–
If VBA7 Then
Public Declare PtrSafe Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As LongPtr, _
lParam As Any) As LongPtr
Else
Public Declare Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
lParam As Any) As Long
End If

‘ Windows メッセージ定数
Public Const WM_SETREDRAW As Long = &HB

”’

”’ フォームの描画更新を一時的に凍結します。
”’

Public Sub LockFormUpdate(ByVal frm As Form)
On Error Resume Next
If Not frm Is Nothing Then
SendMessage frm.hwnd, WM_SETREDRAW, 0, ByVal 0&
End If
End Sub

”’

”’ フォームの描画更新を解除し、強制的に再描画します。
”’

Public Sub UnlockFormUpdate(ByVal frm As Form)
On Error Resume Next
If Not frm Is Nothing Then
SendMessage frm.hwnd, WM_SETREDRAW, 1, ByVal 0&
frm.Repaint
End If
End Sub

フォームのビハインドコード: `Form_F_MainDashboard`

次に、レスポンシブ化を行いたいフォームのモジュールに以下のコードを実装します。

Option Compare Database
Option Explicit

‘ —————————————————————————–
‘ 定数定義(Twips単位: 1インチ = 1440 Twips)
‘ —————————————————————————–
Private Const MIN_FORM_WIDTH As Long = 8000 ‘ フォームの最小幅 (約14cm)
Private Const MIN_FORM_HEIGHT As Long = 6000 ‘ フォームの最小高 (約10.5cm)

Private Const HEADER_HEIGHT As Long = 800 ‘ ヘッダーの固定高
Private Const FOOTER_HEIGHT As Long = 900 ‘ フッターの固定高
Private Const MARGIN_SPACE As Long = 120 ‘ コントロール間の標準マージン

‘ 初期化フラグ(ロード完了前にResizeイベントが走るのを防ぐ)
Private m_bInitialized As Boolean

Private Sub Form_Load()
‘ フォームの初期化処理
m_bInitialized = True
End Sub

Private Sub Form_Resize()
‘ フォームロード完了前、または最小化時のイベントはスルーする
If Not m_bInitialized Then Exit Sub
If Me.CurrentView <> 1 Then Exit Sub ‘ 単票フォーム表示時のみ適用

‘ ————————————————————————-
‘ 1. フォームの有効領域(InsideHeight/InsideWidth)の取得と境界防御
‘ ————————————————————————-
Dim currentWidth As Long
Dim currentHeight As Long

currentWidth = Me.InsideWidth
currentHeight = Me.InsideHeight

‘ 最小サイズを下回る場合は、処理をスキップするか最小値に固定してエラーを防ぐ
If currentWidth < MIN_FORM_WIDTH Then currentWidth = MIN_FORM_WIDTH If currentHeight < MIN_FORM_HEIGHT Then currentHeight = MIN_FORM_HEIGHT ' ------------------------------------------------------------------------- ' 2. 描画更新の凍結(フリッカー防止) ' ------------------------------------------------------------------------- Call LockFormUpdate(Me) On Error GoTo Error_Handler ' ------------------------------------------------------------------------- ' 3. 各コントロールの再配置・サイズ計算 (Moveメソッドの一括適用) ' ------------------------------------------------------------------------- ' ' [レイアウト構造] ' +-------------------------------------------------------+ ' | lblHeader (ヘッダーラベル: 横幅いっぱいに引き伸ばし) | ' +-------------------------------------------------------+ ' | | ' | subFormContainer (メインサブフォーム: 縦横に伸縮) | ' | | ' +-------------------------------------------------------+ ' | [btnSave] [btnClose] (フッターボタン: 右下に追従) | ' +-------------------------------------------------------+ ' --- A. ヘッダーセクションの再配置 --- ' 位置: (MARGIN_SPACE, MARGIN_SPACE) ' サイズ: (横幅 - マージン2, 固定高) Me.lblHeader.Move _ Left:=MARGIN_SPACE, _ Top:=MARGIN_SPACE, _ Width:=currentWidth - (MARGIN_SPACE 2), _ Height:=HEADER_HEIGHT ' --- B. フッターセクション(ボタン群)の再配置 --- ' 右下基準で配置するため、基準点となる座標を算出 Dim footerTop As Long Dim btnWidth As Long Dim btnHeight As Long footerTop = currentHeight - FOOTER_HEIGHT btnWidth = 1500 btnHeight = 500 ' 閉じるボタン (最右端に配置) Me.btnClose.Move _ Left:=currentWidth - btnWidth - MARGIN_SPACE, _ Top:=footerTop + ((FOOTER_HEIGHT - btnHeight) / 2), _ Width:=btnWidth, _ Height:=btnHeight ' 保存ボタン (閉じるボタンの左隣に配置) Me.btnSave.Move _ Left:=Me.btnClose.Left - btnWidth - MARGIN_SPACE, _ Top:=Me.btnClose.Top, _ Width:=btnWidth, _ Height:=btnHeight ' --- C. メインサブフォームの再配置 (残りの全領域を占有) --- ' 位置: ヘッダーの下、マージンを挟む ' サイズ: (横幅 - マージン2, 全体高 - ヘッダー高 - フッター高 - マージン3) Dim subFormHeight As Long subFormHeight = currentHeight - HEADER_HEIGHT - FOOTER_HEIGHT - (MARGIN_SPACE 3) ' 計算結果が不正な値(マイナス)にならないよう、二重の安全弁を設ける If subFormHeight < 1000 Then subFormHeight = 1000 Me.subFormContainer.Move _ Left:=MARGIN_SPACE, _ Top:=HEADER_HEIGHT + (MARGIN_SPACE 2), _ Width:=currentWidth - (MARGIN_SPACE 2), _ Height:=subFormHeight Exit_Handler: ' ------------------------------------------------------------------------- ' 4. 描画更新の解除(一括反映) ' ------------------------------------------------------------------------- Call UnlockFormUpdate(Me) Exit Sub Error_Handler: ' エラー発生時も必ず描画ロックを解除しなければ、Accessがフリーズしたように見える Call UnlockFormUpdate(Me) Debug.Print "Resize Error: " & Err.Number & " - " & Err.Description Resume Exit_Handler End Sub ---

4. ディープダイブ:技術的ブレークスルーと実装の急所

このシンプルなコードの裏には、Access VBAにおける「リソース管理」と「描画アルゴリズム」の深い知見が隠されています。アーキテクトとして押さえておくべき極限のポイントを解説します。

① `InsideWidth` / `InsideHeight` と `Width` の決定的な違い

初心者が最も犯しやすい過ちは、フォーム自体の `Width` プロパティ(これはデザインビューで指定するフォーム領域の「固定幅」)を基準に計算を行うことです。
レスポンシブデザインで基準にすべきは、`Me.InsideWidth``Me.InsideHeight` です。これは、スクロールバー、レコードセレクタ、ウィンドウ枠を除外した「実際にコントロールを描画できる内寸(クライアント領域)」をTwip単位で返します。
これらを使用しなければ、ウィンドウ境界とコントロールの間に不要なスクロールバーが出現するバグを誘発します。

② DPIスケーリング(High DPI)環境の罠

昨今のWindows 10/11環境では、ディスプレイの拡大縮小(125%や150%)が標準化されています。
Access内部の座標系は常に Twips(1 inch = 1440 twips) であり、Windows APIの多くは ピクセル(Pixel) 単位で動作します。
本実装では、Accessオブジェクトモデル内のプロパティ操作(`.Move`)に特化しているため、すべてTwips単位で一貫させています。万一、Windows API(例:`GetClientRect`)を用いてウィンドウサイズを取得する場合は、以下の変換処理を介在させなければ座標がズレます。

‘ ピクセルからTwipsへの変換(一般的な96DPI環境の場合。システムDPIの取得が必要)
Dim twipsX As Long
twipsX = pixelX (1440 / GetDeviceCaps(hDC, LOGPIXELSX))

③ ガベージコレクションとメモリリークの排除

Access VBAにおいて、フォーム上のコントロール(`Me.subFormContainer` など)にアクセスする際、内部的にはCOMオブジェクトのラッパーが都度生成されています。
通常の利用においてこれらはVBAのランタイムによって自動解放されますが、`OnResize` のような「ミリ秒単位で数百回発生し得るイベント」の中では、一時オブジェクトの生成速度が解放を上回り、メモリプレッシャーを引き起こすケースがあります。

これを極限まで最適化する場合、頻繁に参照するコントロールをモジュールレベル変数にキャッシュし、`Form_Unload` で明示的に `Set ctl = Nothing` する手法が有効です。

‘ モジュールレベルで参照を保持
Private m_ctlSubForm As Access.SubForm

Private Sub Form_Load()
Set m_ctlSubForm = Me.subFormContainer
m_bInitialized = True
End Sub

Private Sub Form_Unload(Cancel As Integer)
‘ 参照を明示的に解放し、COMのメモリリークを完全に防ぐ
Set m_ctlSubForm = Nothing
End Sub

5. エピローグ:レガシーをモダンに昇華させるアーキテクトの矜持

Microsoft Accessは、しばしば「過去の遺物」として軽視される傾向にあります。しかし、その描画メカニズムの本質を理解し、OSレベルのAPIを適切にハンドリングすることで、。NET(WPF/WinForms)やモダンなWebアプリケーションに勝るとも劣らない、極めて俊敏で美しいユーザーインターフェースを提供することが可能です。

今回紹介したレスポンシブ・エンジンは、単にコントロールを動かすためのコードではありません。「ハードウェアの性能制限をソフトウェアの設計(API制御と境界防御)で克服する」という、システム開発の本質を体現したアーキテクチャです。

社内システムのUX(ユーザーエクスペリエンス)向上は、業務効率化に直結します。ぜひ、あなたの保守するシステムにこのエンジンを組み込み、レガシーをモダンへと昇華させてください。

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