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

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

【極限の設計思想】Visio VBAにおける「堅牢な自動採番ロジック」— ページ重複エラーを根絶する Single Source of Truth の実装

業務自動化ツールの開発において、最も初歩的でありながら、最も多くのエンジニアが「設計の甘さ」を露呈するのが採番ロジックだ。

特にVisioのような図面作成ツールにおいて、ページ追加の自動化は頻出の要件である。しかし、多くの開発者が「変数にカウンターを持たせて、ページ追加のたびに `+1` する」という、あまりにも脆弱な実装で済ませようとする。

そんなコードをプロダクション環境(本番環境)にデプロイした瞬間、ユーザーの操作ミス一つでシステムは崩壊する。ユーザーが手動でページ名を書き換えた? ページを削除した? ツールを一度閉じて再起動した?
その瞬間に、あなたの書いたコードは「既に存在するページ名」を生成し、ランタイムエラーを吐き出して止まるだろう。

今日は、そんな「動けばいいだけのコード」を卒業し、プロフェッショナルが実践すべき「Single Source of Truth(信頼できる唯一の情報源)」に基づいた、重複を許さないページ命名ロジックを伝授する。

1. なぜ「変数によるカウンター管理」はゴミなのか

まず、非効率かつ危険な設計を明確に定義しておこう。

アンチパターン:状態を外部メモリに依存する設計

' ❌ 絶対にやってはいけない設計例
Dim pageCounter As Integer ' グローバル変数で管理

Sub AddPageBad()
    pageCounter = pageCounter + 1
    ActiveDocument.Pages.Add.Name = "Page_" & pageCounter
End Sub

この設計には、致命的な欠陥が3つある。

1. 揮発性 (Volatility): VBAの変数は、エラー発生やアプリケーションの再起動でリセットされる。再起動後に実行すると、再び `Page_1` を作ろうとしてエラーになる。
2. 不整合 (Inconsistency): ユーザーが手動でページを削除したり、名前を変更したりした場合、変数内の `pageCounter` と実際のドキュメントの状態が乖離する。
3. 非決定性 (Non-determinism): 「現在のドキュメントの状態」を見ていないため、ロジックがドキュメントの真実(Real State)を反映していない。

プロの設計思想はこうだ:
「カウンターはどこにも保存しない。ページ名を生成する直前に、ドキュメントそのものに問い合わせて、現在の最大値を見つけ出す。」

これが、Single Source of Truth (SSoT) の原則である。

2. 堅牢な設計:正規表現を用いたパターンマッチング

重複を防ぐための正しいアルゴリズムは以下のステップになる。

1. 命名パターンの定義: `Prefix_###`(例: `Sys_001`)のような、一貫した命名規則を定める。
2. 全ページのスキャン: ドキュメント内の全ページ名を走査する。
3. 数値の抽出: 正規表現(RegExp)を用い、パターンに合致する名前から「数値部分」だけを正確に抜き出す。
4. 最大値の特定: 抽出した数値の中から最大値 `Max` を特定する。
5. 次数の生成: `Max + 1` を、指定した桁数でゼロパディング(0埋め)して新しい名前を作る。

このアプローチであれば、ユーザーが途中のページを削除しようが、名前を書き換えようが、常に「現存する最大の番号」の次を生成できる。

3. プロダクションクオリティの実装例

以下に、そのまま実務に投入できるレベルのコードを示す。ここでは、保守性を高めるために `RegExp` を使用し、ロジックを関数としてカプセル化している。

Option Explicit

''' <summary>
''' ページ追加のメインルーチン
''' </summary>
Public Sub AddNewPageWithAutoNumbering()
    On Error GoTo ErrorHandler
    
    Dim targetDoc As Visio.Document
    Set targetDoc = ActiveDocument
    
    ' 1. 次のユニークなページ名を決定する
    ' パターン: "Page_001", "Page_002" ...
    Dim nextName As String
    nextName = GetNextUniquePageName(targetDoc, "Page_", 3)
    
    ' 2. ページを追加
    Dim newPage As Visio.Page
    Set newPage = targetDoc.Pages.Add
    
    ' 3. 名前を適用
    ' ※Visioの仕様上、名前変更はページ作成後に行うのが安全
    newPage.Name = nextName
    
    Debug.Print "Successfully added page: " & nextName
    Exit Sub

ErrorHandler:
    MsgBox "ページ追加中に致命的なエラーが発生しました。" & vbCrLf & _
           "Error: " & Err.Description, vbCritical, "Automation Error"
End Sub

''' <summary>
''' ドキュメント内の既存ページ名を走査し、パターンに基づいた次のユニークな名前を生成する
''' </summary>
''' <param name="doc">対象のVisioドキュメント</param>
''' <param name="prefix">ページ名の接頭辞 (例: "Page_")</param>
''' <param name="padding">ゼロ埋めの桁数 (例: 3 なら 001)</param>
''' <returns>生成された新しいページ名</returns>
Private Function GetNextUniquePageName(ByVal doc As Visio.Document, _
                                       ByVal prefix As String, _
                                       ByVal padding As Integer) As String
    
    Dim pg As Visio.Page
    Dim maxNum As Long
    Dim currentNum As Long
    Dim regEx As Object
    Dim matches As Object
    Dim pattern As String
    
    maxNum = 0
    
    ' Late BindingでRegExpオブジェクトを作成 (参照設定不要で配布可能にするため)
    Set regEx = CreateObject("VBScript.RegExp")
    
    ' 正規表現パターン: 接頭辞 + 数字
    ' 例: prefixが "Page_" なら "^Page_(\d+)$"
    pattern = "^" & prefix & "(\d+)$"
    regEx.Pattern = pattern
    regEx.Global = False
    regEx.IgnoreCase = True
    
    ' 全ページをスキャンして最大値を探す
    For Each pg In doc.Pages
        If regEx.Test(pg.Name) Then
            Set matches = regEx.Execute(pg.Name)
            ' 括弧内の数字部分 (SubMatches(0)) を取得
            currentNum = CLng(matches(0).SubMatches(0))
            
            If currentNum > maxNum Then
                maxNum = currentNum
            End If
        End If
    Next pg
    
    ' 次の番号を計算
    maxNum = maxNum + 1
    
    ' ゼロパディングしてフォーマット (Format関数を使用)
    ' 例: 1 -> "001" (padding=3の場合)
    Dim formatStr As String
    formatStr = String(padding, "0")
    
    GetNextUniquePageName = prefix & Format(maxNum, formatStr)
End Function

このコードの「プロの設計」ポイント

1. Late Binding (遅延結合): `CreateObject(“VBScript.RegExp”)` を使用している。これにより、配布先のPCで「Microsoft VBScript Regular Expressions 5.5」の参照設定が漏れていても、コードが動かないという初歩的なミスを防いでいる。
2. 正規表現による厳密な抽出: `pg.Name Like “Page_*”` のような単純な比較ではなく、正規表現 `^Prefix(\d+)$` を使うことで、「Page_123」は抽出するが、「Page_Special_123」は無視するといった、ノイズに対する耐性を持たせている。
3. ゼロパディングの動的制御: `padding` 引数によって、`Page_1` にするか `Page_0001` にするかを容易に切り替えられる。これは大規模な図面管理において、ソート順を維持するために極めて重要だ。
4. エラーハンドリングの分離: 業務ロジックとエラー処理を明確に分け、ユーザーに「何が起きたか」を伝える準備ができている。

4. さらなる高みへ:ファイル・データベース連携時の注意点

もし、この採番ロジックを外部のデータベース(SQL ServerやAccess)や、外部の管理ファイル(CSV/JSON)と連携させる場合、設計の難易度は跳ね上がる。

設計上の警告:
外部DBに「現在の最大番号」を持たせるのは、絶対に避けてくれ。

理由は、分散システムにおける競合(Race Condition)だ。
複数のユーザーが同時にVisioを開き、同時にページ追加ボタンを押した場合、DBから取得した「最大値」が同一になり、結局Visio内部で名前の衝突が発生する。

結論:
採番のマスターデータは、常に「現在操作しているドキュメントそのもの」に置くべきだ。外部ファイルやDBは、あくまで「どのプロジェクトの、どのテンプレートを使用するか」という「メタデータ」の管理に留めるのが、最も堅牢なアーキテクチャである。

まとめ

「動くコード」を書くのは誰にでもできる。しかし、「壊れないコード」を書くには、データの真実がどこにあるのかを常に問い続ける必要がある。

1. 変数を信じるな。ドキュメント(実態)を信じろ。
2. 正規表現でパターンを厳密に制御せよ。
3. 冪等性(何度実行しても同じ結果が得られる性質)を意識せよ。

この原則を守るだけで、あなたの作る自動化ツールは、現場のプロフェッショナルから絶大な信頼を得られるはずだ。

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