Visio階層構造の深淵へようこそ:Shape.Parentを制する者は複雑な自動化を制す
Visio VBAの世界へ足を踏み入れた者が最初に突き当たる壁、それが「Shapeの親は一体誰なのか?」という問いだ。
ExcelやWordと違い、Visioのオブジェクトモデルは極めて動的で、かつ「入れ子」の構造が複雑だ。単なるグループ化ならまだしも、Visio 2010以降に導入された「コンテナ」、そして「リスト」「コールアウト」といった論理的な包含関係が、開発者の頭を悩ませる。
「`Shape.Parent`を呼べばPageが返ってくるはずだ」という安易な思い込みは、大規模な図面を扱うツールにおいて、実行時エラー(Type Mismatch)や、意図しない図形へのデータ書き込みという致命的なバグを招く。
本稿では、伝説的なアーキテクトの視点から、いかなるネスト構造からも確実に「ページ」「ドキュメント」「ルートグループ」を特定し、さらには「コンテナ」という論理的親までをも網羅的に把握する、プロダクション・グレードの階層追跡クラスの実装を伝授する。
—
なぜ「再帰」が必要なのか:Visioの二重構造を理解する
Visioの図形は、大きく分けて2つの「親」を持つ可能性がある。
1. 物理的な親(Parent Property):
グループ化された図形の場合、`Parent`は上位の`Shape`オブジェクトになる。トップレベルの図形であれば、`Parent`は`Page`または`Master`になる。
2. 論理的な親(Container):
コンテナ機能で囲まれている場合、物理的な`Parent`は`Page`のままだが、ユーザーの視覚的にはコンテナに属している。
これらを混同した設計は、データベース連携や自動採番ツールにおいて「どの領域に属する部品か」を判定する際に必ず破綻する。我々が作るべきは、この曖昧さを排除し、「物理階層を遡りつつ、論理階層も同時に解決する」ロジックだ。
—
実装:階層追跡を司る「ShapeInspector」クラス
以下のコードは、単なる関数の寄せ集めではない。プロパティを呼び出した瞬間に必要最小限の再帰を行い、結果をキャッシュする(あるいは動的に計算する)堅牢なクラス設計の一例だ。
クラス名: `ShapeInspector`
‘—————————————————————————————
‘ Class : ShapeInspector
‘ Purpose : Visio.Shapeの階層構造を多角的に解析し、関連オブジェクトを特定する
‘ Developer : Global Chief Architect
‘—————————————————————————————
Option Explicit
Private mTargetShape As Visio.Shape
‘ 初期化:解析対象のShapeをセット
Public Sub Initialize(ByRef shp As Visio.Shape)
Set mTargetShape = shp
End Sub
‘ 1. 物理的なルート図形(最上位グループ)を特定
‘ ネストされたグループ内の図形から、Page直下の図形まで遡る
Public Property Get RootShape() As Visio.Shape
Dim current As Object
Set current = mTargetShape
Do While TypeOf current.Parent Is Visio.Shape
Set current = current.Parent
Loop
Set RootShape = current
End Property
‘ 2. 所属するPageを特定
Public Property Get ParentPage() As Visio.Page
Dim current As Object
Set current = mTargetShape
Do
If TypeOf current.Parent Is Visio.Page Then
Set ParentPage = current.Parent
Exit Property
ElseIf TypeOf current.Parent Is Visio.Master Then
‘ マスターシェイプ内の編集中の場合はPageが存在しないケースがある
Set ParentPage = Nothing
Exit Property
Else
Set current = current.Parent
End If
Loop While Not current Is Nothing
End Property
‘ 3. 所属するDocumentを特定
Public Property Get ParentDocument() As Visio.Document
Dim pg As Visio.Page
Set pg = Me.ParentPage
If Not pg Is Nothing Then
Set ParentDocument = pg.Document
Else
‘ Pageが取れない(Master内など)の場合のフォールバック
Set ParentDocument = mTargetShape.Document
End If
End Property
‘ 4. 所属する「コンテナ」を特定(論理的な親)
‘ Visioのコンテナ機能を使用している場合、そのIDを取得してShapeを返す
Public Property Get BelongingContainer() As Visio.Shape
Dim containerIds() As Long
Dim targetContainer As Visio.Shape
‘ MemberOfContainersはコンテナのID配列を返す
containerIds = mTargetShape.MemberOfContainers
If (Not Not containerIds) <> 0 Then ‘ 配列が空でないかチェック
‘ 最も内側のコンテナ(通常は配列の最初の要素)を返す
Set BelongingContainer = mTargetShape.ContainingPage.Shapes.ItemFromID(containerIds(0))
Else
Set BelongingContainer = Nothing
End If
End Property
‘ 5. 完全な階層パスを取得(デバッグ・ログ用)
‘ 例: “MyDocument > Page-1 > GroupA > SubGroupB > TargetShape”
Public Function GetHierarchyPath() As String
Dim path As String
Dim current As Object
path = mTargetShape.Name
Set current = mTargetShape
Do While Not current.Parent Is Nothing
Set current = current.Parent
path = current.Name & ” > ” & path
If TypeOf current Is Visio.Document Then Exit Do
Loop
GetHierarchyPath = path
End Function
—
現場で差がつくテクニック:なぜ `TypeOf` を使うのか?
多くのVBAサンプルでは `TypeName(obj.Parent) = “Page”` といった文字列比較が散見される。しかし、私はこれを認めない。
1. パフォーマンス: 文字列比較は内部的な型判定(`TypeOf`)に比べて低速だ。数千個のシェイプを走査する場合、この差が蓄積する。
2. 堅牢性: `TypeName`はローカライズされた環境や特定のランタイムで微妙な挙動差を生むリスクがある。`TypeOf`キーワードによるインターフェースの判定こそが、COMオブジェクトを扱う上での正道である。
—
応用:データベース連携や設定ファイルとの紐付け
実務において、このクラスは「データの継承」に威力を発揮する。
例えば、「コンテナに設定された『部門名』を、その中の全ての図形が自動的に参照する」といった仕様を実装する場合だ。
Public Function GetEffectiveDepartment(shp As Visio.Shape) As String
Dim inspector As New ShapeInspector
inspector.Initialize shp
‘ 1. まず図形自身のプロパティを確認
If shp.CellExistsU(“Prop.Department”, 0) Then
GetEffectiveDepartment = shp.CellsU(“Prop.Department”).ResultStr(“”)
If GetEffectiveDepartment <> “” Then Exit Function
End If
‘ 2. なければ所属コンテナのプロパティを確認
Dim cont As Visio.Shape
Set cont = inspector.BelongingContainer
If Not cont Is Nothing Then
If cont.CellExistsU(“Prop.Department”, 0) Then
GetEffectiveDepartment = cont.CellsU(“Prop.Department”).ResultStr(“”)
If GetEffectiveDepartment <> “” Then Exit Function
End If
End If
‘ 3. それでもなければページの設定を確認
‘ …(以下同様に階層を遡る)
End Function
このように、`ShapeInspector`を介することで、「どこにデータがあるか」を探すロジックと、ビジネスロジックを分離(疎結合化)できる。これがメンテナンス性の高いコードの正体だ。
—
チーフアーキテクトからの助言
Visio VBAのコーディングにおいて、`ActiveWindow.Selection` に頼りすぎるのは素人の仕事だ。プロは、ドキュメントがバックグラウンドで開かれていても、あるいは図形が何層にネストされていても、「オブジェクトの所在を絶対的に特定できる」ロジックを構築する。
今回紹介した階層追跡の考え方は、Visioという「キャンバス」を「データベース」へと昇華させるための基礎体力となる。このクラスを君の標準ライブラリに加え、場当たり的なコードから卒業してほしい。
次は、この階層構造を利用した「高速な空間インデックスの構築」について語る機会があればと思う。さらなる高みを目指せ。
