【Visio VBA極限設計】ページ命名規則の自動化 — 「動くだけ」のコードから「壊れない」プロダクションコードへ
エンジニアの諸君、現場で「とりあえず動くスクリプト」を量産していないか?
今回扱うテーマは、一見すると初心者向けの、極めて単純なものだ。「ページ名に作成日を自動付与して、図面管理を効率化する」。
文字列を結合して、ページ名を書き換える。たったこれだけの作業に、どれほどの設計思想を込める必要があるのか? と思う者がいるかもしれない。だが、ここが分かっていない者に、大規模なシステムを支える「堅牢な自動化ツール」を設計する資格はない。
もし君が、日付を結合するだけのコードを書き、その結果として「既に同じ名前のページが存在します」という実行時エラーに泣かされたり、二重に日付が付与されて `20231027_20231027_Page-1` のような醜い名前を生み出したりしているのなら、この記事を最後まで読み、その設計思想を根底から叩き直してほしい。
1. なぜ「単純な実装」では失敗するのか
多くの初学者が書くコードは、おそらく以下のようなものだ。
' --- 典型的な「ダメな」実装例 ---
Sub BadImplementation()
Dim pg As Visio.Page
For Each pg In ActiveDocument.Pages
' 単純な結合:二重実行すると日付が重なり続ける
' 重複チェックがないため、エラーで止まる可能性がある
pg.Name = Format(Date, "yyyymmdd") & "_" & pg.Name
Next pg
End Sub
このコードには、プロフェッショナルとして見過ごせない3つの致命的な欠陥がある。
① 冪等性(Idempotency)の欠如
「同じ操作を何度行っても、結果が変わらない」という性質を冪等性と呼ぶ。上記のコードを2回実行すると、ページ名には日付が2回付与される。これはツールとしての信頼性を著しく損なう。
② 名前衝突(Name Collision)への無策
Visioのページ名は一意(Unique)でなければならない。もし、既存のページ名と生成した名前が衝突した場合、VBAは実行時エラーを吐いて停止する。業務ツールにおいて、エラーによる強制終了は「使い物にならない」と同義だ。
③ 無効な文字への配慮不足
日付フォーマットに `/` や `:` を含めてしまうような初歩的なミス、あるいはファイルシステムやVisioの命名規則に抵触する文字の扱い。これらは、将来的なファイル保存や外部データベース連携の際に、致命的なバグを引き起こす。
2. プロダクション級の設計思想
我々アーキテクトが目指すべきは、「予測可能で、自己修復的なロジック」だ。
設計の要件を以下のように定義する。
1. 命名パターンの定義: `YYYYMMDD_元の名前` という形式を固定する。
2. 状態の検知: すでに日付が付与されているページは、二重処理の対象から除外する。
3. 衝突回避アルゴリズム: もし同名が存在する場合は、末尾に連番(`_1`, `_2`…)を付与して回避する。
4. エラーハンドリング: 予期せぬ事態に備え、エラーを適切に捕捉し、ユーザーに通知する。
3. 実装:堅牢なページ命名自動化ツール
それでは、これら全ての要件を満たした、そのままプロダクション環境へ投入可能なコードを提示する。
Option Explicit
''' <summary>
''' ページ名に作成日を自動付与するメインルーチン。
''' 命名規則: YYYYMMDD_OriginalName
''' </summary>
Public Sub RenamePagesWithDatePattern()
Dim doc As Visio.Document
Dim pg As Visio.Page
Dim currentName As String
Dim newName As String
Dim datePrefix As String
Dim errorCount As Integer
On Error GoTo ErrorHandler
Set doc = ActiveDocument
' 日付プレフィックスの生成 (例: 20231027_)
datePrefix = Format(Date, "yyyymmdd") & "_"
errorCount = 0
' 画面更新を停止してパフォーマンスを向上(大量ページ対策)
' ※VisioではDoEventsや描画制御が重要
For Each pg In doc.Pages
currentName = pg.Name
' 1. 冪等性の確保: すでにプレフィックスが付与されていないかチェック
' 簡易的な判定として、先頭が日付形式(8桁数字 + _)であるかを確認
If Not IsAlreadyFormatted(currentName, datePrefix) Then
' 2. 新しい名前の生成試行
newName = GenerateUniqueName(doc, datePrefix & currentName)
' 3. リネーム実行
On Error Resume Next ' 個別のページエラーでループを止めない
pg.Name = newName
If Err.Number <> 0 Then
Debug.Print "Error renaming page: " & currentName & " -> " & Err.Description
errorCount = errorCount + 1
Err.Clear
End If
On Error GoTo ErrorHandler ' エラーハンドリングを元に戻す
End If
Next pg
MsgBox "処理が完了しました。" & IIf(errorCount > 0, vbExclamation & vbCrLf & "一部のページでエラーが発生しました。", ""), vbInformation
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub
''' <summary>
''' すでに指定のプレフィックスが付与されているかを判定する。
''' </summary>
Private Function IsAlreadyFormatted(ByVal name As String, ByVal prefixTemplate As String) As Boolean
' prefixTemplateは "YYYYMMDD_" の形式。
' 厳密には正規表現を用いるべきだが、実務的な速度を優先し、
' 先頭8文字が数字であるか、および第9文字が "_" であるかを検証する。
If Len(name) < 9 Then
IsAlreadyFormatted = False
Exit Function
End If
Dim i As Integer
Dim isNumeric As Boolean
isNumeric = True
' 先頭8文字が数字かチェック
For i = 1 To 8
If Not IsNumeric(Mid(name, i, 1)) Then
isNumeric = False
Exit For
End If
Next i
IsAlreadyFormatted = isNumeric And (Mid(name, 9, 1) = "_")
End Function
''' <summary>
''' 名前が衝突する場合、末尾に連番を付与して一意の名前を生成する。
''' </summary>
Private Function GenerateUniqueName(ByVal doc As Visio.Document, ByVal targetName As String) As String
Dim candidateName As String
Dim counter As Integer
Dim exists As Boolean
Dim pg As Visio.Page
candidateName = targetName
counter = 1
' Visio内の全ページを走査して重複を確認
' ※大規模ドキュメントでは、Dictionaryオブジェクト等を用いた高速化を検討すべき
Do
exists = False
For Each pg In doc.Pages
If pg.Name = candidateName Then
exists = True
Exit For
End If
Next pg
If exists Then
' 衝突している場合は、末尾に _1, _2... を付与
candidateName = targetName & "_" & counter
counter = counter + 1
Else
Exit Do
End If
Loop
GenerateUniqueName = candidateName
End Function
4. アーキテクトの視点:ここがプロの仕事だ
このコードが、単なる「コピペ用スクリプト」と何が違うのか。その設計意図を解説する。
階層化されたロジック(Separation of Concerns)
`RenamePagesWithDatePattern` というメインルーチンの中に、すべてのロジックを詰め込んでいない。
- 判定のロジック (`IsAlreadyFormatted`)
- 衝突回避のロジック (`GenerateUniqueName`)
これらを独立した関数(Private Function)として切り出すことで、コードの可読性を高め、単体テストを容易にしている。
衝突回避の「Do-Loop」戦略
名前が衝突した場合、単にエラーで止まるのではなく、`candidateName = targetName & “_” & counter` という形で、名前がユニークになるまで試行を繰り返す。これは、大量のページを自動生成するプロセスにおいて、ユーザーの手を止めさせないための必須の設計だ。
パフォーマンスと堅牢性のトレードオフ
`GenerateUniqueName` 内で `For Each pg In doc.Pages` を回している。これは、ページ数が数千規模になる場合には $O(N^2)$ の計算量となり、パフォーマンスが低下する。
もし君が、数万ページ規模の図面を扱うシステムを設計しているなら、ここで `Scripting.Dictionary` を使い、ページ名をハッシュマップとしてメモリ上に保持しておくべきだ。だが、一般的な実務ツールであれば、この実装が「読みやすさ」と「速度」の最適なバランスとなる。
5. まとめ
自動化とは、単に「手作業を機械に置き換えること」ではない。
「人間が介在することによる不確実性を、設計の力で排除すること」だ。
今回紹介した命名規則の自動化は、その第一歩に過ぎない。
「動けばいい」という思考を捨て、「どうすれば壊れないか」「どうすれば保守しやすいか」を常に自問自答せよ。その積み重ねこそが、単なるプログラマーと、真のエンジニアを分かつ境界線なのだ。
Happy Coding.
