【テクニカル・上級編】Visio VBAで実現する完全サイレントなバッチ処理:Application.ShowChangesとUI非表示による超高速自動処理 – Visio VBA解析バイブル

スポンサーリンク

Visio VBAで実現する完全サイレントなバッチ処理:Application.ShowChangesとUI非表示による超高速自動処理

業務自動化の極地において、Microsoft Visioは単なる「図面作成ソフト」ではない。それは、高度なベクトルデータと関係性を保持した「エンジニアリング・トポロジ・エンジン」である。

しかし、多くの開発者がVisioをバッチ処理やサーバーサイド連携の作図エンジンとして利用しようとした際、深刻なボトルネックと不安定さに直面する。画面のちらつき、処理中のポップアップダイアログによる実行停止、そして処理速度の壊滅的な低下。これらはすべて、Visioを「デスクトップGUIアプリケーション」として起動したまま、愚直にマクロを回していることに起因する。

本稿では、Visioを完全に「ヘッドレス(非表示)」化し、極限のパフォーマンスを引き出すためのアーキテクチャを解説する。画面描画の完全抑制、ダイアログの徹底排除、Windows APIを駆使したプロセス制御、そしてメモリリークを根絶するCOMオブジェクトのライフサイクル管理まで、実戦で培われたノウハウをすべて開示する。

1. ヘッドレスVisioを実現する「サイレント化」の三原則

Visioをバックグラウンドで作動させ、何百枚もの図面をミリ秒単位で処理するためには、GUIコンポーネントへのリソース割当を完全に遮断しなければならない。これには、以下の「三原則」の適用が不可欠である。

[Visio Application]

├─ 1. Visible = False (ウィンドウ自体の非表示化)
├─ 2. ShowChanges = False (内部ドキュメント・描画ツリー更新の凍結)
└─ 3. AlertResponse = 7 (すべてのダイアログへの自動応答・抑止)

① `Application.Visible = False` の罠

単に `Application.Visible = False` とするだけでは、Visioはバックグラウンドで依然としてウィンドウメッセージを処理し、描画バッファを確保しようとする。さらに、特定の図形操作やOLEオブジェクトの展開時には、一瞬だけウィンドウが描画される「ゴースト表示」が発生する。

② `Application.ShowChanges = False` による描画ツリーの凍結

Visio VBAにおいて最も劇的なパフォーマンス向上をもたらすプロパティが `ShowChanges` である。
これを `False` に設定すると、Visioは内部の描画ツリー(Rendering Tree)の再計算と画面描画を完全に凍結する。Excel VBAにおける `ScreenUpdating = False` に相当するが、Visioにおいては図形間の接続関係(コネクション)やシェイプシート(ShapeSheet)の再計算が頻繁に発生するため、その効果はExcelの比ではない。処理速度は数倍〜数十倍に跳ね上がる。

③ `Application.AlertResponse` によるダイアログの完全封殺

バッチ処理中に「保存されていない変更があります」「マクロを有効にしますか?」「壊れた接続を修復しますか?」といったダイアログが表示された瞬間、バッチ処理は永久に停止(ハングアップ)する。
`AlertResponse` プロパティに特定の値を設定することで、ダイアログを一切表示させず、指定したデフォルトの応答を自動的に返却させることができる。一般的には、デフォルトボタンを選択させるために `7`(IDCANCEL / いいえ)または `1`(IDOK / はい)を割り当てる。

2. メモリ最適化:COMオブジェクトの明示的解放とリーク対策

VBAからVisioを操作する場合、最も警戒すべきは COMの参照カウント(Reference Count) の管理である。
Visioのオブジェクトモデル(`Application` -> `Document` -> `Page` -> `Shape` -> `Cell`)は非常に深い。ドットで繋いだ「暗黙的な参照」を不用意に記述すると、VBAのガベージコレクションが効かず、Visioのプロセスがメモリ上に「ゾンビプロセス」として残留し続ける。

ゾンビプロセスを生む最悪のコード例

‘ 破滅への片道切符:中間オブジェクトが暗黙的に保持され、メモリがリークする
Dim width As Double
width = ActiveDocument.Pages(1).Shapes(“Process_1”).Cells(“Width”).ResultIU

堅牢な参照管理コード例

すべてのオブジェクトを個別の変数に代入し、使用後は逆順に `Set = Nothing` で明示的に解放しなければならない。

Dim doc As Visio.Document
Dim pg As Visio.Page
Dim shp As Visio.Shape
Dim cl As Visio.Cell

Set doc = app.ActiveDocument
Set pg = doc.Pages(1)
Set shp = pg.Shapes(“Process_1”)
Set cl = shp.Cells(“Width”)

‘ 処理を実行
Dim width As Double
width = cl.ResultIU

‘ 厳密な順序での解放
Set cl = Nothing
Set shp = Nothing
Set pg = Nothing
Set doc = Nothing

特に、何百枚もの図面を一括処理するループ内では、反復ごとに必ずこれらの参照を完全にクリアしなければ、数万個のCOMオブジェクトがメモリを圧迫し、最終的に `Out of Memory`(メモリ不足エラー)でクラッシュする。

3. Windows APIによるプロセス制御とゾンビプロセスの強制駆逐

サイレントバッチ処理において、エラーハンドリングのバグや予期せぬシステム例外が発生した場合、`Visio.Application` が非表示のままOS内に残留することがある。これを放置すると、サーバーのメモリが枯渇する。
これを防ぐため、起動したVisioプロセスの「プロセスID(PID)」を完全に捕捉し、異常終了時にはWindows APIを用いて物理的にプロセスを強制終了(Kill)する機構を組み込む。

API宣言とプロセス制御の実装(VBA7 / 64bit・32bit両対応)

If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Function GetWindowThreadProcessId Lib “user32” (ByVal hWnd As LongPtr, ByRef lpdwProcessId As Long) As Long
Private Declare PtrSafe Function OpenProcess Lib “kernel32” (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As LongPtr
Private Declare PtrSafe Function TerminateProcess Lib “kernel32” (ByVal hProcess As LongPtr, ByVal uExitCode As Long) As Long
Private Declare PtrSafe Function CloseHandle Lib “kernel32” (ByVal hObject As LongPtr) As Long
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Private Declare Function GetWindowThreadProcessId Lib “user32” (ByVal hWnd As Long, ByRef lpdwProcessId As Long) As Long
Private Declare Function OpenProcess Lib “kernel32” (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function TerminateProcess Lib “kernel32” (ByVal hProcess As Long, ByVal uExitCode As Long) As Long
Private Declare Function CloseHandle Lib “kernel32” (ByVal hObject As Long) As Long
End If

Private Const PROCESS_TERMINATE As Long = &H1

このAPI群を用い、起動したVisioの `WindowHandle32` プロパティ(64bit環境でも互換性のためにこの名称が使われる)からプロセスIDを特定し、万が一の際には強制終了させる。

4. 極限のサイレントバッチ処理テンプレート(完全版)

以下に示すのは、本番環境でそのまま稼働させることを想定して設計された、完全サイレント・超高速処理を実現する堅牢なバッチ処理クラス/モジュールである。
指定されたフォルダ内のすべてのVisioファイル(`.vsd`, `.vsdx`)をバックグラウンドで開き、特定のシェイプデータの検証・更新を行い、PDFとしてエクスポートして閉じる一連の処理を実行する。

Option Explicit

‘ ==============================================================================
‘ Visio Silent Batch Processor
‘ ==============================================================================

If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Function GetWindowThreadProcessId Lib “user32” (ByVal hWnd As LongPtr, ByRef lpdwProcessId As Long) As Long
Private Declare PtrSafe Function OpenProcess Lib “kernel32” (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As LongPtr
Private Declare PtrSafe Function TerminateProcess Lib “kernel32” (ByVal hProcess As LongPtr, ByVal uExitCode As Long) As Long
Private Declare PtrSafe Function CloseHandle Lib “kernel32” (ByVal hObject As LongPtr) As Long
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Private Declare Function GetWindowThreadProcessId Lib “user32” (ByVal hWnd As Long, ByRef lpdwProcessId As Long) As Long
Private Declare Function OpenProcess Lib “kernel32” (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function TerminateProcess Lib “kernel32” (ByVal hProcess As Long, ByVal uExitCode As Long) As Long
Private Declare Function CloseHandle Lib “kernel32” (ByVal hObject As Long) As Long
End If

Private Const PROCESS_TERMINATE As Long = &H1

Public Sub ExecuteSilentBatch(ByVal targetFolderPath As String)
On Error GoTo ErrorHandler

Dim fileSystem As Object
Set fileSystem = CreateObject(“Scripting.FileSystemObject”)

Dim targetFolder As Object
If Not fileSystem.FolderExists(targetFolderPath) Then
Err.Raise 53, , “指定されたフォルダが存在しません: ” & targetFolderPath
End If
Set targetFolder = fileSystem.GetFolder(targetFolderPath)

‘ 1. Visioインスタンスの完全サイレント起動
Dim app As Visio.Application
Set app = New Visio.Application

‘ プロセスIDの取得(ゾンビプロセス対策用)
#If VBA7 Then
Dim visioHWnd As LongPtr
Dim visioProcessId As Long
Dim hProcess As LongPtr
#Else
Dim visioHWnd As Long
Dim visioProcessId As Long
Dim hProcess As Long
#End If

visioHWnd = app.WindowHandle32
GetWindowThreadProcessId visioHWnd, visioProcessId

‘ パフォーマンスとサイレント設定の極限最適化
With app
.Visible = False
.ShowChanges = False
.ScreenUpdating = 0 ‘ 0: 描画更新停止
.AlertResponse = 7 ‘ 7: IDCANCEL (ダイアログを自動でキャンセル)
.DeferRecalc = 1 ‘ 1: 式の再計算を遅延させ、一括実行する
End With

Dim fileItem As Object
Dim doc As Visio.Document
Dim pg As Visio.Page
Dim shp As Visio.Shape

‘ フォルダ内のファイルを走査
For Each fileItem In targetFolder.Files
Dim ext As String
ext = LCase(fileSystem.GetExtensionName(fileItem.Path))

If ext = “vsd” Or ext = “vsdx” Or ext = “vsdm” Then
Debug.Print “処理開始: ” & fileItem.Name

‘ ドキュメントを非表示かつ読み取り専用を回避して開く
Set doc = app.Documents.OpenEx(fileItem.Path, visOpenHidden Or visOpenNoWindow)

‘ — ビジネスロジック領域:ここから極限処理を実行 —
‘ 例:全ページの全シェイプをスキャンし、特定のプロパティを更新
For Each pg In doc.Pages
For Each shp In pg.Shapes
‘ 例: 特定のマスターシェイプに属するオブジェクトのテキストを置換
If Not shp.Master Is Nothing Then
If shp.Master.Name = “Process” Then
‘ シェイプシートの値を直接書き換え(最速)
If shp.CellExists(“Prop.Status”, 0) Then
shp.Cells(“Prop.Status”).FormulaForceU = “””Completed”””
End If
End If
End If
Next shp
Next pg

‘ 強制的な接続と計算の再評価(遅延計算の解決)
doc.ReCalc

‘ 例:サイレントPDFエクスポート
Dim pdfPath As String
pdfPath = fileSystem.BuildPath(targetFolderPath, fileSystem.GetBaseName(fileItem.Path) & “.pdf”)
doc.ExportAsFixedFormat visFixedFormatPDF, pdfPath, visDocExIntentScreen, visFrameAllPages

‘ 変更を保存して閉じる(ダイアログはAlertResponseで抑制されているが、コード側で明示指定)
doc.Save
doc.Close

‘ ループ内でのCOM参照の徹底的解放
Set doc = Nothing
Debug.Print “処理完了: ” & fileItem.Name
End If
Next fileItem

‘ クリーンアップ
app.Quit
Set app = Nothing

Debug.Print “すべてのバッチ処理が正常に終了しました。”
Exit Sub

ErrorHandler:
Dim errDesc As String
errDesc = Err.Description
Debug.Print “致命的エラー発生: ” & errDesc

‘ 異常系における回復処理
On Error Resume Next
If Not doc Is Nothing Then
doc.Close
Set doc = Nothing
End If

If Not app Is Nothing Then
app.Quit
Set app = Nothing
End If

‘ プロセスが残留している場合はAPIで物理破壊する
If visioProcessId <> 0 Then
hProcess = OpenProcess(PROCESS_TERMINATE, 0, visioProcessId)
If hProcess <> 0 Then
TerminateProcess hProcess, 999
CloseHandle hProcess
Debug.Print “残留Visioプロセス (PID: ” & visioProcessId & “) を強制終了しました。”
End If
End If
End Sub

5. アーキテクトの視点:サーバーサイド連携と「デスクトップセッション」の罠

このサイレントバッチシステムを、Windows Server上のタスクスケジューラや、IISなどのWebアプリケーション(ASP.NET/PHP等)のバックエンドとして組み込む場合、Microsoft Officeファミリー共通の「セッション0の壁」に衝突する。

1. タスクスケジューラにおける「ユーザーがログオンしているかどうかにかかわらず実行する」の罠

このオプションを選択すると、Visioは非インタラクティブな「セッション0」で起動する。
Visio(および他のOffice製品)は、起動時に「Windowsデスクトップ」環境(具体的には `SystemProfile` 内の `Desktop` フォルダ)が存在することを前提としている。これが存在しない場合、サイレント起動であってもVisioは初期化に失敗し、エラーコード `0x800A01AD`(ActiveXコンポーネントはオブジェクトを作成できません)を吐いて即死する。

【解決策】不足しているシステムディレクトリの物理作成

64bit/32bitそれぞれのWindows Server環境において、以下のディレクトリを手動で作成することで、セッション0でのVisio起動が驚くほど安定する。

  • 64bit環境:

`C:\Windows\SysWOW64\config\systemprofile\Desktop`

  • 32bit環境/共通:

`C:\Windows\System32\config\systemprofile\Desktop`

2. IIS / Web連携におけるDCOMの限界

Webサーバー(IIS)プロセスからダイレクトにVisioのCOMオブジェクトを生成することは、マイクロソフトの公式サポート外であり、デッドロックやメモリリークによるIIS全体のハングアップを引き起こすため、絶対に避けるべきである。

Web連携を行う場合の正しいモダンアーキテクチャは以下の通りである。

[Web UI (ASP.NET / SPA)]
│ (要求受付)

[Queue (MSMQ / RabbitMQ / DB Queue)]
│ (非同期キューイング)

[Windows Service / Console App (C# or VBA Worker Process)]
│ (タスクを1件ずつ処理)

[ヘッドレスVisioインスタンス] (本稿のサイレントバッチを実行)

この疎結合な設計(キューベースの非同期処理)にすることにより、Webサーバーの応答性を維持しつつ、Visioのプロセスライフサイクルを完全に分離・制御することが可能となる。

結論

Visio VBAは、正しく飼い慣らせば極めて強力なバッチドキュメント生成エンジンとなる。
画面描画を `ShowChanges = False` で凍結し、`AlertResponse` でダイアログを封殺し、COMの参照カウントを1つずつ丁寧につぶしていく。この泥臭くも精密なチューニングの積み重ねだけが、エンタープライズ環境で24時間365日ノーメンテナンスで稼働し続ける、真に堅牢な自動化システムを創り出す唯一の道なのである。

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