【テクニカル・上級編】Documentオブジェクトを制す:複数ページを効率よく巡回しプロパティを一括操作する方法 – Visio VBA解析バイブル

スポンサーリンク

Documentオブジェクトを制す:複数ページ巡回とプロパティ一括操作の極限最適化

Visio VBAの現場において、`ActiveDocument` と `ActivePage` の蜜月に依存したコードは、システムが中規模から大規模へ移行した瞬間に破綻する。

「開いているページが意図せず変わる」「バックグラウンド処理中に画面がちらつき、最悪の場合はCOM例外で落ちる」。これらはすべて、Visioの描画エンジン(Visio Drawing Control / Application Instance)とオブジェクトモデルのライフサイクルを理解していない素人仕事の産物だ。

真に堅牢なVisio automationを構築するためには、`Document` オブジェクトを起点とした明示的なスコープ管理と、COMオブジェクトのライフサイクル制御が不可欠である。本稿では、複数ページにわたる大規模図面を一括制御するための極限の知見を、実戦投入可能なコードとともに解説する。

1. Visioオブジェクトモデルの暗部:なぜ `ActiveX` 依存は死を招くのか

多くの開発者は、次のようなコードを書く。

‘ 【アンチパターン】絶対にやってはならない実装
Sub RenamePages_Bad()
Dim i As Long
For i = 1 bTo ActiveDocument.Pages.Count
ActiveDocument.Pages(i).Name = “Page_” & i
‘ 画面が毎回再描画され、パフォーマンスが劇的に低下する
Next i
End Sub

このコードの罪は重い。`ActiveDocument` や `ActivePage` のような暗黙のグローバルプロパティへのアクセスは、内部でCOMのコンテキストスイッチを発生させる。さらに、画面描画(Redraw)が走るため、ページ数が50を超えるようなエンタープライズ図面では、完了までに数分を要することもある。

プロのアーキテクトが満たすべき要件は以下の3点だ。
1. 画面描画の完全な抑制(ScreenUpdatingの遮断)
2. オブジェクト変数の厳密なスコープ限定と解放
3. 例外発生時(エラー時)の確実な状態復元

2. 実装:パフォーマンスと安全性を極限まで高めた一括処理エンジン

以下のコードは、単なるループ処理ではない。VisioのCOMポインタの重みを知り尽くしたエンジニアが書くべき、堅牢な `Document` 巡回・プロパティ一括操作のプロダクションコードである。

Option Explicit

‘ Windows API: 描画・メッセージ処理の制御が必要な場合のフック(必要に応じて拡張)
If VBA7 Then
Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If

Sub Enterprise_BatchProcessDocument()
Dim startTime As Double
startTime = Timer

‘ 1. アプリケーションレベルの最適化
Dim targetApp As Visio.Application
Set targetApp = Application

‘ 画面描画とイベント発火を完全停止(パフォーマンス劇的向上 & チラつき防止)
targetApp.ScreenUpdating = False
targetApp.EventEnabled = False
targetApp.Settings.ShowChanges = False

Dim targetDoc As Visio.Document
Set targetDoc = targetApp.ActiveDocument

‘ ドキュメントが開かれていない場合のガード
If targetDoc Is Nothing Then
MsgBox “処理対象のドキュメントが存在しません。”, vbCritical, “Critical Error”
GoTo Cleanup
End If

‘ トランザクション的変更のためのスコープ(必要ならUndoスコープを設定)
Dim lScopeID As Long
lScopeID = targetDoc.BeginUndoScope(“一括ページプロパティ更新”)

On Error GoTo ErrorHandler

‘ 2. ページコレクションの走査と一括操作
Dim targetPages As Visio.Pages
Set targetPages = targetDoc.Pages

Dim totalPages As Long
totalPages = targetPages.Count

Dim i As Long
Dim currentPage As Visio.Page
Dim processedCount As Long
processedCount = 0

For i = 1 To totalPages
Set currentPage = targetPages(i)

‘ 【極限知見】バックグラウンドページ(マスター等)のスキップ判定
If Not currentPage.Background Then
‘ 例:ページの命名規約適用とページレベルの設定変更
‘ ここでは例としてプレフィックス付与とグリッド設定を同時に行う

Dim originalName As String
originalName = currentPage.Name

‘ 命名規則の適用(例: 既存名が “Sheet” で始まらない場合のみ変更)
If InStr(1, originalName, “Node_”, vbTextCompare) = 0 Then
currentPage.Name = “Node_” & Format(i, “000”) & “_” & originalName
processedCount = processedCount + 1
End If

‘ ドキュメント/ページレベルのプロパティ一括強制設定
‘ 例: ページースケールやガイド表示の統一
Call ApplyEnterprisePageSettings(currentPage)
End If

‘ ループごとのメモリリークを防ぐため、オブジェクト参照を確実に破棄
Set currentPage = Nothing
Next i

‘ トランザクション正常終了
targetDoc.EndUndoScope lScopeID

Debug.Print “処理完了: ” & processedCount & ” ページを更新しました。(処理時間: ” & Format(Timer – startTime, “0.00秒”) & “)”

Cleanup:
‘ 3. 状態の確実な復元(エラー発生時も必ず実行される)
On Error Resume Next
If Not targetDoc Is Nothing Then Set targetDoc = Nothing
If Not targetPages Is Nothing Then Set targetPages = Nothing

If Not targetApp Is Nothing Then
targetApp.ScreenUpdating = True
targetApp.EventEnabled = True
targetApp.Settings.ShowChanges = True
Set targetApp = Application
End If

Exit Sub

ErrorHandler:
‘ 異常終了時のロールバック
If lScopeID <> 0 Then
targetDoc.CancelUndoScope lScopeID
End If

MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“説明: ” & Err.Description, vbCritical, “Architecture Exception”

Resume Cleanup
End Sub

Private Sub ApplyEnterprisePageSettings(ByRef pg As Visio.Page)
‘ ページごとの図面設定をガバナンス基準に強制準拠させる
Dim pgSheet As Visio.Shape
Set pgSheet = pg.PageSheet

‘ 例:ページ幅・高さをA4横に統一(ミリメートル単位での制御)
‘ Visio内部単位(インチ)へ変換して設定
Const mmToInch As Double = 1# / 25.4#

‘ セルの存在確認を行いつつ安全にプロパティ書き込み
If pgSheet.CellExistsU(“PageWidth”, Visio.visExistsAnywhere) Then
pgSheet.CellsU(“PageWidth”).FormulaU = 297# mmToInch
End If
If pgSheet.CellExistsU(“PageHeight”, Visio.visExistsAnywhere) Then
pgSheet.CellsU(“PageHeight”).FormulaU = 210# mmToInch
End If

Set pgSheet = Nothing
End Sub

3. チーフアーキテクトが解説するコードの急所

上記のコードには、単なる「動くVBA」を超えた、エンタープライズアーキテクチャの知見が凝縮されている。

① `ScreenUpdating` と `EventEnabled` の二重遮断

Visioは、ページやシェイプのプロパティが書き換えられるたびに、UIの再描画キューを積み、さらに `PageChanged` などのイベントを発火させようとする。これが数千個のオブジェクトを抱える図面でパフォーマンスが暴落する根本原因だ。
エンジン側で `ScreenUpdating = False` と `EventEnabled = False` を同時に宣言し、OSメッセージループを遮断することで、C++ネイティブアプリケーション並みの爆速処理を実現する。

② Undoスコープ(`BeginUndoScope` / `EndUndoScope`)によるアトミック性

複数ページにわたる一括処理の途中でエラーが発生した場合、図面が「途中まで書き換えられた半端な状態」で保存されるリスクが生じる。
これを防ぐため、処理全体をひとつのトランザクション(Undoスコープ)としてラップする。エラー発生時には `CancelUndoScope` を叩くことで、一瞬で処理前の状態にロールバックされる。この堅牢性こそが、実務で使えるツールの条件だ。

③ COMオブジェクトの明示的解放(`Set xxx = Nothing`)

VBAのガベージコレクションは頼りにならない。特に複数の `Page` や `Shape` オブジェクトをループ内で生成・破棄する場合、参照カウントが残存し、Visioプロセス内にメモリリークが蓄積する。
ループの末尾で必ず `Set currentPage = Nothing` を実行し、スコープを抜ける段階で親オブジェクトの参照も明示的に解放する。この規律が、長時間稼働するバッチ処理での安定性を担保する。

4. レガシー環境・外部システム連携への布石

このアプローチは、ExcelやDatabase(SQL Server / Oracle)からVBA経由、あるいはC# (.NET Interop) からVisioをヘッドレス(非表示)で駆動させる際にもそのまま流用できる。

UIを持たない `Visio.InvisibleApp` インスタンスからドキュメントをロードし、上記の巡回ロジックを走らせることで、サーバサイドでの図面自動生成・一括バリデーション・メタデータ同期パイプラインが完成する。

Visio VBAは玩具ではない。オブジェクトのライフサイクルと描画エンジンの挙動を完全に掌握すれば、企業の設計資産を意のままに操る最強の武器となるのだ。

タイトルとURLをコピーしました