DoCmd.OpenFormの極限制御:モーダルダイアログ設計と処理同期のイディオム
Access VBAを用いた業務システム開発において、避けて通れないのが「入力フォームの制御」である。一覧画面から詳細画面を開き、ユーザーにデータを入力させ、その結果を基に後続処理を継続する――この一連のフローを構築する際、多くの開発者が直面するのが「処理の非同期性」と「オブジェクトのライフサイクル管理の迷走」だ。
とりわけ、`DoCmd.OpenForm` の `WindowMode` 引数を制圧することは、堅牢なAccessアプリケーションを構築するための試金石と言える。
今回は、単なる「モーダルで開く方法」の解説にとどまらない。VBAの実行コンテキストを完全に掌握し、メモリリークを排除し、さらにはWindows APIをも視野に入れた極限のダイアログ設計論を授けよう。
—
1. WindowModeの解剖学:`acModal` と `acDialog` の本質
`DoCmd.OpenForm` には、ウィンドウの振る舞いを決定する `WindowMode` 引数が存在する。ここで多くのエンジニアが混同するのが、`acNormal`(通常)、`acHidden`(非表示)、`acIcon`(最小化)、そして今回の主役である `acModal` と、フォームプロパティで設定する「モーダル」「ポップアップ」の概念だ。
実行スレッドとコードの同期
`WindowMode:=acDialog`(※構文的には `acModal` と同義だが、ダイアログ特有の挙動を誘発する)を指定してフォームを開いた場合、Accessは「呼び出し元プロシージャの実行を完全に一時停止(ブロック)」する。
‘ 呼び出し元のVBAコード
Public Sub OpenEditorSample()
Dim lngID As Long
‘ ここでダイアログを開く。
‘ ユーザーがフォームを閉じるか、非表示(Me.Visible = False)にするまで、
‘ この行でコードの実行が完全にストップする。
DoCmd.OpenForm FormName:=”frmDataEditor”, _
View:=acNormal, _
DataMode:=acFormEdit, _
WindowMode:=acDialog
‘ ダイアログ側から渡された値、またはグローバル/フォーム参照による値の回収
If IsFormLoaded(“frmDataEditor”) Then
lngID = Forms!frmDataEditor!txtID
‘ 後続の同期処理
Call PostProcess(lngID)
‘ オブジェクトの明示的破棄
DoCmd.Close acForm, “frmDataEditor”, acSaveNo
End If
End Sub
この「コードが一時停止する」という特性こそが、入力値の検証と即座のデータベース反映を担保する最大の武器となる。
—
2. 実務で必須となる「ダイアログ非表示イディオム」の極意
ダイアログフォームでありがちな悪手は、データを確定する際に `DoCmd.Close` でフォームを完全にアンロードしてしまうことだ。フォームを閉じてしまうと、呼び出し元に戻った瞬間に `Forms!frmDataEditor` の参照が失われ、入力された値(コントロールの値)を安全に回収する前にオブジェクトが消滅する危険性がある。
これを防ぐためのプロフェッショナルな定石は、「閉じるのではなく、非表示(`Visible = False`)にしてコードのブロックを解除する」というアプローチだ。
ダイアログ側の実装(frmDataEditor)
‘ 【フォームモジュール】frmDataEditor
Private Sub cmdSave_Click()
‘ 1. 入力値のバリデーション
If Not ValidateData() Then Exit Sub
‘ 2. データの永続化(トランザクション制御は必要に応じて)
If Me.Dirty Then Me.Dirty = False
‘ 3. アンロードせずに「非表示」にすることで、呼び出し元に制御を返す
Me.Visible = False
End Sub
Private Sub cmdCancel_Click()
‘ キャンセル時は識別用のフラグやコントロールを操作するか、
‘ 単に非表示にして呼び出し側で判定させる
Me.Tag = “CANCEL”
Me.Visible = False
End Sub
呼び出し元側の厳密な制御
Public Function ShowEditorDialog(ByVal TargetID As Long) As Boolean
On Error GoTo ErrorHandler
ShowEditorDialog = False
‘ フォームを開く(コードはここで停止)
DoCmd.OpenForm FormName:=”frmDataEditor”, _
WindowMode:=acDialog, _
OpenArgs:=CStr(TargetID)
‘ — ここから下はダイアログが非表示(または閉じた)後に実行される —
‘ フォームが存在するか確認(×ボタンで閉じられた場合などの対策)
If Not IsFormLoaded(“frmDataEditor”) Then Exit Function
‘ キャンセル判定
If Forms!frmDataEditor.Tag = “CANCEL” Then GoTo Cleanup
‘ データの確定処理
‘ …
ShowEditorDialog = True
Cleanup:
‘ 確実にメモリから解放する
DoCmd.Close acForm, “frmDataEditor”, acSaveNo
Exit Function
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume Cleanup
End Function
‘ フォームのロード状態を厳密に判定するヘルパー関数
Private Function IsFormLoaded(ByVal FormName As String) As Boolean
Dim obj As Object
For Each obj In CurrentProject.AllForms
If obj.Name = FormName Then
If obj.IsLoaded Then
IsFormLoaded = True
Exit Function
End If
End If
Next obj
IsFormLoaded = False
End Function
—
3. メモリ最適化とAccessオブジェクトモデルの暗黒面
Access VBAにおける最大のリスクは、「暗黙のオブジェクト参照保持によるメモリリーク」と「コンパイラのBloat(肥大化)」である。
`DoCmd.OpenForm` でフォームを開きまくる設計や、`Forms!フォーム名!コントロール名` という遅延バインディングに頼り切ったコードは、内部のCOMコンポーネント参照カウンタを圧迫する。
1. `CurrentDb` の正しいキャッシュと解放
ダイアログ内で頻繁にトランザクションやレコードセット操作を行う場合、`CurrentDb` を乱用してはならない。`CurrentDb` は呼び出すたびに新しいデータベースオブジェクトのインスタンスを生成する。
‘ 【NGアンチパターン】
‘ 呼び出すたびにメモリ上に別インスタンスが生成され、ガベージコレクションのタイミングが曖昧になる
CurrentDb.Execute “UPDATE …”, dbFailOnError
CurrentDb.Execute “INSERT …”, dbFailOnError
‘ 【正しい極限アプローチ】
Dim db As DAO.Database
Set db = CurrentDb ‘ 1つのインスタンスを変数に保持
With db
.BeginTrans
.Execute “UPDATE …”, dbFailOnError
.Execute “INSERT …”, dbFailOnError
.CommitTrans
End With
Set db = Nothing ‘ 明示的解放
2. `OpenArgs` を用いた疎結合なデータ受け渡し
グローバル変数や `Forms!親フォーム!コントロール` を直接参照する密結合な設計は、モジュールの再利用性を完全に破壊する。ダイアログを開く際は、必ず `OpenArgs` 引数を利用してコンテキスト(IDやモード)を文字列として渡し、フォームの `Load` イベントで自己完結させよ。
‘ 呼び出し側
DoCmd.OpenForm “frmDataEditor”, WindowMode:=acDialog, OpenArgs:=”ID=105,Mode=Edit”
‘ ダイアログ側 (Form_Load)
Private Sub Form_Load()
Dim vArgs As Variant
vArgs = Split(Me.OpenArgs, “,”)
‘ 解析して自身の状態を初期化
End Sub
—
4. レガシー環境・マルチウィンドウの罠とAPIによる調停
Accessのモーダルフォームは、時にWindowsのZオーダー(ウィンドウの前後関係)において厄介な挙動を示す。特に外部アプリケーション(ExcelやOutlookなど)との連携時、ダイアログの背後にメインウィンドウが潜り込み、ユーザーがフリーズしたと錯覚する現象が多発する。
これを根本から解決するには、Windows API(User32.dll)を用いてダイアログのウィンドウハンドル(hWnd)を強制的に最前面(Always on Top)に固定するアプローチが有効である。
‘ 【API宣言】
If VBA7 Then
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 uFlags As Long) As Long
Else
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 uFlags As Long) As Long
End If
Private Const HWND_TOPMOST = -1
Private Const SWP_NOMOVE = &H2
Private Const SWP_NOSIZE = &H1
‘ ダイアログフォームの Load イベントで実行
Private Sub Form_Load()
‘ 自身のウィンドウを常に最前面に固定
SetWindowPos Me.hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE
End Sub
このAPIを組み合わせることで、OSレベルでダイアログの視認性と操作性が担保され、ユーザーの誤操作や「画面が消えた」というヘルプデスクへの問い合わせを完全になくすことができる。
—
総括
Access VBAにおける `DoCmd.OpenForm(WindowMode:=acDialog)` を軸としたダイアログ設計は、単なる画面遷移の手段ではない。
1. コードの同期実行によるトランザクショナルな入力制御
2. アンロードせず `Visible = False` を活用した安全な値の回収
3. `CurrentDb` の適切なスコープ管理とメモリ解放
4. Windows APIによるUIスレッドとZオーダーの完全掌握
これらを満たした設計こそが、レガシーと侮られがちなAccessを、ミッションクリティカルな現場で耐えうる堅牢なエンタープライズシステムへと昇華させる唯一の道である。妥協なきコードを書き続けろ。
