VSDXファイル保存時に必須項目を動的に検証!Visio VBAで実現する堅牢な自動化の秘訣
開発プロジェクトの皆さん、いつもお疲れ様です。本日は、Visio VBAにおける高度な自動化、特にVSDXファイル保存時のイベントフックと内容検証について、皆さんの業務効率化に直結する実践的な知見を共有します。
「単に図面を保存する」というシンプルな操作に、なぜここまでこだわるのか?それは、「保存される図面が、常に一定の品質基準を満たしている状態を保証するため」です。特に、他のシステムとの連携や、後続の自動処理にVisio図面を利用する場合、必須項目が未入力だったり、データ形式が不正だったりすると、システム全体に深刻な影響を与えかねません。
多くの開発現場では、このような品質担保の仕組みが後回しにされがちですが、「バグは仕様の甘さから生まれる」ことを肝に銘じ、初期段階から堅牢な設計を心がける必要があります。
今回のテーマは、Visioの「Document_BeforeDocumentSave」イベントを動的にフックし、保存直前の図面内容を検証する、というものです。クラスモジュールを巧みに活用することで、この高度な制御を、保守性が高く、再利用可能な形で実現します。
なぜ「Document_BeforeDocumentSave」イベントなのか?
Visio VBAには、様々なイベントが用意されています。その中でも「Document_BeforeDocumentSave」イベントは、図面がディスクに保存される直前に発生します。つまり、このイベント内で保存処理をキャンセルすれば、不正な状態の図面が保存されるのを未然に防ぐことができるのです。
「保存後」に検証するのでは遅すぎます。保存後にエラーが発生し、再度修正して保存し直す、という手間は、開発者にとっても、そして最終的なユーザーにとっても、非生産的な時間の浪費です。
クラスモジュール活用のメリット:なぜ「コピペで動く」だけでは不十分なのか
「とりあえず動く」コードは、短期的な解決にはなります。しかし、プロジェクトが進行し、要件が変化するにつれて、そのコードは「負債」となっていきます。
クラスモジュールを活用する最大のメリットは、「カプセル化」と「再利用性」です。
- カプセル化: イベントハンドリングのロジックを、特定のクラスに閉じ込めることができます。これにより、メインのコードがシンプルになり、各部分の責務が明確になります。
- 再利用性: 作成したクラスは、他のVisioドキュメントや、別のプロジェクトでも容易に再利用できます。これは、開発効率を飛躍的に向上させます。
- 保守性: ロジックがクラスに集約されているため、修正や機能追加が必要になった際も、影響範囲を限定しやすく、デバッグも容易になります。
設計思想:疎結合と責務の分離
今回の設計における重要な思想は、「疎結合」と「責務の分離」です。
- 疎結合: イベントハンドラを登録するコードと、実際の検証ロジックを分離します。これにより、検証ロジックの変更が、イベントハンドラ登録処理に影響を与えにくくなります。
- 責務の分離:
- `Document_BeforeDocumentSave` イベントをフックし、検証処理を呼び出す責務。
- 図面内の必須項目をチェックし、検証結果を返す責務。
- 検証結果に基づき、保存をキャンセルする責務。
これらの責務を、それぞれ独立したクラス(またはクラス内のメソッド)に割り当てることで、堅牢で保守性の高いコードが生まれます。
実践:プロダクションコード例と解説
それでは、具体的なコード例を見ていきましょう。ここでは、VSDXファイル内の特定のシェイプに、必須のテキストデータが入力されているかを検証するシナリオを想定します。
1. 検証ロジックを担うクラスモジュール (`clsShapeValidator.cls`)
このクラスは、単一のシェイプを検証する責務を持ちます。
‘==============================================================================
‘ クラス名: clsShapeValidator
‘ 責務: 特定のシェイプに対する検証ロジックをカプセル化する
‘==============================================================================
Option Explicit
‘==============================================================================
‘ プロパティ
‘==============================================================================
Private pstrShapeName As String ‘ 検証対象のシェイプ名 (例: “Rectangle.123”)
Private pstrRequiredTextField As String ‘ 必須のテキストフィールド名 (例: “User.Status”)
Private pstrErrorMessage As String ‘ 検証失敗時のエラーメッセージ
‘==============================================================================
‘ メソッド
‘==============================================================================
‘==============================================================================
‘ 初期化メソッド
‘——————————————————————————
‘ シェイプ名と必須フィールド名を指定してインスタンスを作成する
‘==============================================================================
Public Sub Init(ByVal shapeName As String, ByVal requiredTextField As String)
If Trim(shapeName) = “” Or Trim(requiredTextField) = “” Then
Err.Raise vbObjectError + 5001, “clsShapeValidator.Init”, “シェイプ名と必須フィールド名は空にできません。”
End If
Me.pstrShapeName = shapeName
Me.pstrRequiredTextField = requiredTextField
End Sub
‘==============================================================================
‘ 検証実行メソッド
‘——————————————————————————
‘ 指定されたシェイプが必須フィールドに値を持っているかを検証する
‘ 戻り値: Boolean – True (検証OK), False (検証NG)
‘==============================================================================
Public Function Validate(ByVal targetShape As Visio.Shape) As Boolean
Dim isValid As Boolean
isValid = False
Me.pstrErrorMessage = “” ‘ エラーメッセージをリセット
On Error GoTo ErrorHandler
‘ シェイプ名が一致するか確認
If targetShape.Name = Me.pstrShapeName Then
‘ User.Properties または CustomProperties で指定のフィールドの値を取得
‘ Visio 2013以降では CustomProperties が推奨されるが、互換性のために両方チェックする
Dim propValue As String
propValue = “”
‘ User.Properties の場合
On Error Resume Next ‘ User.Properties が存在しない場合のエラーを無視
propValue = targetShape.CellsU(Me.pstrRequiredTextField).ResultIU ‘ .ResultIU で国際単位系で取得
On Error GoTo ErrorHandler ‘ エラーハンドリングを元に戻す
‘ CustomProperties の場合 (User.Properties と同じ名前で存在する場合がある)
If propValue = “” Then
On Error Resume Next
‘ CustomProperties を直接 CellsU で参照する場合、名前が “User.FieldName” 形式である必要がある
‘ もしくは、CustomPropertiesコレクションから探す
Dim prop As Visio.CustomProperty
For Each prop In targetShape.CustomProperties
If UCase(prop.Name) = UCase(Me.pstrRequiredTextField) Then
propValue = prop.Value
Exit For
End If
Next prop
On Error GoTo ErrorHandler
End If
‘ 値が空でないかチェック
If Trim(propValue) <> “” Then
isValid = True
Else
Me.pstrErrorMessage = “‘” & Me.pstrShapeName & “‘ の必須項目 ‘” & Me.pstrRequiredTextField & “‘ が入力されていません。”
End If
Else
‘ シェイプ名が一致しない場合は、このバリデーターの担当ではないため、成功とみなす
‘ (イベントハンドラ側で、担当するバリデーターのメソッドのみを呼び出すようにする)
isValid = True
End If
Validate = isValid
Exit Function
ErrorHandler:
Me.pstrErrorMessage = “検証処理中に予期せぬエラーが発生しました: ” & Err.Description
Validate = False ‘ エラー発生時は検証失敗とする
Debug.Print “Error in clsShapeValidator.Validate: ” & Err.Number & ” – ” & Err.Description
End Function
‘==============================================================================
‘ エラーメッセージ取得メソッド
‘——————————————————————————
‘ 検証失敗時のエラーメッセージを返す
‘==============================================================================
Public Function GetErrorMessage() As String
GetErrorMessage = Me.pstrErrorMessage
End Function
コード解説:
- `Init` メソッド: インスタンス化時に、検証対象のシェイプ名と必須フィールド名を指定します。これにより、バリデーターは特定の責務に特化します。
- `Validate` メソッド: `Visio.Shape` オブジェクトを受け取り、そのシェイプが指定された必須フィールドに値を持っているかをチェックします。
- `CellsU(Me.pstrRequiredTextField).ResultIU`: シェイプのセルから値を取得する標準的な方法です。`.ResultIU` は、国際単位系 (International Units) で値を取得することを保証し、通貨や長さなどの単位による影響を受けにくくします。
- `CustomProperties` のチェック: Visioのバージョンによっては `User.Properties` ではなく `CustomProperties` コレクションに格納されるため、互換性のために両方のケースを考慮しています。
- `GetErrorMessage` メソッド: 検証が失敗した場合に、ユーザーに分かりやすいエラーメッセージを返します。
2. イベントフックと検証実行を管理するクラスモジュール (`clsDocumentEvents.cls`)
このクラスは、Visioドキュメントのイベントをフックし、登録されたバリデーターを実行する責務を持ちます。
‘==============================================================================
‘ クラス名: clsDocumentEvents
‘ 責務: Visioドキュメントのイベントをフックし、検証処理を管理する
‘==============================================================================
Option Explicit
‘ Visioアプリケーションオブジェクトへの参照 (グローバルスコープで設定されることを想定)
Private WithEvents mApp As Visio.Application
‘ 現在アクティブなドキュメントへの参照
Private mDoc As Visio.Document
‘ 登録されたバリデーターのコレクション
‘ Key: シェイプ名, Value: clsShapeValidator オブジェクト
Private mValidatorCollection As Collection
‘==============================================================================
‘ プロパティ
‘==============================================================================
‘==============================================================================
‘ メソッド
‘==============================================================================
‘==============================================================================
‘ 初期化メソッド
‘——————————————————————————
‘ Visioアプリケーションオブジェクトと対象ドキュメントを指定して初期化する
‘==============================================================================
Public Sub Init(ByRef visApp As Visio.Application, ByRef targetDoc As Visio.Document)
If visApp Is Nothing Then
Err.Raise vbObjectError + 5002, “clsDocumentEvents.Init”, “Visio Application オブジェクトが指定されていません。”
End If
If targetDoc Is Nothing Then
Err.Raise vbObjectError + 5002, “clsDocumentEvents.Init”, “対象 Visio Document オブジェクトが指定されていません。”
End If
Set mApp = visApp
Set mDoc = targetDoc
‘ バリデーターコレクションを初期化
Set mValidatorCollection = New Collection
‘ イベントハンドラを登録 (Document_BeforeDocumentSave)
‘ mApp 経由で DocumentBeforeSave イベントをフックする (これはグローバルイベント)
‘ Document オブジェクト自体のイベントもフック可能だが、ここではアプリケーションレベルで管理
End Sub
‘==============================================================================
‘ バリデーター追加メソッド
‘——————————————————————————
‘ 検証ロジックを持つ clsShapeValidator オブジェクトを登録する
‘==============================================================================
Public Sub AddValidator(ByVal validator As clsShapeValidator)
If validator Is Nothing Then Exit Sub
‘ 重複登録を防ぐ (必要であれば)
On Error Resume Next
mValidatorCollection.Add validator, validator.pstrShapeName ‘ シェイプ名をキーとして追加
On Error GoTo 0
End Sub
‘==============================================================================
‘ 検証実行メソッド
‘——————————————————————————
‘ ドキュメント内の全シェイプに対して、登録されたバリデーターを実行する
‘ 戻り値: Boolean – True (すべてOK), False (いずれかNG)
‘==============================================================================
Public Function RunAllValidators() As Boolean
Dim shp As Visio.Shape
Dim validator As clsShapeValidator
Dim allValid As Boolean
Dim errorMessage As String
allValid = True
errorMessage = “”
If mValidatorCollection.Count = 0 Then
‘ バリデーターが登録されていない場合は、検証せずに成功とみなす
RunAllValidators = True
Exit Function
End If
‘ ドキュメント内の全シェイプをループ
For Each shp In mDoc.Pages(1).Shapes ‘ 例として最初のページのみを対象
‘ 各シェイプに対して、登録されているバリデーターを試行
Dim shpName As String
shpName = shp.Name
‘ ValidatorCollection にこのシェイプ名で登録されているバリデーターがあるかチェック
On Error Resume Next
Set validator = mValidatorCollection(shpName) ‘ シェイプ名をキーに取得
On Error GoTo 0
If Not validator Is Nothing Then
‘ このシェイプを担当するバリデーターが見つかった場合
If Not validator.Validate(shp) Then
allValid = False
errorMessage = errorMessage & validator.GetErrorMessage() & vbCrLf
End If
Set validator = Nothing ‘ 次のループのためにリセット
End If
Next shp
‘ 全てのバリデーターが実行された後、エラーメッセージを返す
If Not allValid Then
‘ エラーメッセージを Visio のステータスバーに表示するなどの処理
MsgBox “図面保存エラー:” & vbCrLf & errorMessage, vbCritical, “保存検証エラー”
RunAllValidators = False
Else
RunAllValidators = True
End If
End Function
‘==============================================================================
‘ イベントハンドラ
‘==============================================================================
‘==============================================================================
‘ DocumentBeforeSave イベント
‘——————————————————————————
‘ 図面が保存される直前に呼び出される
‘==============================================================================
Private Sub mApp_DocumentBeforeSave(ByVal Doc As Visio.Document, ByVal SaveFlags As Integer, ByRef pbCancel As Boolean)
‘ 対象ドキュメントの保存イベントか確認
If Doc.FullName = mDoc.FullName Then
‘ 検証を実行
If Not RunAllValidators() Then
‘ 検証が失敗した場合、保存をキャンセルする
pbCancel = True
MsgBox “必須項目が入力されていないため、保存をキャンセルしました。内容を確認してください。”, vbExclamation, “保存キャンセル”
End If
End If
End Sub
‘==============================================================================
‘ クリーンアップメソッド
‘——————————————————————————
‘ オブジェクト参照を解放する
‘==============================================================================
Public Sub Cleanup()
Set mApp = Nothing
Set mDoc = Nothing
Set mValidatorCollection = Nothing
End Sub
コード解説:
- `Init` メソッド: `Visio.Application` オブジェクトと、イベントをフックしたい `Visio.Document` オブジェクトを受け取ります。
- `AddValidator` メソッド: `clsShapeValidator` のインスタンスをコレクションに追加します。シェイプ名をキーとすることで、後で効率的に検索できるようにしています。
- `RunAllValidators` メソッド: ドキュメント内の各ページ(ここでは最初のページのみ)のシェイプをループし、登録されているバリデーターが担当するシェイプであれば、その `Validate` メソッドを呼び出します。
- `mApp_DocumentBeforeSave` イベントハンドラ:
- `WithEvents` キーワードにより、`mApp` オブジェクトで発生するイベントをこのサブプロシージャで捕捉できます。
- `Doc.FullName = mDoc.FullName` で、対象のドキュメントの保存イベントであることを確認します。
- `RunAllValidators` を呼び出し、結果が `False`(検証失敗)であれば、`pbCancel = True` と設定して保存をキャンセルします。
- `Cleanup` メソッド: オブジェクトの参照を解放し、メモリリークを防ぎます。
3. メインモジュール (`modMain.bas`)
このモジュールは、クラスモジュールをインスタンス化し、イベントハンドリングをセットアップする役割を担います。
‘==============================================================================
‘ モジュール名: modMain
‘ 責務: アプリケーションの起動、クラスインスタンスの生成、イベントハンドリングのセットアップ
‘==============================================================================
Option Explicit
‘ グローバル変数として、イベント管理クラスのインスタンスを保持
‘ これにより、アプリケーションのライフサイクル全体でイベントハンドラが有効になる
Public g_DocumentEventHandler As clsDocumentEvents
‘==============================================================================
‘ サブルーチン
‘==============================================================================
‘==============================================================================
‘ InitializeEventHandling
‘——————————————————————————
‘ イベントハンドリングを初期化する
‘ Visio起動時や、特定のドキュメントを開いた際に呼び出す
‘==============================================================================
Public Sub InitializeEventHandling()
Dim visApp As Visio.Application
Dim currentDoc As Visio.Document
On Error GoTo ErrorHandler
‘ Visioアプリケーションオブジェクトを取得
Set visApp = Visio.Application
‘ 現在アクティブなドキュメントを取得
‘ ドキュメントが開かれていない場合は何もしない
If visApp.Documents.Count > 0 Then
Set currentDoc = visApp.ActiveDocument
‘ イベントハンドラクラスのインスタンスを生成・初期化
Set g_DocumentEventHandler = New clsDocumentEvents
g_DocumentEventHandler.Init visApp, currentDoc
‘ — ここで、具体的な検証ロジックを登録 —
‘ 例: ShapeName=”Rectangle.123″, User.Status を必須とする場合
Dim validator1 As clsShapeValidator
Set validator1 = New clsShapeValidator
validator1.Init “Rectangle.123”, “User.Status” ‘ User.Status は ShapeSheet の User-Defined Cells の Name
g_DocumentEventHandler.AddValidator validator1
Set validator1 = Nothing ‘ 不要になったら解放
‘ 例: ShapeName=”MyProcessShape”, User.DueDate を必須とする場合
Dim validator2 As clsShapeValidator
Set validator2 = New clsShapeValidator
validator2.Init “MyProcessShape”, “User.DueDate”
g_DocumentEventHandler.AddValidator validator2
Set validator2 = Nothing
‘ — 必要に応じて、さらにバリデーターを追加 —
Debug.Print “Event handling initialized for document: ” & currentDoc.Name
Else
Debug.Print “No active document found. Event handling not initialized.”
‘ ドキュメントが開かれていない場合、g_DocumentEventHandler を解放しておく
If Not g_DocumentEventHandler Is Nothing Then
g_DocumentEventHandler.Cleanup
Set g_DocumentEventHandler = Nothing
End If
End If
Exit Sub
ErrorHandler:
MsgBox “イベントハンドリングの初期化中にエラーが発生しました: ” & Err.Description, vbCritical, “初期化エラー”
‘ エラー発生時も、既存のハンドラをクリーンアップしておく
If Not g_DocumentEventHandler Is Nothing Then
g_DocumentEventHandler.Cleanup
Set g_DocumentEventHandler = Nothing
End If
Debug.Print “Error in InitializeEventHandling: ” & Err.Number & ” – ” & Err.Description
End Sub
‘==============================================================================
‘ CleanupEventHandling
‘——————————————————————————
‘ イベントハンドリングをクリーンアップする
‘ Visio終了時や、ドキュメントを閉じる際に呼び出す
‘==============================================================================
Public Sub CleanupEventHandling()
If Not g_DocumentEventHandler Is Nothing Then
g_DocumentEventHandler.Cleanup
Set g_DocumentEventHandler = Nothing
Debug.Print “Event handling cleaned up.”
End If
End Sub
‘==============================================================================
‘ テスト用サブルーチン (手動実行用)
‘——————————————————————————
‘ イベントハンドリングを初期化し、検証ロジックを登録する
‘==============================================================================
Public Sub Test_Initialize()
Call InitializeEventHandling
End Sub
‘==============================================================================
‘ テスト用サブルーチン (手動実行用)
‘——————————————————————————
‘ イベントハンドリングをクリーンアップする
‘==============================================================================
Public Sub Test_Cleanup()
Call CleanupEventHandling
End Sub
コード解説:
- `g_DocumentEventHandler`: グローバル変数として `clsDocumentEvents` のインスタンスを保持します。これにより、Visioアプリケーションのライフサイクル全体でイベントハンドラが有効になります。
- `InitializeEventHandling` サブルーチン:
- Visioアプリケーションとアクティブなドキュメントを取得します。
- `clsDocumentEvents` のインスタンスを生成し、`Init` メソッドで初期化します。
- ここが重要: `clsShapeValidator` のインスタンスを生成し、`Init` メソッドで検証対象のシェイプ名と必須フィールド名を指定して `AddValidator` メソッドで登録します。この部分を、実際の図面テンプレートや要件に合わせてカスタマイズしてください。
- `CleanupEventHandling` サブルーチン: `clsDocumentEvents` のインスタンスを解放し、イベントハンドラを無効化します。
- `Test_Initialize` および `Test_Cleanup`: VBAエディタから手動で実行できるテスト用のプロシージャです。
ファイル連携・データベース連携の注意点
VBAで外部ファイルやデータベースと連携する場合、以下の点に注意が必要です。
1. ファイルパスの固定化: コード内に直接ファイルパスを記述するのは避けるべきです。設定ファイルやレジストリ、あるいはVisioドキュメントのカスタムプロパティなどにパスを格納し、動的に読み込むように設計してください。
2. エラーハンドリングの徹底: ファイルが存在しない、アクセス権がない、データベース接続に失敗するなど、外部リソースへのアクセスは常にエラーの温床です。`On Error Resume Next` や `On Error GoTo` を適切に使用し、詳細なエラーログを記録するようにしてください。
3. トランザクション管理: データベース連携の場合、一連の処理が完了しなかった場合に、状態を元に戻すためのトランザクション管理が不可欠です。
4. データ形式の整合性: 外部から読み込んだデータ、あるいは外部へ書き出すデータは、Visio図面内のデータと形式が一致している必要があります。型変換の際には、予期せぬエラーが発生しないよう、十分なテストを行ってください。
5. パフォーマンス: 大量のデータを一度に読み書きすると、Visioの動作が著しく遅くなる可能性があります。バッチ処理や非同期処理の検討、またはデータ量の削減を検討してください。
保守性の高い設計のためのヒント
- 命名規則の徹底: 変数名、プロシージャ名、クラス名には、その役割が明確にわかるような命名規則を適用してください。
- コメントの活用: コードの意図や複雑なロジック、後で修正が必要になる可能性のある箇所には、丁寧なコメントを記述します。
- モジュール化: 各クラスやモジュールは、単一の責務を持つように設計します。これにより、コードの再利用性や保守性が向上します。
- デバッグログ: `Debug.Print` を活用して、処理の流れや変数の値を確認できるようにしておきます。これは、問題発生時の原因究明に非常に役立ちます。
- バージョン管理: Gitなどのバージョン管理システムを使用して、コードの変更履歴を管理します。これにより、以前の状態に戻したり、変更内容を追跡したりすることが容易になります。
まとめ
本日は、Visio VBAの「Document_BeforeDocumentSave」イベントを動的にフックし、図面内容を検証する高度なテクニックについて解説しました。クラスモジュールを効果的に活用することで、堅牢で保守性の高い自動化システムを構築できることをご理解いただけたかと思います。
「保存される図面は常に正しい」という状態を保証することは、後続のプロセスやシステム連携における「バグの温床」を排除する第一歩です。今回ご紹介した設計思想とコード例が、皆さんの開発プロジェクトにおける業務効率化と品質向上の一助となれば幸いです。
不明な点や、さらに踏み込んだ実装についてご質問があれば、遠慮なくお声がけください。皆さんの開発現場に、さらなる効率化と品質向上をもたらすことを期待しています。
