こんにちは!Visioの図面自動化の世界へようこそ。
マクロの記録ボタンを押して生成されたコードを眺めるフェーズは、もう卒業しましたか?
今回は、Visio VBAにおいて多くの開発者が躓き、そして魅了される「コンテナの再帰走査」について徹底解説します。
ここをクリアすれば、単なるお絵描きツールだったVisioが、「高度な構造化データを保持するデータベース」へと劇的に生まれ変わります。
「コンテナの中にコンテナがあるような多層ネスト構造から、配下の全図形を高速に抜き出したい」
そんな現場の切実な要望に応えるプロのアルゴリズムを、一緒に紐解いていきましょう。ここをクリアすれば、Visio VBAの基本とオブジェクトモデルの本質はバッチリですよ!
—
1. なぜ「コンテナの再帰走査」が必要なのか?
Visio 2010以降に導入された「コンテナ機能」は、業務フロー図やネットワーク図を構造化する上で欠かせない強力な武器です。
しかし、VBAからこのコンテナを操作しようとすると、多くのエンジニアが次のような壁にぶつかります。
- `ContainerProperties.GetMemberShapes` メソッドは、直下(1階層目)の図形しか取得してくれない。
- コンテナの中にさらに小コンテナがある場合(多層ネスト)、2階層目以降の図形がごっそり抜け落ちてしまう。
これを力技で解決しようとして、無限ループに陥ったり、メモリリークを起こしたりするコードをよく見かけます。
ここで必要になるのが、自分自身を呼び出しながら階層の深淵へと潜っていく「再帰処理(Recursion)」のテクニックです。
—
2. Visioオブジェクトモデルの基礎知識
再帰処理のコードを書く前に、Visio特有のオブジェクトの階層構造を頭に叩き込んでおきましょう。
Application (Visioのアプリケーション全体)
┗ Document (開いている図面ファイル)
┗ Page (描画キャンバス)
┗ Shape (個々の図形。コンテナも本質は「特殊なShape」です)
┗ ContainerProperties (コンテナ機能を持つ図形だけに存在するプロパティ)
重要なのは、「コンテナもただのShapeである」という点です。そのため、ある図形がコンテナであるかどうかは、`ContainerProperties` オブジェクトが取得できるかどうかで判定できます。
—
3. 実装コード:多層ネスト対応の高速全図形抽出エンジン
それでは、現場で即戦力として使える実用コードを公開します。
このコードは、指定した親コンテナ(またはページ全体)からスタートし、コンテナを見つけるたびに再帰的に潜り込んで、すべての末端図形IDをコレクション(または配列)に回収します。
Option Explicit
‘ ==============================================================================
‘ 処理名 : GetNestedContainerShapes
‘ 概要 : 指定されたShape(またはページ)からコンテナを再帰的に走査し、
‘ 配下にあるすべての図形IDを重複なく収集する
‘ ==============================================================================
Public Sub RunExtractContainerMembers()
Dim targetPage As Visio.Page
Set targetPage = ActivePage
‘ テスト用に選択している図形を起点にする、またはページ上の全トップレベルコンテナを走査
Dim vsoShape As Visio.Shape
Dim allMemberShapeIDs() As Long
If ActiveWindow.Selection.Count = 0 Then
MsgBox “起点となるコンテナ図形を選択してください。”, vbExclamation, “案内”
Exit Sub
End If
Set vsoShape = ActiveWindow.Selection(1)
‘ 再帰処理の実行
CollectMemberShapesRecursive vsoShape, allMemberShapeIDs
‘ 結果の出力(イミディエイトウィンドウへ)
Debug.Print “=== 抽出された配下図形一覧 ===”
If (Not allMemberShapeIDs) Is Nothing Then
MsgBox “配下に対象図形が見つかりませんでした。”, vbInformation
Else
Dim i As Long
For i = LBound(allMemberShapeIDs) To UBound(allMemberShapeIDs)
Dim shp As Visio.Shape
Set shp = targetPage.Shapes.ItemFromID(allMemberShapeIDs(i))
Debug.Print “ID: ” & shp.ID & ” / 名前: ” & shp.Name & ” / テキスト: ” & Left(shp.Text, 20)
Next i
MsgBox UBound(allMemberShapeIDs) + 1 & ” 個の図形を抽出しました。”, vbInformation, “完了”
End If
End Sub
‘ ——————————————————————————
‘ 核心となる再帰プロシージャ
‘ ——————————————————————————
Private Sub CollectMemberShapesRecursive(ByVal parentShape As Visio.Shape, ByRef idList() As Long)
Dim memberIDs() As Long
Dim i As Long
Dim j As Long
Dim isInitialized As Boolean
‘ idList がすでに初期化されているか判定するためのトラップ
On Error Resume Next
isInitialized = (Not idList Is Nothing) And (Err.Number = 0)
On Error GoTo 0
If Not isInitialized Then
‘ 初回のみ配列のベースを作る(要素数-1で初期化)
‘ ※VisioのGetMemberShapesは、コンテナでない場合はエラーを返すのでガードが必要
End If
‘ この図形がコンテナプロパティを持っているかチェック
On Error GoTo NotContainer
Dim cProps As Visio.ContainerProperties
Set cProps = parentShape.ContainerProperties
If cProps Is Nothing Then GoTo NotContainer
‘ 直下のメンバー図形IDを取得
memberIDs = cProps.GetMemberShapes(visMemberShapesAll)
If UBound(memberIDs) < LBound(memberIDs) Then Exit Sub ' メンバーがいない場合
For i = LBound(memberIDs) To UBound(memberIDs)
Dim currentMember As Visio.Shape
Set currentMember = parentShape.ContainingPage.Shapes.ItemFromID(memberIDs(i))
' 1. 現在の図形自体を結果リストに追加する処理
AddIDToArray idList, currentMember.ID
' 2.もし「このメンバー自身もさらにコンテナである場合」は、さらに深く潜る(再帰!)
If currentMember.ContainerProperties IsNot Nothing Then ' ※VBAでは IsNot使えないので If Not currentMember.ContainerProperties Is Nothing Then
CollectMemberShapesRecursive currentMember, idList
End If
Next i
NotContainer:
Exit Sub
End Sub
' ------------------------------------------------------------------------------
' ユーティリティ: 動的配列に重複を防ぎつつIDを追加する関数
' ------------------------------------------------------------------------------
Private Sub AddIDToArray(ByRef ary() As Long, ByVal newID As Long)
Dim isInit As Boolean
Dim i As Long
On Error Resume Next
isInit = (UBound(ary) >= LBound(ary))
On Error GoTo 0
If Not isInit Then
ReDim ary(0 To 0)
ary(0) = newID
Exit Sub
End If
‘ 重複チェック(コンテナ構造の循環参照や多重ヒットを防ぐ防衛的プログラミング)
For i = LBound(ary) To UBound(ary)
If ary(i) = newID Then Exit Sub
Next i
‘ 配列を拡張して追加
ReDim Preserve ary(LBound(ary) To UBound(ary) + 1)
ary(UBound(ary)) = newID
End Sub
—
コードのここがポイント!プロの知見
1. 防衛的プログラミング(Error Handling)
Visioのオブジェクトモデルは、コンテナではない図形に対して `ContainerProperties` を叩くと容赦なく実行時エラーを吐きます。`On Error` を適切に配置し、コンテナ判定を安全に行うのがプロの作法です。
2. 重複排除(Deduplication)の重要性
複雑な図面では、意図せずコンテナの親子関係が複雑化しているケースがあります。`AddIDToArray` 内でしっかりと重複チェックを行っているため、同じ図形を二重にカウントして後続の処理(一括色変更やデータエクスポートなど)がバグるのを防ぎます。
—
4. 陥りやすい罠とパフォーマンスの最適化
実務でこのコードを巨大な図面(図形数が数千個あるフロー図など)に適用する際、注意すべき点が一つあります。
それは、`ReDim Preserve` をループ内で多用することによるパフォーマンス低下です。
今回のサンプルコードは分かりやすさを優先して都度拡張していますが、もし数千・数万の図形を扱う超巨大コンテナを高速処理したい場合は、「最初にページ全体の全図形IDをスキャンして、親-子のツリー構造を一度メモリ(Dictionaryオブジェクトなど)に展開してからメモリ上で走査する」というアプローチをとるのが、プロフェッショナルなアーキテクチャ設計と言えます。
まずは上記の再帰コードをそのままコピペして、自分の図面でコンテナの階層をゴリゴリと掘り下げる快感を味わってみてください。
—
まとめ
今回は、Visio VBAにおける「Shape.ContainerProperties.GetMemberShapesの再帰走査」をテーマに、多層ネストコンテナから全図形を抽出するアルゴリズムを解説しました。
- コンテナも本質は「単なるShape」である。
- 階層構造を攻略するには「再帰処理」が不可欠。
- 予期せぬエラーや重複を防ぐための防衛的コードを入れること。
このテクニックがモノになれば、社内の複雑な業務プロセス図やシステムアーキテクチャ図から、必要なデータを一瞬で抽出し、Excelや外部データベースへ連携する強靭な自動化ツールが作れるようになります。
「ここをもっとこうしたい」「こんな応用はできる?」といった疑問があれば、いつでも先輩エンジニアに聞いてくださいね。それでは、快適なVisio自動化ライフを!
