【テクニカル・上級編】【上級者向け】ページ追加時に「自動採番」を行うためのカウンター管理と、重複ページ名の防止策 – Visio VBA解析バイブル

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

非決定論的な命名を排除せよ:Visio VBAにおける決定論的ページ管理の極意

自動化エンジニアが直面する最も忌むべき事象の一つは、「システムの非決定論的な挙動」である。

Visioにおいてページを追加する際、標準的な `Pages.Add` メソッドに依存していると、既存のページ名と衝突した瞬間にVisioは「親切心」から `(1)` や `-1` といった接尾辞を自動的に付与する。一見、ユーザーフレンドリーな挙動に見えるが、システム間連携や大規模な設計ドキュメントの自動生成において、この挙動は致命的なバグを招く。

外部のデータベースやERPからページ名を注入し、その名前をキーとして後続の解析プロセス(Pythonによる画像解析や、XMLベースの構造抽出)に渡す設計において、名前の不一致はパイプラインの崩壊を意味する。

本稿では、Visioの自動命名メカニズムを完全に封じ込め、厳格な命名規則(Naming Convention)を強制するための、カウンター管理とバリデーションロジックの設計思想を詳説する。

1. アーキテクチャ設計:状態の永続化と整合性

ページ名の一貫性を保つためには、「次に発行すべき番号」をどこに保持すべきかという問題がある。

1. メモリ内変数: プロセス終了と共に消失するため、レガシーな運用には耐えられない。
2. 外部ファイル/レジストリ: システム間連携には適しているが、ファイル共有環境や権限管理において脆弱である。
3. Document Custom Properties: Visioドキュメント自体に状態を埋め込む手法。ポータビリティと堅牢性のバランスが最も高い。

我々が採用するのは、Document Custom Propertiesを用いた状態管理と、既存全ページの走査による物理的バリデーションを組み合わせた、二段構えの防御戦略である。

2. 実装:決定論的ページ生成エンジン

以下に、シニアエンジニアが実装すべき、堅牢なページ追加モジュールのコードを示す。ここでは、単なる追加ではなく、「存在確認」「カウンターの同期」「例外処理」を一つのアトミックな操作としてカプセル化している。

Option Explicit

' ==============================================================================
' Module: PageController
' Description: 決定論的なページ管理を行うためのコントローラー
' Author: Chief Architect
' ==============================================================================

' 命名規則の定数定義
Private Const PROPERTY_NAME_COUNTER As String = "SYS_PAGE_COUNTER"
Private Const PREFIX_DEFAULT As String = "REQ-"
Private Const PADDING_LENGTH As Integer = 4 ' 0001形式

''' <summary>
''' 指定されたプレフィックスに基づき、重複のない新しいページを追加する。
''' </summary>
''' <param name="prefix">ページ名の接頭辞 (例: "REQ-")</param>
Public Sub AddControlledPage(ByVal prefix As String)
    Dim doc As Visio.Document
    Dim newPage As Visio.Page
    Dim targetName As String
    Dim nextIndex As Long
    
    On Error GoTo ErrorHandler
    
    Set doc = ActiveDocument
    
    ' 1. 次のインデックスを取得(カウンター管理)
    nextIndex = GetNextValidIndex(doc, prefix)
    
    ' 2. ターゲットとなる名前を生成 (例: REQ-0001)
    targetName = prefix & Format(nextIndex, String(PADDING_LENGTH, "0"))
    
    ' 3. 物理的バリデーション: 既存ページとの衝突を最終チェック
    If IsNameCollision(doc, targetName) Then
        ' 万が一、カウンターと実態が乖離している場合のフォールバック
        ' ここでエラーを投げるか、インデックスを再計算するロジックを組み込む
        Err.Raise vbErr_Custom + 513, "PageController", _
            "Critical Collision: The generated name '" & targetName & "' already exists."
    End If
    
    ' 4. ページ追加の実行
    ' ScreenUpdatingを停止してパフォーマンスを最適化
    Application.ScreenUpdating = False
    
    Set newPage = doc.Pages.Add
    newPage.Name = targetName
    
    ' 5. カウンターの更新(永続化)
    UpdateCounter doc, nextIndex
    
    ' 6. プロパティの明示的な解放(メモリ最適化)
    Set newPage = Nothing
    Application.ScreenUpdating = True
    
    Debug.Print "Successfully added page: " & targetName
    Exit Sub

ErrorHandler:
    Application.ScreenUpdating = True
    MsgBox "Fatal Error in Page Generation: " & Err.Description, vbCritical, "System Error"
    ' ここでログ出力や、トランザクションのロールバック処理を検討すること
End Sub

''' <summary>
''' ドキュメントプロパティから次の有効なインデックスを算出する。
''' </summary>
Private Function GetNextValidIndex(ByRef doc As Visio.Document, ByVal prefix As String) As Long
    Dim prop As Visio.CustomDocumentProperty
    Dim maxIndex As Long
    Dim i As Integer
    Dim pg As Visio.Page
    Dim currentIdx As Long
    Dim tempName As String
    
    maxIndex = 0
    
    ' A. カウンタープロパティからの取得試行
    On Error Resume Next
    Set prop = doc.CustomDocumentProperties(PROPERTY_NAME_COUNTER)
    If Not prop Is Nothing Then
        maxIndex = prop.Value
    End If
    On Error GoTo 0
    
    ' B. 【重要】既存ページを走査し、カウンターの整合性を強制的に修正(防衛的プログラミング)
    ' 手動削除などによってカウンターが不整合を起こしているケースに対処する
    For Each pg In doc.Pages
        If InStr(pg.Name, prefix) > 0 Then
            ' 接頭辞以降の数値を抽出
            tempName = Mid(pg.Name, Len(prefix) + 1)
            If IsNumeric(tempName) Then
                currentIdx = CLng(tempName)
                If currentIdx > maxIndex Then maxIndex = currentIdx
            End If
        End If
    Next pg
    
    GetNextValidIndex = maxIndex + 1
End Function

''' <summary>
''' 名前が既存のページと衝突しているかを確認する。
''' </summary>
Private Function IsNameCollision(ByRef doc As Visio.Document, ByVal targetName As String) As Boolean
    Dim pg As Visio.Page
    For Each pg In doc.Pages
        If StrComp(pg.Name, targetName, vbTextCompare) = 0 Then
            IsNameCollision = True
            Exit Function
        End If
    Next pg
    IsNameCollision = False
End Function

''' <summary>
''' カウンターをドキュメントプロパティに保存する。
''' </summary>
Private Sub UpdateCounter(ByRef doc As Visio.Document, ByVal nextVal As Long)
    On Error Resume Next
    Dim prop As Visio.CustomDocumentProperty
    Set prop = doc.CustomDocumentProperties(PROPERTY_NAME_COUNTER)
    
    If prop Is Nothing Then
        doc.CustomDocumentProperties.Add PROPERTY_NAME_COUNTER, visDocPropertyTypeFloating, visDocPinNone, nextVal
    Else
        prop.Value = nextVal
    End If
    On Error GoTo 0
End Sub

' 定数定義用
Private Const vbErr_Custom As Long = 5000

3. 深層解説:エンジニアが意識すべき「重み」

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

VBAにおける `Set obj = Nothing` は、単なる作法ではない。特にVisioのような巨大なCOMオブジェクトを扱う場合、参照カウントが正しく管理されないと、バックグラウンドプロセスとして `Visio.exe` がゾンビ化し、メモリリークを引き起こす。本実装では、`newPage` オブジェクトを生成後、明示的に解放する構造を徹底している。

3.2 防衛的プログラミング:カウンターの「不整合」への対処

本実装の核心は `GetNextValidIndex` 関数にある。
多くの開発者は「プロパティに保存した値をインクリメントするだけ」の設計を行う。しかし、ユーザーがGUIからページを削除したり、手動で名前を変更したりした場合、プロパティの値と実際のページ集合との間に「不整合(Inconsistency)」が生じる。

我々は、プロパティを「ヒント」として使いつつ、必ず全ページを走査して物理的な最大値を確認する。これにより、どのような操作を経てドキュメントが汚染されていても、常に決定論的な(重複しない)名前を生成できる。これは、システムの冪等性(Idempotency)を担保するための必須条件である。

3.3 パフォーマンスへの配慮

ページ数が増大した際、`For Each pg In doc.Pages` による走査は $O(n)$ のコストを要する。数千ページ規模の巨大ドキュメントを扱う場合は、この走査コストを抑えるために、ページ名からインデックスを抽出するロジックを、Windows APIの文字列操作や、より高速なハッシュマップ構造(Dictionaryオブジェクト)を用いてキャッシュする検討が必要となる。

結論

Visioの自動化において、標準機能の「便利さ」は時に「毒」となる。
シニアエンジニアに求められるのは、ツールが提供する抽象化された挙動を鵜呑みにせず、その裏側にあるオブジェクトモデルと、データの整合性を守るための厳格なバリデーションレイヤーを構築する能力である。

決定論的な命名規則を維持することは、単なる整理整頓ではない。それは、自動化されたシステム全体に対する信頼の基盤を築く行為である。

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