【入門編】Shape.BoundingBoxによる正確な領域検出:図形同士の重なり(バウンディングボックス)計算 – Visio VBA解析バイブル

スポンサーリンク

こんにちは!Visioのオートメーションの世界へようこそ。
マクロの記録ボタンを押して出てきたコードを眺めるだけの日々は、今日で終わりにしましょう。

今回は、Visio VBAにおいて避けて通れない、しかし多くの人がつまずく「図形同士の重なり判定」の本質に迫ります。
テーマは「`Shape.BoundingBox`による正確な領域検出」です。

「図形をきれいに配置したいのに、文字や枠線が重なってしまう…」
そんな現場の悩みを一発で解決する、プロのアルゴリズムを一緒に紐解いていきましょう。ここをクリアすれば、あなたのVisio VBAスキルは確実に中級者の領域へステップアップしますよ。

1. なぜ `PinX` / `PinY` だけでは図形の重なりが分からないのか?

Visio VBAを触り始めると、真っ先に覚えるのがシェイプの位置を表すプロパティ、`PinX` と `PinY` ですよね。

‘ シェイプの中心座標を取得する基本
Debug.Print myShape.PinX
Debug.Print myShape.PinY

「お、これを使えば図形の位置がわかるぞ!じゃあ、この座標が近いもの同士を重なっていると判定しよう!」……ちょっと待ってください。実はそれ、大きな罠です。

シェイプの「基準点(Pin)」と「実際の占有範囲」のギャップ

Visioのシェイプには「回転の中心」や「基準点」である Pin が存在しますが、これは必ずしも図形の中心とは限りません(図形の左下だったり、中央だったり、あるいは図形から遠く離れた場所にあることもあります)。

さらに、以下の要素が絡み合うと、`PinX`/`PinY` だけでの判定は完全に破綻します。
1. 図形のサイズ(Width / Height)がバラバラ
2. 図形が回転(Angle)している
3. グループ化されたシェイプの入れ子構造

ここで登場するのが、今回主役の `BoundingBox` メソッド です。

2. 救世主 `Shape.BoundingBox` の正体

`BoundingBox` は、指定したシェイプ(またはページ全体、選択範囲)が「画面上のどこからどこまでを実際に占有しているか」を計算し、外接矩形(バウンディングボックス)の座標を教えてくれる極めて強力なメソッドです。

BoundingBoxが返す4つの座標

このメソッドを実行すると、以下の4つの変数が返ってきます(引数の指定方法によって若干挙動が変わりますが、基本形を押さえましょう)。

  • `left` : 最も左側のX座標
  • `bottom` : 最も下側のY座標
  • `right` : 最も右側のX座標
  • `top` : 最も上のY座標

つまり、回転していようが、特殊なピン位置であろうが、「今、その図形が紙面上のどの矩形エリアを陣取っているか」の絶対的な境界線が丸裸になるというわけです。

3. 実践!図形同士の衝突(重なり)を検知するVBAコード

百聞は一見に如かず。アクティブページ上にあるすべてのシェイプを総当たりでチェックし、「おっと、ここが重なっているぞ!」とイミディエイトウィンドウに警告を出す実用的なコードを用意しました。

開発の現場でそのままコピペして、VBE(Visual Basic Editor)の標準モジュールに貼り付けて動かしてみてください。

Sub DetectShapeOverlap()
Dim shpA As Visio.Shape
Dim shpB As Visio.Shape
Dim i As Long, j As Long

‘ BoundingBoxを受け取るための変数
Dim l1 As Double, b1 As Double, r1 As Double, t1 As Double
Dim l2 As Double, b2 As Double, r2 As Double, t2 As Double

Dim overlapCount As Long
overlapCount = 0

‘ ページ上にシェイプが2つ以上なければ意味がないのでチェック
If ActivePage.Shapes.Count < 2 Then MsgBox "比較するシェイプが2つ以上ありません。", vbExclamation Exit Sub End If ' 総当たり(O(N^2)のアルゴリズム)で全ての組み合わせをチェック For i = 1 to ActivePage.Shapes.Count Set shpA = ActivePage.Shapes(i) ' ガード節:ガイド線や非表示レイヤーなどを除外したい場合はここで弾く If shpA.Type <> visTypeGuide Then

‘ shpAの占有範囲を取得
‘ 引数 0 (visBBoxIncludeDataGraphics 等の調整も可能だが基本は0)
shpA.BoundingBox visBBoxExtents, l1, b1, r1, t1

For j = i + 1 to ActivePage.Shapes.Count
Set shpB = ActivePage.Shapes(j)

If shpB.Type <> visTypeGuide Then
‘ shpBの占有範囲を取得
shpB.BoundingBox visBBoxExtents, l2, b2, r2, t2

‘ 【核心】2つの矩形が「重なっている」条件の判定
‘ 逆に「重なっていない条件」を否定(Not)することで、重なりを美しく判定できます
‘ 1. AがBの完全におおよそ右にある OR 2. AがBの完全におおよそ左にある
‘ 3. AがBの完全におおよそ上にある OR 4. AがBの完全におおよそ下にある
Dim isNotOverlap As Boolean
isNotOverlap = (r1 < l2) Or (l1 > r2) Or (t1 < b2) Or (b1 > t2)

If Not isNotOverlap Then
overlapCount = overlapCount + 1
Debug.Print “【衝突検知】 ‘” & shpA.Name & “‘ と ‘” & shpB.Name & “‘ が重なっています!”

‘ [応用] 重なっている相手の色を赤く変更する視覚的フィードバック
‘ shpA.CellsSRC(visSectionCharacter, 0, visCharacterColor).FormulaU = “RGB(255,0,0)”
End If

End If
Next j
End If
Next i

‘ 結果報告
MsgBox “チェック完了。 ” & overlapCount & ” 件の重なりを検知しました。”, vbInformation, “領域検出結果”
End Sub

コードのここがポイント!

  • `visBBoxExtents`: BoundingBoxメソッドの第一引数です。これにより、線の太さやシャドウ効果なども含めた、シェイプの「真の境界」を取得できます。
  • AABB(Axis-Aligned Bounding Box)の交差判定ロジック:

「重なっていない状態」とは何か?を定義して、その真逆を取るのが矩形同士の衝突判定の定石です。このロジックはゲーム開発やCADの領域でも使われる非常に汎用性の高いテクニックですよ。

4. 陥りやすい罠とエンジニアの知見

この `BoundingBox` ですが、実務で使う上で知っておくべき「プロの暗黙の了解(注意点)」がいくつかあります。

1. グループシェイプの扱いに注意
グループ化された親シェイプに対して `BoundingBox` を呼ぶと、子シェイプ総てを包み込んだ外枠が返ってきます。しかし、グループの「中身」と「外部の図形」の重なりを精密に見たい場合は、子シェイプ(`Shapes` コレクション)を再帰的に掘り下げる必要があります。
2. パフォーマンス(速度)への配慮
今回のコードはシンプルな二重ループ($O(N^2)$)です。ページ上のシェイプが数百個を超えてくると、VBAのインタープリター実行では若干のタイムラグ(重たさ)を感じるようになります。もし数千個のオブジェクトを扱う場合は、空間分割法(グリッド分割など)を頭の片隅に置いておくと、一目置かれるエンジニアになれます。

まとめ

今回は `Shape.BoundingBox` を使った正確な領域検出と、図形同士の重なり判定のアルゴリズムについて解説しました。

  • `PinX`/`PinY` だけでは回転やサイズの変化に対応できない。
  • `BoundingBox` を使えば、図形の実際の占有エリア(左・下・右・上)が正確に手に入る。
  • 矩形の交差判定ロジック(重なっていない状態の否定)を用いることで、衝突を完璧に検知できる。

ここをマスターすれば、単なる「お絵描きマクロ」から、意図した通りにレイアウトを自動制御する「本格的な図面自動生成システム」へと、あなたのVBAプログラムは大きく進化します。

「ここをもっとこうしたい」「グループ化された図形の場合はどう書くべき?」といった疑問が出てきたら、いつでも先輩エンジニアに聞いてくださいね。それでは、次のステップでお会いしましょう!

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