Visio VBAを掌握する極限の知見:ActivePage依存からの脱却。複数図面オープン時のヌル参照を防ぐ堅牢なオブジェクト設計
開発プロジェクトの現場において、Visio VBAのコードレビューを行うと、いまだに散見される悪習がある。それが `ActiveWindow.Page` や `ActivePage` といった、「現在画面にアクティブ表示されている状態」に依存したコーディングだ。
「マクロを実行するんだから、今開いているページが操作対象になるのは当たり前だ」――そう考えていないだろうか?
その甘い設計が、将来的にどれほどの致命傷をもたらすか。プロの自動化エンジニアであれば、そのリスクを痛感しているはずだ。
本記事では、複数図面(Document)が同時オープンされる実務環境において、バックグラウンド処理や非同期的な操作でも微動だにしない、真に堅牢なVisio VBAのオブジェクト設計と実装アプローチを徹底解説する。
—
なぜ `ActivePage` は「地雷」なのか?
Visioのオブジェクトモデルにおいて、`ActiveApplication.ActivePage` や `ActiveWindow` は、ユーザーのUI操作と密結合している。
実務で自動化ツールやアドイン、他システム連携バッチを開発する際、この「UI依存」が以下のような深刻なバグを引き起こす。
1. フォーカスの競合によるヌル参照(Run-time error ’91’: Object variable or With block variable not set)
ユーザーが処理実行の瞬間に別のVisioウィンドウをクリックしたり、タスクバーから別アプリにフォーカスを移したりしただけで、`ActivePage` は `Nothing` を返す。これにより、容赦なくマクロはクラッシュする。
2. 意図しない図面・ページへの書き込み(サイレント・データ破損)
複数図面を開きながら作業しているユーザーが、マクロ実行中に別の図面を手前に持ってきた場合、意図とは全く異なる図面・ページに対して図形生成やプロパティ書き換えが実行される。これはエラーさえないため、データ破損として発覚が遅れる最悪のバグとなる。
3. 描画イベント・画面更新のオーバーヘッド
アクティブウィンドウを切り替えながら処理を行うコードは、画面の再描画(Redraw)が発生するため、処理速度が劇的に低下する。
プロが採るべき原則:「コンテキストの完全な特定」
Visio VBAで業務自動化ツールを作るなら、原則はただ一つ。
「UIの状態(何がアクティブか)に一切頼らず、コード内で明示的にDocument、Page、Shapeの参照を保持し、制御する」ことだ。
—
堅牢なオブジェクトチェーンの構築
安全なコードを書くための基本方針は、`Application` から起算するのではなく、「対象のDocumentオブジェクト」を起点として下位階層(Page、Shape)を直接手繰ることである。
[Application]
└─ [Documents (Collection)]
└─ [Document (Target)]
└─ [Pages (Collection)]
└─ [Page (Target)]
└─ [Shapes (Collection)]
この階層をコード上で明示的に変数に格納し、`.ActivePage` のようなプロパティを一切経由させずに処理を完結させる。
—
【実践】プロダクションコード例
以下に、実務のファイル連携や自動レイアウト生成を想定した、極めて堅牢なプロシージャのテンプレートを提示する。
このコードは、仮に裏でユーザーが別の図面を操作していたとしても、指定したファイルを正確にターゲットにして処理を安全に完結させる。
Option Explicit
‘ =========================================================================
‘ プロジェクト名: VisioAutomationMaster
‘ モジュール名 : mExportSafetyControl
‘ 概要 : アクティブ状態に依存しない、安全なページ・図面操作の実装例
‘ =========================================================================
Public Sub ExecuteRobustShapeProcessing()
Dim targetFilePath As String
targetFilePath = “C:\Data\AutomationTarget.vsdm” ‘ 実務では引数や設定ファイルから取得
Dim targetDoc As Visio.Document
Dim targetPage As Visio.Page
Dim isDocOpenedByMe As Boolean
‘ 画面描画とアラートを一時停止(パフォーマンス向上と予期せぬUI干渉の防止)
Dim originalScreenUpdating As Boolean
originalScreenUpdating = Application.ScreenUpdating
Application.ScreenUpdating = False
Application.ShowAlerts = False
On Error GoTo ErrorHandler
‘ ———————————————————————
‘ 1. Documentの安全な取得(既に開いていれば流用し、なければサイレントで開く)
‘ ———————————————————————
Set targetDoc = GetOrOpenDocument(targetFilePath, isDocOpenedByMe)
If targetDoc Is Nothing Then
Err.Raise vbObjectError + 1000, “ExecuteRobustShapeProcessing”, “対象ドキュメントの取得に失敗しました: ” & targetFilePath
End If
‘ ———————————————————————
‘ 2. Pageの安全な取得(ActivePageを使わず、名前またはインデックスで直接特定)
‘ ———————————————————————
‘ ※ここでは例として “システム構成図” という名前のページを直接指定
Set targetPage = GetPageByName(targetDoc, “システム構成図”)
If targetPage Is Nothing Then
Err.Raise vbObjectError + 1001, “ExecuteRobustShapeProcessing”, “指定されたページが見つかりません: システム構成図”
End If
‘ ———————————————————————
‘ 3. ページに対する安全なビジネスロジックの実行
‘ ———————————————————————
Call ProcessShapesOnPage(targetPage)
‘ 変更を保存(必要に応じて)
targetDoc.Save
MsgBox “処理が正常に完了しました。”, vbInformation, “自動化ツール”
CleanUp:
‘ ———————————————————————
‘ 4. 厳格な後処理(自分が開いた場合のみ閉じる、画面描画の復元)
‘ ———————————————————————
If isDocOpenedByMe And Not (targetDoc Is Nothing) Then
targetDoc.Close
End If
Application.ScreenUpdating = originalScreenUpdating
Application.ShowAlerts = True
Exit Sub
ErrorHandler:
‘ 異常系ハンドリング
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical, “致命的なエラー”
‘ 必要に応じたログ出力やロールバック処理をここに記述
Resume CleanUp
End Sub
‘ =========================================================================
‘ ヘルパー関数: ドキュメントのオープン状態を管理し確実に取得する
‘ =========================================================================
Private Function GetOrOpenDocument(ByVal filePath As String, ByRef outOpenedByMe As Boolean) As Visio.Document
Dim doc As Visio.Document
Dim fileNameOnly As String
fileNameOnly = Mid(filePath, InStrRev(filePath, “\”) + 1)
outOpenedByMe = False
‘ 既にアプリケーション内で開かれているかチェック
On Error Resume Next
Set doc = Application.Documents(fileNameOnly)
On Error GoTo 0
If doc Is Nothing Then
‘ 開いていない場合は、不可視(あるいは通常のバックグラウンド)で開く
‘ 実務では Workspaces や ReadOnly オプションも適宜調整
Set doc = Application.Documents.Open(filePath)
outOpenedByMe = True
End If
Set GetOrOpenDocument = doc
End Function
‘ =========================================================================
‘ ヘルパー関数: ActivePageに頼らず名前からPageオブジェクトを直接解決する
‘ =========================================================================
Private Function GetPageByName(ByVal targetDoc As Visio.Document, ByVal pageName As String) As Visio.Page
Dim pg As Visio.Page
For Each pg In targetDoc.Pages
‘ Visioのページ名比較(必要に応じて UCase 等で大文字小文字を吸収)
If pg.Name = pageName Or pg.NameU = pageName Then
Set GetPageByName = pg
Exit Function
End If
Next pg
‘ 見つからなかった場合
Set GetPageByName = Nothing
End Function
‘ =========================================================================
‘ ビジネスロジック本体: Pageオブジェクトを完全にカプセル化して処理
‘ =========================================================================
Private Sub ProcessShapesOnPage(ByVal pg As Visio.Page)
Dim shp As Visio.Shape
‘ 例: ページ上の全シェイプに対して何らかのプロパティ操作を行う
For Each shp in pg.Shapes
‘ ここに実務ロジックを記述(例:特定のレイヤーに属する図形の改行処理など)
‘ Debug.Print shp.Name
Next shp
End Sub
—
コードのアーキテクチャ的解説
1. `Application.ScreenUpdating = False` の徹底
バックグラウンド処理において、画面描画の抑制は必須である。これを行わないと、複数図面が開いている環境ではWindowsのウィンドウマネージャーが描画競合を起こし、予期せぬフリーズや描画崩壊を誘発する。
2. 自己責任のライフサイクル管理 (`isDocOpenedByMe`)
ツールが勝手に図面を開いたのであれば、終了時に閉じるべきだ。しかし、ユーザーがすでに開いていた図面を勝手に閉じると大クレームにつながる。そのため、「このマクロがオープンに関与したか」をフラグで厳格に管理し、クリーンアップ処理を分岐させる設計にしている。
3. `GetPageByName` による名前解決
`ActivePage` の代わりに、`Document.Pages` コレクションをイテレートして目的のページを確実につかむ。Visioではユーザーがページ名を変更するリスクがあるため、必要に応じてローカル名(`.Name`)とUI独立名(`.NameU`)の両方を考慮できるように拡張しておくと、多言語環境やテンプレート展開時にも耐えうる。
—
チーフアーキテクトからの提言
「動けばいい」という妥協の産物は、開発者自身が現場を離れた後、必ず負債となって組織に跳ね返る。特にVisioは、図面やステンシル、マルチウィンドウが複雑に絡み合う特殊なアプリケーションだ。
`ActivePage` や `ActiveWindow` といった甘い誘惑に逃げ込むのは今日で終わりにしよう。
「ドキュメントを特定し、ページを特定し、状態に依存しないコードを書く」。この鉄則を遵守するだけで、あなたの組んだ自動化ツールは、プロフェッショナルが認める「ビクともしない堅牢なインフラストラクチャ」へと生まれ変わる。
