Visio VBAを掌握する極限の知見:Page.Shapesコレクションの効率的な走査と破壊的ループの鉄則
こんにちは。業務自動化エンジニアのチーフアーキテクトだ。
これまでに数多くの巨大なフロアマップ、ネットワーク図、そして複雑な業務プロセス定義の自動生成・集計システムをVisio VBAで構築してきた。
その中で、ジュニアクラスの開発者が最も頻繁に踏み抜く「地雷」がある。
それが、`Page.Shapes` コレクションの走査と、そこでの図形削除・変更処理だ。
「全ての図形を走査して処理するだけなら `For Each` でいいだろう」
そう考えて安易にコードを書き、本番環境で「一部の図形が謎にスキップされる」「突如として `Run-time error ‘-2004 (800407ff)’: 図形が見つかりません。’` が発生してマクロがクラッシュする」というトラブルに直面したことはないだろうか?
今回は、Visioオブジェクトモデルの裏側にある「コレクションのインデックス変動のメカニズム」を解き明かし、1万個の図形が蠢く巨大図面であっても、1ミリの狂いもなく安全かつ高速に処理し切るための極限の知見を授けよう。
—
1. なぜ「普通のループ」では破綻するのか?
まず、Visioの `Shapes` コレクションの特殊性を理解しなければならない。Excelの `Range` や `Worksheets` とは異なり、Visioの `Shape` は親子関係(グループ化)やZオーダー(重なり順)が絡み合う動的なコレクションだ。
罠その1:`For Each` での図形削除
`For Each` はコレクションの走査には非常にエレガントだが、「ループの最中に現在のコレクションから要素を削除する」という破壊的変更に対して極めて脆弱だ。裏でインデックスの再割り当てが発生するため、VBAエンジンがポインタ迷子になり、処理すべき図形をスキップしてしまうか、最悪の場合はランタイムエラーで強制終了する。
罠その2:先頭からの順方向ループ(`For i = 1 To Count`)での削除
削除を行うためにインデックスによる `For` ループ(`1` から `Count` まで)を書いたとする。これも悪手だ。
例えば、インデックス `1` の図形を削除すると、それまでインデックス `2` だった図形が自動的に `1` に繰り上がる。しかし、ループ変数は `2` へ進んでしまうため、繰り上がってきた元・図形2が完全にスルーされるという致命的なバグが生まれる。
—
2. 鉄則:破壊的走査には「逆順ループ(Decrement Loop)」を使え
図形の削除や、動的なレイヤー移動など、コレクションの要素数が変化する可能性のあるすべての処理において、業界標準かつ唯一無二の安全な解が「末尾から先頭に向かう逆順ループ」だ。
Dim i As Long
For i = activePage.Shapes.Count To 1 Step -1
‘ 処理
Next i
なぜこれが安全なのか?
仮に末尾の図形(`Count` 番目)を削除したとしても、それより前にある図形(1番から `Count – 1` 番目)のインデックスは一切変動しない。そのため、次に処理すべき前方(インデックスが若い方)の図形への影響が完全に遮断されるのだ。
—
3. 【プロダクションコード】安全・高速に全図形を走査・制御する模範実装
実務でそのまま使える、堅牢なテンプレートコードを提示しよう。
このコードでは、以下の要素を網羅している。
1. 画面描画の凍結(`ScreenUpdating` / `EventEnabled` の制御)による爆速化
2. グループ図形(`Group`)の再帰的走査への配慮
3. 安全な逆順ループによる図形の一括削除・属性変更
Option Explicit
‘ ==============================================================================
‘ プロジェクト名: VisioAutomationMaster
‘ モジュール名 : mShapeOperation
‘ 概要 : Page.Shapesコレクションの安全な逆順走査と一括処理のサンプル
‘ ==============================================================================
Sub SafeProcessAllShapes()
Dim startTime As Double
startTime = Timer
‘ 1. パフォーマンス最優先の作法:画面描画とイベントを完全に殺す
‘ ※これを怠ると、図形1つ処理するたびにVisioが再描画走り、数分待たされることになる。
Application.ScreenUpdating = False
Application.EventEnabled = False
Dim targetPage As Visio.Page
Set targetPage = ActivePage
Dim totalShapes As Long
totalShapes = targetPage.Shapes.Count
If totalShapes = 0 Then
MsgBox “処理対象の図形が存在しません。”, vbInformation
GoTo CleanUp
End If
Debug.Print “— 処理開始: 総図形数 = ” & totalShapes & ” —”
Dim i As Long
Dim shp As Visio.Shape
Dim deletedCount As Long
deletedCount = 0
‘ 2. 【極限の知見】削除・変更を含む場合は必ず「逆順ループ」を使用する
For i = totalShapes To 1 Step -1
‘ 毎回オブジェクトを取得
Set shp = targetPage.Shapes(i)
‘ ———————————————————————-
‘ ここに実際のビジネスロジックを記述する
‘ ———————————————————————-
‘ 例として、「特定の条件(例:名前に特定の文字列が含まれる、あるいは特定のレイヤー)」を満たす図形を削除する判定
If shp.Name Like “Temp” Then
‘ グループの親か子かなどの判定も必要に応じてここに挟む
shp.Delete
deletedCount = deletedCount + 1
Else
‘ 削除しない図形に対するプロパティ変更の例(例:全図形の線色を黒にする)
‘ shp.Cells(“LineColor”).FormulaU = “RGB(0,0,0)”
End If
Next i
Debug.Print “— 処理完了: 削除図形数 = ” & deletedCount & ” / 処理時間: ” & Format(Timer – startTime, “0.00秒”) & ” —
CleanUp:
‘ 3. 必ず環境を元の状態に戻す(エラーフックがあっても通るようにする)
Application.ScreenUpdating = True
Application.EventEnabled = True
MsgBox “図形の走査・処理が正常に完了しました。”, vbInformation, “自動化完了”
Exit Sub
ErrorHandler:
‘ 予期せぬエラー時のフェイルセーフ
Application.ScreenUpdating = True
Application.EventEnabled = True
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End Sub
—
4. チーフアーキテクトからの実務アドバイス:データベース・外部連携時の注意点
この走査処理をベースにして、データベース(SQL ServerやAccess)やExcelからデータを読み込み、図形と紐付ける業務ツールを作る際の実践的な注意点を最後に授けておこう。
- 形状ID(`ID`)とインデックス(`Index`)の混同に注意せよ
`targetPage.Shapes(i)` の `i` は、あくまでコレクション内の「現在の並び順(Zオーダー依存)」にすぎない。図形が削除・追加されるとこの番号は変わる。しかし、図形個体が持つ `shp.ID` は、その図形が生存している限り不変の固有値だ。
外部DBと連携して「どの図形がどのレコードに対応するか」を管理・マッピングする際は、絶対に `shp.ID` をキーとして保存・照合しなければならない。
- マスターシェイプ(`Master`)の扱い
ドキュメントステンシルからドロップされた図形はマスターへのリンクを持っている。一括置換や一括プロパティ変更を行う際は、マスター側をいじるのか、インスタンス(ページ上の図形)側をいじるのかのスコープを明確に設計図に起こしてからコードを叩くこと。
まとめ
Visio VBAにおける `Page.Shapes` コレクションの走査は、一見地味な基礎技術に見える。しかし、ここでの設計ミスは、数千個の図形を扱うエンタープライズ領域において、システム全体の信頼性を崩壊させる致命傷となる。
「逆順ループ」と「画面描画の抑制」。この2つの鉄則を体に叩き込み、実務で絶対に止まらない強靭な自動化ツールを構築してほしい。君の健闘を祈る。
