はじめに:なぜ、あなたのフォーム入力チェックは「スパゲッティコード」になるのか
業務システムをAccessで構築する際、避けて通れないのが入力値のバリデーション(妥当性チェック)だ。
多くの開発現場で、次のようなコードを目にして絶望することがある。
‘ ── 悪夢のようなアンチパターン ──
Private Sub txt1_BeforeUpdate(Cancel As Integer)
If Me.txt1.Value = “” Then
MsgBox “入力してください”, vbCritical
Cancel = True
End If
End Sub
Private Sub txt2_BeforeUpdate(Cancel As Integer)
If Not IsNumeric(Me.txt2.Value) & Then
MsgBox “数値を入れてください”, vbCritical
Cancel = True
End If
End Sub
画面にある数十個のテキストボックス一つひとつに、似たようなイベントプロシージャをコピペし、コントロール名(`txt1`, `txt2`…)をハードコーディングする。仕様変更で「このフィールドは桁数を変更してくれ」と言われた日には、地獄のような修正作業が待っている。
プロフェッショナルなエンジニアであれば、「どのコントロールから呼び出されても、自身を特定し、型や属性に応じた検証を動的に行う単一のルーチン」を構築すべきだ。
今回は、Accessの`Screen.ActiveControl`を完全に掌握し、保守性・堅牢性の極限に達した汎用バリデーション関数の設計思想と実装を伝授する。
—
1. アーキテクチャの核心:`Screen.ActiveControl` の罠と正しいライフサイクル
動的な入力チェックを作る際、多くの初心者が陥る罠が「どのタイミングでどのプロパティを参照するか」というオブジェクトモデルの無理解だ。
ここで重要となるのが、イベントの発生源と `Screen.ActiveControl` の関係性である。
コントロールのフォーマットとイベント順序
入力値の検証において最も適したイベントは、コントロールからフォーカスが離れる瞬間に発生する `BeforeUpdate` イベント である。このイベントの最大の特徴は、`Cancel = True` を指定することで、不正なデータの確定を物理的に阻止できる点にある。
ここで、共通関数側から「今、どのコントロールが操作されているか」を特定するために `Screen.ActiveControl` を利用する。
[ユーザーの入力]
↓
[BeforeUpdate イベント発火]
↓ (ここで共通バリデーション関数を呼び出す)
[Screen.ActiveControl でアクティブなコントロールを取得]
↓ (Value, Tag, Name などのメタデータを動的解析)
[判定結果に応じて Cancel = True または 処理続行]
なぜ `Me.ActiveControl` ではなく `Screen.ActiveControl` なのか?
フォームモジュール内であれば `Me.ActiveControl` も使えるが、もし将来的にサブフォームの連鎖や、複数の親フォームから共通の標準モジュール(グローバル関数)へ処理を委譲する場合、`Screen.ActiveControl` の方がスコープの取り回しにおいて圧倒的に有利である。ただし、フォーカスが完全に失われている状態や、タイマーイベント内では `Screen.ActiveControl` はエラーを吐くため、必ずフォーカスが存在するイベント(BeforeUpdate等)からのみ呼び出すという制約(ガード条件)を厳守する必要がある。
—
2. 堅牢な設計:コントロールの「メタデータ」を Tag プロパティに埋め込む
動的バリデーションの肝は、コード側に「このテキストボックスは数値で、かつ5桁まで」という条件をハードコーディングしないことだ。
代わりに、Accessの各コントロールが持つ `Tag`(タグ)プロパティ をメタデータ格納庫としてフル活用する。
例えば、コントロールの `Tag` プロパティに次のようにパイプ区切りでルールを記述する。
> `Type=Num;Min=1;Max=100;Required=True`
これにより、共通関数側で `Tag` をパースするだけで、あらゆるコントロールのバリデーションルールを動的に構築できる。設計の美しさと拡張性がここに宿る。
—
3. プロダクションコード:コピペで使える最強の汎用バリデーションモジュール
それでは、実際の現場でそのまま稼働できるプロダクションコードを提示する。
標準モジュール(例: `modValidation`)に配置し、各コントロールの `BeforeUpdate` から呼び出す設計とする。
標準モジュール:`modValidation`
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 汎用コントロールバリデーションエンジン
‘ 呼出元: 各コントロールの BeforeUpdate イベント (例: Call ValidateActiveControl(Cancel))
‘ ==============================================================================
Public Sub ValidateActiveControl(ByRef Cancel As Integer)
On Error GoTo ErrorHandler
Dim ctl As Control
Set ctl = Screen.ActiveControl
‘ コントロールがテキストボックスやコンボボックス等であるか確認
If Not (TypeOf ctl Is TextBox Or TypeOf ctl Is ComboBox) Then Exit Sub
Dim vValue As Variant
vValue = ctl.Value
‘ 1. 必須チェックとNull許容判定
If IsNull(vValue) Or vValue = “” Then
If ParseTag(ctl.Tag, “Required”) = “True” Then
MsgBox “【入力エラー】” & vbCrLf & GetControlLabel(ctl) & “は必須入力です。”, vbCritical, “入力検証”
Cancel = True
FocusAndSelect ctl
Exit Sub
Else
‘ 必須ではなく値が空の場合は、以降の型チェック等をスキップして正常終了
Exit Sub
End If
End If
‘ 2. データ型に基づく動的バリデーション
Dim sDataType As String
sDataType = ParseTag(ctl.Tag, “Type”)
Select Case UCase(sDataType)
Case “NUM”, “INTEGER”, “LONG”
If Not IsNumeric(vValue) Then
MsgBox “【入力エラー】” & vbCrLf & GetControlLabel(ctl) & “には数値を入力してください。”, vbCritical, “入力検証”
Cancel = True
FocusAndSelect ctl
Exit Sub
End If
‘ 範囲チェックの動的評価
Dim dMin As Double, dMax As Double
If IsNumeric(ParseTag(ctl.Tag, “Min”)) Then
dMin = CDbl(ParseTag(ctl.Tag, “Min”))
If CDbl(vValue) < dMin Then
MsgBox "【入力エラー】" & vbCrLf & GetControlLabel(ctl) & "は " & dMin & " 以上で入力してください。", vbCritical, "入力検証"
Cancel = True
FocusAndSelect ctl
Exit Sub
End If
End If
If IsNumeric(ParseTag(ctl.Tag, "Max")) Then
dMax = CDbl(ParseTag(ctl.Tag, "Max"))
If CDbl(vValue) > dMax Then
MsgBox “【入力エラー】” & vbCrLf & GetControlLabel(ctl) & “は ” & dMax & ” 以下で入力してください。”, vbCritical, “入力検証”
Cancel = True
FocusAndSelect ctl
Exit Sub
End If
End If
Case “DATE”
If Not IsDate(vValue) Then
MsgBox “【入力エラー】” & vbCrLf & GetControlLabel(ctl) & “には正しい日付を入力してください。”, vbCritical, “入力検証”
Cancel = True
FocusAndSelect ctl
Exit Sub
End If
Case “TEXT”, “”
‘ 文字列長チェック
Dim iMaxLength As Integer
If IsNumeric(ParseTag(ctl.Tag, “MaxLength”)) Then
iMaxLength = CInt(ParseTag(ctl.Tag, “MaxLength”))
If Len(CStr(vValue)) > iMaxLength Then
MsgBox “【入力エラー】” & vbCrLf & GetControlLabel(ctl) & “は ” & iMaxLength & ” 文字以内で入力してください。”, vbCritical, “入力検証”
Cancel = True
FocusAndSelect ctl
Exit Sub
End If
End If
End Select
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
Cancel = True
End Sub
‘ ==============================================================================
‘ 補助関数: Tagプロパティから指定されたキーの値を取り出す
‘ 例: “Type=Num;Min=10;Required=True” から “Min” の値 “10” を抽出
‘ ==============================================================================
Private Function ParseTag(ByVal tagString As String, ByVal keyName As String) As String
Dim pairs() As String
Dim kv() As String
PiarsiTag = “”
If Trim(tagString) = “” Then Exit Function
pairs = Split(tagString, “;”)
Dim i As Long
For i = LBound(pairs) To UBound(pairs)
kv = Split(pairs(i), “=”)
If UBound(kv) = 1 Then
If Trim(UCase(kv(0))) = Trim(UCase(keyName)) Then
ParseTag = Trim(kv(1))
Exit Function
End If
End If
Next i
ParseTag = “”
End Function
‘ ==============================================================================
‘ 補助関数: コントロールに対応するラベル(日本語名)を自動取得する
‘ (コントロールの直前にある、または紐づくラベルコントロールを推測)
‘ ==============================================================================
Private Function GetControlLabel(ByRef ctl As Control) As String
On Error Resume Next
Dim lbl As Control
Set lbl = ctl.Controls(0) ‘ 添付ラベルの取得を試みる
If Not lbl Is Nothing Then
GetControlLabel = lbl.Caption
Exit Function
End If
‘ ラベルが取得できない場合はコントロール名をフォールバック
GetControlLabel = ctl.Name
On Error GoTo 0
End Function
‘ ==============================================================================
‘ 補助関数: エラー時のフォーカス維持と全選択
‘ ==============================================================================
Private Sub FocusAndSelect(ByRef ctl As Control)
ctl.SetFocus
If TypeOf ctl Is TextBox Then
ctl.SelStart = 0
ctl.SelLength = Len(ctl.Value & “”)
End If
End Sub
フォーム側(クラスモジュール)での実装方法
フォーム側の記述は、これ以上ないほどシンプルになる。各コントロールのイベントにたった1行書くだけで完了する。
Private Sub txtAge_BeforeUpdate(Cancel As Integer)
Call ValidateActiveControl(Cancel)
End Sub
Private Sub txtUserName_BeforeUpdate(Cancel As Integer)
Call ValidateActiveControl(Cancel)
End Sub
例えば、`txtAge` の `Tag` プロパティに `Type=Num;Min=0;Max=120;Required=True` と設定しておくだけで、年齢の数値チェック、範囲チェック、必須チェックが完全に自動化されるのだ。
—
4. チーフアーキテクトからの実践的アドバイス:運用時の注意点
この設計を実際の業務システムに投入するにあたり、現場で起きがちなトラブルを防ぐための知見を共有しておく。
1. マルチユーザー環境(ネットワーク共有)における注意点
Accessをファイルサーバー上で複数人に共有させる場合、コードのコンパイル状態が破損するリスク(BSTRの破損など)が常に伴う。フロントエンド(UI)とバックエンド(データ)は必ず分割し、フロントエンドは各ローカルPCに必ずローカルコピーとして配置して運用すること。さもなくば、この優美なバリデーション関数がネットワークの瞬断によって動作不安定になる。
2. トランザクションとフォーム全体の最終チェック
今回紹介した `BeforeUpdate` による個別チェックは「入力の取りこぼし」を防ぐ最強の盾だが、フォーム全体の保存直前(フォームの `BeforeUpdate` イベント)において、再度すべての入力項目を走査する「一括バリデーション」を組み合わせることで、堅牢性はさらに別次元へと引き上げられる。
—
おわりに:保守性を制する者が、Access開発を制する
「動けばいいや」で作られたスパゲッティコードは、数ヶ月後の自分、あるいは後任のエンジニアにとっての呪いとなる。
今回紹介した `Screen.ActiveControl` と `Tag` プロパティを組み合わせた動的バリデーションの設計手法は、コードの行数を劇的に削減し、仕様変更に対する圧倒的な耐性を生み出す。
プロフェッショナルとして、美しく、拡張性があり、誰が見てもロジカルなコードを書き続けよう。あなたの作る業務システムが、現場のユーザーにとって最高に信頼できるツールになることを確信している。
