Visio VBAの世界へようこそ!伝説のチーフアーキテクトが贈る、極限の知見シリーズへようこそ。
今回は、単なるマクロの記録や、既存のイベントを少し触るだけでは決して到達できない、Visio VBAの真髄に触れるテーマです。それは、VSDXファイルの保存処理を根底から掌握し、あなたのビジネスロジックを組み込む、まさに「神の領域」とも言える制御メカニズム。
「図面を保存する前に、特定の情報がきちんと入力されているか確認したい…」
「もし不備があったら、強制的に保存をキャンセルして、ユーザーに修正を促したい…」
こんな高度な要求、`ThisDocument`モジュールだけで事足りるとお考えですか?残念ながら、それは表層的な理解に過ぎません。複数のドキュメントを同時に制御したり、アプリケーション全体で一貫したルールを適用したりする際には、もっと洗練されたアプローチが求められます。
さあ、プログラミング初学者の方も、マクロ記録の呪縛から解き放たれたい方も、ご安心ください。私が、その「なぜ」と「どうすれば」を、優しく、そして本質的に解説していきます。ここをクリアすれば、Visio VBAの基本はバッチリですよ!
—
【上級者向け】VSDXファイル内の「Document_BeforeDocumentSave」イベントを動的にフックし、保存内容を検証する
1. なぜ「動的なフック」が必要なのか?:イベント駆動プログラミングの本質
Visio VBAにおけるイベントハンドリングは、特定の出来事(イベント)が発生したときに、あらかじめ定義しておいた処理(イベントプロシージャ)を実行する仕組みです。
通常、特定のドキュメント(ファイル)に関するイベント(例えば、ドキュメントが開かれた、保存される直前など)を処理したい場合、多くの人はそのドキュメントのVBAプロジェクトにある`ThisDocument`モジュールにコードを記述します。
‘ ThisDocumentモジュールに記述する一般的なイベントハンドラ
Private Sub Document_BeforeDocumentSave(ByVal Doc As Visio.Document)
‘ ここに保存前の処理を書く
MsgBox “保存直前です!”
‘ Cancel = True ‘ これで保存をキャンセルできる
End Sub
これはこれで機能します。しかし、もしあなたがこんな状況に遭遇したらどうでしょう?
- 開いている全てのVisioドキュメントに対して、同じ保存前検証ルールを適用したい。
- 特定の条件を満たすドキュメント(例:特定のテンプレートから作成されたもの)だけを対象に、イベントを監視したい。
- VBAプロジェクトを持たない(または持たせたくない)ドキュメントに対しても、外部からイベント監視を仕掛けたい。
`ThisDocument`モジュールは、そのドキュメント自身に紐づく「自己申告型」のイベントハンドラです。これでは、他のドキュメントや、まだ開かれていないドキュメントを「監視する」ことはできません。
そこで登場するのが、「クラスモジュールを活用した動的なイベントフック」です。これは、Visioアプリケーションが提供する「イベント発生器」に対して、あなたが独自に作成した「イベントキャッチャー(捕獲網)」を仕掛けるようなイメージです。
2. イベントキャッチャーの作成:`WithEvents`キーワードの魔法
Visioには、大きく分けて二種類のイベントがあります。
1. Applicationレベルのイベント: Visioアプリケーション全体で発生するイベント(例: 新しいドキュメントが作成された、Visioが終了する、など)。
2. Documentレベルのイベント: 特定のドキュメント内で発生するイベント(例: ドキュメントが保存される、ページが追加された、シェイプが選択された、など)。
今回のテーマである`Document_BeforeDocumentSave`は、まさに2番目のDocumentレベルのイベントです。これを動的に捕まえるには、`WithEvents`キーワードを使い、イベントを発生させるオブジェクト(この場合は`Visio.Document`オブジェクト)をクラスモジュール内で宣言します。
ステップ1:イベントハンドラを持つクラスモジュールを作成する
まず、VBAエディタ(Alt + F11)を開き、「挿入」メニューから「クラスモジュール」を選択します。作成したクラスモジュールの名前を、例えば`clsDocumentEvents`とします。(プロパティウィンドウで名前を変更できます)
そして、そのクラスモジュールに以下のコードを記述します。
‘————————————————————————————————–
‘ クラスモジュール名: clsDocumentEvents
‘ 目的: Visioドキュメントのイベントを動的にフックし、保存前検証ロジックを実行する
‘————————————————————————————————–
Option Explicit
‘ Public WithEvents を使用して、Visio.Document オブジェクトのイベントを「捕まえる」準備をします。
‘ AppDoc という名前は任意ですが、ここでは「アプリケーションのドキュメント」としています。
Public WithEvents AppDoc As Visio.Document
‘ Document_BeforeDocumentSave イベントプロシージャ
‘ ドキュメントが保存される直前にVisioによって自動的に呼び出されます。
‘ 引数 Doc: 保存しようとしている Visio.Document オブジェクト自身
‘ 引数 Cancel: True に設定すると、保存処理がキャンセルされます
Private Sub AppDoc_BeforeDocumentSave(ByVal Doc As Visio.Document)
Debug.Print “イベント発生: Document_BeforeDocumentSave for ” & Doc.Name
Dim blnIsValid As Boolean
blnIsValid = True ‘ 初期値は「有効(検証OK)」とします
‘———————————————————————————————-
‘ ここに保存内容を検証するロジックを記述します
‘ 例: アクティブページの特定のシェイプデータ(Shape Data)が入力されているかチェック
‘———————————————————————————————-
Const C_TARGET_SHAPEDATA_NAME As String = “Prop.ProjectName” ‘ 検証対象のShape Data名 (例: プロジェクト名)
Dim shp As Visio.Shape
Dim page As Visio.Page
Dim strShapeDataValue As String
‘ 現在アクティブなページを取得
Set page = Doc.ActivePage
‘ アクティブページ内の全てのシェイプをループ
For Each shp In page.Shapes
‘ 指定したShape Dataが存在するかチェック
If shp.CellExistsU(C_TARGET_SHAPEDATA_NAME, visExistsAnywhere) Then
‘ Shape Dataの値を取得
strShapeDataValue = shp.CellsU(C_TARGET_SHAPEDATA_NAME).ResultStr(“”)
‘ 値が空文字列でないか検証
If Trim(strShapeDataValue) = “” Then
‘ 値が空であれば、検証失敗
blnIsValid = False
Exit For ‘ 最初の不備が見つかった時点でループを抜ける
End If
End If
Next shp
‘ 検証結果に基づいて処理を分岐
If Not blnIsValid Then
‘ 検証が失敗した場合、ユーザーにメッセージを表示し、保存をキャンセル
MsgBox “エラー: アクティブページ内に必須の『” & Replace(C_TARGET_SHAPEDATA_NAME, “Prop.”, “”) & “』情報を持つ” & _
“シェイプが見つからないか、そのプロパティが空です。” & vbCrLf & _
“保存をキャンセルします。内容を確認・修正してください。”, _
vbCritical + vbOKOnly, “Visio保存検証エラー”
Cancel = True ‘ ここが重要!保存処理をキャンセルします
Else
Debug.Print “保存前検証: OK. ドキュメントの保存を続行します。”
End If
‘ 他の複雑な検証ロジックもここに追記できます
‘ 例: 特定のレイヤー上のシェイプ数、特定のテキストボックスの内容、など
End Sub
コードのポイント:
- `Public WithEvents AppDoc As Visio.Document`: これが魔法の呪文です。`AppDoc`という名前の`Visio.Document`オブジェクトが、イベントを発生させたら「私に教えて!」と宣言しています。`Public`にしておくことで、外部からこの`AppDoc`に実際の`Visio.Document`オブジェクトを割り当てられるようになります。
- `Private Sub AppDoc_BeforeDocumentSave(…)`: `WithEvents`で宣言したオブジェクト名(`AppDoc`)と、Visioが提供するイベント名(`BeforeDocumentSave`)を組み合わせたプロシージャ名です。この中に、保存直前に実行したい検証ロジックを記述します。
- `Cancel = True`: 検証の結果、保存を許可しないと判断した場合に、この行を実行することで保存処理を中断させることができます。これは非常に強力な制御です。
3. イベントキャッチャーをVisioドキュメントに「仕掛ける」
次に、作成したイベントキャッチャー(`clsDocumentEvents`のインスタンス)を、実際にVisioドキュメントに関連付ける必要があります。これは、標準モジュールか、あるいは特定の`ThisDocument`モジュールに記述します。
ここでは、VBAプロジェクト全体で開いている全てのドキュメントを監視するパターンを想定し、標準モジュールに記述します。
ステップ2:標準モジュールにイベントフックの管理ロジックを記述する
「挿入」メニューから「標準モジュール」を追加し、モジュール名を例えば`modEventHooks`とします。
‘————————————————————————————————–
‘ 標準モジュール名: modEventHooks
‘ 目的: clsDocumentEvents クラスのインスタンスを管理し、Visioドキュメントにイベントフックを設定する
‘————————————————————————————————–
Option Explicit
‘ イベントハンドラを保持するためのコレクション
‘ このコレクションがイベントハンドラのインスタンスをメモリ上に維持する「命綱」です。
‘ これがないと、インスタンスがすぐに破棄されてしまい、イベントを捕まえられなくなります。
Private colDocEventHandlers As Collection
‘————————————————————————————————–
‘ Visio起動時、または手動でイベントフックを設定するサブルーチン
‘————————————————————————————————–
Sub InitializeDocumentEventHandlers()
‘ 既存のコレクションがあれば、一旦クリアしてリセットします。
‘ これにより、以前のイベントフックが解除され、新しいフックが再設定されます。
If Not colDocEventHandlers Is Nothing Then
Set colDocEventHandlers = Nothing
End If
Set colDocEventHandlers = New Collection
Call HookAllDocumentsInApplication
MsgBox “全ての開いているVisioドキュメントに対して、保存前検証イベントをフックしました。”, _
vbInformation + vbOKOnly, “イベントフック設定完了”
End Sub
‘————————————————————————————————–
‘ 現在開いている全てのVisioドキュメントにイベントフックを設定する
‘————————————————————————————————–
Sub HookAllDocumentsInApplication()
Dim doc As Visio.Document
Dim clsHandler As clsDocumentEvents
‘ Visioアプリケーションが現在開いている全てのドキュメントをループします
For Each doc In Visio.Application.Documents
‘ 新しいイベントハンドラクラスのインスタンスを作成
Set clsHandler = New clsDocumentEvents
‘ クラスモジュール内の Public WithEvents AppDoc に、現在のドキュメントを割り当てます
‘ これで、このドキュメントのイベントが発生すると、clsHandler 内のイベントプロシージャが呼び出されます
Set clsHandler.AppDoc = doc
‘ 作成したイベントハンドラインスタンスをコレクションに追加します
‘ キーとしてドキュメントIDを使用することで、特定のドキュメントのハンドラを識別できます
‘ これにより、clsHandler インスタンスがメモリ上に保持され、イベントを継続的に監視できます
colDocEventHandlers.Add clsHandler, CStr(doc.ID)
Debug.Print “フック設定: ドキュメント ‘” & doc.Name & “‘ (ID: ” & doc.ID & “)”
Next doc
End Sub
‘————————————————————————————————–
‘ イベントフックを解除するサブルーチン
‘ Visioを閉じる前や、不要になった場合に呼び出します。
‘ これにより、メモリリソースを解放し、不要なイベント監視を停止します。
‘————————————————————————————————–
Sub UnhookAllDocuments()
If Not colDocEventHandlers Is Nothing Then
Set colDocEventHandlers = Nothing ‘ コレクションを破棄すると、その中の全てのインスタンスも解放されます
MsgBox “全てのVisioドキュメントの保存前検証イベントフックを解除しました。”, _
vbInformation + vbOKOnly, “イベントフック解除完了”
Else
MsgBox “イベントフックは設定されていません。”, _
vbInformation + vbOKOnly, “イベントフック解除”
End If
End Sub
‘————————————————————————————————–
‘ 応用: 新しいドキュメントが開かれたり作成されたりしたときに、自動的にフックを追加する
‘ (この機能は、Visio.Application オブジェクトのイベントを WithEvents でフックする必要があります)
‘ この例では、アプリケーションレベルのイベントハンドラを別途用意する必要があります。
‘ そのためのクラスモジュール(例: clsApplicationEvents)が必要になりますが、
‘ 今回のテーマの範囲を超えるため、ここではコメントアウトのみとします。
‘————————————————————————————————–
‘ Private WithEvents g_VisioApp As Visio.Application
‘ Private Sub Class_Initialize()
‘ Set g_VisioApp = Visio.Application
‘ End Sub
‘ Private Sub g_VisioApp_DocumentOpened(ByVal Doc As Visio.Document)
‘ Dim clsHandler As clsDocumentEvents
‘ Set clsHandler = New clsDocumentEvents
‘ Set clsHandler.AppDoc = Doc
‘ colDocEventHandlers.Add clsHandler, CStr(Doc.ID)
‘ Debug.Print “新規ドキュメントオープン時にフック追加: ” & Doc.Name
‘ End Sub
‘ Private Sub g_VisioApp_DocumentCreated(ByVal Doc As Visio.Document)
‘ Dim clsHandler As clsDocumentEvents
‘ Set clsHandler = New clsDocumentEvents
‘ Set clsHandler.AppDoc = Doc
‘ colDocEventHandlers.Add clsHandler, CStr(Doc.ID)
‘ Debug.Print “新規ドキュメント作成時にフック追加: ” & Doc.Name
‘ End Sub
コードのポイント:
- `Private colDocEventHandlers As Collection`: これが非常に重要です!`WithEvents`で宣言したクラスのインスタンスは、どこかの変数に保持されていなければ、VBAによってすぐにメモリから解放されてしまいます(ガベージコレクションの対象となるため)。`Collection`オブジェクトにインスタンスを追加しておくことで、VBAプロジェクトが実行されている間、これらのインスタンスが「生き続け」、イベントを監視し続けることができます。
- `Set clsHandler = New clsDocumentEvents`: `clsDocumentEvents`クラスの新しいインスタンスを作成しています。
- `Set clsHandler.AppDoc = doc`: ここで、新しく作成した`clsDocumentEvents`のインスタンス(`clsHandler`)に対して、実際に監視したい`Visio.Document`オブジェクト(`doc`)を割り当てています。これにより、`clsHandler`内の`AppDoc_BeforeDocumentSave`が、この`doc`の保存イベントに反応するようになります。
- `colDocEventHandlers.Add clsHandler, CStr(doc.ID)`: インスタンスをコレクションに追加しています。`doc.ID`をキーとして使うことで、どのドキュメントのイベントハンドラかを識別しやすくしています。
- `UnhookAllDocuments()`: アプリケーションが終了する際や、不要になったイベントフックを明示的に解除するためのプロシージャです。`Set colDocEventHandlers = Nothing`とすることで、コレクションとその中の全ての`clsDocumentEvents`インスタンスがメモリから解放され、イベント監視が停止します。これにより、メモリリークを防ぎ、アプリケーションの安定性を高めます。
4. 動作確認と注意点
動作確認
1. VBAエディタで上記のクラスモジュールと標準モジュールをセットアップします。
2. Visioで新しい図面ファイルを開くか、既存の図面ファイルを開きます。
3. 標準モジュール内の`InitializeDocumentEventHandlers`プロシージャを実行します。
- VBAエディタでプロシージャ内にカーソルを置き、F5キーを押すか、実行ボタンをクリックします。
- 「全ての開いているVisioドキュメントに対して、保存前検証イベントをフックしました。」というメッセージが表示されるはずです。
4. Visioに戻り、図面内にShape Data「ProjectName」を持つシェイプを作成し、その値を空のままにして保存を試みます。
- (シェイプを作成し、右クリック → データ → シェイプデータ で「ProjectName」という行を追加し、値を空にします)
- `BeforeDocumentSave`イベントが発火し、`clsDocumentEvents`で記述した検証ロジックが実行されます。
- Shape Dataが空の場合、「Visio保存検証エラー」のメッセージボックスが表示され、保存がキャンセルされるはずです。
5. 次に、Shape Data「ProjectName」に何らかの値を入力して、再度保存を試みます。
- 今度は検証がパスし、図面が正常に保存されるはずです。
6. `UnhookAllDocuments`を実行し、フックを解除してから保存を試みてください。
- 検証メッセージが表示されずに保存されるはずです。
オブジェクトのライフサイクルとパフォーマンスの重み:伝説のチーフアーキテクトからの助言
今回のような`WithEvents`を使った動的なイベントハンドリングは非常に強力ですが、その裏側にある「オブジェクトのライフサイクル」と「パフォーマンス」について深く理解しておく必要があります。
- インスタンスの寿命:
`clsDocumentEvents`のインスタンスは、`colDocEventHandlers`コレクションがメモリに存在し続ける限り、「生きて」イベントを監視し続けます。もしコレクションへの追加を忘れたり、コレクション自体が破棄されてしまったりすると、インスタンスはすぐに寿命を終え、イベントは機能しなくなります。これが、コレクションを使う最大の理由です。
- メモリとパフォーマンス:
イベントハンドラのインスタンスは、それぞれがメモリを消費します。多数のドキュメントを開き、それぞれに複雑なイベントハンドラを仕掛けると、それだけメモリ使用量が増加し、アプリケーションの応答性が低下する可能性があります。
不要になったイベントフックは、必ず`Set colDocEventHandlers = Nothing`などで明示的に解放(破棄)することが重要です。これにより、メモリリークを防ぎ、安定した動作を維持できます。これは、システムを設計する上で最も基本的ながら、最も見落とされがちなポイントの一つです。
5. まとめと次のステップ
お疲れ様でした!今回は、Visio VBAでVSDXファイルの保存イベントを動的にフックし、保存内容を検証する高度なテクニックを学びました。
- `ThisDocument`モジュールだけでは限界があること。
- `WithEvents`キーワードを使ったクラスモジュールで、イベントを「捕まえる」方法。
- `Collection`オブジェクトを使って、イベントハンドラのインスタンスを管理し、その寿命を制御する方法。
- `Cancel = True`で、イベント発生時の処理をキャンセルする強力な制御。
- そして、オブジェクトのライフサイクルとメモリ管理の重要性。
これであなたは、Visio VBAマスターへの道を一歩踏み出しました。
この知識を応用すれば、例えば:
- 複数の特定のテンプレートから作成されたドキュメントだけを対象に、特定の検証ロジックを適用する。
- ドキュメントの開閉イベントをフックし、自動的に図面の初期設定を行ったり、終了時にレポートを生成したりする。
- アプリケーションレベルのイベントをフックし、Visio全体の挙動を制御する。
など、あなたのアイデア次第で無限の可能性が広がります。
プログラミングは、単にコードを記述するだけでなく、その裏にあるオブジェクトの振る舞いや、システム全体への影響を深く理解することで、真の力を発揮します。今日の学びが、あなたのVisio VBAスキルを次のレベルへと引き上げることを願っています!
