【上級】Shape.SpatialRelationを活用した空間検索エンジン:指定範囲内の図形を高速抽出する
開発現場でこんな絶望的な要件を突きつけられたことはないだろうか。
「図面上にある数千個の機器シンボルの中から、指定したエリア(矩形範囲)に完全に含まれるもの、あるいは一部が重なっているものを一瞬で抽出し、データベースと突合しろ」
素人がこれを実装しようとすると、決まって各シェイプの `PinX` と `PinY` を取得し、自前で `If x1 <= shp.PinX And shp.PinX <= x2 Then...` といった幾何学的判定ループを組み始める。
結果はどうなるか。図面が巨大化するにつれて処理は重くなり、回転角を持ったシェイプや、複雑な2Dバウンディングボックスを持つグループ図形の前には完全にお手上げ。コードはバグだらけになり、実用に耐えないガラクタが完成する。
忘れてはならない。Visioの背後には、C++で最適化された強靭な幾何学エンジン(Spatial Engine)が常に稼働している。
自前で泥臭い座標計算を書くなどという車輪の再発明は、今すぐ捨て去るべきだ。
今回は、Visioの真のポテンシャルを引き出す `Shape.SpatialRelation` メソッドを極め、ミリ秒単位で空間クエリを処理する「高速空間検索エンジン」の設計と実装を伝授する。
—
1. なぜ自前座標計算は破綻するのか?
Visio VBAにおける空間判定で、初心者が陥る罠は以下の3点に集約される。
1. ピン座標(PinX / PinY)の罠: シェイプの基準点は、必ずしも中心とは限らない(ピン位置の定義に依存する)。さらに、回転(Angle)が入った途端、単純な大小比較は完全に破綻する。
2. グループ化の階層構造: 親グループの座標系と、子シェイプのローカル座標系の変換(Transform)を自前で計算しようとすると、行列計算の地獄を見る。
3. パフォーマンスの劣化: すべてのシェイプを総当り(O(N))で判定しているようでは、実務の巨大図面(数千〜数万シェイプ)を相手にしたとき、フリーズしたかのような遅延を引き起こす。
救世主:`SpatialRelation` メソッドとは何か?
Visioの `Shape` オブジェクトには、他のシェイプや特定の矩形領域との位置関係を直接判定する `SpatialRelation` というネイティブメソッドが備わっている。
ret = targetShape.SpatialRelation(otherShape, tolerance, flags)
このメソッドは、単なる「重なり判定」にとどまらず、包含(Containment)、隣接(Adjacency)、距離(Distance)を高精度かつ高速に判定する。さらに、仮想的な矩形領域(Bounding Box)を対象にすることも可能だ。これを使わない手はない。
—
2. 堅牢な空間検索エンジンのアーキテクチャ設計
プロダクション環境で耐えうるツールを作るためには、以下の設計思想が不可欠である。
- 責任の分離: 検索エリアの定義、空間クエリの実行、結果のハンドリング(色変更やDB連携)を明確に分離する。
- エラーハンドリングと保護: 削除済みシェイプやレイヤー非表示のオブジェクトなど、実行時エラーの温床を徹底的に排除する。
- トランザクション制御: 大量のシェイププロパティを操作する場合、画面描画を停止(`ScreenUpdating`)し、Undoのスコープを適切に管理する。
—
3. 【実装】指定範囲内の図形を高速抽出するプロダクションコード
以下のコードは、指定した矩形エリア(仮想的な検索枠)と交差、または完全に包含されているシェイプをページ内から高速に抽出し、該当シェイプの色を変更した上でイミディエイトウィンドウに出力する実用モジュールだ。
Option Explicit
‘ =========================================================================
‘ モジュール名: MdlSpatialSearchEngine
‘ 概要: VisioのSpatialRelationを用いた高速空間検索エンジン
‘ =========================================================================
Public Sub RunSpatialSearchDemo()
Dim targetPage As Visio.Page
Set targetPage = ActivePage
‘ 1. 検索対象エリア(矩形)の定義 [単位: インチ]
‘ ※実際の業務では、フォームの入力値や特定の「検索枠シェイプ」から取得する
Dim searchX1 As Double, searchY1 As Double
Dim searchX2 As Double, searchY2 As Double
searchX1 = 2.0: searchY1 = 2.0 ‘ 左下座標
searchX2 = 6.0: searchY2 = 6.0 ‘ 右上座標
‘ 2. 検索用の一時的な矩形(バウンディングシェイプ)をページ上に作成
‘ ※SpatialRelationに渡すため、一時的に不可視の矩形シェイプを生成する
Dim queryBox As Visio.Shape
Set queryBox = CreateQueryBox(targetPage, searchX1, searchY1, searchX2, searchY2)
If queryBox Is Nothing Then
MsgBox “検索枠の生成に失敗しました。”, vbCritical
Exit Sub
End If
‘ パフォーマンス最適化の基本:画面描画とイベントの停止
With Application
.ScreenUpdating = False
.EventsEnabled = False
.UndoBracket “Spatial Search Operation”
End With
On Error GoTo ErrorHandler
Dim shp As Visio.Shape
Dim relationResult As Integer
Dim matchCount As Long
matchCount = 0
Debug.Print “=== 空間検索開始 (Area: ” & searchX1 & “,” & searchY1 & ” – ” & searchX2 & “,” & searchY2 & “) ===”
‘ 3. ページ内の全シェイプに対して空間クエリを実行
For Each shp in targetPage.Shapes
‘ 検索枠自身は除外
If shp.ID <> queryBox.ID Then
‘ visSpatialRelationContains (完全に含まれる)
‘ または visSpatialRelationOverlap (部分的に重なっている) を判定
‘ 許容値(Tolerance)は 0.0001 インチ程度を指定
relationResult = queryBox.SpatialRelation(shp, 0.0001, visSpatialRelationIncludeHidden)
‘ 判定フラグのチェック(包含 または 交差)
If (relationResult And (visSpatialRelationContains + visSpatialRelationOverlap + visSpatialRelationInside)) <> 0 Then
matchCount = matchCount + 1
‘ ヒットしたシェイプに対する処理(例: ログ出力とハイライト)
Debug.Print “Hit! Shape ID: ” & shp.ID & ” Name: ” & shp.Name
‘ 視覚的フィードバック(赤色に変更)
shp.CellsU(“FillForegnd”).FormulaU = “RGB(255, 0, 0)”
End If
End If
Next shp
Debug.Print “=== 空間検索終了 (ヒット数: ” & matchCount & “件) ===”
MsgBox “空間検索が完了しました。ヒット数: ” & matchCount & “件”, vbInformation
CleanUp:
‘ 4. 一時的な検索枠の削除
If Not queryBox Is Nothing Then
queryBox.Delete
End If
‘ 環境の復元
With Application
.UndoCloseBracket
.EventsEnabled = True
.ScreenUpdating = True
End With
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘ =========================================================================
‘ 補助関数: 検索用の矩形シェイプを一時生成する
‘ =========================================================================
Private Function CreateQueryBox(pg As Visio.Page, x1 As Double, y1 As Double, x2 As Double, y2 As Double) As Visio.Shape
On Error GoTo ErrorHandler
Dim width As Double, height As Double, pinX As Double, pinY As Double
width = Abs(x2 – x1)
height = Abs(y2 – y1)
pinX = msoMin(x1, x2) + width / 2
pinY = msoMin(y1, y2) + height / 2
‘ 矩形を描画 (DrawRectangleの引数は x1, y1, x2, y2)
Dim shp As Visio.Shape
Set shp = pg.DrawRectangle(x1, y1, x2, y2)
‘ 検索用なので、線なし・塗りなし、非印刷に設定して存在感を消す
shp.CellsU(“LinePattern”).FormulaU = “0”
shp.CellsU(“FillPattern”).FormulaU = “0”
shp.CellsU(“NoPrint”).FormulaU = “1”
Set CreateQueryBox = shp
Exit Function
ErrorHandler:
Set CreateQueryBox = Nothing
End Function
‘ 自前Min関数(Excel/VBAの環境差異を吸収するため)
Private Function msoMin(a As Double, b As Double) As Double
If a < b Then msoMin = a Else msoMin = b
End Function
---
4. プロジェクト実戦投入時の注意点とデータベース連携
このエンジンを実際の業務システム(データベース連携や外部ファイル出力)に組み込む際は、以下のポイントを必ず押さえておくこと。
① 単位系(Unit)の罠に気をつけろ
Visioの内部データはすべてインチ(Inches)で保持されている。画面上の表示が「ミリメートル(mm)」であっても、VBAから渡す座標や `SpatialRelation` の許容値はインチに換算するか、Visioの `IUtoFunc` / `TransformResult` などの単位変換メソッドを通すこと。これを怠ると、実世界の位置と完全にズレる。
② 大規模図面におけるパフォーマンスの極意
数万個のシェイプが存在する図面で `For Each` を回すと、たとえC++のエンジンと言えども数秒のブロックが発生する。
もし「特定のレイヤー」や「特定のマスターシェイプ」に絞り込めるのであれば、ページ全体を走査する前に、シェイプの `Layer` プロパティや `Master` プロパティで事前フィルタリング(ガード節の挿入)を行え。無駄な `SpatialRelation` の呼び出しを減らすことが、エンジニアとしての腕の見せ所だ。
③ データベース連携時のID管理
抽出したシェイプの `Shape.UniqueID`(または `GUID`)を取得し、データベースの外部キーとして保存することで、Visio図面と外部システム(設備管理DB、進捗管理Excel等)の完全な同期が可能になる。
Dim shapeGUID As String
shapeGUID = shp.UniqueID(Visio.VisUniqueIDArgs.visGetUniqueIDExisting)
‘ この GUID をキーにしてDBにデータを投げる
—
チーフアーキテクトからの総括
「なんとなく動くコード」を書く時代は終わった。
Visioのオブジェクトモデルとネイティブエンジンが提供する強力なAPIを正しく理解し、適材適所で呼び出すこと。それこそが、保守性が高く、バグを寄せ付けないプロフェッショナルなVBA開発の極意である。
座標計算の呪縛から解放され、洗練された空間検索エンジンを君のプロジェクトに実装してほしい。
