Word VBAの世界において、素人とプロフェッショナルを分かつ境界線はどこにあるか。それは単に「動くコード」を書くことではなく、「ユーザーの操作体系(UX)を破壊しないコード」を書けるかどうかに集約される。
特に、数千行に及ぶ文書に対して数百回の置換を繰り返す自動化ツールを構築する際、ユーザーが「一回やり直したい」とCtrl+Zを押した瞬間、数百回分の置換履歴が一つずつ巻き戻される光景は、設計者としての敗北を意味する。
今回は、Word 2010以降で導入された`UndoRecord`オブジェクトを軸に、複雑な置換処理を単一のトランザクションとしてカプセル化する「極限の整合性制御」について語ろう。
—
1. UndoRecord:操作の原子性を保証する
Word VBAにおける`UndoRecord`は、データベースで言うところの「トランザクション」に相当する。複数の検索・置換、書式変更を一つの「意味のある単位」としてスタックに積み上げるための機構だ。
なぜUndoRecordが必要なのか
標準的な`Range.Find.Execute Replace:=wdReplaceAll`をループさせた場合、WordのUndoスタックには各置換が個別に記録される。これを放置することは、保守性の低いスパゲッティコードを放置することと同義だ。ユーザーのUndoバッファを汚染し、実質的に「戻せない」状態を作り出してしまう。
アーキテクチャの要諦
`UndoRecord`を使用する際は、必ず`CustomUndoRecordBefore`と`CustomUndoRecordAfter`の状態を意識しなければならない。そして何より、エラーハンドリングによる確実なCloseが絶対条件だ。
—
2. 実装:堅牢なトランザクション・プロトコル
以下に示すコードは、単なる置換のサンプルではない。Windows APIによる描画停止と、エラー発生時でもUndoスタックを破損させない「防弾仕様」のアーキテクチャだ。
Option Explicit
‘ Windows APIによる描画更新の制御(極限のパフォーマンスを求める場合)
‘ ※Word標準のScreenUpdatingよりも低レイヤーでの制御が可能
Private Declare PtrSafe Function LockWindowUpdate Lib “user32″ (ByVal hwndLock As LongPtr) As Long
”’
”’
Public Sub ExecuteAtomicReplacement()
Dim objUndo As UndoRecord
Set objUndo = Application.UndoRecord
‘ 処理対象のドキュメント整合性チェック
If Documents.Count = 0 Then Exit Sub
On Error GoTo ErrorHandler
‘ 1. トランザクションの開始
‘ この名前がUndoメニューに表示される。「礼節」として適切な名称を付与せよ。
objUndo.StartCustomRecord “一括正規化処理(社内規定第12条に基づく)”
‘ 2. パフォーマンスの最適化
Application.ScreenUpdating = False
‘ 3. 置換エンジンのコア・ロジック
‘ Findオブジェクトは使い回すのが定石。メモリ再確保のオーバーヘッドを削る。
Call PerformComplexReplacements(ActiveDocument.Content)
‘ 4. トランザクションの正常終了
objUndo.EndCustomRecord
GoTo Finalize
ErrorHandler:
‘ エラー発生時、スタックが開いたままになるとWordの動作が不安定になる。
‘ アーキテクトとして、このクリーンアップを怠ることは許されない。
If Not objUndo Is Nothing Then
objUndo.EndCustomRecord
End If
MsgBox “致命的なエラーが発生しました。処理を中断し、Undoスタックを保護します。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical
Finalize:
Application.ScreenUpdating = True
‘ オブジェクトの明示的解放(VBAの参照カウンタに頼り切らない)
Set objUndo = Nothing
End Sub
”’
”’
Private Sub PerformComplexReplacements(ByRef rng As Range)
‘ 複数の置換パターンを配列または外部定義から読み込む
‘ ここでは例としてハードコードするが、本番環境では定数管理すべきである
Dim patterns As Variant
patterns = Array( _
Array(“【旧組織名】”, “【新組織名】”), _
Array(“([0-9]{4})年”, “\1年度”), _
Array(” +”, ” “) _
)
Dim i As Long
For i = LBound(patterns) To UBound(patterns)
With rng.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = patterns(i)(0)
.Replacement.Text = patterns(i)(1)
.Forward = True
.Wrap = wdFindContinue
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = True ‘ 正規表現(ワイルドカード)の有効化
.Execute Replace:=wdReplaceAll
End With
Next i
End Sub
—
3. シニアエンジニアが押さえるべき深淵
3.1 UndoRecordのネスト挙動
`UndoRecord`はネストが可能だ。しかし、ネストされた内部の`StartCustomRecord`は、実質的に最上位のレコードにマージされる。複雑なクラスモジュール構造をとる場合、どのレイヤーでトランザクションを開始するかを厳密に定義せねばならない。無計画なネストはスタックの不透明化を招く。
3.2 メモリ空間とガベージコレクションの境界
VBAはCOM(Component Object Model)ベースである。`UndoRecord`をオープンしたままオブジェクト参照が宙に浮くと、Wordのプロセスが終了してもメモリが解放されない、あるいは`Normal.dotm`の保存時に競合が発生するリスクがある。
上記のコードで`Set objUndo = Nothing`を徹底しているのは、単なる儀式ではない。COM参照カウントを確実にゼロへ導くための「意志」である。
3.3 Windows APIによる「真の」描画ロック
`Application.ScreenUpdating = False`は、実は万能ではない。Wordの内部イベントやアドインの干渉により、意図せず描画が走ることがある。
極限のパフォーマンスとユーザー体験を求めるなら、`user32.dll`の`LockWindowUpdate`を検討すべきだ。ただし、これはOSレベルでウィンドウの描画を止めるため、エラー時に解除し忘れると「OSごとフリーズしたように見える」諸刃の剣である。本稿のコードではあえて標準命令に留めているが、これが選択肢にあること自体がプロの証だ。
—
4. レガシー環境との対峙
もし貴殿が保守しているシステムが、いまだにOffice 2007以前の環境(`UndoRecord`未実装)をサポートしなければならない不運に見舞われているなら、代替案は一つしかない。
「Undoスタックの回数をカウントし、一括で`Undo`メソッドをループ実行するラッパーを作る」ことだ。しかし、これは不安定極まりない。現代のアーキテクトとしては、プラットフォームのアップデートを提言するのも重要な職務である。
結論
`UndoRecord`の活用は、単なる「便利機能」の利用ではない。それは、プログラムによる破壊的変更に対して、開発者が全責任を負うための宣言である。
一括置換という「暴力的な力」を振るうコードに、Undoという「慈悲」を実装する。この調和こそが、真に美しい自動化システムの姿である。貴殿の書くコードが、次にCtrl+Zを押すユーザーにとっての救いとなることを願ってやまない。
