【テクニカル・上級編】図形同士の接触・重なりを正確に検知:Shape.HitTestメソッドによるレイアウト干渉チェック – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:Shape.HitTestによる完全なるレイアウト干渉検知と自動回避アルゴリズム

図面自動生成の現場において、最もエンジニアを絶望させる瞬間は何か。それは、プログラムが吐き出した数千のシェイプが無慈悲に重なり合い、配線が絡み合い、幾何学的なカオスを生み出している光景を見た時だ。

「画面上で見栄え良く配置する」という人間にとっては直感的な処理も、VBAの冷徹なオブジェクトモデルにおいては、空間計算の連続である。特に、動的に生成されるネットワーク図、組織図、プラントのP&D図などにおいて、シェイプ同士の接触・重なり(干渉)を検知し、それを動的に回避するロジックは、プロフェッショナルなVisioソリューションの成否を分ける核心領域である。

今回は、Visio VBAの隠れた名機能である `Shape.HitTest` メソッドを極限まで使い倒し、ミリ単位の精度で干渉を検知、さらに安全な空き領域へ自動退避させるアルゴリズムの全貌を解説する。

1. なぜ `HitTest` なのか? 座標計算の罠とオブジェクトモデルの真実

シェイプの重なりを判定しようとするアマチュアプログラマは、決まってこう書く。

> 「シェイプAの `PinX` と `Width`、シェイプBの `PinX` と `Width` を比較して、バウンディングボックス(BBox)の交差判定を自前で実装しよう」

愚の骨頂と言わざるを得ない。

Visioのシェイプは単なる長方形ではない。回転(Angle)を持ち、複雑なジオメトリ(パス)を持ち、グループ化され、時には不可視のピンやコントロールハンドルを持つ。これらを自前の数学的ロジックで判定しようとすれば、回転角を考慮した座標変換マトリクスをVBA上で実装する羽になり、パフォーマンスは泥沼化し、バグの温床となる。

ここで登場するのが、Visioの描画エンジン(C++コア)に直接判定を委譲する `Shape.HitTest` メソッド である。

`HitTest` のメカニズム

`HitTest` は、対象シェイプのジオメトリ境界または内部に対して、指定したポイント、あるいは別のシェイプが「ヒット(接触・包含)」しているかを判定し、その結果を整数値(`VisHitTestResults` 定数)で返す。

  • `visHitTestIntersect` (2): 境界線が交差している
  • `visHitTestInside` (1): 完全に内包されている
  • `visHitTestOutside` (0): 接触していない

このネイティブ実装された幾何学エンジンをVBAから叩くことこそが、高速かつ正確な干渉検知の唯一の解である。

2. 実装:`HitTest` を用いた干渉検知エンジン

以下のコードは、指定したターゲットシェイプが、ページ内の他のどのシェイプと干渉しているかを総当たりで高速検知し、重なりがある場合に自動的に座標をシフト(回避)する実用的なプロシージャである。

メモリリークを防ぐためのオブジェクトの明示的解放、およびVBA特有のパフォーマンス劣化を防ぐ最適化(画面描画の抑止)を完備している。

Option Explicit

‘ メイン:選択されたシェイプの干渉チェックと自動退避
Sub ExecuteInterferenceCheckAndAvoid()
Dim vsoPage As Visio.Page
Set vsoPage = ActivePage

If ActiveWindow.Selection.Count = 0 Then
MsgBox “対象となるシェイプを選択してください。”, vbExclamation, “干渉検知エンジン”
Exit Sub
End If

Dim targetShape As Visio.Shape
Set targetShape = ActiveWindow.Selection(1)

‘ パフォーマンス最大化のための描画・イベント抑制
With Application
.ScreenUpdating = False
.EventsEnabled = False
.UndoEnabled = False
End With

On Error GoTo ErrorHandler

Dim isColliding As Boolean
Dim maxRetries As Integer
Dim retryCount As Integer

maxRetries = 50 ‘ 無限ループ防止
retryCount = 0

Do
isColliding = False
Dim shp As Visio.Shape

For Each shp Is vsoPage.Shapes
‘ 自分自身との比較はスキップ
If shp.ID <> targetShape.ID Then

‘ HitTestによる交差判定
‘ ※ ここではシェイプ同士の境界交差を検知するため HitTest を利用
‘ (厳密な重なり判定には、相手シェイプのジオメトリ全体との判定を行う)
Dim hitResult As Integer
hitResult = targetShape.HitTest(shp)

If hitResult <> visHitTestOutside Then
isColliding = True

‘ 干渉が検知された場合、安全な空き領域へ右方向へシフト
Call ShiftShapeToSafePosition(targetShape, shp)
Exit For ‘ 1つ重なりが見つかったら位置を修正して再判定
End If
End If

‘ ループ内でのオブジェクト解放(VBAメモリ管理の鉄則)
Set shp = Nothing
Next shp

retryCount = retryCount + 1
If retryCount > maxRetries Then
MsgBox “安全な配置スペースが見つかりませんでした(最大試行回数超過)。”, vbCritical, “レイアウト異常”
Exit Do
End If

Loop While isColliding

MsgBox “レイアウト最適化が完了しました。試行回数: ” & retryCount, vbInformation, “完了”

ErrorHandler:
If Err.Number <> 0 Then
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
End If

‘ 環境の復元
With Application
.ScreenUpdating = True
.EventsEnabled = True
.UndoEnabled = True
End With

‘ 最終的なオブジェクト解放
Set targetShape = Nothing
Set vsoPage = Nothing
End Sub

‘ 補助プロシージャ:衝突相手の右側へ安全にシフトするアルゴリズム
Private Sub ShiftShapeToSafePosition(ByRef target As Visio.Shape, ByRef obstacle As Visio.Shape)
Dim targetPinX As Double, targetPinY As Double
Dim obstaclePinX As Double, obstaclePinY As Double
Dim obstacleWidth As Double

‘ 座標と幅の取得 (CellsUによるインチ単位での厳密な取得)
targetPinX = target.CellsU(“PinX”).ResultIU
obstaclePinX = obstacle.CellsU(“PinX”).ResultIU
obstacleWidth = obstacle.CellsU(“Width”).ResultIU

‘ 障害物の右端 + マージン(例: 0.2インチ)へ移動
Dim margin As Double
margin = 0.2

Dim newPinX As Double
newPinX = obstaclePinX + (obstacleWidth / 2) + margin + (target.CellsU(“Width”).ResultIU / 2)

‘ 新しいPinXを適用
target.CellsU(“PinX”).FormulaU = CStr(newPinX)

‘ 必要に応じてY軸方向の調整もここに記述する
End Sub

3. チーフアーキテクトが教える:実務における罠とパフォーマンス最適化

上記のコードは実用の第一歩に過ぎない。エンタープライズ環境の巨大な図面(数千個のシェイプを持つP&D図やネットワーク図)でこれを実行すると、いくつかの致命的な壁にぶ当たる。

1. ガベージコレクションとメモリリークの回避

VBAの `For Each` ループ内でCOMオブジェクト(`Visio.Shape`)を回し続けると、背後でCOMラッパーの参照カウントが増加し続け、メモリリークや最悪の場合はVisio自体のクラッシュを引き起こす。
上記のコード例にある通り、ループの各イテレーションの終わりで必ず `Set shp = Nothing` を明示的に実行し、スコープを離れる前に関連オブジェクトを完全に解放すること。これはVisio VBA開発における絶対の戒律である。

2. `CellsU` と `Cells` の使い分け

レガシーなコードでは `Cells(“PinX”)` とロケール依存のプロパティが使われているケースを見るが、これは言語環境(日本語版、英語版)によって評価式が変わりバグの温床となる。
必ず国際対応の `CellsU` を使用し、値の取得・設定には `.ResultIU` (Internal Units = インチ) および `.FormulaU` を用いること。 Visio内部の数値はすべてインチで処理されるため、センチメートルやミリメートルで思考してはならない。

3. 総当たり(O(N^2))の限界と空間分割の思想

シェイプ数 $N$ が増えると、単純な全シェイプ走査は $O(N^2)$ の計算量となり、処理がフリーズしたようになる。
もし本格的な自動レイアウトエンジンを構築するのであれば、ページ全体を仮想的なグリッド(空間インデックス)に分割し、近傍のシェイプのみを `HitTest` の対象とするアルゴリズム(グリッドハッシングやRツリーの簡易実装)をVBA上に構築する必要がある。

総括

Visio VBAにおける空間制御は、単なる「お絵描きマクロ」の領域を超えた、立派な幾何学計算システムである。

`Shape.HitTest` をマスターすれば、人間が手作業で行っていた無駄な図形整理の苦作業から解放され、外部データベース(SQL ServerやC#バックエンドからのJSON等)から動的に流し込まれるデータを、一切の破綻なく自動整列させる堅牢なドキュメント生成パイプラインが完成する。

手元のレガシーなVBAコードを見直し、ネイティブエンジンの力を正しく引き出すこと。それこそが、真にスケーラブルなVisio自動化アーキテクチャへの唯一の道である。

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