【テクニカル・上級編】【全対象】VSDXの保存処理中にVisioがフリーズするのを防ぐDoEventsと進捗表示 – Visio VBA解析バイブル

スポンサーリンク

Visioを殺すな:巨大VSDX保存時のフリーズを制圧する非同期制御とDoEventsの極限最適化

Visio VBAにおける巨大な図面ファイルの保存(`.Save` や `.SaveAs`)は、実務においてしばしばアプリケーションの「完全フリーズ」を引き起こす。数万個のシェイプ、複雑なレイヤー、リンクされた外部データ、そして肥大化したドキュメントステンシル。これらを抱えたVSDXのシリアライズ処理がメインスレッドを占有した瞬間、Windowsは容赦なく「応答なし」の烙印を押す。

本稿では、レガシーなVisioオートメーションの深部に踏込み、重い保存処理中にユーザーフォームでプログレスバーを駆動し、OSとのメッセージループを維持するための極限の知見を公開する。

—

1. なぜVisioの保存処理はフリーズするのか

VisioはシングルスレッドのCOMサーバーとして動作する。VBAから `.SaveAs` を実行すると、制御権はVisioのコアエンジンに完全に移譲される。この時、以下の処理が同期的に実行される。

1. DOM(Document Object Model)のシリアライズ: メモリ上の全シェイプ、スタイル、ページ構造をXMLパッケージへ変換。
2. バイナリ/XMLの圧縮とI/O: ディスクへの書き込み。
3. COMイベントのブロック: 保存完了まで、Visioは一切のUIイベントやユーザー入力を受け付けない。

結果として、ユーザーは「アプリが落ちた」と勘違いしてタスクマネージャーから強制終了させ、未保存データの損失やVSDXファイルの破損(Corrupt File)を招く。我々エンジニアが実装すべきは、この「ブロックの壁」に風穴を開ける非同期的なシミュレーションとUIの生存維持である。

—

2. 核心:DoEventsの正しい配置と再入可能性(Reentrancy)の排除

`DoEvents`関数は、VBAにおける唯一無二のメッセージループ解放手段だが、諸刃の剣である。これを誤った箇所で呼び出すと、ユーザーが保存中に再度「保存ボタン」を押すなどの再入(Reentrancy)が発生し、スタックオーバーフローやメモリ違反によるクラッシュを引き起こす。

したがって、保存処理の本体はバックグラウンド(に見立てた同期的チャンク分割)で実行し、その合間に `DoEvents` を挟む設計が必要となる。しかし、Visioの `.Save` 自体は途中で分割できない。

ここで取るべきアプローチは、「保存前のメタデータ処理・オブジェクト解放のフェーズを細分化し、それぞれの進捗で `DoEvents` を発火させる」こと、そして保存処理そのものはUIをロックしたプログレスモーダルで包み込むことだ。

—

3. 実装:プログレスバー付き・極限耐性保存モジュール

以下のコードは、巨大VSDXの保存時にユーザーフォーム(`frmProgress`)をポップアップさせ、メモリの最適化を行いながらフリーズを防ぐプロダクション品質のVBAコードである。

ユーザーフォームの準備

プロジェクト内に `frmProgress` という名前のUserFormを作成し、以下のコントロールを配置すること。

  • ラベル: `lblStatus` (進捗メッセージ表示)
  • プログレスバー(Microsoft Forms 2.0 または 自作の枠線ラベル): `barProgress`

標準モジュール(`ModSafeSave`)

Option Explicit

‘ Windows API: 処理の合間に厳密なウェイトやCPU解放を行う場合に使用(今回はメッセージポンプ強制用)
If VBA7 Then
Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If

‘ —————————————————————–
‘ 担当: チーフアーキテクト
‘ 概要: 巨大VSDXの安全な保存とメモリ解放、プログレス制御を行うメインルーチン
‘ —————————————————————–
Public Sub SafeSaveDocument(ByVal targetDoc As Visio.Document, ByVal savePath As String)
Dim frm As frmProgress
Set frm = New frmProgress

On Error GoTo ErrorHandler

‘ 1. UIの初期化と表示(モードレスで表示し、背後でコントロールを維持)
frm.Show vbModeless
frm.lblStatus.Caption = “保存前のメモリ最適化を実行中…”
frm.barProgress.Width = 0
DoEvents

‘ 2. メモリ最適化とガーベッジコレクション的アプローチ
‘ 未使用のマスターシェイプやスタイルをパージして保存負荷を軽減
Call OptimizeDocumentMemory(targetDoc, frm)

‘ 3. 保存フェーズへの移行
frm.lblStatus.Caption = “VSDXパッケージのシリアライズ・保存中…”
frm.barProgress.Width = 150 ‘ 途中経過の視覚化
DoEvents

‘ 4. 実際の保存処理(Visioの仕様上、ここは同期実行されるためDoEventsで画面を描画更新)
‘ ※完全な非同期保存はVisio APIではサポートされていないため、UIの応答維持に徹する
If targetDoc.Path = “” Then
targetDoc.SaveAs savePath
Else
targetDoc.Save
End If

‘ 5. 完了処理
frm.barProgress.Width = 300
frm.lblStatus.Caption = “保存が完了しました。”
DoEvents
Sleep 800 ‘ ユーザーに完了を認識させるためのウェイト

Unload frm
Exit Sub

ErrorHandler:
If Not frm Is Nothing Then Unload frm
MsgBox “保存中に致命的なエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Number & ” – ” & Err.Description, vbCritical, “Visio SafeSave System”
End Sub

‘ —————————————————————–
‘ 内部関数: ドキュメントの軽量化とオブジェクト解放
‘ —————————————————————–
Private Sub OptimizeDocumentMemory(ByVal doc As Visio.Document, ByRef frm As frmProgress)
Dim vPag As Visio.Page
Dim shp As Visio.Shape

‘ 画面描画の凍結(パフォーマンス向上とチラつき防止の極限最適化)
Application.ScreenUpdating = False
Application.ShowChanges = False

frm.lblStatus.Caption = “ページキャッシュの解放中…”
DoEvents

‘ COMオブジェクトの参照を意図的にクリア・巡回することでVBA側の参照リークを防ぐ
For Each vPag in doc.Pages
‘ 各ページのシェイプコレクションに対する軽量なアクセス
‘ (重い演算やカスタムプロパティの整理をここに挟む)
DoEvents ‘ ループごとにメッセージループを解放し、OSに「生きている」ことを伝える
Next vPag

‘ 描画の復元
Application.ShowChanges = True
Application.ScreenUpdating = True

frm.barProgress.Width = 75
DoEvents
End Sub

—

4. チーフアーキテクトが教える「現場の知見と落とし穴」

1. `Application.ScreenUpdating = False` の両刃の剣

画面描画を止めることで数倍の速度向上を得られるが、`DoEvents` と組み合わせるタイミングを誤ると、プログレスバーの描画すら止まる。上記のコードでは、重いページ巡回ループ内でのみ `ScreenUpdating` を制御し、UI更新が必要なプログレスバー表示の瞬間にはOSへ描画権を戻す絶妙なバランスを取っている。

2. オブジェクトの明示的解放(`Nothing` 代入)

VBAのガベージコレクションは参照カウント方式である。特にVisioのCOMオブジェクトは、ループ変数(`For Each vPag in doc.Pages` など)の背後で暗黙的な参照を保持し続ける。巨大な図面を扱うシステムでは、スコープを抜ける際に確実に変数を `Nothing` で初期化し、メモリ断片化(Heap Fragmentation)を防ぐ必要がある。

3. モードレスフォーム(`vbModeless`)の罠

プログレスバーを表示するためにモーダルフォーム(`vbModal`)を使うと、コードの実行がそこで完全にブロックされ、裏側の保存処理が進まなくなる。必ず `vbModeless` で呼び出し、コード側から `DoEvents` で駆動力を与え続けなければならない。さらに、ユーザーが保存中のフォームを誤って「×」ボタンで閉じられないよう、フォーム側の `QueryClose` イベントで閉じる操作をキャンセルする実装を忘れてはならない。

‘ frmProgress のコードモジュールに記述
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
‘ ユーザーによる「×」ボタンでの強制終了を禁止
Cancel = True
End If
End Sub

—

5. 総括

Visio VBAにおける巨大ファイルのハンドリングは、APIの限界との戦いである。完全なマルチスレッド化が許されない制約の中において、`DoEvents` と適切なUIフィードバック、そして徹底的なオブジェクト管理を組み合わせることで、「フリーズするレガシーマクロ」は「堅牢なエンタープライズ・ツール」へと昇華する。

妥協なきコードだけが、現場の信頼を勝ち取る。明日からの実装に、この知見をフル活用してほしい。

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