【テクニカル・上級編】DoCmd.OpenFormの「WindowMode」を駆使した、ダイアログ形式でのデータ入力設計 – Access VBA解析バイブル

スポンサーリンク

序論:`acDialog` を「ただのモーダル表示」で終わらせてはならない

Access VBA開発における最大の落とし穴の一つが、画面遷移に伴う「非同期処理の無秩序な散漫」「グローバル変数への状態依存」である。

初心者向けリファレンスには「`DoCmd.OpenForm` の引数 `WindowMode` に `acDialog` を渡せば、ダイアログ表示になる」としか書かれていない。しかし、シニアアーキテクトの視点に立てば、`acDialog` の本質は単なるUIスタイルの変更ではない。それは「呼び出し元のVBA実行スレッド(コールスタック)を一時停止させ、入力完了またはキャンセルを確定させる同期制御機構」そのものである。

本稿では、`WindowMode:=acDialog` を用いて決定論的な同期データ入力パイプラインを構築する極意を伝授する。大企業の大規模レガシーシステムを破綻から救い、メモリ管理を完全掌握するための技術の真髄をここに提示する。

1. `acDialog` 同期制御の物理メカニズムと「Hide」パターン

実行スレッドの凍結と再開

`DoCmd.OpenForm` で `acDialog` を指定した瞬間、AccessのVBAエンジンは呼び出し元プロシージャのコード実行を当該ラインで物理的に凍結する。画面上のダイアログフォームが破棄(`Close`)されるか、不可視化(`Visible = False`)されるまで、次のコード行へ制御が移ることはない。

[呼び出し元プロシージャ]

├─► DoCmd.OpenForm “frm_InputDialog”, WindowMode:=acDialog
│ │
│ ├─► (実行スレッド一時停止)
│ │ │
│ │ ├─► [ダイアログフォーム] ユーザー操作・検証
│ │ │ │
│ │ │ └─► Me.Visible = False (非表示化)
│ │ │
│ ├─► (実行スレッド再開)
│ │
├─► ダイアログのプロパティからデータ抽出
├─► ダイアログを明示的に Close

「Close に見せかけた Hide」が設計の核心

多くの未熟なエンジニアは、ダイアログ側の「確定ボタン」で `DoCmd.Close` を呼んでしまう。これではフォームインスタンスが破棄され、入力された値もメモリ上から消滅するため、呼び出し側へ値を渡すためにグローバル変数やテンポラリテーブルへ頼るハメになる。これはアーキテクチャの敗北である。

正解は「確定時は `Me.Visible = False` で潜伏させ、キャンセル時は `DoCmd.Close` で破壊する」

呼び出し元は、スレッド再開直後にフォームが「まだロードされているか(=確定)」か「破棄されたか(=キャンセル)」を判定することで、グローバル変数を一切汚染せずに安全にデータを受け取ることができる。

2. 実践実装:完全カプセル化されたダイアログ入力アーキテクチャ

以下に、実務でそのまま運用可能なプロダクションクオリティのコードを示す。

1. ダイアログフォーム側コード (`frm_UserInputDialog`)

フォームモジュール内に状態(DTO相当)を持たせ、プロパティ経由で呼び出し元に安全にデータを提供する。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ フォーム名: frm_UserInputDialog
‘ 役割: ユーザー入力を同期的に取得するモーダルダイアログ
‘ 設計思想: 外部からの直接参照を禁止し、Property経由で型安全に値を公開する
‘ ==============================================================================

Private m_IsCanceled As Boolean
Private m_SelectedID As Long
Private m_InputNote As String

‘ 読み取り専用プロパティ:キャンセルされたか否か
Public Property Get IsCanceled() As Boolean
IsCanceled = m_IsCanceled
End Property

‘ 読み取り専用プロパティ:入力されたデータ
Public Property Get SelectedID() As Long
SelectedID = m_SelectedID
End Property

Public Property Get InputNote() As String
InputNote = m_InputNote
End Property

Private Sub Form_Open(Cancel As Integer)
‘ 初期状態の設定
m_IsCanceled = True ‘ 初期値はキャンセル扱いにしておく
m_SelectedID = 0
m_InputNote = vbNullString
End Sub

‘ 確定ボタン(cmdOK)クリック時
Private Sub cmdOK_Click()
‘ — 1. 入力値の徹底検証(バリデーション) —
If IsNull(Me.txtID) Or Not IsNumeric(Me.txtID) Then
MsgBox “有効なIDを入力してください。”, vbExclamation, “入力エラー”
Me.txtID.SetFocus
Exit Sub
End If

‘ — 2. 内部状態の確定 —
m_SelectedID = CLng(Me.txtID)
m_InputNote = Nz(Me.txtNote, vbNullString)
m_IsCanceled = False

‘ — 3. フォームを破壊せず「不可視化」してスレッドを呼び出し元へ返す —
Me.Visible = False
End Sub

‘ キャンセルボタン(cmdCancel)クリック時
Private Sub cmdCancel_Click()
m_IsCanceled = True
‘ フォームを破棄(呼び出し元には IsLoaded の判定でキャンセルを検知させる)
DoCmd.Close acForm, Me.Name, acSaveNo
End Sub

‘ フォーム右上「×」ボタン対策
Private Sub Form_Unload(Cancel As Integer)
‘ アンロード時、まだ不可視化されていない(ユーザーが直接閉じようとした)場合はキャンセル扱い
If Me.Visible Then
m_IsCanceled = True
End If
End Sub

2. 呼び出し元モジュールコード (`mod_Orchestrator`)

呼び出し側はダイアログのライフサイクルを完全にコントロールする。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ モジュール名: mod_Orchestrator
‘ 役割: モーダルダイアログを起動し、同期的に結果を受領して後続処理を制御する
‘ ==============================================================================

Public Sub ExecuteDataEntryPipeline()
Const DIALOG_NAME As String = “frm_UserInputDialog”
Dim targetID As Long
Dim targetNote As String

On Error GoTo ErrorHandler

‘ 1. ダイアログをモーダル同期モードで起動
‘ (この時点で、ダイアログ側で Hide または Close されるまで以下のコードは実行されない)
DoCmd.OpenForm FormName:=DIALOG_NAME, _
View:=acNormal, _
WindowMode:=acDialog

‘ 2. スレッド再開後の状態判定
‘ フォームがまだロードされているかを確認(Hideされている場合は Loaded が True になる)
If CurrentProject.AllForms(DIALOG_NAME).IsLoaded Then

Dim dlgForm As Form_frm_UserInputDialog
Set dlgForm = Forms(DIALOG_NAME)

‘ 確定かキャンセルの内部フラグを確認
If Not dlgForm.IsCanceled Then
‘ プロパティ経由で安全に値を取得
targetID = dlgForm.SelectedID
targetNote = dlgForm.InputNote

‘ — 後続のビジネスロジックを実行 —
Call ProcessBusinessLogic(targetID, targetNote)

MsgBox “処理が正常に完了しました。”, vbInformation, “成功”
Else
MsgBox “処理はキャンセルされました。”, vbInformation, “中断”
End If

‘ 役目を終えたダイアログを明示的に破棄
DoCmd.Close acForm, DIALOG_NAME, acSaveNo
Set dlgForm = Nothing

Else
‘ フォームが直接閉じた(キャンセル操作等)場合
MsgBox “処理はキャンセルされました。”, vbInformation, “中断”
End If

Exit Sub

ErrorHandler:
‘ 異常発生時のクリーンアップ処理
If CurrentProject.AllForms(DIALOG_NAME).IsLoaded Then
DoCmd.Close acForm, DIALOG_NAME, acSaveNo
End If
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End Sub

Private Sub ProcessBusinessLogic(ByVal id As Long, ByVal note As String)
‘ 取得したデータに基づくデータベース更新などの重厚な処理
Debug.Print “データ処理実行: ID=” & id & “, Note=” & note
End Sub

3. レガシーの罠を打破する:Win32 APIによるウィンドウ制御の極限補正

Access標準の `acDialog` には、マルチディスプレイ環境や高DPI環境において「ウィンドウが想定外の画面端に吹っ飛ぶ」「フォーカス移動によって背後に隠れて操作不能(デッドロック状態)になる」という凶悪な仕様バグが長年存在する。

プロのアーキテクトであれば、これを標準機能のせいにせず、Win32 APIをダイレクトに叩いてウィンドウのZオーダーと表示位置を絶対制御する。

以下は、64bit / 32bit双方に対応した(VBA7 / Win64完全準拠)ダイアログ位置&最前面固定APIモジュールである。

Option Compare Database
Option Explicit

If VBA7 Then
Private Declare PtrSafe Function GetWindowLong Lib “user32” Alias “GetWindowLongA” (ByVal hwnd As LongPtr, ByVal nIndex As Long) As Long
Private Declare PtrSafe Function SetWindowLong Lib “user32” Alias “SetWindowLongA” (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare PtrSafe Function SetWindowPos Lib “user32” (ByVal hwnd As LongPtr, ByVal hWndInsertAfter As LongPtr, ByVal X As Long, ByVal Y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
Private Declare PtrSafe Function GetSystemMetrics Lib “user32” (ByVal nIndex As Long) As Long
Else
Private Declare Function GetWindowLong Lib “user32” Alias “GetWindowLongA” (ByVal hwnd As Long, ByVal nIndex As Long) As Long
Private Declare Function SetWindowLong Lib “user32” Alias “SetWindowLongA” (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare Function SetWindowPos Lib “user32” (ByVal hwnd As Long, ByVal hWndInsertAfter As Long, ByVal X As Long, ByVal Y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
Private Declare Function GetSystemMetrics Lib “user32” (ByVal nIndex As Long) As Long
End If

Private Const GWL_EXSTYLE As Long = -20
Private Const WS_EX_TOPMOST As Long = &H8&
Private Const HWND_TOPMOST As LongPtr = -1
Private Const SWP_NOSIZE As Long = &H1
Private Const SWP_NOMOVE As Long = &H2
Private Const SWP_SHOWWINDOW As Long = &H40

Private Const SM_CXSCREEN As Long = 0
Private Const SM_CYSCREEN As Long = 1

‘ ==============================================================================
‘ 関数名: LockFormToTopMost
‘ 役割: 指定されたフォームのウィンドウをOSレベルで最前面に強制固定する
‘ ==============================================================================
Public Sub LockFormToTopMost(ByVal frm As Form)
#If VBA7 Then
Dim hwnd As LongPtr
#Else
Dim hwnd As Long
#End If

hwnd = frm.hwnd

‘ ウィンドウの拡張スタイルを取得し、TOPMOST属性を付与
Call SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE Or SWP_SHOWWINDOW)
End Sub

ダイアログフォーム側の `Form_Open` または `Form_Load` イベントで `Call LockFormToTopMost(Me)` を実行すれば、ユーザーがAccessの外にあるExcelやWebブラウザをクリックしても、ダイアログが背後に隠れて操作不能になる怪現象を物理的に防止できる。

4. メモリ管理とオブジェクトライフサイクルの厳格化

Access VBAが長期間の連続稼働でクラッシュする原因の9割は、「フォームオブジェクトとRecordsetの解体漏れ」にある。

`acDialog` を用いた設計で特に注意すべきは、`Forms(“ダイアログ名”)` の参照を生成した後の明示的解放である。

‘ 悪しき例: 暗黙の参照を残したまま閉じる
Dim v As String
v = Forms(“frm_InputDialog”).txtValue
DoCmd.Close acForm, “frm_InputDialog”
‘ -> 内部でCOM参照カウントが残り、メモリリーク(リソース枯渇)を引き起こす

‘ 厳格な例: 参照を明確にし、スコープを抜ける前に完全にNothing化する
Dim dlg As Form_frm_InputDialog
Set dlg = Forms(“frm_InputDialog”)

targetValue = dlg.InputNote

Set dlg = Nothing ‘ 強制的に参照カウントをデクリメント
DoCmd.Close acForm, DIALOG_NAME, acSaveNo

さらに、ダイアログが非拘束(Unbound)フォームではなく、一時テーブルにバインドされている(Bound)フォームである場合は、`Me.Visible = False` を行う直前に `Me.Dirty = False`(強制コミット) を呼び出し、バッファを確実にディスクへフラッシュしなければならない。これを怠ると、呼び出し元が後続処理でそのレコードにアクセスした際、「書き込み競合(Lock Error)」が発生する。

5. 結論:真のアーキテクトが目指すべき境界設計

単に画面を表示するだけなら、誰でも書ける。しかし、以下を完璧にコントロールできて初めて、「プロフェッショナルな業務自動化エンジニア」と名乗ることができる。

1. `acDialog` による同期制御スレッドの完全同期化
2. 「Hide」による型安全なデータ提供とグローバル変数の完全排除
3. Win32 APIによるOSレベルでのUI挙動の確実化
4. COMオブジェクトとメモリの厳格なライフサイクル管理

Accessという堅牢かつレガシーな環境だからこそ、テキトーなコードと極限まで計算されたアーキテクチャの差が顕著に現れる。`acDialog` を単なる表示モードではなく、「状態とスレッドを分離・同期させるための境界インターフェース」として再定義し、美しく堅牢なシステムを構築していただきたい。

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