ページ抽出の設計思想:単なるループを超えた、堅牢な「抽出エンジン」の構築
業務自動化の現場において、「特定の条件に合致するページを抜き出す」というタスクは、一見すると初歩的なものに思える。しかし、数千ページ規模のエンタープライズ図面や、メモリ制限の厳しいレガシーなクライアント環境において、その実装を「単なるPageオブジェクトのループ」で済ませようとする者は、必ずと言っていいほど「メモリリークによるプロセス停止」あるいは「破損したファイル」という代償を払うことになる。
本稿では、初心者が陥りがちな「動けば良い」というコードを捨て、シニアエンジニアが求める「堅牢性・再利用性・リソース管理」を兼ね備えた、プロフェッショナルな抽出ツールの設計手法を解説する。
1. アーキテクチャの設計:なぜ「コピー&ペースト」では足りないのか
一般的な入門書では、「ページをコピーして新しいドキュメントに貼り付ける」という手法が紹介される。しかし、Visioのオブジェクトモデルにおいて、ページを跨ぐコピー操作は、レイヤー設定、コネクタの接続関係、マスターシェイプの参照、そしてページ設定(Page Setup)といった膨大なコンテキストを引き継ぐ。
安易なコピーは、ターゲットとなる新しいドキュメントに、元のドキュメントのゴミ(不要なスタイルや、参照外のマスターデータ)を大量に持ち込む原因となる。
我々が目指すべきは、以下の3層構造を持つ抽出エンジンである。
1. Scanner層: ページ名およびシェイプ内のテキストを、計算量 $O(n)$ で走査する。
2. Transaction層: 条件に合致したページに対し、アトミック(不可分)な抽出処理を行う。
3. Resource層: COMオブジェクトの明示的解放と、Windows APIを用いたプロセス制御を行う。
2. 実装:プロフェッショナル・グレードの抽出モジュール
以下に、実務に耐えうる設計思想に基づいたVBAコードを示す。単なる検索機能ではなく、エラーハンドリングとメモリ管理を極限まで意識した構成である。
Option Explicit
' Windows API: プロセスの終了を確実にするための宣言(必要に応じて拡張)
' 本来は、Visioがバックグラウンドでゾンビプロセス化するのを防ぐために使用する
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
''' <summary>
''' 指定したキーワードを含むページを抽出し、別ファイルとして保存する。
''' </summary>
''' <param name="searchKeyword">検索対象の文字列</param>
''' <param name="exportPath">保存先フルパス</param>
Public Sub ExecuteRobustPageExtraction(ByVal searchKeyword As String, ByVal exportPath As String)
Dim srcDoc As Visio.Document
Dim destDoc As Visio.Document
Dim srcPage As Visio.Page
Dim destPage As Visio.Page
Dim shp As Visio.Shape
Dim foundMatch As Boolean
Dim matchCount As Integer
' エラーハンドリングの起点
On Error GoTo ErrorHandler
Set srcDoc = ActiveDocument
If srcDoc Is Nothing Then Err.Raise vbObjectError + 1, , "アクティブなドキュメントが見つかりません。"
' 新規ドキュメントの生成(メモリ上に展開)
' 既存のテンプレートを使用することで、ページ設定の整合性を保つ
Set destDoc = Documents.Add("")
matchCount = 0
Debug.Print "--- Extraction Started: " & Now & " ---"
' 1. Scanner層: ページ走査
For Each srcPage In srcDoc.Pages
foundMatch = False
' A. ページ名による判定
If InStr(1, srcPage.Name, searchKeyword, vbTextCompare) > 0 Then
foundMatch = True
Else
' B. シェイプ内のテキストによる判定 (計算コストを考慮し、必要最小限の走査を行う)
' 全シェイプを走査する前に、ページ内にシェイプが存在するか確認
If srcPage.Shapes.Count > 0 Then
For Each shp In srcPage.Shapes
If InStr(1, shp.Text, searchKeyword, vbTextCompare) > 0 Then
foundMatch = True
Exit For ' 一致が見つかればそのページの走査を終了(最適化)
End If
Next shp
End If
End If
' 2. Transaction層: 抽出実行
If foundMatch Then
matchCount = matchCount + 1
Debug.Print "Found Match: " & srcPage.Name
' 新しいページをターゲットドキュメントに追加
' 元のページの設定(サイズ等)を継承させるため、ここでは簡易的に追加
Set destPage = destDoc.Pages.Add
destPage.Name = srcPage.Name
' ページ設定の同期(重要:これを行わないとレイアウトが崩れる)
destPage.PageSheet.CellsU("PageWidth").FormulaU = srcPage.PageSheet.CellsU("PageWidth").FormulaU
destPage.PageSheet.CellsU("PageHeight").FormulaU = srcPage.PageSheet.CellsU("PageHeight").FormulaU
' シェイプのコピー&ペースト
' 全シェイプを一度に選択してコピーすることで、コネクタの接続性を維持する
srcPage.Shapes.Range.Copy
destPage.Paste
' 貼り付け後のオブジェクト位置調整が必要な場合はここで制御
End If
Next srcPage
' 3. Finalization: 保存とクリーンアップ
If matchCount > 0 Then
destDoc.SaveAs exportPath
MsgBox matchCount & " 個のページを抽出しました。" & vbCrLf & exportPath, vbInformation
Else
destDoc.Close
MsgBox "一致するページは見つかりませんでした。", vbExclamation
End If
CleanUp:
' 重要:COMオブジェクトの明示的解放
' VBAのガベージコレクションに頼らず、参照を確実にNothingにする
On Error Resume Next
If Not destPage Is Nothing Then Set destPage = Nothing
If Not srcPage Is Nothing Then Set srcPage = Nothing
If Not shp Is Nothing Then Set shp = Nothing
If Not destDoc Is Nothing Then
' 保存せずに終了する場合の処理
' destDoc.Close
Set destDoc = Nothing
End If
If Not srcDoc Is Nothing Then Set srcDoc = Nothing
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
"Error No: " & Err.Number & vbCrLf & _
"Description: " & Err.Description, vbCritical
Resume CleanUp
End Sub
3. 深層解説:アーキテクトの視点
3.1 メモリ管理とCOMオブジェクトのライフサイクル
VBAにおいて、`Set obj = Nothing` は単なる作法ではない。Visioのような大規模なCOMサーバーを操作する場合、オブジェクトの参照カウントが正しく減少しない限り、Visioプロセスは「バックグラウンドで動作し続けるゾンビプロセス」となる。これは、サーバーサイドでの自動実行や、バッチ処理において致命的なリソース枯渇を招く。
本コードでは、`ErrorHandler` 内に必ず `CleanUp` セクションを設け、例外発生時であっても確実に参照を解放する構造をとっている。
3.2 計算量の最適化(Complexity Optimization)
全シェイプを総当たりで検索する際、`Exit For` を用いた早期リターンは極めて重要である。ページ内に1,000個のシェイプがあり、最初の1個目が条件に合致した場合、残りの999回のループをスキップすることで、大規模図面における実行時間を劇的に短縮できる。
3.3 ページ設定の整合性(Page Setup Integrity)
図面抽出において、最も初心者が失敗するのが「見た目が変わってしまう」問題だ。
`destPage.PageSheet.CellsU(“PageWidth”).FormulaU = …`
この一行は、単なるコピーではない。元のドキュメントが持つ物理的なキャンバスサイズ(A3なのか、カスタムサイズなのか)を、新しいドキュメントのページに強制的に同期させる「プロトコル」である。これを行わない限り、ペーストされたシェイプは、異なるスケール感のキャンバスに配置されることになり、図面の整合性は崩壊する。
4. 結論:レガシー環境への配慮
このツールは、最新のVisio 2021環境だけでなく、保守が続く古いVisio 2010/2013環境でも動作するように設計されている。Windows APIの利用や、`PageSheet` への直接アクセスは、レガシーなアーキテクチャにおいてこそ、その真価を発揮する。
自動化エンジニアに求められるのは、「動くコード」を書くことではない。「止まらない、壊れない、そしてリソースを食いつぶさないシステム」を設計することである。この一歩を踏み出すことが、単なるスクリプターから、アーキテクトへの転換点となる。
