Visio VBAを掌握する極限の知見:Shape.SpatialPropertiesによる超高速空間検索とメモリ最適化の実装
レガシーなCADや図面管理システムから移行されず、今なおミッションクリティカルな業務プロセスやプラント設計の可視化で稼働し続けるMicrosoft Visio。この巨大なドキュメント空間において、数千、数万のシェイプが入り乱れる図面から特定の領域にあるオブジェクトをミリ秒単位で抽出する――。
一般のプログラマがやりがちな「すべてのシェイプを走査して`BoundingBox`を比較する」というナイーブな実装は、Visio VBAの実行コンテキストにおいて最悪のアンチパターンだ。COMの境界を無駄にまたぎ、CPUを焼き尽くす。
今回は、Visioのネイティブ空間インデックスを直接叩く `SpatialProperties` メソッドを駆使し、圧倒的なパフォーマンスで空間検索を実現する極限の知見を公開する。
—
1. なぜ「全走査(ループ)」は現場を殺すのか
Visioの `Page.Shapes` コレクションを `For Each` で回し、個々の `Shape` の座標を取得して判定するコードを見たことがあるだろう。図形数が500を超えたあたりから処理が目に見えて重くなり、10,000を超えればフリーズと見紛うほどの硬直時間を生む。
原因は明確だ。
1. COM境界のオーバーヘッド: VBAとVisioのC++コアエンジン間で、オブジェクトのプロパティ(`PinX`, `PinY` 等)にアクセスするたびにマーシャリングが発生する。
2. 空間インデックスの欠如: アプリケーション側で総当たり計算を行うため、アルゴリズムのオーダーが $O(N)$(最悪の場合はそれ以上)に跳ね上がる。
これに対し、Visioが内部に持つR樹(R-tree)などの空間インデックス構造に直接クエリを投げるのが `SpatialProperties` だ。この機能を使えば、ハードウェアの限界に近い速度で、指定された矩形や他のシェイプと交差(Intersect)あるいは包含(Contain)関係にある図形群を一撃で抽出できる。
—
2. `SpatialProperties` の核心とオブジェクトのライフサイクル
`SpatialProperties` は、`Shape` オブジェクトから取得できる隠れた(しかし非常に強力な)プロパティであり、図形同士の位置関係や領域判定を司る。
しかし、ここでシニアエンジニアが留意すべきは 「Visio VBAにおけるCOMオブジェクトのメモリ管理と参照リーク」 である。
`SpatialProperties` を呼び出す際、一時的なオブジェクトやサブコレクションが生成されることが多い。これらを適切に解放せず、VBAのグローバルな参照プールに放置すれば、大規模図面のバッチ処理時に確実にメモリリーク(Out of Memory)を引き起こす。
以下の実装パターンでは、明示的なオブジェクトの破棄(`Set obj = Nothing`)を徹底し、長時間の連続稼働でもメモリ消費量が微動だにしない堅牢なコードを構築する。
—
3. 実装コード:指定矩形内(または交差)のシェイプ群をミリ秒で抽出する
以下の実用コードは、指定した座標範囲(矩形)にヒットするシェイプを `SpatialProperties` を用いて高速に抽出し、そのIDリストを返すプロシージャだ。
Option Explicit
‘ ==============================================================================
‘ 処理名: GetShapesInRegionHighSpeed
‘ 概要 : 指定された矩形領域(Bounding Box)と空間的に交差するシェイプのID配列を高速取得する
‘ 著者 : チーフアーキテクト
‘ ==============================================================================
Public Function GetShapesInRegionHighSpeed( _
ByVal targetPage As Visio.Page, _
ByVal dblLeft As Double, _
ByVal dblBottom As Double, _
ByVal dblRight As Double, _
ByVal dblTop As Double) As Long()
Dim vsoSelection As Visio.Selection
Dim lngShapeIDs() As Long
Dim i As Long
Dim count As Long
‘ エラーハンドリングの準備
On Error GoTo ErrorHandler
‘ 1. Pageオブジェクトに対して空間検索用のSelectionを作成
‘ 第1引数: 判定基準(visSpatialIncludeHidden は非表示レイヤーを考慮するかどうか等)
‘ 第2引数: 判定アルゴリズム(visSpatialOverlap または visSpatialContainedIn など)
‘ 第3引数: 許容誤差(通常は 0)
‘ 注意: ページ全体から矩形領域にヒットするものを探すため、
‘ まず一時的な矩形シェイプを非表示で作成するか、CreateSelectionを用いる。
‘ ここでは、最も高速な SpatialSearch メソッドをPageに対して適用するアプローチをとる。
‘ Visio.Page.SpatialSearch パラメータ:
‘ X1, Y1, X2, Y2, Relation, Tolerance
‘ Relation: visSpatialOverlap (0) = 交差する, visSpatialContainedIn (1) = 完全に含まれる
Set vsoSelection = targetPage.SpatialSearch( _
dblLeft, dblBottom, dblRight, dblTop, _
VisSpatialRelationCodes.visSpatialOverlap, _
0.0001)
count = vsoSelection.Count
If count > 0 Then
ReDim lngShapeIDs(1 To count)
For i = 1 To count
lngShapeIDs(i) = vsoSelection(i).ID
Next i
Else
‘ ヒットしなかった場合は空の配列を返す
lngShapeIDs = VB.Left.Empty ‘ VBAの仕様に合わせるためリサイズのみ
ReDim lngShapeIDs(0 To -1)
End If
‘ 2. 戻り値のセット
GetShapesInRegionHighSpeed = lngShapeIDs
CleanUp:
‘ 3. COMオブジェクトの明示的解放(メモリリークの完全防止)
If Not vsoSelection Is Nothing Then
Set vsoSelection = Nothing
End If
Exit Function
ErrorHandler:
Debug.Print “Error in GetShapesInRegionHighSpeed: ” & Err.Description
Resume CleanUp
End Function
—
4. 実戦での活用:一括操作プロシージャの構築
上記の高速抽出関数を利用して、特定のプラント図面エリア(例: X=10〜50, Y=20〜80 の範囲)に存在するすべてのバルブ・配管シェイプの色を一括変更する実戦的バッチ処理の例を示す。
Public Sub BatchUpdateSpatialArea()
Dim targetPage As Visio.Page
Dim shapeIDs() As Long
Dim i As Long
Dim shp As Visio.Shape
‘ パフォーマンス最適化のための定番イディオム
‘ 画面描画とイベントを一時停止し、CPUサイクルを計算だけに集中させる
With Application
.ScreenUpdating = False
.EventsEnabled = False
.WaitCursor = True
End With
On Error GoTo SafeExit
Set targetPage = ActivePage
‘ 領域を指定してシェイプIDを一瞬で取得 (単位はインチ、またはDocumentの設定に依存)
shapeIDs = GetShapesInRegionHighSpeed(targetPage, 2.0, 2.0, 10.0, 10.0)
If UBound(shapeIDs) >= LBound(shapeIDs) Then
For i = LBound(shapeIDs) To UBound(shapeIDs)
‘ IDから直接シェイプを取得 (O(1) の高速アクセス)
Set shp = targetPage.Shapes.ItemFromID(shapeIDs(i))
‘ ビジネスロジックの適用(例: 色を赤に変更)
‘ ※ セルの書き込みも必要最小限に抑える
shp.Cells(“FillForegnd”).ResultIU = 2 ‘ 赤色など
Set shp = Nothing
Next i
MsgBox UBound(shapeIDs) – LBound(shapeIDs) + 1 & ” 個のシェイプを高速処理しました。”, vbInformation
Else
MsgBox “対象領域にシェイプは見つかりませんでした。”, vbInformation
End If
SafeExit:
‘ 状態の復元(例外発生時でも必ず実行させる)
With Application
.ScreenUpdating = True
.EventsEnabled = True
.WaitCursor = False
End With
If Err.Number <> 0 Then
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
End If
End Sub
—
5. チーフアーキテクトからの実務的助言
1. 画面描画の停止(ScreenUpdating)の絶対遵守:
Visio VBAにおいて、シェイプのプロパティを変更するたびにGUIが再描画されては、どれほど空間検索が高速でも意味がない。バッチ処理の前後で `ScreenUpdating = False / True` を挟むことは、プロとしての最低限の責務である。
2. 座標系(Units)の罠:
`SpatialSearch` に渡す座標は、常にページの内 部単位(インチ:Inches)である点に注意せよ。ユーザーがUI上でセンチメートルやミリメートルで設定している場合でも、コード内では `Visio.VisUnitCodes.visInches` またはそれに換算された生値で処理しなければ、予期せぬヒット漏れを起こす。
3. レガシー環境におけるVBEの安定性:
巨大な図面を扱うシステムでは、COMの解放漏れが積もり積もってExcelやVisio本体のプロセスを不安定にする。ローカル変数の `Set obj = Nothing` は怠るな。
空間検索を制する者が、Visio VBAを制する。安易なループ処理に別れを告げ、ネイティブインデックスの圧倒的なパフォーマンスを君のソリューションに組み込んでほしい。
