黎明からの脱却:Page.GetIDs が切り拓く「数万図形」の高速走査領域
業務自動化の最前線において、我々が直面する最大の壁の一つが「Visioの肥大化」だ。
数千、あるいは数万のコネクタとシェイプが複雑に絡み合うプラント設計図やネットワーク構成図。これらをナイーブな(素朴な)`For Each` 文で走査しようとするのは、素人の仕事である。
COM(Component Object Model)の境界を越えるコストを理解しているエンジニアならば、`Page.Shapes` コレクションへの逐次アクセスがいかに破滅的なパフォーマンス劣化を招くかを知っているはずだ。
今回は、Visio VBAにおいて「数万個の図形」を瞬時に掌握するための唯一の解、`Page.GetIDs` メソッドを用いた配列プロセッシングについて、その深淵を解説する。
—
1. なぜ「For Each」は大規模図面で死に至るのか
通常の `For Each shp In Page.Shapes` は、ループのたびに Visio 本体に対して「次の Shape オブジェクトを生成して渡せ」というリクエストを投げる。
この「COMマーシャリング」と呼ばれるプロセスは、図形数が増えるにつれて指数関数的にオーバーヘッドを増大させる。
さらに、Shape オブジェクトはメモリを大量に消費する「重い」オブジェクトだ。数万のインスタンスを不用意に生成し続ければ、メモリの断片化を招き、最終的には `Out of Memory` やアプリケーションの異常終了を引き起こす。
対して `Page.GetIDs` は、図形そのものではなく、その「ID(Long型)」のみを配列として一括取得する。メモリ消費は極小であり、何より Visio プロセスとの通信を一度で完結させる 点に真髄がある。
—
2. 実践:Page.GetIDs による極限の配列プロセッシング
以下のコードは、単なるサンプルではない。数万のシェイプを抱えるエンタープライズ環境での運用に耐えうる、最適化されたボイラープレートだ。
Option Explicit
‘ Windows API を用いた高精度タイマー(パフォーマンス計測用)
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32″ (lpFrequency As Currency) As Long
”’
”’
Public Sub ProcessLargeScaleDiagram()
Dim targetPage As Visio.Page
Set targetPage = ActivePage
‘ 1. 高速化の儀式:描画とイベントの抑制
Dim originalScreenUpdating As Boolean
Dim originalEventsEnabled As Boolean
originalScreenUpdating = Application.ScreenUpdating
originalEventsEnabled = Application.EventsEnabled
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
Application.EventsEnabled = False
Application.DeferRecalc = True ‘ 計算の遅延実行
‘ パフォーマンス計測開始
Dim startTime As Currency, endTime As Currency, freq As Currency
QueryPerformanceFrequency freq
QueryPerformanceCounter startTime
‘ 2. Page.GetIDs による ID 一括取得
‘ visGetIDsNone (0) を指定することで、すべての図形IDを取得
Dim shapeIDs() As Long
On Error Resume Next
shapeIDs = targetPage.GetIDs(visGetIDsNone)
On Error GoTo ErrorHandler
‘ 図形が存在しない場合の早期リターン
If (Not shapeIDs) = -1 Then
Debug.Print “No shapes found.”
GoTo CleanUp
End If
‘ 3. 配列プロセッシング
Dim i As Long
Dim currentID As Long
Dim targetShape As Visio.Shape
Debug.Print “Processing ” & UBound(shapeIDs) + 1 & ” shapes…”
For i = LBound(shapeIDs) To UBound(shapeIDs)
currentID = shapeIDs(i)
‘ 必要な時だけ Shape オブジェクトにアクセスする
‘ ここで Shape オブジェクトを使い捨てるのがメモリ管理の肝
Set targetShape = targetPage.Shapes.ItemFromID(currentID)
‘ — 業務ロジック開始 —
‘ 例: 特定のマスターを持つ図形のみを処理する
‘ If Not targetShape.Master Is Nothing Then
‘ ‘ 重い処理はここで行う
‘ End If
‘ — 業務ロジック終了 —
‘ 4. 明示的な解放
‘ ループ内でのオブジェクト解放は、大規模処理における鉄則
Set targetShape = Nothing
‘ 定期的に DoEvents を挟むかは、UIの応答性とトレードオフ
‘ If i Mod 1000 = 0 Then DoEvents
Next i
QueryPerformanceCounter endTime
Debug.Print “Execution Time: ” & Format((endTime – startTime) / freq, “0.000”) & ” sec”
CleanUp:
‘ 状態の復元
Application.ScreenUpdating = originalScreenUpdating
Application.EventsEnabled = originalEventsEnabled
Application.DeferRecalc = False
Exit Sub
ErrorHandler:
Debug.Print “Error: ” & Err.Description
Resume CleanUp
End Sub
—
3. アーキテクトが語る最適化の急所
Shape オブジェクトの「生存期間」を最小化せよ
`ItemFromID` で取得した `Shape` オブジェクトは、ループの最後で必ず `Set targetShape = Nothing` とせよ。VBAのガベージコレクションは非力だ。数万回のループの中で参照が残り続けると、メモリ空間が急速に汚染される。
GetIDs の引数 `visGetIDsFlags` を使い分ける
`visGetIDsNone` (0) 以外にも、選択されている図形のみを対象とする `visGetIDsSelected` などがある。しかし、真に高速化を求めるなら、まずは全IDを配列に放り込み、VBA側のメモリ上でフィルタリング(特定のIDパターンや属性による選別)を行う方が、Visioのオブジェクトモデルを叩き続けるよりも遥かに速い。
究極の連携:他システムへのエクスポート
もし目的が「図形データの抽出(CSVやSQL Serverへの流し込み)」であるなら、ループ内で `targetShape.Cells(“Prop.Value”).ResultStr(“”)` などとプロパティを一つずつ叩くのは三流だ。
その場合は、`Page.GetResults` メソッドを併用し、セル値も一括で配列取得することを検討せよ。
—
4. レガシー環境への敬意と現代的アプローチ
Visio VBA は 32bit 環境で動作しているケースも未だ多い。32bit VBA のメモリ限界は 2GB だが、実質的には 1GB を超えたあたりから不安定になる。
今回紹介した `GetIDs` による配列処理は、オブジェクト生成を最小限に抑えるため、この「2GBの壁」に対する最も有効な防護策となる。
また、VB.NET や C# (VSTO) から Visio を制御する場合も、この考え方は変わらない。むしろ、.NET の場合は COM 相互運用(Interop)のコストが VBA より高いため、`GetIDs` のようなバルク操作メソッドの恩恵はさらに大きくなる。
結言
コードの美しさは、その「静かさ」に宿る。
数万の図形を前にして、画面を激しく明滅させ、メモリを食いつぶしながら鈍重に動くプログラムは、プロフェッショナルの仕事ではない。
`Page.GetIDs` を使いこなし、Visio という巨大なデータ構造を「単なる数値の配列」として手懐けること。それが、真のシステムアーキテクトへの第一歩である。
