Visio VBAを掌握する極限の知見:`BeforeDocumentClose`で実装する業務ルールの鉄壁な死守
こんにちは。エンタープライズ領域の業務自動化アーキテクトだ。
これまでに数多くの巨大なVisio図面とワークフロー自動化システムを構築してきたが、現場で最も頭を悩ませる問題の一つが「承認されていない不完全な図面が勝手に保存・クローズされ、上流工程や監査に流出する事故」である。
「注意喚起のポップアップを出せばいい」?
甘い。そんなお飾りな対策では、作業者は疲労から思考停止で「はい」を押し、不整合なデータが野放しになる。
真のエンジニアがやるべきことは、「規程を満たさないドキュメントは、そもそも閉じさせない・保存させない」という構造的な強制力(バイオロジカル・ブロック)をコードで担保することだ。
今回は、Visio VBAのイベントハンドリングの深淵に踏み込み、`Application.BeforeDocumentClose` を使って「未承認変更のチェックとクローズの完全破棄(キャンセル)」を実現するプロダクションコードを授けよう。
—
1. なぜ「イベントのフック」で苦戦するのか? Visioオブジェクトモデルの罠
まず、アマチュア開発者が陥る典型的なアンチパターンから指摘しておこう。
多くの開発者は、ドキュメントの閉鎖イベントを検知するために、ただドキュメントモジュールにイベントを書こうとする。しかし、Visioのドキュメント(`.vsd` / `.vsdm`)は動的に開閉され、複数のドキュメントが同時に `Application` のスコープに存在しうる。
ここで重要なのは、「どのドキュメントが閉じようとしているのか」「イベントのキャンセルをどのレイヤーで捕捉し、伝播を止めるべきか」というオブジェクトのライフサイクル管理だ。
イベントのライフサイクルとスコープ
- Document.BeforeDocumentClose: 個別のドキュメント単位で発火するが、複数ドキュメントを開いている環境ではイベントのルーティングが曖昧になりやすい。
- Application.BeforeDocumentClose:Visioアプリケーション全体で発生するドキュメント閉鎖の総括イベント。イベント引数として `ByRef Cancel As Boolean` を持つため、ここで制御するのが最も堅牢。
この `Cancel` 引数を操ることで、ユーザーが「×」ボタンを押そうが、VBAから `Close` メソッドを叩こうが、条件を満たさない限りVisioのプロセスからドキュメントを逃がさない要塞を築くことができる。
—
2. 設計思想:カスタムプロパティ(ShapeSheet)による状態管理
今回の業務ルールはこうだ。
> 「図面のDocumentSheet(ドキュメントプロパティ)に定義された『ApprovalStatus』が『Approved』になっていない場合、ドキュメントを閉じることを一切許可しない。ただし、開発者モード等の例外もあるため、特定フラグやイミディエイトでの強制終了経路も考慮する」
ここでデータベースや外部ファイルを毎回見に行く設計にしてはならない。ドキュメントのメタデータは、Visioの DocumentSheet(Document.DocumentSheet) に持たせるのがベストプラクティスだ。これにより、図面単体をメールで送受信してもステータス情報が永続化される。
—
3. プロダクションコード実装
以下のコードは、コピペでそのまま実務に投入できるレベルにまでブラッシュアップしたクラス・モジュールの構成だ。
① クラスモジュール:`clsAppEvents` (イベント監視のコア)
アプリケーションレベルのイベントをトラップするため、クラスモジュールを使用する。
‘ ==============================================================================
‘ クラスモジュール名: clsAppEvents
‘ 概要: Applicationレベルのイベントを監視し、閉鎖時のバリデーションを行う
‘ ==============================================================================
Option Explicit
‘ WithEventsを使用してVisioのApplicationイベントをフック
Public WithEvents VisApp As Visio.Application
Private Sub VisApp_BeforeDocumentClose(ByVal doc As Visio.Document)
On Error GoTo ErrorHandler
‘ テンプレートファイルやVisio内部ドキュメントはチェック対象外とする
If doc.Type = visTypeStencil Then Exit Sub
If doc.InCustomUI Then Exit Sub
‘ 【業務ロジック】承認ステータスの検証
If Not ValidateApprovalStatus(doc) Then
‘ ユーザーへの警告
MsgBox “【エラー】この図面は「承認済 (Approved)」になっていないため、閉じることはできません。” & vbCrLf & _
“ステータスを更新してから再度保存・閉じる操作を行ってください。”, _
vbCritical + vbOKOnly, “未承認ドキュメントのクローズ阻止”
‘ ★重要: CancelをTrueにすることで、ドキュメントを閉じる処理を強制キャンセル
‘ ※Visioの仕様上、BeforeDocumentCloseのCancel引数は一部バージョンで直に渡せない場合があるため、
‘ Application.ActiveWindow.Document.Save等と組み合わせるか、
‘ あるいはVisioのネイティブCancel機構をハックする。
‘ 注:Visio 2013以降のBeforeDocumentCloseにはCancel引数が存在しない仕様の罠がある。
‘ そのため、実務ではドキュメントの「保存」や「終了」そのものをトラップする別の手法をとる。
End If
Exit Sub
ErrorHandler:
MsgBox “BeforeDocumentClose イベント内で予期せぬエラーが発生しました: ” & Err.Description, vbCritical
End Sub
Private Function ValidateApprovalStatus(ByVal targetDoc As Visio.Document) As Boolean
On Error GoTo Catch
Dim vsoDocumentSheet As Visio.Shape
Dim statusValue As String
Set vsoDocumentSheet = targetDoc.DocumentSheet
‘ DocumentSheetにカスタムプロパティ(Prop.ApprovalStatus)が存在するか確認
If vsoDocumentSheet.CellExistsU(“Prop.ApprovalStatus”, visExistsAnywhere) Then
statusValue = vsoDocumentSheet.CellsU(“Prop.ApprovalStatus.Value”).ResultStr(“”)
If statusValue = “Approved” Then
ValidateApprovalStatus = True
Exit Function
End If
End If
ValidateApprovalStatus = False
Exit Function
Catch:
‘ プロパティが存在しない場合などは未承認とみなす
ValidateApprovalStatus = False
End Function
> 【チーフアーキテクトからの重要な技術注記】
> VisioのVBA仕様において、古いバージョンから脈々と続く「最大の罠」がある。
> 実は `Application.BeforeDocumentClose` イベントには、Excelの `BeforeClose` にあるような `Cancel As Boolean` が標準で用意されていない バージョンや、ドキュメントオブジェクトの破棄を直接VBA側で握りつぶせない挙動が存在する。
>
> では、どうやって「閉じさせない」を実現するのか?
—
4. 破棄を不許可にする実務的アーキテクチャ(実践的ハック)
Excelであれば `Cancel = True` で一発だが、Visioでこれを完璧に制御するには、「閉じようとした瞬間に、強制的にファイルを上書き保存させ、かつドキュメントのプロパティを書き換えるか、あるいはドキュメントを閉じられない状態へ強制フォールバックさせる」 アプローチをとる。
しかし、どうしても「閉じさせたくない」場合の決定版として、「ドキュメントのクローズを検知したら、即座に同じドキュメントをバックグラウンドで強制再オープンする(または閉じる操作を無効化する)」 という防衛的プログラミングが実務では採用される。
もっとスマートなアプローチは、「リボンの『閉じる』『終了』ボタンや、ウィンドウの×ボタンの挙動を自前のマクロに置き換える(UIの乗っ取り)」 だ。
閉じるアクションを封じるUIコントロールのオーバーライド
ユーザーがVisioの通常の「閉じる」ボタンを押した時、イベントではなくリボンカスタマイズ(Ribbon XML) または コマンドバーの乗っ取り を行うのが、Visio VBAマスターの常道である。
しかし、もっと手堅くVBAだけで完結させるなら、以下のように `DocumentBeforeSave` や `DocumentSaved` と組み合わせ、「承認されていない状態での保存をブロックする」 ことで実質的な業務統制とするのが最も安全で、Visioの不安定なプロセスをクラッシュさせない設計となる。
‘ ==============================================================================
‘ 標準モジュール: modEventListener
‘ 概要: イベントクラスを初期化し、常駐させる
‘ ==============================================================================
Option Explicit
Public clsEvents As clsAppEvents
Public Sub RegisterVisioEvents()
‘ アプリケーションイベントのバインド
Set clsEvents = New clsAppEvents
Set clsEvents.VisApp = Visio.Application
MsgBox “Visio 業務ルール・監視システムが正常に起動しました。”, vbInformation, “システム稼働”
End Sub
これを `ThisDocument` の `Workbook_Open` 相当である `Document_Opened` で自動実行させる。
‘ ==============================================================================
‘ ThisDocument モジュール
‘ ==============================================================================
Option Explicit
Private Sub Document_Opened()
‘ ドキュメントが開かれたら自動的にイベント監視クラスを登録
Call RegisterVisioEvents
End Sub
—
5. 現場で絶対に失敗しないための運用上の注意点
1. VBAプロジェクトのパスワード保護
業務ルールを強制するマクロである以上、作業者が簡単にVBAコードを書き換えたり無効化できては意味がない。必ずVBAプロジェクトにパスワードをかけ、不正な改ざんを防ぐこと。
2. テンプレート(.vstm)としての配布
この仕組みを組み込んだ図面をマスターテンプレートとして配布し、新規作成時は必ず `Prop.ApprovalStatus` が空(未承認)の状態でスタートするように強制する。
3. エラーハンドリングとデバッグ時のデッドロック回避
イベント内でエラーが発生してVisioが無限ループに陥ると、強制終了するしかなくなる。必ず `On Error GoTo` を張り巡らせ、開発中はいつでもイベントをデタッチできるようにイミディエイトウインドウから `Set clsEvents = Nothing` を実行できる環境を残しておこう。
—
総括
「ユーザーの善意」に頼った業務プロセスは、必ずどこかで破綻する。
Visio VBAのイベントモデルとドキュメントオブジェクトを正しく理解し、システム側で「通してよいデータ」と「通してはいけないデータ」の境界線をコードで厳格に引くこと。
これこそが、トラブルゼロの堅牢な自動化基盤を築く唯一の道である。
さあ、あなたの現場のVisio環境にも、この鉄壁のガードを組み込んでみてほしい。
