【Visio VBA実務知見】ページ削除の罠:なぜ昇順ループは破滅を招くのか?安全な逆順ループの設計思想
こんにちは。チーフアーキテクトの私だ。
これまで数多くの大規模図面自動生成システム、CAD/GIS連携基盤、そして社内ニッチなワークフロー自動化ツールを構築してきたが、Visio VBAの現場で最も頻繁にエンジニアの足元をすくう「初歩的かつ致命的なバグ」の一つが、複数ページの動的削除処理だ。
「指定した条件に合致するページをゴソッと削除したい」
そう考えたとき、君はどのようなコードを書くだろうか?
まさか、上から順に(昇順で)ループさせてはいないだろうか。
今回は、Visioのオブジェクトモデルの裏側にある「コレクションの動的シフト」のメカニズムを紐解き、実務で絶対に破綻しない堅牢なページ削除の設計手法をシャープに伝授しよう。
—
1. なぜ「昇順ループ」でのページ削除は破滅に向かうのか?
まずは、悪魔的なバグを生む「やってはいけない実装」を見てほしい。
‘ 【アンチパターン】絶対にやってはいけない昇順ループ
Dim i As Long
For i = 1 To ActiveDocument.Pages.Count
‘ 条件に合致するページを削除したいとする
If ActiveDocument.Pages(i).Name Like “Temp_” Then
ActiveDocument.Pages(i).Delete
End If
Next i
一見すると、何の問題もない美しいコードに見えるかもしれない。しかし、これを実行した瞬間、図面は想定外の挙動を起こすか、あるいは「実行時エラー」で強制終了することになる。
原因は「コレクションのインデックス再割り当て(シフト)」にある
Visioの `Document.Pages` コレクションは、ページが削除されるたびに、残ったページのインデックス番号を自動的に詰める(繰り上げる)仕様になっている。
ここでシミュレーションしてみよう。
総ページ数が「3」のドキュメントがあり、1ページ目と2ページ目が削除対象だと仮定する。
1. `i = 1` のとき:
- `Pages(1)` (元の1ページ目)を削除する。
- この瞬間、元々2ページ目だったものが `Pages(1)` に繰り上がり、3ページ目だったものが `Pages(2)` にシフトする。
2. `i = 2` のとき:
- ループのカウンターが `2` に進む。
- ここで参照される `Pages(2)` は、「元々3ページ目だったもの」になる。
- つまり、「元々2ページ目だったページ」は、一度も評価されることなくスキップされるのだ。
3. さらにページ数が減った場合:
- 存在しないインデックスを参照しようとして、「インデックスが範囲外です(Error 9)」などの実行時エラーに直面する。
実務において、このバグは「特定のページ数や条件の時だけ発生する」という、最も厄介な不具合(いわゆるHeisenbug)を引き起こす。これを根絶するのが、プロの設計者である。
—
2. 解決策:後ろから数える「逆順ループ(Decrement Loop)」
この問題をエレガントかつ確実に解決する唯一無二の定石が、「最大インデックスから1に向かって減算していく逆順ループ」だ。
アーキテクチャの核心
後ろ(末尾)のページから削除を始めれば、仮にページが消滅したとしても、それより前方(若いインデックス)のページの番号は一切変動しない。したがって、インデックスのズレによる参照エラーや評価漏れが物理的に起こらなくなるのだ。
—
3. 【プロダクションコード】安全なページ削除の完全実装
それでは、実務の現場でそのままコピー&ペーストして即戦力として使える、堅牢なプロシージャを提示しよう。
このコードでは、単なるインデックス操作だけでなく、「図面保護(Protection)」の考慮や「最低1ページは残す」というVisioの仕様への配慮といった、実務で必ず直面するエッジケースへの対策も網羅している。
Option Explicit
/
- 指定したプレフィックスを持つページを安全に一括削除するプロシージャ
- @TargetPrefix: 削除対象とするページの名前のプレフィックス (例: “Draft_”)
/
Sub RemovePagesSafely(ByVal TargetPrefix As String)
Dim doc As Visio.Document
Set doc = ActiveDocument
Dim totalPages As Long
totalPages = doc.Pages.Count
‘ デバッグ用または処理前の安全確認ログ
Debug.Print “処理開始: 総ページ数 = ” & totalPages
Dim i As Long
Dim targetPage As Visio.Page
‘ 【極意】必ず末尾(Count)から1に向かって逆順でループを回す
For i = totalPages To 1 Step -1
Set targetPage = doc.Pages(i)
‘ 条件判定(例: 名前に特定文字列が含まれるか、あるいは特定の条件を満たすか)
If InStr(1, targetPage.Name, TargetPrefix, vbTextCompare) > 0 Then
‘ Visioの仕様として、ドキュメントには最低1枚のページが必要なため、
‘ 最後の1枚を削除しようとした場合はスキップまたはエラーハンドリングを入れる
If doc.Pages.Count <= 1 Then
MsgBox "これ以上のページを削除することはできません(ドキュメントには最低1ページ必要です)。", vbExclamation, "安全装置"
Exit For
End If
' オブジェクトの保護状態やバックグラウンドページかどうかのガード節
If targetPage.Background = False Then
On Error Resume Next
targetPage.Delete
If Err.Number <> 0 Then
Debug.Print “警告: ページ ‘” & targetPage.Name & “‘ の削除に失敗しました。理由: ” & Err.Description
Err.Clear
Else
Debug.Print “ページ削除成功: ” & targetPage.Name
End If
On Error GoTo 0
End If
End If
‘ ループ内でのメモリ解放(COMコンポーネントの参照リーク防止)
Set targetPage = Nothing
Next i
Debug.Print “処理完了: 残存ページ数 = ” & doc.Pages.Count
End Sub
—
4. プロとして押さえておくべき「実務上の3大注意点」
コードを動かすだけなら上記で十分だが、企業内の業務自動化システムや、データベース/外部ファイル連携ツールの一部として組み込む場合は、以下の3点に留意して設計をブラッシュアップしてほしい。
① COMオブジェクトの参照解放(メモリリーク対策)
VBAからVisioを操作する場合、`Visio.Page` などのオブジェクト変数をループ内で使い回したり、生成しっぱなしにすると、VBAの背後にあるCOMランタイムのメモリ解放が遅れ、長時間のバッチ処理などでメモリ消費量が肥大化することがある。
上記のコード例のように、ループの最後で `Set targetPage = Nothing` を明示的に呼び出す習慣を徹底してほしい。
② イベントの無効化によるパフォーマンス劇的向上
もし削除対象のページ数が数十〜数百に及ぶ場合、ページを1枚消すごとにVisioの画面描画やイベントリスナーが反応し、処理が極端に重くなる。
大量の一括処理を行う前頭・後尾では、必ず以下の設定を挟むのがプロの技だ。
‘ 処理の高速化と画面ちらつき防止
Application.ScreenUpdating = False
Application.EventsEnabled = False
‘ — ここに削除ループ処理 —
Application.EventsEnabled = True
Application.ScreenUpdating = True
Application.ActiveWindow.Redraw
③ 外部データ(Excel/DB)との同期ズレ
「外部のExcelマスタに記載されているページ名リストを元に、Visio側のページを削除・再構築する」という要件は非常によくある。
このとき、Excel側の行番号とVisio側のページインデックスを安易に紐づけてループさせると、データ不整合の原因になる。
「Visio側のページを処理する際は、常に名前(Name)やID(IDプロパティ)をキーにして検索・特定する」という原則を忘れないこと。インデックス番号は、あくまで「コレクションを走査するための一時的な座標」に過ぎない。
—
総括
Visio VBAにおけるコレクション操作の本質は、「オブジェクトのライフサイクルと、背後にあるインデックスの変動を完全にコントロールすること」にある。
今回解説した「逆順ループ」は、ページ削除だけでなく、シェイプの削除 (`Shapes.Count To 1 Step -1`) や、レイヤー、マスターシェイプの削除など、Visio VBAのあらゆる「動的コレクション破棄処理」に応用できる汎用的なデザインパターンだ。
泥臭いバグに悩まされる時間はもう終わりにするべきだ。
洗練されたロジックで、堅牢かつ高速な自動化ツールを君の手で組み上げてくれ。
