階層構造の深淵を穿つ:マスターシェイプの依存関係を解析する再帰的アルゴリズム
Visioを用いた大規模なシステム構成図や、複雑な回路図、あるいは製造ラインのレイアウト設計において、我々が対峙するのは単なる「図形」ではない。それは、高度に抽象化され、幾重にもラップされた「オブジェクトの階層構造(Object Graph)」である。
多くの開発者は、`ActivePage.Shapes` をループで回し、表面的なプロパティを操作することで満足する。しかし、エンタープライズレベルの自動化において、真に価値を持つのは「グループ化されたマスターシェイプの深層に潜む、特定の意味論的パーツ」を正確に特定し、操作する能力である。
本稿では、マスターシェイプ内のネストされたサブシェイプ構造を走査するための、再帰的アルゴリズムの設計と、メモリ管理、およびパフォーマンス最適化の極限について詳述する。
1. 浅薄な探索が招く、システム崩壊の予兆
標準的な `For Each shp In Page.Shapes` というアプローチは、トップレベルのシェイプには有効だが、グループ化されたシェイプの内部には無力である。
複雑なマスターシェイプは、以下のような構造を持つ。
`Master Shape (Group)` $\rightarrow$ `Sub-Group (Group)` $\rightarrow$ `Component (Shape)` $\rightarrow$ `Connector/Label (Shape)`
この階層を無視してプロパティを検索しようとすれば、目的のデータに辿り着けないばかりか、不適切なオブジェクトへの参照が原因で、Visioのプロセスがメモリリークを起こし、最終的には `Out of Memory` でクラッシュする。これは、レガシーな自動化システムが、長時間の運用中に不安定になる典型的なパターンである。
2. 再帰的探索アルゴリズムの設計思想
我々が実装すべきは、深さ優先探索 (Depth-First Search: DFS) をベースとした再帰エンジンである。ただし、単なる再帰では不十分だ。以下の3点を設計原則に組み込む必要がある。
1. 述語(Predicate)によるフィルタリング: 各ノード(シェイプ)が解析対象かどうかを判定するロジックを分離する。
2. 決定論的なメモリ解放: 再帰のスタックが深くなるにつれ、COMオブジェクトの参照カウントが肥大化する。これを防ぐための明示的な解放。
3. COMマーシャリングの最小化: `Shape.Cells` や `Shape.Text` へのアクセスは、VBAからVisioのコアエンジンへの境界を越える重い処理である。探索中にこれらを乱発してはならない。
3. 実装:高度なシェイプ解析エンジン
以下に、クラスモジュールを用いた、堅牢な解析エンジンの実装例を示す。ここでは、特定の「タグ(Shape Data)」を持つサブシェイプを抽出する戦略をとる。
クラスモジュール: `clsShapeAnalyzer`
'-------------------------------------------------------------------------------
' Class: clsShapeAnalyzer
' Purpose: マスターシェイプ内の階層構造を再帰的に解析する高度なエンジン
'-------------------------------------------------------------------------------
Option Explicit
Private m_targetTag As String
Private m_foundShapes As Collection
' 検索結果を格納するためのコレクション
Public Property Get FoundShapes() As Collection
Set FoundShapes = m_foundShapes
End Property
''' <summary>
''' 解析の初期化
''' </summary>
''' <param name="tag">抽出対象とするShape Dataのタグ名</param>
Public Sub Initialize(ByVal tag As String)
m_targetTag = tag
Set m_foundShapes = New Collection
End Sub
''' <summary>
''' 指定されたルートシェイプから再帰的に探索を開始する
''' </summary>
Public Sub Execute(ByVal rootShape As Visio.Shape)
If rootShape Is Nothing Then Exit Sub
' 探索開始
RecursiveTraverse rootShape
End Sub
''' <summary>
''' 再帰的探索のコアロジック (DFS)
''' </summary>
Private Sub RecursiveTraverse(ByVal currentShape As Visio.Shape)
On Error GoTo ErrorHandler
' 1. 判定ロジック: 特定のタグを持つか?
' ※ Shape Dataへのアクセスはコストが高いため、条件判定を最優先で行う
If HasTargetTag(currentShape) Then
m_foundShapes.Add currentShape
End If
' 2. 子要素への展開
' Visioにおいて、グループシェイプは内部にShapeコレクションを持つ
If currentShape.Type = visTypeGroup Then
Dim subShape As Visio.Shape
For Each subShape In currentShape.Shapes
' 再帰呼び出し
RecursiveTraverse subShape
Next subShape
End If
' 3. メモリ管理の極意
' VBAのスコープを抜ける際、参照を明示的に切ることで
' COMの参照カウントの不整合を防ぐ。
' (注: subShapeはループ変数だが、大規模ループでは明示的解放が推奨される)
ExitProcedure:
Exit Sub
ErrorHandler:
' 実務環境では、ここでエラーログをWindowsイベントログやファイルに書き出す
Debug.Print "Error traversing shape: " & currentShape.Name & " - " & Err.Description
Resume ExitProcedure
End Sub
''' <summary>
''' 指定したタグが存在するかを高速に判定する
''' </summary>
Private Function HasTargetTag(ByVal shp As Visio.Shape) As Boolean
On Error Resume Next
' SectionExistsでセクションの存在を確認してからCellを取得するのが定石
' これにより、存在しないセルへのアクセスによる低速なエラーハンドリングを回避する
If shp.SectionExists(visSectionProp, 0) Then
' タグの値が空でないことを確認
HasTargetTag = (shp.CellU("Prop.Tag").ResultIU <> 0)
Else
HasTargetTag = False
End If
On Error GoTo 0
End Function
標準モジュール: 実行制御
'-------------------------------------------------------------------------------
' Module: modMain
' Purpose: エンジンの呼び出しとシステム連携
'-------------------------------------------------------------------------------
Option Explicit
Public Sub RunDeepAnalysis()
Dim targetPage As Visio.Page
Dim rootShp As Visio.Shape
Dim analyzer As clsShapeAnalyzer
Dim foundItem As Visio.Shape
Set targetPage = ActivePage
Set analyzer = New clsShapeAnalyzer
' 解析対象のタグを指定
analyzer.Initialize "ComponentID"
' ページ上の全シェイプを起点として探索を開始
' (マスターシェイプ自体がトップレベルにある場合を想定)
For Each rootShp In targetPage.Shapes
analyzer.Execute rootShp
Next rootShp
' 結果の出力と操作
Debug.Print "--- Analysis Complete ---"
Debug.Print "Found: " & analyzer.FoundShapes.Count & " items."
For Each foundItem In analyzer.FoundShapes
' 見つかったパーツに対して、システム連携(例:外部DBへの書き出し)を行う
Debug.Print "Found Target: " & foundItem.Name & " [ID: " & foundItem.CellU("Prop.ComponentID").ResultStr(visNone) & "]"
' 例: 見つかったパーツを強調する
foundItem.Cells("LineColor").FormulaU = "RGB(255,0,0)"
Next foundItem
' --- 徹底したクリーンアップ ---
' オブジェクトのライフサイクルを制御し、メモリリークを完全に封殺する
Set analyzer = Nothing
Set targetPage = Nothing
End Sub
4. アーキテクトの視点:パフォーマンスと安定性の極限
このコードを、単なる「動くコード」から「プロフェッショナルな武器」へと昇華させるための、知見をここに記す。
4.1. COMマーシャリングのコスト
`currentShape.Cells(“Prop.Tag”)` を呼び出すたびに、VBAプロセスはVisioのプロセスに対してコンテキストスイッチを要求する。これは非常に高コストな操作だ。
もし、大量のシェイプを解析する場合、「一度取得したプロパティを構造体(User Defined Type)や専用のクラスにキャッシュする」設計に切り替えるべきである。
4.2. Windows APIによるメモリ監視(応用)
極めて大規模な図面(数万シェイプ)を扱う場合、VBAのメモリ管理だけでは不十分なケースがある。`GetProcessMemoryInfo` などの Windows API を呼び出し、解析の各ステップでプロセスの `Working Set` を監視することで、メモリリークの発生タイミングを特定し、必要に応じて自動的にVisioプロセスを再起動するような、自己修復型アーキテクチャの構築が可能になる。
4.3. レガシー環境への配慮
古いVisio(2010以前)や、32bit/64bitが混在するレガシーな社内システムでは、スタックオーバーフローのリスクが常に付きまとう。再帰が深くなりすぎる懸念がある場合は、再帰ではなく、「スタック(Stack)構造を用いた明示的なループ(Iterative DFS)」に書き換える勇気を持て。
結論
マスターシェイプの深層を解析することは、単なる図形操作ではない。それは、複雑なシステムの構造をデジタルツインとして再構築する作業である。
再帰アルゴリズムを制御し、オブジェクトのライフサイクルを支配下に置くこと。それこそが、混沌としたレガシーアーキテクチャの中で、堅牢な自動化システムを構築できるエンジニアと、単なるスクリプターを分かつ境界線である。
