【実務・中級編】【実務直結】Visio VBAにおける「ページ削除」時のオブジェクト参照エラーを回避する安全なループ処理 – Visio VBA解析バイブル

スポンサーリンク

【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のあらゆる「動的コレクション破棄処理」に応用できる汎用的なデザインパターンだ。

泥臭いバグに悩まされる時間はもう終わりにするべきだ。
洗練されたロジックで、堅牢かつ高速な自動化ツールを君の手で組み上げてくれ。

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