【テクニカル・上級編】【プロフェッショナル】Application.WindowSelectionChangeイベントによるリアルタイム校正:選択されたTextRangeの文字数や禁則処理を監視しステータスバーで警告するアシスタント – PowerPoint VBA解析バイブル

スポンサーリンク

PowerPoint UIスレッドを調伏する:WindowSelectionChangeイベントとWin32 APIタイマーによる超高速・非同期リアルタイム校正エンジンの構築

PowerPointは、WordやExcelと比較して「UIスレッドの占有度」が極めて高いアプリケーションである。

スライド上のテキスト、図形、グラフなどのオブジェクトは、すべて単一のメインスレッド(STA: Single-Threaded Apartment)上でレンダリングとイベント駆動が行われている。このような環境において、ユーザーがテキストを選択・編集するたびに発生する `Application.WindowSelectionChange` イベントを愚直にフックし、重い校正ロジックや正規表現による走査を同期実行すれば、結果は火を見るより明らかである。タイピング遅延、UIのフリーズ、そして最悪の場合はCOM(Component Object Model)の破綻による強制終了(クラッシュ)を引き起こす。

本稿では、レガシーなPowerPoint VBAの限界を突破し、エンタープライズ環境で実用に耐えうる「デバウンス(遅延実行)制御を組み込んだリアルタイム校正支援システム」のアーキテクチャを提示する。

Win32 APIを用いたタイマー制御、メモリリークを完全に排除するCOMオブジェクトのライフサイクル管理、32bit/64bit双方に対応するアドイン構成、そして社内ガイドラインの動的配信まで、現場の最前線で求められる極限の知見を解説する。

1. アーキテクチャ設計:同期処理の罠と「デバウンス」による解決

PowerPointの `WindowSelectionChange` イベントは、ユーザーが文字を一文字入力する、あるいはカーソルを移動させるたびに猛烈な頻度で発生する。

このイベント内で直接 `TextRange` の解析(文字数チェック、禁則文字の走査、NGワードの正規表現マッチングなど)を実行すると、ユーザーのタイピング速度に対して解析処理が追いつかなくなる。

同期イベントハンドリングのボトルネック

1. COMバインディングのオーバーヘッド: VBAからPowerPointのオブジェクト(`Selection`、`TextRange`)にアクセスするたびに、COMの境界を越える通信(マーシャリング)が発生し、これがミリ秒単位の遅延を蓄積させる。
2. 描画(ペイント)のブロッキング: 校正結果を `Application.StatusBar` に描画する処理が走ると、PowerPointはステータスバーの再描画を強制され、UIスレッドがブロックされる。

解決策としての「デバウンス(Debounce)処理」

ユーザーがタイピングを止め、「最後の操作から指定ミリ秒(例:300ms)が経過した瞬間」にのみ実際の校正処理を実行する。

これを実現するために、Windows OSのメッセージループに直接介入する `SetTimer` / `KillTimer` APIを利用する。VBA自体はマルチスレッドをサポートしていないが、Win32 APIのタイマーコールバックを利用することで、疑似的な非同期・遅延実行を実現できる。

[ユーザーの操作] -> Event発生 -> 既存Timerを破棄 (KillTimer)
-> 新規Timerを開始 (SetTimer: 300ms)
|
| (300ms間、次の操作がなければ…)
v
[TimerCallback実行] -> 校正エンジン起動 -> ステータスバー警告

2. 実装:コンプリート・プロフェッショナル・コード

本システムは、以下の3つのコンポーネントで構成される。

1. `clsAppEvents`(クラスモジュール): PowerPointのアプリケーションイベントをフックする。
2. `modTimer`(標準モジュール): Win32 APIタイマーの制御とコールバック関数を司る。
3. `modProofreader`(標準モジュール): 高速な文字列解析と、ステータスバーへの安全な描画を担う。

2.1. `clsAppEvents`(クラスモジュール)

PowerPoint全体のイベントを捕捉するためのクラスモジュール。インスタンスはグローバルに維持される。

Option Explicit

‘ PowerPointアプリケーションオブジェクトをイベント付きで宣言
Private WithEvents App As PowerPoint.Application

Private Sub Class_Initialize()
Set App = PowerPoint.Application
End Sub

Private Sub Class_Terminate()
‘ 安全なクリーンアップ
Set App = Nothing
End Sub

‘ 選択範囲変更イベント
Private Sub App_WindowSelectionChange(ByVal Sel As Selection)
On Error GoTo ErrorHandler

‘ 選択状態がテキスト、またはテキストを内包する図形であるか検証
If Sel.Type = ppSelectionText Or Sel.Type = ppSelectionShapes Then
‘ 既存のタイマーをリセットし、デバウンス(300ミリ秒)を開始
Call RestartProofreadTimer
End If

Exit Sub

ErrorHandler:
‘ イベントハンドラ内での例外はサイレントに処理し、ホストをクラッシュさせない
Debug.Print “Error in WindowSelectionChange: ” & Err.Description
End Sub

2.2. `modTimer`(標準モジュール)

Windows APIを宣言し、タイマーのライフサイクルを完全に制御する。32bit/64bit(VBA7)の互換性を完全に保証する。

Option Explicit

‘ Win32 API 宣言 (VBA7 / 64bit互換)
If VBA7 Then
Private DeclarePtrSafe Function SetTimer Lib “user32” ( _
ByVal hWnd As LongPtr, _
ByVal nIDEvent As LongPtr, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As LongPtr) As LongPtr

Private DeclarePtrSafe Function KillTimer Lib “user32” ( _
ByVal hWnd As LongPtr, _
ByVal nIDEvent As LongPtr) As Long

Private mTimerID As LongPtr
Else
Private Declare Function SetTimer Lib “user32” ( _
ByVal hWnd As Long, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As Long) As Long

Private Declare Function KillTimer Lib “user32″ ( _
ByVal hWnd As Long, _
ByVal nIDEvent As Long) As Long

Private mTimerID As Long
End If

‘ タイマーIDの一意な識別子
Private Const TIMER_ID As Long = 1001
‘ デバウンス時間(ミリ秒)
Private Const DEBOUNCE_DELAY As Long = 300

‘ グローバルイベントクラスのインスタンス保持用
Public GoAppEvents As clsAppEvents

”’

”’ システム開始:アドインロード時または初期化時にコールする
”’

Public Sub InitializeAddIn()
If GoAppEvents Is Nothing Then
Set GoAppEvents = New clsAppEvents
End If
End Sub

”’

”’ システム停止:アドインアンロード時に必ずコールし、タイマーを解放する
”’

Public Sub TerminateAddIn()
Call StopTimer
Set GoAppEvents = Nothing
End Sub

”’

”’ 既存のタイマーを破棄し、新たにタイマーを設定する(デバウンスの核心)
”’

Public Sub RestartProofreadTimer()
Call StopTimer
‘ 300ms後に TimerProc をコールバックするように設定
mTimerID = SetTimer(0&, TIMER_ID, DEBOUNCE_DELAY, AddressOf TimerProc)
End Sub

”’

”’ タイマーの確実な停止
”’

Private Sub StopTimer()
If mTimerID <> 0 Then
Call KillTimer(0&, mTimerID)
mTimerID = 0
End If
End Sub

”’

”’ Windows OSからコールバックされるタイマープロシージャ
”’

If VBA7 Then
Public Sub TimerProc(ByVal hWnd As LongPtr, ByVal uMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As Long)
Else
Public Sub TimerProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal idEvent As Long, ByVal dwTime As Long)
End If
‘ タイマーコールバックに入ったら、再入を防ぐため即座にタイマーを止める
On Error Resume Next
Call StopTimer
On Error GoTo 0

‘ 校正エンジンの実行
Call ExecuteProofread
End Sub

2.3. `modProofreader`(標準モジュール)

テキスト情報を安全に取得し、メモリ効率に優れた手法で高速に解析する校正エンジン。

Option Explicit

‘ ガイドライン定数
Private Const MAX_CHAR_COUNT As Long = 80 ‘ スライド1枚あたりの推奨最大文字数(警告ライン)
Private Const FORBIDDEN_WORDS As String = “絶対,100%,バグ,落ちる,~” ‘ NGワード(カンマ区切り)

”’

”’ 選択されたテキストのコンテキストを解析し、検証ルールを適用する
”’

Public Sub ExecuteProofread()
Dim oActiveWindow As DocumentWindow
Dim oSel As Selection
Dim oTargetRange As TextRange

On Error GoTo ErrorHandler

‘ アクティブウィンドウの存在確認
If PowerPoint.Application.Windows.Count = 0 Then Exit Sub
Set oActiveWindow = PowerPoint.Application.ActiveWindow
Set oSel = oActiveWindow.Selection

‘ 選択オブジェクトの検証(COM参照を安全に取得)
If oSel.Type = ppSelectionText Then
Set oTargetRange = oSel.TextRange
ElseIf oSel.Type = ppSelectionShapes Then
‘ シェイプが選択されている場合、テキストフレームを内包しているかチェック
If oSel.ShapeRange.Count = 1 Then
If oSel.ShapeRange.HasTextFrame Then
If oSel.ShapeRange.TextFrame.HasText Then
Set oTargetRange = oSel.ShapeRange.TextFrame.TextRange
End If
End If
End If
End If

‘ ターゲットとなるテキストが存在する場合のみ解析
If Not oTargetRange Is Nothing Then
Call AnalyzeText(oTargetRange.Text)
Else
‘ 選択が外れた場合はステータスバーをクリア
PowerPoint.Application.StatusBar = “”
End If

CleanUp:
‘ COMオブジェクトの明示的解放(参照カウンタのデクリメント)
Set oTargetRange = Nothing
Set oSel = Nothing
Set oActiveWindow = Nothing
Exit Sub

ErrorHandler:
‘ タイマーコールバック内でのエラーは、最悪の場合Excel/PowerPointを巻き込んで落ちるため、
‘ ここで完全に例外をトラップして握り潰す。
Debug.Print “Error in ExecuteProofread: ” & Err.Description
Resume CleanUp
End Sub

”’

”’ 高速な文字列走査によるルール適用エンジン
”’

Private Sub AnalyzeText(ByVal text As String)
Dim textLength As Long
Dim isViolated As Boolean
Dim alertMessage As String

textLength = Len(text)

‘ ルール1: 文字数制限チェック
If textLength > MAX_CHAR_COUNT Then
alertMessage = “【警告】文字数超過 (” & textLength & “/” & MAX_CHAR_COUNT & “文字) ”
isViolated = True
End If

‘ ルール2: 禁則・NGワードチェック(高速なInStr走査)
Dim words() As String
words = Split(FORBIDDEN_WORDS, “,”)

Dim i As Long
For i = LBound(words) To UBound(words)
If InStr(1, text, words(i), vbBinaryCompare) > 0 Then
alertMessage = alertMessage & “【NGワード検出: “”” & words(i) & “””】 ”
isViolated = True
End If
Next i

‘ ルール3: 禁則処理(句読点が行頭に来ていないかの簡易チェック)
‘ ※簡易的に、改行コードの直後に句読点があるかを高速走査
If InStr(1, text, vbCr & “。”, vbBinaryCompare) > 0 Or _
InStr(1, text, vbCr & “、”, vbBinaryCompare) > 0 Then
alertMessage = alertMessage & “【行頭禁則違反】 ”
isViolated = True
End If

‘ ステータスバーへの描画制御(ちらつき防止のため、必要な場合のみ更新)
Static lastMessage As String
If isViolated Then
If lastMessage <> alertMessage Then
PowerPoint.Application.StatusBar = alertMessage
lastMessage = alertMessage
End If
Else
If lastMessage <> “” Then
PowerPoint.Application.StatusBar = “【校正クリア】テキストはガイドラインに適合しています。”
lastMessage = “”
End If
End If
End Sub

3. 技術的深掘りと極限の最適化

3.1. メモリ管理とCOM参照カウンタの真実

VBAは自動参照カウンタ(ARC)形式のメモリ管理を採用しているが、PowerPointのCOMオブジェクト(特に `Selection` や `TextRange`)は、内部的に重厚なC++のラッパーオブジェクトである。

これらを解放せずにループ内や高頻度イベント内で放置すると、VBA内部のガベージコレクションが追いつかず、一時的にメモリ空間(ヒープ)を圧迫し、最終的に「アウト・オブ・メモリー」または「オートメーション・エラー」を引き起こす。

本コードでは、以下のプラクティスを徹底している。

  • 明示的な `Nothing` の代入: プロシージャの終了直前に、取得したすべてのCOMオブジェクトに対して `Set obj = Nothing` を明示的に実行し、COMの参照カウンタを即座にデクリメントする。
  • ドット記述の最小化: `ActiveWindow.Selection.ShapeRange.TextFrame.TextRange` のような、ドットを深く繋げた記述(ドットプロパティチェーン)は避ける。ドットの数だけ中間COMオブジェクトが暗黙的に生成され、VBAの管理外でメモリに残留する原因となる。コード例のように、ステップごとにオブジェクト変数に切り出し、それぞれを確実に解放する。

3.2. Win32 APIタイマー使用時の「デバッグ時クラッシュ」の回避

VBA開発において、`SetTimer` APIの使用には致命的なリスクが伴う。「タイマーが動作している最中に、VBAコードの実行をリセット(停止ボタンを押下)またはデバッグ中にコードを編集すると、VBAの仮想マシンがクラッシュし、PowerPointプロセスごと強制終了する」という現象である。

これは、Windows OSが登録されたメモリアドレス(`AddressOf TimerProc`)に向けて定期的にメッセージを送信し続けるのに対し、VBA側がリセットによってそのアドレスの関数をメモリから破棄してしまうために発生する(アクセスバイオレーション)。

防御策

1. 開発中の注意: 開発・デバッグ時は、必ずタイマーが停止していることを確認する。
2. アドイン解放イベントのフック:
PowerPointのアドイン(`.ppam`)としてビルドする場合、あるいはプレゼンテーションを閉じる際、`Auto_Close` または `PresentationClose` イベントで確実に `KillTimer` を呼び出す機構を実装する(下記コード参照)。

‘ 標準モジュール(Auto_Close等での確実な回収)
Public Sub Auto_Close()
‘ アドインアンロード時にタイマーを完全に殺す
Call TerminateAddIn
End Sub

4. エンタープライズ展開:レガシー保守とシステム間連携

この校正支援システムを全社規模、あるいは数百人規模のプロフェッショナル環境に展開する場合、ソースコード内にルール(NGワードや文字数)をハードコーディングするのは運用上、破綻を意味する。

広報部や法務部が作成する「社内表記ガイドライン」は頻繁に更新されるため、これを動的に配信・反映する設計が必要不可欠である。

4.1. ルールエンジンの外部化と動的デプロイ(システム間連携)

VBAからイントラネット上の共有ネットワークフォルダ、あるいは社内Web API(IISやSaaS等)からJSONファイルをサイレントにフェッチし、起動時にキャッシュする。

以下は、社内Webサーバーから最新のNGワードリストを非同期(または高速な同期)で取得し、システムに適用する設計パターンである。

”’

”’ イントラネット上のJSON/テキストから最新ルールをフェッチする
”’

Public Function FetchLatestGuidelines() As String
Dim xmlHttp As Object
Dim targetUrl As String

targetUrl = “http://internal-portal.corp/assets/guideline_rules.txt”
On Error GoTo ErrorHandler

Set xmlHttp = CreateObject(“MSXML2.ServerXMLHTTP.6.0”)
xmlHttp.Open “GET”, targetUrl, False ‘ 同期通信
xmlHttp.send

If xmlHttp.Status = 200 Then
FetchLatestGuidelines = xmlHttp.responseText
Else
‘ フォールバック:サーバー応答エラー時はローカルのデフォルト値を使用
FetchLatestGuidelines = “絶対,100%,バグ,落ちる”
End If

Set xmlHttp = Nothing
Exit Function

ErrorHandler:
‘ ネットワーク切断時などのフォールバック
FetchLatestGuidelines = “絶対,100%,バグ,落ちる”
Set xmlHttp = Nothing
End Function

4.2. 32bit / 64bit Officeの共存とレガシー資産の保護

大企業のインフラストラクチャにおいては、いまだに32bit版のOffice(Excel/PowerPoint)と、新しく導入された64bit版のOfficeが混在しているケースが極めて多い。

本システムで採用している条件付きコンパイル(`#If VBA7`)は、この二つの環境で「同一のコードベース(Single Codebase)」を維持するための生命線である。

  • VBA7の導入(Office 2010以降): `PtrSafe` キーワードをサポートし、64bitのアドレスポインタ型 `LongPtr` を利用可能にする。
  • `LongPtr` の適切な選択: ウィンドウハンドル(`hWnd`)やタイマーID(`idEvent`)など、OSのメモリ空間のアドレスを指す値は、必ず `LongPtr` で宣言する。これを誤って単なる `Long`(32bit符号付き整数)で宣言すると、64bit環境でメモリ空間の上位ビットが切り捨てられ、即座にポインタ破壊によるブルースクリーンやアプリケーションクラッシュを招く。

5. チーフアーキテクトの総括

PowerPoint VBAによるリアルタイムアプリケーションの開発は、COMの性質、シングルスレッドの制約、そしてWin32メッセージループの深い理解なくしては成立しない。

本稿で提示した「デバウンス制御を組み込んだリアルタイム校正支援システム」は、単なるマクロの領域を超え、実用的な「アドイン・バックエンド」としての堅牢性を備えている。

1. ユーザー体験を損なわない設計: デバウンスによるUIスレッドへの負荷削減。
2. OSレベルでの制御: Win32 APIを用いたタイマー管理の最適化。
3. 徹底的な防御的プログラミング: 例外のサイレントトラップとCOMオブジェクトの明示的解放。

この3原則を徹底することで、レガシーと言われるVBAは、今なおモダンな業務自動化・執筆支援における強力なフロントエンド・統合環境として君臨し続けることができる。現場の要求水準がどれほど高くとも、アーキテクチャの基本に忠実であれば、必ずそれに応えるシステムが構築可能である。

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