【実務・中級編】Page.GetIDsを用いた大容量図面での図形ID一括取得と配列プロセッシング – Visio VBA解析バイブル

スポンサーリンク

序文:数万の図形を「紙」として扱うか、「データ」として扱うか

現場のエンジニア諸君、今日もVisioの複雑なオブジェクトモデルと格闘していることと思う。
もし君が、数千、数万というシェイプが配置された大規模な系統図やプラント図面を前にして、`For Each shp In Page.Shapes` という牧歌的なループを書いているのなら、即座にその指を止めてほしい。

COMオートメーションの世界において、プロセスの境界を越える「往復(ラウンドトリップ)」は最大のコストだ。1万個のシェイプに対して1万回プロパティを照会するのは、1万回東京-大阪間を往復して荷物を1個ずつ運ぶようなものだ。

真のアーキテクトは、「一括取得(Bulk Fetch)」「メモリ内処理」を選択する。
今回は、大規模図面処理のボトルネックを破壊する究極の武器、`Page.GetIDs` メソッドを用いた高速配列プロセッシングの極意を伝授する。

1. なぜ `Page.Shapes` コレクションを回してはいけないのか

通常の `For Each` ループが大規模図面で死ぬほど遅い理由は、VBA側からVisio本体(別プロセス)に対して、ループのたびに「次のオブジェクトをくれ」と要求を投げ、COMのマーシャリング(データの整形と転送)が発生するためだ。

これに対し、`Page.GetIDs` は図面上の全シェイプのIDを「一度の呼び出し」でVBA側の配列にメモリコピーする。

  • Shapesコレクション: $O(N)$ 回のプロセス間通信。
  • GetIDs: $1$ 回のプロセス間通信 + $O(N)$ 回のローカルメモリ・アクセス。

勝負は火を見るより明らかだ。数万件の処理では、実行速度が数分から数秒へと、文字通り桁が変わる。

2. 実践:`Page.GetIDs` を使いこなすプロフェッショナル・コード

単にIDを取るだけでは不十分だ。実務では「特定のレイヤーにあるか」「データを持っているか」といったフィルタリングが必要になる。これをいかにスマートに実装するか。

以下に、保守性と堅牢性を担保したプロダクションレベルのコードを示す。

‘ ==============================================================================
‘ Description: 大規模図面から高速にShapeIDを取得し、バッチ処理を行う
‘ Note: GetIDsはIDの配列を返す。Shapeオブジェクトの生成は最小限に留めるのが鉄則。
‘ ==============================================================================
Public Sub ProcessLargeScaleDrawing()
Dim targetPage As Visio.Page
Set targetPage = ActivePage

‘ 1. 高速化の儀式:描画更新とイベントの停止
On Error GoTo ErrorHandler
ToggleVisioOptimization True

‘ 2. Shape IDの一括取得
‘ visGetIDsNone: 全ての図形IDを取得
‘ visGetIDsMemberOfGroup: グループ内の図形を除外したい場合はフラグを調整
Dim shapeIDs() As Long
On Error Resume Next
targetPage.GetIDs visGetIDsNone, shapeIDs
If Err.Number <> 0 Then
Debug.Print “図形が存在しません。”
Exit Sub
End If
On Error GoTo ErrorHandler

‘ 3. 配列プロセッシング(メモリ効率の最大化)
Dim i As Long
Dim currentID As Long
Dim targetShape As Visio.Shape

Debug.Print “処理開始: ” & UBound(shapeIDs) + 1 & ” 件の図形をスキャン中…”

‘ 配列を直接回す。この段階ではまだShapeオブジェクトには触れない。
For i = LBound(shapeIDs) To UBound(shapeIDs)
currentID = shapeIDs(i)

‘ 必要な時だけShapeオブジェクトをバインドする
‘ これを「遅延バインディング的アプローチ」と呼ぶ
Set targetShape = targetPage.Shapes.ItemFromID(currentID)

‘ ロジックの例:特定のテキストを持つ図形のみDB連携対象とする
‘ ※ここで全てのプロパティに触れると遅くなるため、フィルタリングは慎重に。
If targetShape.CellExistsU(“Prop.ExternalID”, visExistsAnywhere) Then
DoSomethingEfficient targetShape
End If

‘ 4. メモリリーク防止:数千個ごとにオブジェクトを解放する
If i Mod 1000 = 0 Then
Set targetShape = Nothing
‘ 必要に応じてDoEventsを挟むが、速度重視なら最小限に。
End If
Next i

MsgBox “処理完了: ” & (UBound(shapeIDs) + 1) & ” 件を走査しました。”, vbInformation

CleanUp:
ToggleVisioOptimization False
Exit Sub

ErrorHandler:
Debug.Print “Error: ” & Err.Description
Resume CleanUp
End Sub

‘ 描画・計算停止のユーティリティ
Private Sub ToggleVisioOptimization(ByVal enable As Boolean)
With Application
.ScreenUpdating = Not enable
.DeferRecalc = enable
.EventsEnabled = Not enable
End With
End Sub

‘ 実際の業務ロジック(例:外部キーの検証など)
Private Sub DoSomethingEfficient(ByRef shp As Visio.Shape)
‘ ここでDB連携や重い処理を行う
‘ 基本はIDと変更値のペアをコレクションに溜め、最後に一括でDB更新するのが定石
End Sub

3. 現場で差がつくアーキテクトの視点

IDは「不変」ではないと知れ

Visioの `Shape.ID` は、そのドキュメント内では一意だが、コピー&ペーストや図面の再構成によって変わる可能性がある。データベースと連携させる場合は、`Shape.ID` をそのまま主キーにするのではなく、「独自に定義したシェイプデータ(隠しプロパティなど)にGUIDを書き込む」設計を推奨する。`GetIDs` で取得したIDは、あくまで「その実行セッションにおけるハンドル」として扱うべきだ。

配列は「Long型」である

`GetIDs` が返すのは `Long` 型の配列だ。これを `Variant` で受けてはいけない(動くが、メモリ効率が落ちる)。また、図形がゼロの場合、このメソッドはエラーを吐くか空の挙動をするため、上記コードのように `On Error Resume Next` でトラップするのが定型句となる。

フィルタリングの優先順位

数万個の図形がある場合、すべての `Shape` オブジェクトを `ItemFromID` で生成するとメモリを食いつぶす。
可能であれば、`Page.Select` メソッドやレイヤー機能を使って、「処理対象のID群を事前に絞り込む」 ことを検討せよ。

  • `Page.GetIDs` の第一引数には、選択されている図形のみを取得する `visGetIDsSelected` など、強力なフラグが存在する。

4. まとめ:極限まで速いツールを作るために

1. 通信回数を削れ: `For Each` を捨て、`GetIDs` で一括取得せよ。
2. オブジェクト生成を遅延させよ: ID配列をフィルタリングし、真に必要な図形だけを `ItemFromID` で実体化せよ。
3. 環境を固定せよ: `ScreenUpdating` と `DeferRecalc` を止めるのは、もはやマナーである。

このアプローチは、単なる「テクニック」ではない。大規模なデータを扱うソフトウェア開発における「規律」だ。
これを知っているか否かで、君が作るツールの価値は「使い物にならないゴミ」か「手放せない神ツール」かに分かれるだろう。

現場での成功を祈る。

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