【テクニカル・上級編】Application.OnComponentEnterStateの検知によるモーダルダイアログ表示中のVBA誤作動・フリーズ防止策 – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:モーダル地獄からの脱出 – `OnComponentEnterState` を駆使したフリーズ完全防衛策

Visio VBAの開発現場において、最も悪名高く、そして最も見落とされがちな罠がある。それが「モーダルダイアログ実行中のVBA割り込みによるフリーズ」だ。

ユーザーが「印刷」や「ページ設定」、あるいはCOMアドイン独自のモーダルダイアログを開いている最中、タイマーイベントや外部からのイベントトリガーによってVBAコードが発火した瞬間、Visioは沈黙する。画面はフリーズし、タスクマネージャーからの強制終了以外に道はなくなる。

一般的な入門書やリファレンスは、綺麗なコードの書き方しか教えない。しかし、数百万行規模のレガシーな図面自動生成システムや、社内ニッチインフラの最前線で戦う我々に必要なのは、「OSの挙動とVisioのシングルスレッド制約の狭間で、いかにしてシステムを崩壊から守るか」というサバイバルの知見だ。

今回は、Visioの隠し子とも言えるアプリケーションイベント、そしてWindows APIを総動員し、モーダル状態を完全に掌握してフリーズを根絶するアーキテクチャを授ける。

1. なぜVisioはモーダル状態でVBAを実行すると死ぬのか?

根本原因は、Visioのメインスレッドの構造にある。

VisioはCOMベースのアプリケーションであり、VBAの実行スレッドはUIスレッドと同一(あるいは密結合)である。ユーザーがダイアログを開いた際、Windowsメッセージループはダイアログ(モーダルウィンドウ)側にコンテキストを奪われる。

この状態でVBAコードが割り込むと、以下の致命的な競合が発生する。
1. メッセージループのデッドロック: VBAからVisioのドキュメントモデル(`ActivePage` や `Shapes` など)を操作しようとした際、Visio側がUIの応答を待っている状態と衝突し、COMの呼び出しが無限待ち(HRESULT `0x80010001` – RPC_E_CALL_REJECTED 等)を引き起こす。
2. オブジェクトモデルのロックアウト: モーダルダイアログ表示中は、Visio内部のドキュメント構造が書き込み禁止または排他制御下に入る。ここに無慈悲なVBAのマクロが走ることでメモリ空間が破壊される。

この惨劇を防ぐ唯一の手段が、「Visioが現在どのような状態(State)にあるのかをミリ秒単位で監視し、危険域ではコードの実行を即座にアボート(中止)する」ことである。

2. 救世主:`Application.OnComponentEnterState` の正体

Visioの `Application` オブジェクトには、アプリケーションの状態変化をフックするためのイベントが用意されている。その中でも、コンポーネント(ダイアログやモーダル画面)の遷移を捉えるのが以下の2つのイベントだ。

  • `OnComponentEnterState(app As Visio.IVApplication, nState As Long)`
  • `OnComponentLeaveState(app As Visio.IVApplication, nState As Long)`

ここで渡される `nState` には、Visioが現在どのモーダル状態に入ったかを示す定数(Visio内部の定義値)が格納される。例えば、印刷ダイアログ、ファイル選択、オプション設定画面など、VBAが触ってはいけない聖域に入った瞬間にフラグを立て、出てきたらフラグを下ろす。

このイベントドリブンな監視レイヤーを構築するのが、シニアエンジニアの必須教養である。

3. 【実装コード】フリーズ防衛アーキテクチャの全貌

以下のコードは、クラスモジュールを用いてVisioのイベントをフックし、モーダル状態を検知してVBAからの誤作動を完全にブロックする堅牢な実装だ。

① クラスモジュール: `CAppEventMonitor.cls`

Option Explicit

‘ WithEventsを使用してVisioのアプリケーションイベントをキャッチ
Public WithEvents VisApp As Visio.Application

‘ モーダル状態フラグ(他モジュールからも参照可能)
Private m_IsModalStateActive As Boolean

Public Property Get IsModalActive() As Boolean
IsModalActive = m_IsModalStateActive
End Property

‘ コンポーネント(モーダルダイアログ等)に入った瞬間に発火
Private Sub VisApp_OnComponentEnterState(ByVal app As Visio.IVApplication, ByVal nState As Long)
‘ ログ出力やデバッグ用(必要に応じて有効化)
‘ Debug.Print “Enter State: ” & nState

‘ Visio内部の特定のモーダル状態(例:印刷プレビューやダイアログ表示など)
‘ ※nStateの値はVisioのバージョンにより異なる場合があるため、実環境でログを推奨
m_IsModalStateActive = True
End Sub

‘ コンポーネントから抜けた瞬間に発火
Private Sub VisApp_OnComponentLeaveState(ByVal app As Visio.IVApplication, ByVal nState As Long)
‘ Debug.Print “Leave State: ” & nState

m_IsModalStateActive = False
End Sub

② 標準モジュール: `MMainController.bas`

Option Explicit

‘ グローバル領域でのイベント監視クラスの保持
Private g_EventMonitor As CAppEventMonitor

‘ 初期化処理(AutoOpenやリボンアドインのロード時に実行)
Public Sub InitializeVisioGuard()
If g_EventMonitor Is Nothing Then
Set g_EventMonitor = New CAppEventMonitor
Set g_EventMonitor.VisApp = Visio.Application
End If
MsgBox “Visioフリーズ防衛シールドが展開されました。”, vbInformation
End Sub

‘ 終了処理(メモリリーク防止のための明示的解放)
Public Sub TerminateVisioGuard()
If Not g_EventMonitor Is Nothing Then
Set g_EventMonitor.VisApp = Nothing
Set g_EventMonitor = Nothing
End If
End Sub

‘ 【業務処理のエントリーポイント】
Public Sub ExecuteBusinessLogicSafely()
‘ 1. ガードクラスが初期化されているかチェック
If g_EventMonitor Is Nothing Then
Call InitializeVisioGuard
End If

‘ 2. モーダル状態(印刷中やダイアログ表示中)のチェック
If g_EventMonitor.IsModalActive Then
MsgBox “現在Visioがモーダルダイアログ処理中です。” & vbCrLf & _
“システムのフリーズを防ぐため、処理を中断しました。”, vbCritical, “防衛シールド作動”
Exit Sub
End Sub

‘ 3. エラーハンドリング付きの安全な処理実行
On Error GoTo ErrorHandler

‘ — ここに本来の重厚な図面処理・API連携コードを書く —
Call ProcessHeavyShapes()

Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbExclamation
Call TerminateVisioGuard
End Sub

Private Sub ProcessHeavyShapes()
‘ オブジェクトのメモリ最適化を意識した形状走査のサンプル
Dim vPage As Visio.Page
Set vPage = ActivePage

Dim vShapes As Visio.Shapes
Set vShapes = vPage.Shapes

Dim i As Long
Dim totalCount As Long
totalCount = vShapes.Count

For i = 1 To totalCount
‘ 再度ループ内でも状態変化を警戒する極限の防衛策
If g_EventMonitor.IsModalActive Then Err.Raise 9999, , “モーダル割り込み検知による強制中断”

‘ 処理の本体
‘ Debug.Print vShapes(i).Name
Next i

‘ 【メモリ最適化の極意】オブジェクト参照の即座の解放
Set vShapes = Nothing
Set vPage = Nothing
End Sub

4. チーフアーキテクトが教える「メモリ最適化とレガシー保守」の極意

このアーキテクチャを現場に導入するにあたり、プロフェッショナルとして知っておくべき「裏の仕様」を共有する。

1. 参照の循環とメモリリークの根絶

`WithEvents` を用いたクラスモジュールは、通常の変数よりもメモリ上に強固に居座りやすい。`Visio.Application` を保持したままアドインやドキュメントを閉じると、COMの参照カウントが適切にデクリメントされず、Visioプロセス(visio.exe)がバックグラウンドにゾンビとして残るという現象が多発する。
これを防ぐため、必ず `TerminateVisioGuard` のようなクリーンアップルーチンを設け、`Set g_EventMonitor.VisApp = Nothing` によって明示的にCOMコネクションを切断しなさい。

2. 多重フックの罠とイベントの伝播コスト

エンタープライズ環境では、複数のVBAマクロやCOMアドインが同時にVisioを監視しようとすることがある。イベントハンドラ内で重い処理を記述すると、Visio全体のパフォーマンスが劣化する。`OnComponentEnterState` の内部は極限まで軽量に保ち、単なる「フラグのトグル(True/Falseの切り替え)」だけに専念させることが鉄則だ。

3. モーダルダイアログをWin32 APIでハックする(上級者向け)

Visio標準のイベントだけでは捉えきれない、外部DLL(C++等)から呼び出された独自のモーダルウィンドウや、Windows標準のファイル選択ダイアログなどが背後で動いている場合がある。その場合は、`FindWindow` や `GetForegroundWindow` といったWindows APIをポーリング、あるいはサブクラス化して監視するアプローチと組み合わせることで、鉄壁の守りを手に入れることができる。

総括

「動けばいい」という妥協の産物は、現場で必ずシステム停止という形でしっぺ返しを食らう。
特にVisioのような複雑なグラフィックエンジンを内蔵したホストアプリケーションにおいて、シングルスレッドとモーダル状態の制御を制することは、シニアエンジニアとアマチュアを分かつ決定的な境界線である。

今回授けた `OnComponentEnterState` を軸とした防衛設計をあなたのソリューションに組み込み、二度とユーザーを「フリーズの恐怖」に直面させない堅牢なシステムを構築してほしい。それが、コードに魂を込めるプロフェッショナルの仕事である。

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