Visio VBAを掌握する極限の知見:Shape.BoundingBoxによる正確な領域検出と衝突回避アルゴリズム
Visio VBAにおける図形操作の大部分は、`PinX` と `PinY`、そして `Width` と `Height` の操作に終始しがちだ。しかし、現場で複雑なダイアグラムの自動生成や、ノードの自動レイアウトエンジンを構築した者なら誰もが一度は絶望する壁がある。
「回転角(Angle)を持った図形」や「複数のグループ化されたシェイプ」、「異形なパスを持つ図形」において、`PinX/PinY` をベースにした単純な座標計算は完全に破綻する。回転によって生じる外接矩形の変化を無視すれば、図形同士は無残に重なり合い、自動生成された図面は使い物にならないゴミと化す。
今回は、Visioのオブジェクトモデルの深層に切り込み、`Shape.BoundingBox` メソッドを用いた真の占有範囲の検出と、実用に耐えうる衝突回避・再配置アルゴリズムの全貌を解説する。
—
1. なぜ `PinX/PinY` では重なり判定に失敗するのか
多くの初学者が陥る罠は、シェイプの位置を `PinX`, `PinY`(通常は図形の回転中心、多くはシェイプの重心)で捉え、幅と高さから矩形範囲を割り出そうとすることだ。
‘ 【アンチパターン】これでは回転した図形の重なりを正確に検知できない
Dim left1 As Double, right1 As Double, top1 As Double, bottom1 As Double
left1 = shp1.Cells(“PinX”).ResultIU – shp1.Cells(“Width”).ResultIU / 2
‘ …このような計算は破綻する
Visioのシェイプが回転(`Angle` セルが 0 以外)した瞬間、その占有領域(Bounding Box)は幾何学的に拡大する。`PinX/PinY` を基準にした計算では、回転後の実際の物理的な専有面積をカバーできず、衝突判定の誤検知(偽陽性・偽陰性)が頻発する。
ここで投入すべき唯一無二の解が、`Shape.BoundingBox` メソッドである。
—
2. `Shape.BoundingBox` の仕様とメモリ/パフォーマンスの極限最適化
`Shape.BoundingBox` は、指定したシェイプの正確な外接矩形をページ座標(内部単位:インチ)で返す。
構文と引数の罠
shp.BoundingBox Flags, pllLeft, pllBottom, pllRight, pllTop
引数 `Flags` に指定する値によって、取得できる矩形の性質が変わる。
- `visBBoxIncludeVisible` (一眼レフのファインダーのように、表示されている図形要素の範囲)
- `visBBoxUpToLayer`
- などがあるが、実務上は `visBBoxIncludeData` やデフォルトの挙動を理解しつつ、テキストや影を含めるか否かのフラグを制御することが求められる。
【極限の知見】オブジェクトのライフサイクルとメモリ管理
Visio VBAで数千個のシェイプを走査するレイアウトエンジンを書く場合、メモリリークとCOMオブジェクトの解放漏れは致命的なパフォーマンス低下(あるいはVisio自体のフリーズ)を引き起こす。
`BoundingBox` 自体はメソッドであり、戻り値として直接座標の変数を渡すため、VBAのオブジェクト参照のようなインスタンス肥大化は起きにくいが、ループ内で不必要にシェイプのプロパティやセルにアクセスするオーバーヘッドは極限まで削らなければならない。
—
3. 実装:正確なバウンディングボックス取得と衝突検知エンジン
以下に、2つのシェイプが空間的に重なっているか(あるいは一定のクリアランスを保っているか)を高精度に判定し、衝突している場合に自動で位置をずらすプロダクション品質のVBAコードを示す。
Option Explicit
‘ ユーザー定義型:矩形座標を保持する構造体
Private Type RectF
Left As Double
Bottom As Double
Right As Double
Top As Double
End Type
Public Sub ExecuteCollisionAvoidanceLayout()
Dim vsoPage As Visio.Page
Set vsoPage = ActivePage
Dim vsoShapes As Visio.Shapes
Set vsoShapes = vsoPage.Shapes
Dim i As Long, j As Long
Dim shpA As Visio.Shape, shpB As Visio.Shape
‘ パフォーマンス最適化:画面描画とイベントの停止
With Application
.ScreenUpdating = False
.EventEnabled = False
.UndoEnabled = False
End With
On Error GoTo ErrorHandler
‘ 簡易的なO(N^2)衝突検知と回避ループ(実運用ではR木やグリッド分割を推奨)
For i = 1 To vsoShapes.Count
Set shpA = vsoShapes(i)
‘ ガイドや非表示レイヤー、コネクタなどを除外する条件分岐
If IsValidTargetShape(shpA) Then
Dim boxA As RectF
boxA = GetAccurateBoundingBox(shpA)
For j = i + 1 To vsoShapes.Count
Set shpB = vsoShapes(j)
If IsValidTargetShape(shpB) Then
Dim boxB As RectF
boxB = GetAccurateBoundingBox(shpB)
‘ 衝突(重なり)判定
If CheckCollision(boxA, boxB) Then
‘ 衝突している場合、shpBを右方向へ退避させる(単純な回避アルゴリズム)
ResolveCollision shpB, boxA, boxB
‘ shpBが移動したため、boxAとの再判定用にboxBを更新
boxB = GetAccurateBoundingBox(shpB)
End If
End If
Next j
End If
Next i
ErrorHandler:
‘ 確実な環境復元
With Application
.ScreenUpdating = True
.EventEnabled = True
.UndoEnabled = True
End With
If Err.Number <> 0 Then
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
End If
End Sub
‘ シェイプの正確なBoundingBoxを取得する関数
Private Function GetAccurateBoundingBox(shp As Visio.Shape) As RectF
Dim lLeft As Double, lBottom As Double, lRight As Double, lTop As Double
‘ VisioのBoundingBoxメソッドを呼び出し
‘ 引数 0 (visBBoxIncludeVisible) は標準的な表示領域を対象とする
shp.BoundingBox 0, lLeft, lBottom, lRight, lTop
Dim rect As RectF
rect.Left = lLeft
rect.Bottom = lBottom
rect.Right = lRight
rect.Top = lTop
GetAccurateBoundingBox = rect
End Function
‘ 2つの矩形が交差(衝突)しているかを判定
Private Function CheckCollision(r1 As RectF, r2 As RectF) As Boolean
‘ AABB(Axis-Aligned Bounding Box)の交差判定ロジック
If (r1.Left < r2.Right) And (r1.Right > r2.Left) And _
(r1.Bottom < r2.Top) And (r1.Top > r2.Bottom) Then
CheckCollision = True
Else
CheckCollision = False
End If
End Function
‘ 衝突を解消するための位置補正ロジック
Private Sub ResolveCollision(shpTarget As Visio.Shape, rStatic As RectF, rTarget As RectF)
‘ 簡易的に、動かすべきシェイプを静的シェイプの右端まで移動させ、さらにマージンを設ける
Dim margin As Double
margin = 0.2 ‘ 0.2インチのクリアランス
Dim currentPinX As Double
currentPinX = shpTarget.Cells(“PinX”).ResultIU
‘ 移動差分の計算
Dim shiftX As Double
shiftX = (rStatic.Right – rTarget.Left) + margin
‘ ピン座標を直接更新
shpTarget.Cells(“PinX”).ResultIU = currentPinX + shiftX
End Sub
‘ 処理対象外とすべきシェイプのフィルタリング
Private Function IsValidTargetShape(shp As Visio.Shape) As Boolean
‘ コネクタ(Line)やガイドを除外し、通常のマスターシェイプ/グループのみを対象にする
If shp.Type = visTypeShape Then
‘ シェイプシートのレイヤーや特定プロパティによる除外判定をここに記述可能
IsValidTargetShape = True
Else
IsValidTargetShape = False
End If
End Function
—
4. チーフアーキテクトからの実践的助言:パフォーマンスの限界を突破するために
上記のコードは基本的な衝突検知のメカニズムを示しているが、対象シェイプが数千個規模に達した場合、`O(N^2)` の総当たりループは確実にシステムを重くする。
1. 空間分割法(Spatial Indexing / Grid Partitioning)の導入
キャンバス全体を格子状(グリッド)に分割し、各シェイプがどのグリッドセルに属しているかをあらかじめインデックス化すること。これにより、全シェイプ同士を比較するのではなく、近傍のグリッドにあるシェイプのみに `BoundingBox` の比較対象を絞り込むことが可能になる。
2. マスターシェイプとインスタンスのキャッシュ
ループ内で毎回 `.Cells(“PinX”)` や ShapeSheet へのアクセスを行うと、COM層を跨ぐ通信コスト(マーシャリングコスト)が蓄積する。一括で配列にデータを読み込んで処理するアーキテクチャ設計も、極限のパフォーマンスを求める現場では検討すべきである。
Visio VBAはレガシーな技術と見なされがちだが、そのオブジェクトモデルの奥底にある数学的挙動(とりわけ今回解説した `BoundingBox` のような幾何学API)を完全に掌握すれば、C#や外部アプリケーションに頼らずとも、高速で堅牢なドキュメント自動生成基盤を構築できる。
妥協のないコードで、あなたのVisio自動化システムを次のステージへと引き上げてほしい。
