【極限の知見】Visio VBAによるページ抽出ツールの設計論:単なる「検索」を「プロダクション級の自動化」へ昇華させる
エンジニア諸君、現場で「これ、自動化しといて」と頼まれたとき、君たちはどう動く?
「ページ名をループで回して、`If`文で文字列を判定し、新しいドキュメントにコピーする」……。もし、そんな書き方でコードを書き始めたのなら、今すぐその手を止めてほしい。それは「自動化」ではない。単なる「脆弱なスクリプト」だ。
実務における自動化ツールには、「例外への耐性」「リソースの管理」「保守性の高い設計」という3つの柱が不可欠である。特にVisioのようなグラフィカルなオブジェクトモデルを扱う場合、メモリ上のオブジェクトライフサイクルや、ページ間の接続関係(Connector)の断絶といった、初心者が見落としがちな「落とし穴」が至る所に潜んでいる。
本記事では、特定の文字列を含むページを検出し、別ファイルとして一括エクスポートするツールを題材に、「動くだけのコード」を「業務で耐えうるプロダクションコード」へ昇華させるための設計思想を伝授する。
1. 凡ミスが招く「壊れやすい設計」の正体
多くの開発者が陥る罠は、「検索ロジック」と「エクスポートロジック」を密結合させてしまうことだ。
例えば、以下のような書き方は避けるべきだ。
- `On Error Resume Next` でエラーを握りつぶす(どこで失敗したか分からなくなる)。
- シェイプのテキスト検索を、ページ名の検索と同じレベルの単純なループで行う(計算量の増大と、階層構造の無視)。
- 新しいファイルを作成する際、元のページのレイヤー設定やページ設定(サイズ・向き)を考慮しない。
これでは、抽出されたファイルが「見た目も設定もバラバラなゴミの集まり」になり、結局、人間が手直しする羽目になる。これでは自動化の意味がない。
2. アーキテクチャの設計:関心の分離
堅牢なツールを作るためには、機能を以下の3つのフェーズに分離(Decoupling)して設計する。
1. Search Engine (検索エンジン): 条件に合致するページオブジェクトを特定する。
2. Collector (コレクター): 特定されたページをメモリ上に一時保持、あるいはリスト化する。
3. Exporter (エクスポートエンジン): 新規ドキュメントを生成し、ページ設定を継承しながらデータを書き出す。
この設計により、「検索条件を『ページ名』から『シェイプ内の特定のプロパティ』に変えたい」といった仕様変更に対しても、検索エンジンの一部を書き換えるだけで対応が可能になる。
3. 実装:プロダクション・グレードの抽出ツール
以下に、実務でそのまま使えるレベルのコードを示す。単なるコピペで終わらせず、コメントに記した「なぜこう書くのか」という意図を読み取ってほしい。
Option Explicit
' ==============================================================================
' Module: PageExtractor
' Description: 特定の文字列を含むページを抽出し、新ファイルへエクスポートする
' Author: Chief Architect
' ==============================================================================
Sub ExecutePageExtraction()
Dim targetKeyword As String
Dim destinationPath As String
Dim sourceDoc As Visio.Document
Dim matchedPages As Collection
Dim i As Integer
' --- 1. 初期設定と入力バリデーション ---
targetKeyword = InputBox("検索するキーワードを入力してください", "抽出条件設定")
If targetKeyword = "" Then Exit Sub ' キャンセル時は即終了
Set sourceDoc = ActiveDocument
Set matchedPages = New Collection
' エラーハンドリングの開始
On Error GoTo ErrorHandler
' --- 2. 検索フェーズ (Search Engine) ---
' ページ名だけでなく、シェイプ内のテキストも走査する
Debug.Print "Searching for: " & targetKeyword
Call FindMatchingPages(sourceDoc, targetKeyword, matchedPages)
If matchedPages.Count = 0 Then
MsgBox "該当するページは見つかりませんでした。", vbInformation
Exit Sub
End If
' --- 3. エクスポートフェーズ (Exporter) ---
' 保存先のパスを生成(実行ファイルと同階層を想定)
destinationPath = sourceDoc.Path & "\Extracted_" & Format(Now, "yyyymmdd_hhnnss") & ".vsdx"
MsgBox matchedPages.Count & " 件のページが見つかりました。エクスポートを開始します。"
Call ExportPagesToNewDoc(sourceDoc, matchedPages, destinationPath)
MsgBox "完了しました!" & vbCrLf & "保存先: " & destinationPath, vbInformation
CleanUp:
' オブジェクトの解放(メモリリーク防止)
Set matchedPages = Nothing
Set sourceDoc = Nothing
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
"Error No: " & Err.Number & vbCrLf & _
"Description: " & Err.Description, vbCritical
Resume CleanUp
End Sub
''' <summary>
''' 指定されたキーワードを含むページを特定し、コレクションに格納する
''' </summary>
Private Sub FindMatchingPages(ByRef doc As Visio.Document, ByVal keyword As String, ByRef pageCol As Collection)
Dim pg As Visio.Page
Dim shp As Visio.Shape
For Each pg In doc.Pages
' 条件A: ページ名にキーワードが含まれるか
If InStr(1, pg.Name, keyword, vbTextCompare) > 0 Then
pageCol.Add pg
Else
' 条件B: ページ内のシェイプのテキストにキーワードが含まれるか
' 全シェイプを走査。階層構造を考慮し、再帰ではなく標準的なループで実装(パフォーマンス重視)
For Each shp In pg.Shapes
If shp.Text != "" Then
If InStr(1, shp.Text, keyword, vbTextCompare) > 0 Then
pageCol.Add pg
Exit For ' 1つ見つかればそのページは確定
End If
End If
Next shp
End If
Next pg
End Sub
''' <summary>
''' 特定されたページを新しいドキュメントへコピー&ペーストする
''' </summary>
Private Sub ExportPagesToNewDoc(ByRef srcDoc As Visio.Document, ByRef pageCol As Collection, ByVal savePath As String)
Dim newDoc As Visio.Document
Dim srcPg As Visio.Page
Dim newPg As Visio.Page
Dim i As Integer
' 新規ドキュメントの作成(テンプレートなしの空ドキュメント)
' ※実務では、会社の標準テンプレートを指定して作成するのが定石
Set newDoc = Documents.Add("")
For i = 1 To pageCol.Count
Set srcPg = pageCol(i)
' 1. 新しいページを作成
Set newPg = newDoc.Pages.Add
newPg.Name = srcPg.Name
' 2. ページ設定の継承(ここがプロの仕事)
' サイズ、向き、グリッド設定などを同期させる
newPg.PageSheet.CellsU("PageWidth").FormulaU = srcPg.PageSheet.CellsU("PageWidth").FormulaU
newPg.PageSheet.CellsU("PageHeight").FormulaU = srcPg.PageSheet.CellsU("PageHeight").FormulaU
newPg.PageSheet.CellsU("PageOrientation").FormulaU = srcPg.PageSheet.CellsU("PageOrientation").FormulaU
' 3. シェイプのコピー&ペースト
' Page.Shapes.SelectAll は不安定なため、各シェイプをループしてコピーする手法が最も堅牢
' ただし、大量のシェイプがある場合は Selection.Copy を検討する
srcPg.Shapes.SelectAll
ActiveWindow.Selection.Copy
newPg.Paste
' コピー後の選択解除
newPg.Shapes.SelectAll ' 一旦全選択
ActiveWindow.Selection.Deselect ' 選択解除(Visioの挙動により調整が必要な場合あり)
Next i
' 最初の空ページ(デフォルトで作られるページ)を削除
If newDoc.Pages.Count > pageCol.Count Then
newDoc.Pages(1).Delete
End If
' 保存
newDoc.SaveAs savePath
newDoc.Close
End Sub
4. チーフアーキテクトによる「技術的解説」
このコードがなぜ「ただのスクリプト」ではないのか。その設計意図を解説する。
① 計算量と検索の効率化
`FindMatchingPages` 内では、ページ名で見つかった瞬間に `Exit For` を実行している。これは、1つのページ内に大量のシェイプが存在する場合、無駄なスキャンを避けるためだ。また、`InStr` 関数に `vbTextCompare` を指定することで、大文字・小文字の差異を無視した柔軟な検索を実現している。
② ページ設定(PageSheet)の継承
ここが最も重要なポイントだ。単にシェイプをコピーするだけでは、元の図面が「A3横」であっても、新しいファイルでは「A4縦」になってしまう。
`newPg.PageSheet.CellsU(“PageWidth”).FormulaU = …`
この一行によって、図面の物理的コンテキストを維持している。これこそが、ユーザーが「使える」と感じるツールの差である。
③ リソース管理とエラーハンドリング
VBAはオブジェクトの解放を怠ると、Visioのプロセスがメモリに残り続け、アプリケーションが不安定になる原因となる。`CleanUp` ラベルを用いた構造化エラーハンドリングにより、エラーが発生した際でも必ずオブジェクトを `Nothing` にして終了するように設計している。
④ 接続関係(Connectors)に関する警告
本コードはシェイプをコピーしているが、Visioの特性上、「別ページにあるシェイプへのコネクタ」は、コピー先では切断される。もし、ページをまたいだ接続関係を維持したままエクスポートする必要があるなら、全ページを一度に新しいドキュメントへコピーする、より高度なロジックが必要になる。設計要件を確認する際は、必ずこの点に触れておくべきだ。
まとめ:プロフェッショナルとしての視点
自動化ツールを開発する際、君たちは「コードを書く人」ではなく「業務プロセスを再設計する人」でなければならない。
- 「何を探すか」だけでなく「どのように維持するか」を考えろ。
- 「成功ルート」だけでなく「失敗ルート」を設計しろ。
- 「動く」だけでなく「保守できる」コードを書け。
この視点を持つだけで、君たちの書くコードの価値は、単なる作業補助から、企業の資産へと変わるはずだ。健闘を祈る。
