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

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

ページ管理の深淵:Visio VBAにおける堅牢な自動採番アーキテクチャ

大規模な図面作成プロセスを自動化する際、多くのエンジニアが最初に直面する壁がある。それは「ページ追加」という、一見単純に見える操作に潜む名前の衝突(Name Collision)という名の地雷だ。

単に `Pages.Add` を呼び出し、カウンター変数で `i = i + 1` とインクリメントするだけのコードは、プロトタイプとしては機能する。しかし、ユーザーが手動でページ名を変更したり、途中のページを削除したりした瞬間、そのロジックは瓦解する。実行時に「名前が重複しています」というランタイムエラーを吐き出し、オートメーション・プロセスは無残に停止する。

本稿では、そのような脆弱な設計を排し、既存のページ空間(Namespace)を動的にスキャンしてユニークな識別子を生成する、実戦投入レベルの「ページ管理ロジック」について、メモリ管理とアルゴリズムの観点から深掘りしていく。

1. 脆弱な設計と「状態」の不一致

なぜ、単純なカウンターでは失敗するのか。それは、プログラムが保持する「内部状態(変数)」と、Visioドキュメントが保持する「実態(Pagesコレクション)」が乖離(Divergence)するからだ。

手法 メリット デメリット 破綻するシナリオ
変数による管理 高速、実装が容易 ドキュメントの状態を反映しない ページ削除、手動リネーム
全件走査型 高い整合性 ページ数増大に伴う計算コスト増 数百ページ規模の極端なドキュメント

シニアエンジニアが目指すべきは、「常にドキュメントの現在地を正(Truth)とする」設計である。

2. 高度な命名ロジック:正規表現によるパターン抽出

単に文字列を比較するのではなく、ページ名に含まれる「数値部分」を正規表現(RegExp)を用いて抽出し、その最大値を取得するアプローチが最も堅牢である。これにより、`Page_001`, `Page_002` といったゼロパディングされた形式でも、正確に次のシーケンスを導き出せる。

実装の要諦:

1. Namespaceの走査: `ActiveDocument.Pages` コレクションを全件走査する。
2. パターンマッチング: 正規表現を用いて、命名規則に合致する数値部分を抽出する。
3. 最大値の特定: 抽出された数値の中から最大値を特定し、それに `+1` する。

3. 実装:RobustPageManager クラス

以下に、単なるプロシージャではなく、オブジェクト指向の概念を取り入れた、再利用性の高いクラスモジュールの設計例を示す。

Class Module: `clsPageAutomator`

VERSION 1.0 CLASS
BEGIN
  // 属性定義
  // ---------------------------------------------------------
  // ページ管理の責務を単一化(Single Responsibility Principle)
  ' ---------------------------------------------------------
Attribute VB_Name = "clsPageAutomator"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False

' --- メンバ変数 ---
Private mPrefix As String        ' ページ名の接頭辞 (例: "Sheet_")
Private mPadding As Integer     ' ゼロパディングの桁数 (例: 3 -> "001")

' --- プロパティ ---
Public Property Let Prefix(ByVal vNewValue As String)
    mPrefix = vNewValue
End Property

Public Property Let Padding(ByVal vNewValue As Integer)
    mPadding = vNewValue
End Property

' --- メソッド ---

''' <summary>
''' 次に利用可能なユニークなページ名を生成する
''' </summary>
Public Function GetNextUniqueName() As String
    Dim pg As Visio.Page
    Dim regEx As Object
    Dim matches As Object
    Dim maxVal As Long
    Dim currentVal As Long
    Dim currentName As String
    
    maxVal = 0
    
    ' Windows Script HostのRegExpオブジェクトを利用
    ' (Late Bindingにより参照設定の依存を排除)
    Set regEx = CreateObject("VBScript.RegExp")
    With regEx
        .Global = True
        .IgnoreCase = True
        ' 接頭辞の直後に続く数字をキャプチャするパターン
        .Pattern = mPrefix & "(\d+)"
    End With
    
    ' 1. 既存のページ名を走査 (O(n) complexity)
    For Each pg In ActiveDocument.Pages
        currentName = pg.Name
        If regEx.Test(currentName) Then
            Set matches = regEx.Execute(currentName)
            ' 最初のマッチンググループ(数字部分)を取得
            currentVal = CLng(matches(0).SubMatches(0))
            If currentVal > maxVal Then maxVal = currentVal
        End If
    Next pg
    
    ' 2. 最大値に基づいて新しい名前を構成
    ' Format関数を用いてゼロパディングを適用
    GetNextUniqueName = mPrefix & Format(maxVal + 1, String(mPadding, "0"))
    
    ' オブジェクトの明示的解放 (メモリリーク防止)
    Set regEx = Nothing
    Set matches = Nothing
End Function

''' <summary>
''' 新しいページを安全に追加する
''' </summary>
Public Sub AddNewPage()
    Dim newName As String
    Dim newPg As Visio.Page
    
    On Error GoTo ErrorHandler
    
    ' ユニークな名前の取得
    newName = GetNextUniqueName()
    
    ' ページ追加の実行
    Set newPg = ActiveDocument.Pages.Add
    newPg.Name = newName
    
    Debug.Print "Successfully added page: " & newName
    
CleanUp:
    Set newPg = Nothing
    Exit Sub

ErrorHandler:
    ' システム管理者が追跡可能なエラーハンドリング
    Err.Raise Err.Number, "clsPageAutomator.AddNewPage", _
        "Failed to add page with name: " & newName & ". Error: " & Err.Description
    Resume CleanUp
End Sub

Standard Module: 呼び出し例

Sub ExecuteAutomation()
    Dim automator As clsPageAutomator
    
    ' インスタンス化
    Set automator = New clsPageAutomator
    
    ' 設定の注入 (Dependency Injectionの概念)
    With automator
        .Prefix = "Task_Report_"
        .Padding = 3
    End With
    
    ' 高速化のための設定: 描画更新の停止 (パフォーマンス最適化)
    ' 大規模なページ追加時は、画面更新を止めるのが鉄則
    Application.ScreenUpdating = False
    
    On Error GoTo CriticalError
    
    ' 連続追加テスト
    Dim i As Integer
    For i = 1 To 5
        automator.AddNewPage
    Next i

CleanUp:
    Application.ScreenUpdating = True
    Set automator = Nothing ' オブジェクトの破棄
    Exit Sub

CriticalError:
    MsgBox "致命的なエラーが発生しました。プロセスを中断します。" & vbCrLf & Err.Description, vbCritical
    Resume CleanUp
End Sub

4. エンジニアリングの極致:パフォーマンスとメモリ

4.1 オブジェクト・ライフサイクルの管理

VBAにおいて、特にCOMオブジェクト(VisioのPageやShapeなど)を扱う際、`Set obj = Nothing` による明示的な解放は、単なる「作法」ではない。長時間稼働するサーバーサイドのオートメーションや、数百の図面をバッチ処理する環境においては、これが不十分だとメモリリークを引き起こし、最終的に `Out of Memory` でプロセスがクラッシュする。

特に `GetNextUniqueName` 内で使用している `RegExp` オブジェクトは、ループの外部で定義するか、使用後に確実に `Nothing` を代入すべきだ。

4.2 計算量の考察

本アルゴリズムの計算量は $O(n)$ である。$n$ はページ数に比例する。
もし、数万ページに及ぶような極端な図面を扱う場合、毎回全件走査を行うのは非効率になる可能性がある。その場合は、ドキュメントの「カスタムプロパティ」に、最後に使用したインデックス値をキャッシュしておく(State Persistence)という、より高度なキャッシュ戦略を検討すべきだろう。

5. 結論

ページ名の自動採番は、単なる文字列操作ではない。それは、ドキュメントという「動的なデータベース」に対する、整合性を保つためのインデックス管理である。

1. Namespaceを走査し、実態を把握せよ。
2. 正規表現を用い、構造化されたパターンから値を抽出せよ。
3. オブジェクトのライフサイクルを厳格に管理せよ。

この原則を遵守することで、あなたの構築するオートメーションは、単なる「スクリプト」から、信頼に足る「システム」へと昇華する。

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