Word VBAの深淵:差分同期エンジンによる「書式汚染」の根絶
Wordの自動化において、最も忌むべきは「全設定の力技上書き(Full-Overwrite)」だ。`Style`オブジェクトを強引に適用したり、`Range.Font`を全プロパティ書き換えたりするコードは、文書のメモリ消費量を増大させ、Undoスタックを汚染し、最終的には「Wordが勝手に書式を変える」という怪奇現象の原因となる。
真の自動化エンジニアは、「変更が必要なプロパティのみを特定し、最小限のバイナリ操作で同期する」というアルゴリズムを実装する。本稿では、文書間で段落書式の差分のみを抽出し、適用する同期エンジンの核心を解説する。
—
1. 差分同期エンジンのアーキテクチャ
Wordの`ParagraphFormat`や`Font`オブジェクトは、COM経由でアクセスすると極めて重い。プロパティを一つ参照するたびに、Wordの内部エンジンは書式の階層構造(Style → Paragraph → Character → Direct Formatting)を遡って解決(Resolution)を行う。
これをループ内で頻繁に行うことは、パフォーマンスの自殺行為に等しい。我々は、以下の戦略でこれを回避する。
1. プロパティの列挙とハッシュ化: 対象となるプロパティを配列化し、構造体(UDT)としてキャッシュする。
2. 比較演算の最適化: `If A <> B Then A = B`の形式を守り、未変更のプロパティへの代入を徹底的に排除する。
3. Undoブロックの制御: `UndoRecord`オブジェクトを明示的に使用し、処理全体を単一のトランザクションとして扱う。
—
2. 実装:差分抽出アルゴリズム
以下は、二つの段落オブジェクトを比較し、差分がある場合のみ更新を行うコアロジックだ。
‘ 差分同期エンジンのコアロジック
‘ @param targetPara 更新対象の段落
‘ @param sourcePara 参照元の段落
Public Sub SyncParagraphFormat(ByRef targetPara As Paragraph, ByRef sourcePara As Paragraph)
Dim tFmt As ParagraphFormat: Set tFmt = targetPara.Format
Dim sFmt As ParagraphFormat: Set sFmt = sourcePara.Format
‘ Undoスタックを汚染しないためのトランザクション開始
Dim undo As Object
Set undo = Application.UndoRecord
undo.StartCustomRecord “SyncFormat”
‘ 必要なプロパティのみをピンポイントで同期
‘ プロパティへの書き込みを最小限に抑えることでWordの再描画負荷を軽減
With tFmt
If .LeftIndent <> sFmt.LeftIndent Then .LeftIndent = sFmt.LeftIndent
If .SpaceBefore <> sFmt.SpaceBefore Then .SpaceBefore = sFmt.SpaceBefore
If .SpaceAfter <> sFmt.SpaceAfter Then .SpaceAfter = sFmt.SpaceAfter
If .LineSpacingRule <> sFmt.LineSpacingRule Then .LineSpacingRule = sFmt.LineSpacingRule
‘ 境界線などの複雑なプロパティは、nullチェックを挟むのが定石
If .Borders.Enable <> sFmt.Borders.Enable Then .Borders.Enable = sFmt.Borders.Enable
End With
undo.EndCustomRecord
‘ オブジェクトの明示的解放(VBAのGCは気まぐれであるため必須)
Set tFmt = Nothing
Set sFmt = Nothing
Set undo = Nothing
End Sub
—
3. メモリの最適化とCOM参照の制御
レガシーなVBA環境において、最も危険なのは「参照の持ちすぎ」によるメモリリークだ。特に`Document`や`Range`、`Paragraph`をグローバル変数や静的配列に保持し続けると、COMポインタの解放漏れが累積し、数時間の運用でWordがクラッシュする。
極限のメモリ管理術
- イベントハンドラの回避: `DocumentBeforeSave`等に重い同期ロジックを置かないこと。非同期に近い処理が必要な場合は、`OnTime`メソッドを用いたキュー処理を検討せよ。
- Variant型の禁止: 型指定のない変数は、実行時に`IDispatch`のクエリを繰り返すため、極めて低速になる。必ず`ParagraphFormat`等の具象型を指定すること。
- Windows APIの活用: 数万行を超える文書を同期する場合、`LockWindowUpdate`(`user32.dll`)を用いて画面描画を停止させ、更新中のチラつきと再計算を抑止する。
‘ 描画停止のためのAPI定義
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hWndLock As LongPtr) As Long
Public Sub EfficientSync(doc As Document)
‘ 描画を停止してパフォーマンスを最大化
LockWindowUpdate Application.hWnd
‘ … ここに同期処理の実装 …
‘ 描画を再開
LockWindowUpdate 0
End Sub
—
4. アーキテクトからの提言
この「差分同期」のアプローチは、単なるコードの高速化ではない。文書管理システム(DMS)と連携した際に、「どこが変更されたか」という履歴を追跡可能にするための基盤でもある。
全設定を上書きするシステムは、変更履歴(Track Changes)を破壊する。差分のみを書き換えるシステムは、変更履歴を正しく維持する。どちらが「システム」として優れているかは自明だろう。
VBAは、古い技術ではない。Wordという巨大で複雑なCOMサーバーを操るための、最も鋭利な外科手術用メスなのだ。このメスの扱いを誤らず、対象を正確に切開すること。それが、我々エンジニアに課せられた責務である。
