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

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

レイヤー管理の規律:Visioにおける「未分類シェイプ」を抹殺する堅牢な自動化設計

大規模なシステム構成図やネットワーク図をVisioで運用している現場では、必ずと言っていいほど「レイヤー管理の崩壊」という問題に直面する。

特定のレイヤーを非表示にして情報を整理しようとした際、意図せず画面に残るシェイプ。それは、作成者がレイヤー設定を忘れた「未分類のゴミ」である。数千個のシェイプが存在する大規模図面において、これらを手動で探すのは時間の無駄であり、ヒューマンエラーの温床でしかない。

本記事では、単に「レイヤーがないシェイプを移動させる」という初歩的なコードの書き方ではなく、プロダクション環境(実業務)で耐えうる、エラーハンドリングとパフォーマンスを両立させた設計思想を伝授する。

1. なぜ「単純なループ」では不十分なのか

初心者が書くコードは、大抵の場合こうだ。

' ⚠️ アンチパターン:プロフェッショナルが書かないコード
For Each shp In ActivePage.Shapes
    If shp.LayerCount = 0 Then
        shp.AddLayer TargetLayer
    End If
Next shp

これでは、実務の荒波には耐えられない。理由は3つある。

1. 存在しないオブジェクトへの依存: `TargetLayer` が事前に作成されている保証がない。
2. ロックされたオブジェクトへの無策: プロテクト(保護)されたシェイプにレイヤーを追加しようとした瞬間、ランタイムエラーでプロセスが停止する。
3. スケーラビリティの欠如: 大規模図面において、エラーが発生した際に「どこまで処理が進んだか」が分からず、図面が中途半端な状態で放置される。

我々アーキテクトが目指すべきは、「失敗してもシステムを壊さず、かつ、何が起きたかを明確に報告する」堅牢なルーチンである。

2. 堅牢な設計思想:防御的プログラミング

今回のツールを設計するにあたり、以下の3つの原則を適用する。

① 事前検証(Pre-validation)

レイヤーが存在するか、ページが編集可能か、ターゲットとなるレイヤーが既に定義されているかを、処理の「前」に確定させる。

② 境界条件のハンドリング(Boundary Conditions)

シェイプがロックされている場合、あるいはページ全体が保護されている場合、それを「異常」として落とすのではなく、「スキップ対象」としてログに記録する。

③ 状態の原子性(Atomicity)

大量のシェイプを処理する際、エラーが発生しても、それまでの処理結果を保持しつつ、安全に終了させる。

3. プロダクション・コード実装例

以下に、そのまま業務ツールとして組み込めるレベルのコードを示す。このコードは、単なるスクリプトではなく、再利用性を考慮したモジュール構造を意識している。

Option Explicit

''' <summary>
''' ページ内の未分類シェイプを特定し、指定のレイヤーへ一括移動させる。
''' </summary>
Public Sub OrganizeUnclassifiedShapes()
    Const UNCLASSIFIED_LAYER_NAME As String = "未分類"
    
    Dim targetPage As Visio.Page
    Dim targetLayer As Visio.Layer
    Dim shp As Visio.Shape
    Dim unclassifiedCount As Long
    Dim errorCount As Long
    Dim skipCount As Long
    
    ' 1. 初期化と環境チェック
    On Error GoTo ErrorHandler
    Set targetPage = ActivePage
    
    If targetPage Is Nothing Then
        MsgBox "有効なページが見つかりません。", vbCritical
        Exit Sub
    End If

    ' 2. ターゲットレイヤーの確保(存在しなければ作成)
    Set targetLayer = GetOrCreateLayer(targetPage, UNCLASSIFIED_LAYER_NAME)
    
    unclassifiedCount = 0
    errorCount = 0
    skipCount = 0

    ' 3. メイン・スキャン・ルーチン
    ' 大規模図面を想定し、Shapeオブジェクトのライフサイクルを意識した走査
    For Each shp In targetPage.Shapes
        ' 隠しシェイプや接続点などを除外するためのフィルタリング(必要に応じて拡張)
        If shp.Type = visTypeShape Then
            
            ' レイヤー未設定の判定
            If shp.LayerCount = 0 Then
                On Error Resume Next ' 個別のシェイプ操作におけるエラーを局所化
                
                ' シェイプがロックされていないか確認
                If shp.Cells("ProtectionLocked").ResultIU = 0 Then
                    shp.AddLayer targetLayer
                    unclassifiedCount = unclassifiedCount + 1
                Else
                    ' ロックされている場合はスキップ
                    skipCount = skipCount + 1
                End If
                
                If Err.Number <> 0 Then
                    ' 予期せぬエラー(メモリ不足やオブジェクト破損など)
                    errorCount = errorCount + 1
                    Err.Clear
                End If
                On Error GoTo ErrorHandler ' エラーハンドリングを元に戻す
                
            End If
        End If
    Next shp

    ' 4. 結果の報告
    ReportResults unclassifiedCount, skipCount, errorCount

    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical, "System Failure"
End Sub

''' <summary>
''' 指定した名前のレイヤーを取得する。存在しない場合は新規作成する。
''' </summary>
Private Function GetOrCreateLayer(targetPage As Visio.Page, layerName As String) As Visio.Layer
    Dim lyr As Visio.Layer
    
    ' 既存レイヤーの走査
    For Each lyr In targetPage.Layers
        If lyr.Name = layerName Then
            Set GetOrCreateLayer = lyr
            Exit Function
        End If
    Next lyr
    
    ' 存在しない場合は新規作成
    Set lyr = targetPage.Layers.Add(layerName)
    Set GetOrCreateLayer = lyr
End Function

''' <summary>
''' 実行結果をユーザーに通知する。
''' </summary>
Private Sub ReportResults(moved As Long, skipped As Long, errors As Long)
    Dim msg As String
    msg = "【整理プロセス完了】" & vbCrLf & vbCrLf
    msg = msg & "・未分類から移動: " & moved & " 件" & vbCrLf
    
    If skipped > 0 Then
        msg = msg & "・保護によりスキップ: " & skipped & " 件" & vbCrLf
    End If
    
    If errors > 0 Then
        msg = msg & "・エラー発生: " & errors & " 件" & vbCrLf
        msg = msg & vbCrLf & "※エラーが発生したシェイプは手動で確認してください。"
    End If
    
    MsgBox msg, vbInformation, "Execution Summary"
End Sub

4. アーキテクトの視点:コードの深層解説

なぜ `GetOrCreateLayer` を分離したのか?

レイヤーの有無を確認し、なければ作るというロジックをメインルーチンに混ぜると、コードの可読性が著しく低下する。これを独立した関数(Helper Function)に切り出すことで、メインのアルゴリズムが「何を目指しているのか」を明確にしている。これは単一責任の原則(Single Responsibility Principle)に基づいた設計である。

`On Error Resume Next` の戦略的運用

このコードの肝は、`On Error Resume Next` を「どこで使い、どこで戻すか」にある。
全域でこれを使うのは怠慢であり、バグを見逃す原因となる。しかし、「個々のシェイプに対する操作(AddLayer)」のように、失敗する可能性はあるがプロセス全体を止める必要のない箇所に対しては、局所的に適用するのがプロの技だ。

パフォーマンスの重み

`For Each shp In targetPage.Shapes` は、Visioのオブジェクトモデルにおいて非常に強力だが、シェイプ数が数万規模になるとオーバーヘッドが生じる。もしさらに高速化が必要であれば、`Shape.PageID` を利用したフィルタリングや、非表示シェイプを最初から除外するロジックを検討すべきだが、通常の業務範囲では、この「堅牢性重視」の設計が最もコストパフォーマンスが高い。

5. 結論:自動化は「制御」である

業務自動化エンジニアにとって、コードを書くことはゴールではない。「予期せぬ事態をいかに制御下に置くか」が真の目的である。

今回紹介したツールは、単にレイヤーを分けるだけではない。「ロックされたシェイプを無視する」「エラーをカウントする」「結果を報告する」という一連の振る舞いを通じて、ユーザーに対して「このツールは信頼できる」という安心感を与える。

次に君がツールを作る時、そのコードには「もし失敗したらどうするか?」という問いへの答えが組み込まれているだろうか?

その思考こそが、単なるプログラマーと、伝説的なアーキテクトを分かつ境界線である。

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