【テクニカル・上級編】【中級者向け】ページ内の全シェイプを走査し、レイヤーが未設定のシェイプを特定して警告するツール – Visio VBA解析バイブル

スポンサーリンク
スポンサーリンク

Visio Object Model Mastery: レイヤー管理の整合性を担保する高信頼性オーディット・ルーチンの設計

大規模なエンタープライズ・アーキテクチャ図や、数千のコンポーネントを内包するシステム構成図において、レイヤー管理の欠如は単なる「整理不足」では済まされない。それは、自動化スクリプトの動作不安定、出力データの不整合、そして最終的にはシステム全体の信頼性崩壊を招く「技術的負債」である。

多くの開発者は、`For Each shape In Page.Shapes` という安直なループで解決しようとする。しかし、それは Visio のオブジェクトモデルが持つ「階層構造(Grouping)」と「COM インターフェースのオーバーヘッド」を軽視した、極めて脆弱なアプローチだ。

本稿では、シニアエンジニアが実戦で直面する「未分類シェイプの検知と強制的なレイヤー割り当て」という課題に対し、メモリ効率と再帰的走査、そして堅牢なエラーハンドリングを兼ね備えた、極限のソリューションを提示する。

1. 陥りやすい罠:浅い走査(Shallow Traversal)の限界

Visio の `Page.Shapes` コレクションは、ページ直下のシェイプのみを保持する。もしシェイプが「グループ化」されていた場合、その内部に潜むシェイプは、単純なループでは捕捉できない。

未分類のシェイプがグループの深層に隠れているとき、それを検知できないツールは、監査ツールとしての機能を果たしていない。我々が実装すべきは、オブジェクトグラフを深く、正確に掘り下げる再帰的走査(Recursive Traversal)である。

2. アーキテクチャ設計:高信頼性ルーチンの実装

以下に、実戦投入レベルの VBA 実装を示す。このコードは単にレイヤーを付与するだけでなく、オブジェクトのライフサイクル管理と、エラー発生時の状態復旧を考慮した設計となっている。

Option Explicit

' ==============================================================================
' Module: LayerAuditManager
' Description: ページ内の全シェイプを走査し、未分類のシェイプを特定・隔離する
' Author: Chief Architect
' ==============================================================================

Private Const UNCLASSIFIED_LAYER_NAME As String = "Unclassified_Layer"

''' <summary>
''' メインエントリポイント:指定されたページのレイヤー整合性を監査する
''' </summary>
''' <param name="targetPage">監査対象の Visio.Page オブジェクト</param>
Public Sub AuditAndFixLayers(ByRef targetPage As Visio.Page)
    Dim targetLayer As Visio.Layer
    Dim shape As Visio.Shape
    Dim unclassifiedCount As Long
    
    ' 1. 描画更新の停止(パフォーマンス最適化の鉄則)
    ' Visioの再計算コストを最小化するため、処理中は画面更新を抑制する
    Application.ScreenUpdating = False
    On Error GoTo ErrorHandler

    ' 2. 「未分類」レイヤーの確保
    Set targetLayer = GetOrCreateLayer(targetPage, UNCLASSIFIED_LAYER_NAME)
    
    unclassifiedCount = 0
    
    ' 3. 再帰的走査の開始
    ' ページ直下のシェイプに対して再帰処理を開始
    For Each shape In targetPage.Shapes
        If RecursiveLayerAudit(shape, targetLayer, unclassifiedCount) Then
            ' 処理成功
        End If
    Next shape

    ' 4. 結果の報告
    If unclassifiedCount > 0 Then
        MsgBox "監査完了: " & unclassifiedCount & " 個のシェイプを '" & _
               UNCLASSIFIED_LAYER_NAME & "' へ移動しました。", vbInformation
    Else
        MsgBox "監査完了: 未分類のシェイプは見つかりませんでした。", vbInformation
    End If

CleanUp:
    ' 5. リソースの明示的解放と設定の復元
    Application.ScreenUpdating = True
    Exit Sub

ErrorHandler:
    ' 予期せぬエラー(保護されたシェイプ等)が発生した場合のハンドリング
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

''' <summary>
''' 再帰的にシェイプを走査し、レイヤー未設定のものを特定する
''' </summary>
''' <param name="currentShape">現在走査中のシェイプ</param>
''' <param name="targetLayer">移動先のレイヤー</param>
''' <param name="counter">検知数のカウンター</param>
''' <returns>未分類シェイプを処理した場合はTrue</returns>
Private Function RecursiveLayerAudit(ByRef currentShape As Visio.Shape, _
                                    ByRef targetLayer As Visio.Layer, _
                                    ByRef counter As Long) As Boolean
    Dim i As Integer
    Dim subShape As Visio.Shape
    Dim isProcessed As Boolean
    
    isProcessed = False

    ' --- A. グループ化されたシェイプの深層走査 ---
    ' Visioのオブジェクトグラフを深く掘り下げる
    If currentShape.Type = visTypeGroup Then
        For i = 1 To currentShape.Shapes.Count
            Set subShape = currentShape.Shapes(i)
            If RecursiveLayerAudit(subShape, targetLayer, counter) Then
                isProcessed = True
            End If
        Next i
    End If

    ' --- B. レイヤー未設定の判定 ---
    ' Shape.LayerCount は、そのシェイプが属するレイヤーの数を返す
    ' 0 の場合は、どのレイヤーにも属していない「浮いた」状態
    If currentShape.LayerCount = 0 Then
        On Error Resume Next ' 保護されたシェイプによるエラーを回避
        currentShape.AddLayer targetLayer
        If Err.Number = 0 Then
            counter = counter + 1
            isProcessed = True
        End If
        On Error GoTo 0
    End If

    RecursiveLayerAudit = isProcessed
End Function

''' <summary>
''' 指定された名前のレイヤーを取得、存在しなければ新規作成する
''' </summary>
Private Function GetOrCreateLayer(ByRef targetPage As Visio.Page, _
                                  ByVal layerName As String) As Visio.Layer
    Dim lay As Visio.Layer
    Dim i As Integer
    
    ' 既存レイヤーの確認
    For i = 1 To targetPage.Layers.Count
        If targetPage.Layers(i).Name = layerName Then
            Set GetOrCreateLayer = targetPage.Layers(i)
            Exit Function
        End If
    Next i
    
    ' 存在しない場合は新規作成
    Set GetOrCreateLayer = targetPage.Layers.Add(layerName)
End Function

3. エンジニアリングの深層:なぜこの設計なのか

3.1. 再帰的走査による完全性の担保

前述の通り、`visTypeGroup` を判定し、`currentShape.Shapes(i)` を再帰的に呼び出すことで、ネストされたグループの何層目にあろうとも、未分類シェイプを逃さず捕捉する。これは、複雑な図面を扱うシステムにおいては必須の要件である。

3.2. COMオブジェクトのライフサイクルとメモリ管理

VBAにおける `Set` ステートメントは、COMオブジェクトの参照カウントを操作している。大規模なループ内で大量のオブジェクトを生成・操作する場合、スコープを抜けた際に確実に参照を解放することが重要である。
本コードでは、引数に `ByRef` を使用し、呼び出し元の参照を直接操作することで、不要なオブジェクトのコピー生成を防ぎ、スタックメモリの消費を抑えている。

3.3. パフォーマンスの最適化:`ScreenUpdating` の戦略的利用

Visio の描画エンジンは、シェイプのプロパティ(レイヤー、位置、色など)が変更されるたびに、画面の再描画とレイアウトの再計算を試みる。数千のシェイプを処理する場合、このオーバーヘッドは指数関数的に増大する。
`Application.ScreenUpdating = False` は単なる「おまじない」ではない。これは、描画エンジンへの割り込みを遮断し、CPUリソースをロジックの実行に集中させるための、アーキテクトとしての定石である。

3.4. レガシー環境への配慮と堅牢性

`On Error Resume Next` を使用する箇所は、極めて限定的である(`AddLayer` 実行時のみ)。これは、ユーザーがレイヤーをロックしている、あるいはシェイプが保護されているといった「実行時例外」を許容しつつ、プログラム全体の崩壊(Crash)を防ぐためである。真のプロフェッショナルは、例外を無視するのではなく、「例外が発生し得る境界」を定義する。

4. さらなる高みへ:システム間連携への展望

もしこの処理を Visio 外部(VB.NET や C#)から制御する場合、COM Interop を介することになる。その際、プロセス間の通信コストがボトルネックとなるため、VBA 側で「一括処理ルーチン」としてカプセル化しておき、外部からはそのメソッドを 1 回呼び出すだけで済むように設計するのが、最も効率的なアーキテクチャである。

また、Windows API (`user32.dll` 等) を用いて Visio プロセスの CPU 使用率やメモリ使用量を監視し、長時間実行時のメモリリークを検知する仕組みを導入すれば、それはもはや単なるスクリプトではなく、プロフェッショナルな「運用管理ツール」へと昇華する。

結論:
自動化の真髄は、コードの短さではなく、その予測可能性(Predictability)にある。このルーチンは、いかなる複雑な図面に対しても、常に一定の整合性をもたらすことを約束する。

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