【Access VBA極限の知見】DoCmd.OpenFormが織りなす同期の世界:モーダルダイアログによる堅牢な入力設計の極意
業務システムを構築する際、最も遭遇する要件は何だろうか。それは「ユーザーに確実に入力させ、その結果を受け取って次の処理へ進む」という、一見シンプルだが実装を誤ると致命的なバグを生む同期処理だ。
世の入門書やネットの断片的なコードは、「とりあえず `DoCmd.OpenForm` を使えば開く」としか教えない。しかし、オブジェクトのライフサイクルやVBAの実行スレッドの挙動を理解していない者が書いたコードは、画面の背後に隠れる(Zオーダーの崩壊)、親フォームとのデータ競合、エラー時のゾンビフォーム生成など、現場を地獄に変える。
今回は、`DoCmd.OpenForm` の `WindowMode:=acDialog` を極限まで使い倒し、「呼び出し元を完全にハング(一時停止)させ、値の授受を安全に行うプロフェッショナルなダイアログ設計」の全貌を伝授する。
—
1. なぜ `acDialog` なのか? ── 非同期の悪夢から脱却せよ
通常、`DoCmd.OpenForm` でフォームを開いた場合、VBAのコードはフォームの表示を待たずに次の行へ進む(非同期処理)。そのため、呼び出し元が「入力された値」を取得しようとしても、まだユーザーが入力していない、あるいはフォーム自体がまだ存在しないというタイミングエラー(実行時エラー)が頻発する。
これを防ぐために `WindowMode:=acDialog` を指定すると、以下の劇的な変化が起きる。
1. コードの完全停止(同期):ダイアログとして開かれたフォームが閉じるか、非表示になるまで、呼び出し元のVBAの実行は完全にブロックされる。
2. モーダル制約:対象フォームが閉じられるまで、Access内の他のウィンドウ操作が原則ロックされる。
3. 安全な値の持ち出し:フォームが「閉じる」瞬間、そのインスタンスやコントロールの値はまだメモリ上に存在するため、呼び出し元から確実に値を回収できる。
しかし、ここで一つ大きな罠がある。「ダイアログとして開いたフォームは、`DoCmd.Close` でアンロードすると、呼び出し元からコントロールの値にアクセスしようとした瞬間にメモリから消滅し、エラーになる」という点だ。
この問題をスマートに解決する設計思想こそが、今回のキモである。
—
2. 堅牢なダイアログ設計の3大原則
実務で破綻しないダイアログを構築するためには、以下の3点を死守せよ。
- 原則1:フォームは「破棄」せず「非表示(Hidden)」にする
閉じるボタン等でフォームを消す際、メモリからアンロードしてはならない。`Visible = False` にして隠すだけにとどめる。これにより、呼び出し元が値を取得し終わるまでデータを保持できる。
- 原則2:公開プロパティ(Property Get)でカプセル化する
呼び出し元が `Forms!frmInput!txtValue` のようにグローバルな名前空間を直接参照するのは素人のやることだ。フォームモジュール内に `Property Get` を定義し、安全に値を取り出せ。
- 原則3:キャンセルのハンドリング
ユーザーが「×」ボタンやキャンセルボタンを押して離脱した場合を想定し、戻り値やステータスフラグで正常終了かを判定できるようにする。
—
3. プロダクションコード:実用に耐える実装例
ここからは、実際に現場でそのまま使えるコードを示す。
シナリオはこうだ:「顧客一覧画面から『新規顧客追加』ボタンを押し、ダイアログで入力された会社名を受け取ってテーブルに書き込む」。
① ダイアログフォーム側のコード (`frmCustomerInput`)
このフォームには、「登録」ボタン(`cmdOK`)と「キャンセル」ボタン(`cmdCancel`)が配置されているとする。
Option Compare Database
Option Explicit
‘ 内部状態管理用変数
Private m_IsCancelled As Boolean
‘ —————————————————————-
‘ フォームロード時
‘ —————————————————————-
Private Sub Form_Load()
‘ 初期値はキャンセル扱いとする
m_IsCancelled = True
End Sub
‘ —————————————————————-
‘ 登録ボタンクリック
‘ —————————————————————-
Private Sub cmdOK_Click()
‘ 入力チェック(バリデーション)
If Trim(Me.txtCustomerName.Value & “”) = “” Then
MsgBox “顧客名を入力してください。”, vbExclamation, “入力エラー”
Me.txtCustomerName.SetFocus
Exit Sub
End If
‘ 正常終了フラグを立てる
m_IsCancelled = False
‘ ★重要:アンロード(メモリ破棄)せず、単に非表示にする
Me.Visible = False
End Sub
‘ —————————————————————-
‘ キャンセルボタンクリック
‘ —————————————————————-
Private Sub cmdCancel_Click()
m_IsCancelled = True
Me.Visible = False
End Sub
‘ —————————————————————-
‘ フォームの×ボタン(閉じる)対策
‘ —————————————————————-
Private Sub Form_Unload(Cancel As Integer)
‘ ×ボタンで閉じられた場合も非表示でトラップする
Cancel = True ‘ 実際のアンロードをキャンセル
m_IsCancelled = True
Me.Visible = False
End Sub
‘ ================================================================
‘ パブリックインターフェース(外部からの値取得用)
‘ ================================================================
‘ キャンセルされたかどうか
Public Property Get IsCancelled() As Boolean
IsCancelled = m_IsCancelled
End Property
‘ 入力された顧客名
Public Property Get CustomerName() As String
CustomerName = Trim(Me.txtCustomerName.Value & “”)
End Property
② 呼び出し元(親フォームまたは標準モジュール)のコード
親側では、フォームを開いた後にプロパティを叩き、最後に明確にフォームのインスタンスを破棄する。
Option Compare Database
Option Explicit
Public Sub OpenCustomerDialogSample()
Dim frm As Form
Dim strNewName As String
On Error GoTo ErrorHandler
‘ 1. フォームをダイアログとして開く
‘ (この行でコードの実行が一時停止し、ユーザーの入力待ちになる)
DoCmd.OpenForm FormName:=”frmCustomerInput”, _
WindowMode:=acDialog
‘ ─── ここから下は、ダイアログが非表示(または閉じる)になった後に実行される ───
‘ 2. 開いたフォームの参照を取得
‘ ※まだメモリ上に存在するため Forms コレクションから取得可能
Set frm = Forms(“frmCustomerInput”)
‘ 3. キャンセルされたかチェック
If frm.IsCancelled Then
MsgBox “入力をキャンセルしました。”, vbInformation
GoTo Cleanup
End If
‘ 4. プロパティ経由で安全に値を取得
strNewName = frm.CustomerName
‘ 5. データベースへの書き込み処理(例:DAOによる追加)
Call RegisterCustomerToDatabase(strNewName)
MsgBox “顧客「 ” & strNewName & ” 」を登録しました。”, vbInformation
Cleanup:
‘ 6. 役目を終えたダイアログフォームを確実にメモリから破棄する
If IsFormLoaded(“frmCustomerInput”) Then
DoCmd.Close acForm, “frmCustomerInput”
End If
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume Cleanup
End Sub
‘ —————————————————————-
‘ フォームがロードされているか判定するヘルパー関数
‘ —————————————————————-
Private Function IsFormLoaded(ByVal formName As String) As Boolean
Dim obj As Object
For Each obj In Application.AllForms
If obj.Name = formName Then
If obj.IsLoaded Then
IsFormLoaded = True
Exit Function
End If
End If
Next obj
IsFormLoaded = False
End Function
‘ —————————————————————-
‘ ダミーの登録用プロシージャ
‘ —————————————————————-
Private Sub RegisterCustomerToDatabase(ByVal custName As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb()
Set rs = db.OpenRecordset(“M_Customer”, dbOpenDynaset)
rs.AddNew
rs!CustomerName = custName
rs!RegisteredDate = Now
rs.Update
rs.Close
Set rs = Nothing
Set db = Nothing
End Sub
—
4. チーフアーキテクトからの実践的アドバイス:なぜこの設計が最強なのか
上記のコードを見て、「たかがダイアログを開くのに、なぜここまで周りくどいことをするのか?」と思ったならば、設計者としての経験値がまだ足りないと言わざるを得ない。
1. メモリリークとゾンビオブジェクトの根絶
多くの開発者は、ダイアログ側で `DoCmd.Close` を実行し、呼び出し元で `Forms!frm!txt` を参照しようとして「オブジェクトが見つからない」というエラーを踏むか、逆にフォームを閉じ忘れてメモリを圧迫させる。`Visible = False` で隠し、呼び出し元が責任を持って `DoCmd.Close` で片付けるという「ライフサイクルの明確な分離」こそが、長期稼働するシステムでバグを出さない唯一の道だ。
2. 保守性の向上
フォームのコントロール名(`txtCustomerName` など)を変更した際、呼び出し元のコードまで修正する必要があるような設計は悪だ。フォーム内部のコントロール構造を隠蔽し、`Property Get CustomerName` というインターフェースだけを外部に公開することで、UIの変更に強い堅牢なアーキテクチャが実現できる。
Access VBAはレガシーな言語と揶揄されがちだが、オブジェクト指向の原則(カプセル化、ライフサイクル管理)を正しく適用すれば、極めて堅牢で実用的なエンタープライズアプリケーションの構築が可能だ。
「動けばいい」のフェーズはもう卒業しよう。プロフェッショナルな設計思想を武器に、現場で信頼される真の自動化・システム化を成し遂げてほしい。
