【テクニカル・上級編】フォームの「ActiveControl」プロパティを動的に取得し、汎用的な入力チェック関数を作成する – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:ActiveControlを制する者が入力バリデーションを制す

レガシーシステムの寿命は、往々にして「入力インターフェースの保守性」で決まる。
何百とあるフォーム、数千に及ぶコントロール。その一つひとつに泥臭いバリデーションコードを記述してきた開発者なら、誰もが一度は絶望を覚えたことがあるはずだ。

「どのコントロールからフォーカスが当たっても、自律的に自身の型と値を判別し、一元化されたバリデーションエンジンへ処理を委譲する仕組みを作れないのか?」

結論から言えば、可能だ。
今回は、Accessの `Screen.ActiveControl` とオブジェクトモデルの深部を突くことで、フォームのスパゲッティコードを根絶し、保守性の極限に達する汎用バリデーションアーキテクチャを解説する。

1. なぜ「個別のコントロールイベント」に頼るべきではないのか

初学者は、テキストボックスの `BeforeUpdate` や `Exit` イベントに直接検証ロジックを書き込む。小規模なシステムであればそれで動く。しかし、業務の肥大化に伴い、このアプローチは破綻する。

  • コードの重複: 似たようなNullチェックや桁数制限が全フォームに散らばる。
  • 仕様変更の悪夢: 「電話番号のフォーマットが変わった」という要件だけで、全フォームの該当イベントを修正する地獄が待っている。
  • トランザクション制御の欠如: どのコントロールでエラーが発生したのかを画面全体のコンテキストとして把握できず、メッセージングがバラバラになる。

真のアーキテクトが目指すべきは、「イベントの集約(Event Aggregation)」「自己記述型コントロールの動的解釈」である。

2. アーキテクチャの核心:`Screen.ActiveControl` の動的取得と型安全な判定

VBAは動的型付け言語の側面を持つがゆえに、オブジェクトの型判定を怠ると実行時エラー(実行時エラー 438: オブジェクトは、このプロパティまたはメソッドをサポートしていません)の温床となる。

フォーム上のどのコントロールからでも呼び出し可能な共通関数を作成する場合、まず直面するのが「今、どのコントロールにフォーカスがあるか」の正確な捕捉と、そのコントロールが持つプロパティの安全なパースである。

以下のコードは、現在のフォーカス位置を動的に特定し、コントロールの `Tag` プロパティ(メタデータ)と組み合わせてバリデーションを実行する汎用プロシージャの心臓部である。

‘ ==============================================================================
‘ モジュール名: basValidationEngine
‘ 概要: ActiveControlを動的に解析し、型に応じたバリデーションを執行する
‘ ==============================================================================
Option Compare Database
Option Explicit

‘ バリデーション結果を格納する構造体
Public Type ValidationResult
IsValid As Boolean
ErrorMessage As String
TargetControl As Control
End Type

/

  • アクティブなコントロールの値を動的に検証する汎用エントリーポイント
  • @return ValidationResult 判定結果とメッセージ

/
Public Function ExecuteActiveControlValidation() As ValidationResult
Dim ctl As Control
Dim res As ValidationResult

res.IsValid = True
res.ErrorMessage = “”

‘ エラーハンドリングの要:Screenオブジェクトの安全な取得
On Error GoTo ErrorHandler

‘ フォーカスを持つコントロールが存在するか確認
If Screen.ActiveForm Is Nothing Then
res.IsValid = False
res.ErrorMessage = “アクティブなフォームが特定できません。”
Exit Function
End If

Set ctl = Screen.ActiveForm.ActiveControl
Set res.TargetControl = ctl

‘ コントロールが値を持つ型(TextBox, ComboBox等)か判定
If Not HasValueProperty(ctl) Then
‘ 値を持たないコントロール(CommandButton等)は検証対象外
Exit Function
End If

‘ 1. 必須チェック (Tagプロパティに “REQ” が含まれているか判定)
If InStr(1, Nz(ctl.Tag, “”), “REQ”, vbTextCompare) > 0 Then
If IsNull(ctl.Value) Or Trim(CStr(ctl.Value)) = “” Then
res.IsValid = False
res.ErrorMessage = “[” & GetControlLabel(ctl) & “] は必須入力です。”
Exit Function
End If
End If

‘ 値が空で、必須ではない場合は後続の型チェックをスキップして正常終了
If IsNull(ctl.Value) Or Trim(CStr(ctl.Value)) = “” Then
Exit Function
End If

‘ 2. コントロールの型およびTagに応じた詳細バリデーション
Select Case ctl.ControlType
Case acTextBox
res = ValidateTextBox(ctl)

Case acComboBox
res = ValidateComboBox(ctl)

Case acListBox
‘ リストボックス固有の検証ロジック

Case Else
‘ その他のコントロール
End Select

Exit Function

ErrorHandler:
‘ 予期せぬエラーの捕捉
res.IsValid = False
res.ErrorMessage = “バリデーション実行中にシステムエラーが発生しました: ” & Err.Description
End Function

3. メモリ管理とオブジェクト参照の作法

VBAにおけるオブジェクト変数の解放は、JavaやC#のガベージコレクションに慣れたプログラマにとって油断しやすいポイントである。特にフォームやコントロールの参照を保持し続けると、COMコンポーネントの参照カウントが適切にデクリメントされず、メモリリークやAccess特有の「理由不明のフリーズ・強制終了」を引き起こす。

上記のコードにおいて、`Set res.TargetControl = ctl` のようにオブジェクト参照を構造体に格納している点に注目してほしい。
プロシージャのスコープを抜ける際、ローカル変数は破棄されるが、複雑なクラスモジュール間やグローバル領域でオブジェクトをやり取りする場合は、以下の鉄則を遵守しなければならない。

1. 参照の明示的なNothing化: 使い終わったコントロールやレコードセットは、必ず `Set obj = Nothing` で解放する。
2. `CurrentDb` の濫用を避ける: `CurrentDb` は呼び出すたびに新しいデータベースのインスタンス(COMオブジェクト)をメモリ上に生成する。ループ内で `CurrentDb.Execute` を行うような愚を犯してはならない。必ず変数に一度だけ格納し、使い回せ。

‘ 良い例:CurrentDbのキャッシュと確実な解放
Dim db As DAO.Database
Set db = CurrentDb
‘ … 処理 …
Set db = Nothing

4. 現場で使える!コントロールのメタデータ(Tag)駆動設計

シニアエンジニアとして提案したいのは、「コードを書かずにTagプロパティでバリデーションルールを定義する」というアプローチだ。

例えば、テキストボックスの `Tag` プロパティに以下のように記述する。
`REQ;TYPE=NUMERIC;MIN=1;MAX=100;LABEL=単価`

これを先ほどの `ValidateTextBox` 関数内で解析(Parsing)する。

Private Function ValidateTextBox(ByRef ctl As Control) As ValidationResult
Dim res As ValidationResult
Dim tagVal As String
res.IsValid = True
tagVal = Nz(ctl.Tag, “”)

‘ 数値チェックの例
If InStr(1, tagVal, “TYPE=NUMERIC”, vbTextCompare) > 0 Then
If Not IsNumeric(ctl.Value) Then
res.IsValid = False
res.ErrorMessage = “[” & GetControlLabel(ctl) & “には数値を入力してください。”
Exit Function
End If

‘ 範囲チェックの動的抽出(簡易実装)
‘ 実運用では正規表現やスプリット関数でパラメータを抽出する
Dim numVal As Double
numVal = CDbl(ctl.Value)
If numVal < 0 Then ' 例としての下限 res.IsValid = False res.ErrorMessage = "[" & GetControlLabel(ctl) & "には0以上の値を指定してください。" Exit Function End If End If res.TargetControl = ctl Exit Function End Function さらに実用性を高めるため、コントロールの隣にある「ラベル(Label)」のキャプションを動的に取得するヘルパー関数を組み合わせる。これにより、「テキストボックス1にエラーがあります」ではなく、「[単価] に数値を入力してください」という、ユーザーフレンドリーなエラーメッセージを自動生成できる。 Private Function GetControlLabel(ByRef ctl As Control) As String On Error Resume Next Dim lbl As Label Set lbl = ctl.Controls(0) ' コントロールに紐付くラベルの取得 If Not lbl Is Nothing Then GetControlLabel = lbl.Caption Else GetControlLabel = ctl.Name End If On Error GoTo 0 End Function Private Function HasValueProperty(ByRef ctl As Control) As Boolean ' 値を持つコントロールタイプを絞り込む Select Case ctl.ControlType Case acTextBox, acComboBox, acListBox, acCheckBox, acOptionGroup HasValueProperty = True Case Else HasValueProperty = False End Select End Function ---

5. フォーム側での実装:たった1行のイベントハンドラ

このアーキテクチャを導入した場合、各フォームの `BeforeUpdate` イベントは以下のように極限までシンプルになる。

Private Sub Form_BeforeUpdate(Cancel As Integer)
Dim result As ValidationResult

‘ 汎用バリデーションエンジンを叩く
result = ExecuteActiveControlValidation()

If Not result.IsValid Then
MsgBox result.ErrorMessage, vbCritical, “入力エラー”

‘ エラーが発生したコントロールへ強制的にフォーカスを戻す
If Not result.TargetControl Is Nothing Then
result.TargetControl.SetFocus
End If

‘ 更新をキャンセル
Cancel = True
End If
End Sub

すべての入力検証ロジックが `basValidationEngine` に集約されるため、将来的な仕様変更や、エラーログの外部出力(WindowsイベントログやDBへの書き込み)への拡張も、エンジン側を1箇所修正するだけで全フォームに適用される。

結び:レガシーの殻を破るエンジニアリング

Access VBAは、しばしば「おもちゃの言語」と揶揄される。しかし、それは書く側の設計思想が稚拙である場合に限る。
オブジェクトモデルの本質を理解し、メモリライフサイクルを制御し、拡張性のあるアーキテクチャを構築すれば、Accessは基幹系をも支える堅牢なプラットフォームへと変貌を遂げる。

「動けばいい」の時代は終わった。
プロフェッショナルとして、コードの美しさと堅牢性を追求し続けよう。

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