【PowerPoint非同期タイマー】Windows API(SetTimer/KillTimer)を駆使し、VBAの限界を突破する極限実装
Excel VBAには `Application.OnTime` という強力な非同期実行のプリミティブが存在する。しかし、PowerPoint VBAのオブジェクトモデルに目を向けると、そこには絶望的なまでの「空白」が広がっている。PowerPointには、指定時間後にマクロを呼び出すネイティブな非同期タイマー機能が標準で用意されていないのだ。
自動バックグラウンド保存、リアルタイムな外部データポーリング、あるいはサイネージスライドの高度な動的制御。これらをPowerPoint単体で実現しようとしたとき、多くの開発者は `DoEvents` を挟んだ無限ループという名の「CPUを焼き尽くす悪夢」に堕ちていく。
本稿では、Windows APIである `SetTimer` と `KillTimer` をPowerPoint VBAから直接制御し、メモリリークとクラッシュの罠を完全に回避しながら、堅牢な非同期タイマー基盤を構築する極限の知見を公開する。
—
1. なぜPowerPoint VBAのタイマー実装は危険なのか?
プロフェッショナルなシニアエンジニアであれば、VBAからWindows APIを呼び出すこと自体のリスクを熟知しているはずだ。特にタイマー割り込み(コールバック)をVBAで扱う場合、以下の2点において致命的なクラッシュを引き起こす可能性が潜んでいる。
1. メモリ空間とポインタの乖離:
Windows APIのタイマーは、指定時間が経過すると「コールバック関数(WndProcや専用のプロシージャ)」を呼び出す。しかし、VBAの標準機能では関数ポインタ(AddressOf)を標準モジュール以外で正しく保持できず、さらにインスタンスのライフサイクルと同期させることが極めて困難である。
2. ガベージコレクションとVBAIDEの不安定性:
タイマー割り込みの最中にVBAの変数が解放されたり、予期せぬエラーでコードが中断されたりすると、ExcelやPowerPointのプロセスごと沈黙する(Hard Crash)。
このリスクを最小化し、安定稼働させるための設計思想が「標準モジュールによる安全なイベントルーティング」と「厳格なライフサイクル管理」である。
—
2. アーキテクチャ設計
今回の実装では、以下のコンポーネントを組み合わせる。
- `user32.dll` の `SetTimer`: OSレベルでタイマーイベントを登録する。
- `user32.dll` Id `KillTimer`: プレゼンテーションのクローズ時や処理終了時にタイマーを確実に破棄する。
- 標準モジュール(`modTimerManager`): APIからのシグナルを受け取り、安全にVBAのマクロへディスパッチ(転送)する。
—
3. 実装コード:プロフェッショナル・タイマーモジュール
以下のコードをPowerPointのVBAプロジェクトに挿入する。必ず標準モジュール(例: `modTimerManager`)を作成し、そこに記述してほしい。クラスモジュールに記述すると `AddressOf` 演算子がコンパイルエラーになるためだ。
Option Explicit
‘ ==============================================================================
‘ 致命的なクラッシュを防ぐためのWindows API宣言
‘ ==============================================================================
If VBA7 Then
‘ 64ビット環境(およびモダンなOffice環境)に対応するAPI宣言
Declare PtrSafe Function SetTimer Lib “user32” ( _
ByVal hwnd As LongPtr, _
ByVal nIDEvent As LongPtr, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As LongPtr) As LongPtr
Declare PtrSafe Function KillTimer Lib “user32” ( _
ByVal hwnd As LongPtr, _
ByVal nIDEvent As LongPtr) As Long
Else
‘ レガシー32ビット環境用
Declare Function SetTimer Lib “user32” ( _
ByVal hwnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long
Declare Function KillTimer Lib “user32″ ( _
ByVal hwnd As Long, _
ByVal nIDEvent As Long) As Long
End If
‘ ==============================================================================
‘ 内部状態管理変数
‘ ==============================================================================
Private m_TimerID As LongPtr
Private m_IsRunning As Boolean
”’
”’
”’ 実行間隔(ミリ秒)例: 5000 = 5秒
Public Sub StartAsyncTimer(ByVal IntervalMilliSeconds As Long)
‘ 既にタイマーが稼働中の場合は二重起動を防ぐために一度破棄
If m_IsRunning Then
Call StopAsyncTimer
End If
‘ SetTimerの第4引数にAddressOfを指定し、コールバック関数を紐付ける
‘ 第1引数(hwnd)を 0、第2引数(nIDEvent)を任意のID(例: 1)とする
m_TimerID = SetTimer(0, 1, IntervalMilliSeconds, AddressOf TimerCallback)
If m_TimerID = 0 Then
MsgBox “Windows APIのタイマー作成に失敗しました。”, vbCritical, “致命的エラー”
m_IsRunning = False
Else
m_IsRunning = True
Debug.Print “[Timer] 非同期タイマーが正常に開始されました。ID: ” & m_TimerID
End If
End Sub
”’
”’
Public Sub StopAsyncTimer()
If m_IsRunning And m_TimerID <> 0 Then
Dim result As Long
result = KillTimer(0, m_TimerID)
m_TimerID = 0
m_IsRunning = False
Debug.Print “[Timer] 非同期タイマーを停止し、リソースを解放しました。”
End If
End Sub
”’
”’ 標準モジュールに配置し、パブリックかつ正確なシグネチャを保つこと
”’
If VBA7 Then
Public Sub TimerCallback(ByVal hwnd As LongPtr, ByVal uMsg As LongPtr, ByVal idEvent As LongPtr, ByVal dwTime As Long)
Else
Public Sub TimerCallback(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idEvent As Long, ByVal dwTime As Long)
End If
On Error GoTo ErrorHandler
‘ ————————————————————————–
‘ ここに定期実行したいビジネスロジックを記述する
‘ 例: バックグラウンドでの自動保存、外部ログの書き込み、状態監視など
‘ ————————————————————————–
Debug.Print “[Timer Execution] 割り込み処理実行時刻: ” & Format(Now, “hh:nn:ss”)
‘ 実装例:アクティブなプレゼンテーションが存在する場合のみ自動保存チェック
If Application.Presentations.Count > 0 Then
‘ ドキュメントの変更フラグが立っている場合の処理などをここに記述
End If
Exit Sub
ErrorHandler:
‘ コールバック内で未処理エラーが発生した場合、VBAの暴走を防ぐため
‘ 即座にタイマーを殺して安全にイグジットする
Debug.Print “[Timer Error] 致命的なエラーが発生しました: ” & Err.Description
Call StopAsyncTimer
End Sub
—
4. ライフサイクル管理の極意:ゾンビプロセスの防止
Windows APIを用いたVBA拡張において最も恐ろしいのは、「プレゼンテーションを閉じた後も、メモリ上にタイマーイベントが残り続け、背後でコードが空撃ちされ続ける」という現象だ。これを放置すると、PowerPointのプロセスがバックグラウンドに残留(ゾンビ化)し、メモリリークやファイルロックを引き起こす。
これを完璧に防ぐため、クラスモジュール(例: `clsEventListener`)またはプレゼンテーションのイベントハンドラを併用し、プレゼンテーションが閉じられる瞬間(`PresentationClose` 等)に必ず `StopAsyncTimer` を叩く仕組みを構築しなければならない。
ThisPresentationモジュールに以下のコードを追加する。
Option Explicit
Private Sub Document_Close()
‘ プレゼンテーションが閉じられる際、確実にタイマーのOSリソースを解放する
On Error Resume Next
Call StopAsyncTimer
On Error GoTo 0
End Sub
—
5. チーフアーキテクトからの実践的助言
1. デバッグ中のブレークポイントに注意せよ:
タイマーのコールバック関数 (`TimerCallback`) の実行中にVBAコードのブレークポイント(一時停止)をヒットさせると、OSのメッセージループとVBA環境がデッドロックを起こし、PowerPointが強制終了することがある。デバッグ時は、一度タイマーを停止した状態でロジックを単体テストすることを強く推奨する。
2. パフォーマンスへの配慮:
`IntervalMilliSeconds` に極端に短い値(例: 100ミリ秒以下)を指定すると、VBAのインタープリターが処理しきれずにイベントがキューイングされ、パフォーマンスの急低下やクラッシュを誘発する。実用的な監視処理であっても、最短で1000ミリ秒(1秒)以上のインターバルを確保すべきである。
Excelの `OnTime` のように手軽とは言えないが、この `SetTimer` アプローチをマスターすれば、PowerPointは単なる「静的なスライドショー閲覧ソフト」から、「自律的に稼働する堅牢なエージェントシステム」へと進化する。
極限まで最適化されたコードベースで、真の自動化の領域へ踏み出してほしい。
