【上級者向け】大規模VSDXファイルの高速ページ分割保存における極限のオブジェクト管理
エンタープライズのシステムインテグレーションにおいて、数万点におよぶ設備、ネットワーク、あるいは業務プロセスのフローを記述した大規模なVisio図面(.vsdx)のハンドリングは、常にパフォーマンスのボトルネックとなります。
数百ページにおよぶ巨大な単一ファイルを、システム連携の要件やセキュリティ境界の策定のために「1ページ=1ファイル」へと高速に分割・切り出す処理――。これを愚直に「コピー&ペースト」や、単純な「保存と削除の繰り返し」で実装すると、メモリ枯渇(Out of Memory)、COM呼び出しのスタック、そして再計算エンジンのオーバーヘッドによって容易にシステムがハングアップします。
本稿では、Visioの描画エンジン、ShapeSheetの依存関係、そしてWindows APIレベルの描画制御を掌握し、極限まで最適化された高速分割処理のアーキテクチャと、それを実装した堅牢なVBAコードを公開します。
—
1. 分割アプローチの技術的比較:なぜ「コピー&ペースト」は破綻するのか
ページ分割を実装するにあたり、開発者が最初に思い浮かべるアプローチは以下の2つに大別されます。
| 評価軸 | A: コピー&ペースト法(新規ドキュメントへ複製) | B: 複製・削ぎ落とし法(SaveCopyAs+不要ページ削除) |
| :— | :— | :— |
| 整合性 | 最悪。ShapeSheetの参照(Pages[X]!Sheet[Y])、ユーザー定義セル、ドキュメント固有のスタイル、レイヤー定義、背景ページ(Background Page)の紐付けがすべて寸断される。 | 完璧。元ドキュメントの完全なクローンから開始するため、スタイル、マスターシェイプ、背景ページの参照、ドキュメントレベルのメタデータが完全に維持される。 |
| 速度 | 極めて遅い。COMを介したクリップボード経由のオブジェクトコピーは、数千個のシェイプが存在する場合、リソースのシリアライズに莫大な時間を要する。 | 制御次第で高速。不要ページの削除は、内部のXMLツリー構造のトリミングに近いため、描画と再計算を抑止すれば最速の処理となる。 |
| ファイルサイズ | 比較的小さい。 | 初期状態は大きいが、不要ページ削除後に `PurgeUnused`(未使用マスタのクリーンアップ)を適用することで適正化可能。 |
以上の比較から、エンタープライズ品質に耐えうるアプローチは 「B: 複製・削ぎ落とし法」 一択となります。
しかし、このアプローチを高速化するには、Visioの重厚な描画エンジンと再計算メカニズムを一時的に「完全停止」させる必要があります。
—
2. 高速化を支える3つのコア・テクノロジー
① Windows APIによるウィンドウ更新の物理的抑止
Visioの `Application.ScreenUpdating = False` は強力ですが、COMの階層下で発生する特定の描画イベントや、ダイアログ表示に伴うOSレベルの再描画を完全にはブロックできません。
Windows APIの `LockWindowUpdate` を使用し、Visioのメインウィンドウハンドル(`WindowHandle32`)に対してOSレベルで描画をフリーズさせることで、グラフィックカードとCPU間の不要なコンテキストスイッチを極限まで排除します。
② イベントハンドラの完全切断と遅延再計算
Visioのシェイプには、数多くの `CellChanged` や `FormulaChanged` などのイベントが仕込まれています。また、ページを削除するたびに、残されたページのシェイプシートの再計算が走ります。
- `Application.EventsEnabled = False` でイベント駆動型のアドインやVBAのトリガーを抑止。
- `Application.DeferRecalc = True` で、オブジェクト削除中のシェイプシートの再計算をサスペンド(一時停止)状態にします。
③ COMオブジェクトの厳密なライフサイクル管理
VBAは参照カウント方式のガベージコレクションを採用しています。ループ内で `Page` や `Document` などのCOMオブジェクトへの参照を明示的に解放(`Set obj = Nothing`)しない場合、一時オブジェクトがメモリ空間(L1/L2キャッシュおよびヒープ)に滞留し、システムリソースを圧迫します。各ループステップの終端で、すべてのオブジェクト変数を完全に初期化します。
—
3. 極限高速分割マクロ:完全実装コード
以下のコードは、64bit/32bitオフィス環境に両対応した、本番環境向けの完全な実装です。
実行すると、アクティブなドキュメントの各ページを抽出し、指定フォルダへ超高速で個別保存します。
Option Explicit
‘ ==============================================================================
‘ Windows API 宣言(描画ロック用)
‘ ==============================================================================
If VBA7 Then
Private DeclarePtrSafe Function LockWindowUpdate Lib “user32” (ByVal hwndLock As LongPtr) As Long
Private DeclarePtrSafe Function GetFrequency Lib “kernel32” Alias “QueryPerformanceFrequency” (ByRef lpFrequency As Currency) As Long
Private DeclarePtrSafe Function GetCounter Lib “kernel32” Alias “QueryPerformanceCounter” (ByRef lpPerformanceCount As Currency) As Long
Else
Private Declare Function LockWindowUpdate Lib “user32” (ByVal hwndLock As Long) As Long
Private Declare Function GetFrequency Lib “kernel32” Alias “QueryPerformanceFrequency” (ByRef lpFrequency As Currency) As Long
Private Declare Function GetCounter Lib “kernel32” Alias “QueryPerformanceCounter” (ByRef lpPerformanceCount As Currency) As Long
End If
‘ ==============================================================================
‘ メイン処理:高速ページ分割保存
‘ ==============================================================================
Public Sub SplitActiveDocumentPagesHighSpeed()
Dim sourceDoc As Visio.Document
Set sourceDoc = ActiveDocument
If sourceDoc Is Nothing Then
MsgBox “アクティブなドキュメントが見つかりません。”, vbCritical, “エラー”
Exit Sub
End If
‘ 出力先ディレクトリの設定(ソースファイルと同一ディレクトリ)
Dim outputDir As String
outputDir = sourceDoc.Path & “SplitPages_” & Format(Now, “yyyymmdd_hhnnss”) & “\”
‘ ディレクトリ作成
If Dir(outputDir, vbDirectory) = “” Then
MkDir outputDir
End If
‘ パフォーマンス計測開始
Dim startTime As Currency, endTime As Currency, freq As Currency
GetFrequency freq
GetCounter startTime
‘ Visioアプリケーションの状態退避と超高速化設定
Dim app As Visio.Application
Set app = Visio.Application
Dim originalScreenUpdating As Boolean
Dim originalEventsEnabled As Boolean
Dim originalDeferRecalc As Boolean
Dim originalAlertResponse As Long
originalScreenUpdating = app.ScreenUpdating
originalEventsEnabled = app.EventsEnabled
originalDeferRecalc = app.DeferRecalc
originalAlertResponse = app.AlertResponse
‘ 最適化の有効化
app.ScreenUpdating = False
app.EventsEnabled = False
app.DeferRecalc = True
app.AlertResponse = 7 ‘ アラートに対して「いいえ」またはデフォルトの自動応答(保存確認などをスキップ)
‘ Windows APIによるウィンドウ描画の物理ロック
Dim visHwnd As LongPtr
#If VBA7 Then
visHwnd = CLngPtr(app.WindowHandle32)
#Else
visHwnd = CLng(app.WindowHandle32)
#End If
LockWindowUpdate visHwnd
On Error GoTo ErrorHandler
Dim totalPages As Integer
totalPages = sourceDoc.Pages.Count
Dim pageIdx As Integer
For pageIdx = 1 To totalPages
Dim targetPageName As String
targetPageName = sourceDoc.Pages(pageIdx).Name
‘ ファイル名に使用できない文字をサニタイズ
Dim safePageName As String
safePageName = SanitizeFileName(targetPageName)
Dim destFilePath As String
destFilePath = outputDir & safePageName & “.vsdx”
‘ Step 1: マスター図面を別名で高速コピー保存(ディスクI/O)
sourceDoc.SaveCopyAs destFilePath
‘ Step 2: コピーしたファイルを「非表示(Hidden)」かつ「マクロ無効(NoMacros)」でオープン
Dim targetDoc As Visio.Document
Set targetDoc = app.Documents.OpenEx(destFilePath, visOpenHidden Or visOpenNoMacros)
‘ Step 3: 対象ページ以外の不要なページを「逆順」で高速削除
‘ ※インデックスのズレを防ぐため、常に末尾から走査する
Dim delIdx As Integer
For delIdx = targetDoc.Pages.Count To 1 Step -1
Dim evalPage As Visio.Page
Set evalPage = targetDoc.Pages(delIdx)
If evalPage.Name <> targetPageName Then
‘ 背景ページでない、または削除対象ページが背景ページを参照していない場合は削除
‘ ※背景ページの依存関係を維持するためのロジック
If evalPage.Background = False Then
evalPage.Delete True ‘ True引数で、関連するページ上のオブジェクトを強制削除
Else
‘ 背景ページの場合、残すページがこれを使用しているか検証
If Not IsBackgroundPageReferenced(targetDoc, targetPageName, evalPage.Name) Then
evalPage.Delete True
End If
End If
End If
Set evalPage = Nothing
Next delIdx
‘ Step 4: 未使用のマスターシェイプ、スタイル、テーマのパージ(ファイルサイズ極小化)
On Error Resume Next
targetDoc.PurgeUnused
On Error GoTo ErrorHandler
‘ Step 5: 保存とクローズ、およびCOMメモリの明示的解放
targetDoc.Save
targetDoc.Close
Set targetDoc = Nothing
‘ ガーベジコレクションの擬似誘発
DoEvents
Next pageIdx
Cleanup:
‘ Windows APIによるウィンドウ描画ロックの解除
LockWindowUpdate 0
‘ アプリケーション状態の復元
app.ScreenUpdating = originalScreenUpdating
app.EventsEnabled = originalEventsEnabled
app.DeferRecalc = originalDeferRecalc
app.AlertResponse = originalAlertResponse
GetCounter endTime
Dim elapsedTime As Double
elapsedTime = CDbl(endTime – startTime) / CDbl(freq)
MsgBox “分割処理が正常に完了しました。” & vbCrLf & _
“出力先: ” & outputDir & vbCrLf & _
“処理時間: ” & Format(elapsedTime, “0.00”) & ” 秒”, vbInformation, “処理完了”
Exit Sub
ErrorHandler:
MsgBox “エラーが発生しました。” & vbCrLf & _
“番号: ” & Err.Number & vbCrLf & _
“説明: ” & Err.Description, vbCritical, “致命的エラー”
Resume Cleanup
End Sub
‘ ==============================================================================
‘ サポート関数:背景ページが対象ページから参照されているか判定
‘ ==============================================================================
Private Function IsBackgroundPageReferenced(ByRef doc As Visio.Document, ByVal targetPageName As String, ByVal bgPageName As String) As Boolean
On Error Resume Next
Dim pg As Visio.Page
Set pg = doc.Pages(targetPageName)
If Not pg Is Nothing Then
If pg.BackPage Is Nothing Then
IsBackgroundPageReferenced = False
Else
If pg.BackPage.Name = bgPageName Then
IsBackgroundPageReferenced = True
Else
IsBackgroundPageReferenced = False
End If
End If
End If
On Error GoTo 0
End Function
‘ ==============================================================================
‘ サポート関数:ファイル名禁止文字のサニタイズ
‘ ==============================================================================
Private Function SanitizeFileName(ByVal fileName As String) As String
Dim invalidChars As Variant
invalidChars = Array(“\”, “/”, “:”, “”, “?”, “”””, “<", ">“, “|”)
Dim result As String
result = fileName
Dim i As Integer
For i = LBound(invalidChars) To UBound(invalidChars)
result = Replace(result, invalidChars(i), “_”)
Next i
SanitizeFileName = result
End Function
—
4. コードの深層解説:なぜこの実装が速いのか
このコードには、単純な自動化スクリプトとは一線を画す、エンタープライズレベルの最適化ノウハウが凝縮されています。
① `SaveCopyAs` と `OpenEx` の黄金コンビ
最も重要な点は、「コピー元(`sourceDoc`)を開いたまま、一度もそれを破壊的な操作に晒さない」 という点です。
`SaveCopyAs` は、メモリ上の現在のドキュメントステートを、ディスク上に別名でバイナリコピーします。このAPIは非同期的にディスクへバッファリングされるため、極めて高速です。
そして、出力されたコピーファイルを `app.Documents.OpenEx` で開く際、以下のフラグを立てています。
- `visOpenHidden`: ウィンドウを一切描画せずにメモリ上にドキュメントを展開します。これにより、Visioのレイアウトエンジンやビューポートの再計算が完全にスキップされます。
- `visOpenNoMacros`: ドキュメントに埋め込まれた `DocumentOpened` などのVBAイベントやアドインのキックを防ぎ、セキュリティと速度を担保します。
② 逆順(Descending Order)によるページ削除の必然性
ページ削除ループが `To 1 Step -1` と逆順になっているのは、コレクション操作の鉄則です。
仮に1から順に削除した場合、インデックスがその都度前詰めにシフトされるため、Visioの内部コレクションマネージャは内部のポインタ配列を再構築せざるを得ず、インデックスの整合性崩壊(境界外エラー)やO(N^2)の計算量ペナルティを支払うことになります。末尾からの削除は、配列の末尾を単に切り捨てるだけの定数時間処理 O(1) に近づきます。
③ 背景ページの依存関係(Dependency)の解決
Visio特有の落とし穴が「背景ページ(Background Page)」です。
表面のページ(Foreground Page)が背景として別のページを参照している場合、その背景ページまで盲目的に削除すると、残されたページの背景が白紙化し、共通フレーム(図枠やタイトルブロック)が消失します。
`IsBackgroundPageReferenced` 関数によって、残すべき対象ページが参照している背景ページを厳密に特定し、それ以外の不要な背景ページのみを安全にパージするロジックを組み込んでいます。
④ `PurgeUnused` によるファイルサイズの最適化
`SaveCopyAs` を行った直後のファイルには、元ドキュメントに含まれていた「すべてのマスターシェイプ(Stencil List)」や「カスタムスタイル」が残存しています。
不要ページを削除した後に `targetDoc.PurgeUnused` を実行することで、そのページで使用されていないマスターシェイプやリソースがXML構造から完全にクリーンアップされ、ファイルサイズが数分の一〜数十分の一に縮小されます。これにより、ネットワーク転送やシステム間連携の負荷が大幅に軽減されます。
—
5. 本番運用におけるアーキテクチャ上の注意点
OneDrive/SharePoint同期フォルダ配下での実行抑止
この高速分割処理は、短時間に大量のファイルを作成・書き換え・クローズします。
もし実行ディレクトリが OneDrive や SharePoint の同期対象配下であった場合、OSのファイルシステムフィルタドライバ(`clfs.sys`など)がファイルをロックし、Visioの `Save` や `Close` と激しく競合(競合コピーの発生、あるいは `Permission Denied` エラー)を起こします。
必ずローカルの一時ディレクトリ(`C:\Temp\` 等)で処理を完結させ、処理終了後に一括して同期フォルダへムーブ(移動)させるアーキテクチャ設計にしてください。
メモリリーク対策としての外部プロセス化
数百ページを超える極限状態においては、VBAの同一プロセス(`Visio.exe`)内で処理を続けると、VBAのスタック領域やCOMコンテキストが徐々に汚染されます。
さらに堅牢性を求めるシステム管理者の場合、このVBAコードを WSH (Windows Script Host) / VBScript、あるいは .NET (C# / PowerShell) から外部プロセスとしてVisioを制御し、例えば「50ページ処理するごとにVisioプロセスを一度完全にKillし、再起動して処理を継続する」という プロセス・リサイクリング・パターン を導入することで、数千ページ規模の図面であっても、完全に無人で、リソース限界に達することなく処理を完結させることが可能となります。
この領域に達して初めて、Visio自動化は「単なるマクロ」から「エンタープライズに耐えうるバッチプロセッサ」へと昇華するのです。
