100回の置換を1回の「戻る」で。Word VBAにおけるUndoRecordトランザクション管理の極意
「マクロを実行した後、Ctrl+Zを何度も連打しなければ元に戻せない」
もしあなたが作成したツールがユーザーにこのような強要をしているなら、それはエンジニアとして二流と言わざるを得ない。
大量の文字列置換やフォーマット変更を行う自動化ツールにおいて、「操作の塊(トランザクション)」を制御できていないことは、ユーザー体験を損なうだけでなく、データの整合性を破壊するリスクを孕んでいる。
今回は、Word VBAにおける「Undo(元に戻す)」のスタックを論理的にグループ化する、`Application.UndoRecord` オブジェクトの徹底活用術を伝授する。
—
1. なぜ「UndoRecord」が必要なのか
Word VBAで `Find.Execute Replace:=wdReplaceAll` をループで回すと、Wordは内部的に「置換1回ごと」にUndoスタックを積み上げる。1,000箇所の置換を行えば、Undoスタックは1,000個消費される。
ユーザーが「あ、今のマクロ実行、間違えたな」と思ってCtrl+Zを押したとき、戻るのは「最後の1箇所の置換」だけであり、文書全体をマクロ実行前の状態に戻すには、指が腱鞘炎になるまで連打を繰り返すことになる。
プロフェッショナルの仕事は、マクロによる一連の破壊的変更を「一つの不可分な操作(Atomic Operation)」として定義することにある。
これを実現するのが、Word 2010から導入された `UndoRecord` だ。
—
2. UndoRecord の基本構造と「死の罠」
基本操作は非常にシンプルだ。
Dim ur As UndoRecord
Set ur = Application.UndoRecord
ur.StartCustomRecord “一括変換処理”
‘ — ここに大量の置換処理 —
ur.EndCustomRecord
しかし、このコードには致命的な欠陥がある。
「エラーハンドリングの欠如」だ。
もし `StartCustomRecord` と `EndCustomRecord` の間でランタイムエラーが発生し、プロシージャが中断された場合、WordのUndoスタックは「開いたまま」の状態になる。こうなると、その後のユーザーの手動操作までもがすべて一つのUndoに統合され続け、最悪の場合、Wordの挙動が不安定になりクラッシュを招く。
—
3. 堅牢な設計:トランザクション・ラッパー・パターン
実務で使うなら、エラーが発生しても確実に `EndCustomRecord` を呼び出す、いわゆる「トランザクション管理」の設計が不可欠だ。
実践的なプロダクションコード例
以下のコードは、複数の用語を辞書(配列)に基づいて一括置換し、それを一つのUndo操作にまとめる堅牢なテンプレートである。
”’
”’
Public Sub ExecuteMassiveReplace()
Dim ur As UndoRecord
Set ur = Application.UndoRecord
‘ 1. 二重開始の防止(既に別のカスタムレコードが開いていないか確認)
‘ ※ネストも可能だが、管理を単純化するためトップレベルでの制御を推奨
If ur.CustomRecordLevel > 0 Then
Call PerformReplaceInternal
Exit Sub
End If
On Error GoTo ErrorHandler
‘ 2. カスタムレコードの開始
‘ ここで指定した文字列が、Wordの「元に戻す」メニューに表示される
ur.StartCustomRecord “用語集の一括適用(自動処理)”
‘ 3. 実際の処理(ロジックを分離することで保守性を高める)
Call PerformReplaceInternal
‘ 4. 正常終了
ur.EndCustomRecord
Exit Sub
ErrorHandler:
‘ 5. エラー時も必ずレコードを閉じる
If ur.CustomRecordLevel > 0 Then ur.EndCustomRecord
MsgBox “置換処理中にエラーが発生しました。処理を中断します。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “システムエラー”
End Sub
Private Sub PerformReplaceInternal()
‘ 具体的な置換ロジック
‘ ※ここでは例として単純な置換を示すが、実際は外部ファイルやDBから読み込むことが多い
Dim findTexts As Variant: findTexts = Array(“旧システム”, “旧担当者”, “2023年度”)
Dim replaceTexts As Variant: replaceTexts = Array(“新システム”, “新担当者”, “2024年度”)
Dim i As Long
For i = LBound(findTexts) To UBound(findTexts)
With ActiveDocument.Content.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = findTexts(i)
.Replacement.Text = replaceTexts(i)
.Forward = True
.Wrap = wdFindContinue
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = False
‘ 全置換を実行
.Execute Replace:=wdReplaceAll
End With
Next i
End Sub
—
4. 現場で差が出る高度な注意点
① データベース・ファイル連携時の整合性
この `UndoRecord` は、Word文書内の操作のみを対象とする。
マクロ内で「外部データベースのステータスを更新する」あるいは「別名でファイルを保存する」といった処理を含めている場合、Word側でUndo(元に戻す)を行っても、外部の変更はロールバックされない。
「文書は戻ったが、DBのログには完了と残っている」という不整合を防ぐため、外部連携を含む場合は独自のロールバックロジックを検討すべきだ。
② UndoRecordのネスト(入れ子)
`UndoRecord` はネストが可能だ。しかし、複雑なツールでネストを多用すると、どのタイミングで `EndCustomRecord` が呼ばれるべきかの管理が困難になる。
基本的には、「エントリーポイント(ユーザーがボタンを押す一番外側のメソッド)」でのみ開始・終了を制御するのが、バグを未然に防ぐアーキテクチャの鉄則である。
③ パフォーマンスへの影響
実は、大量の置換を `UndoRecord` で包むと、WordがUndoスタックをメモリ上にバッファリングするため、極めて巨大な文書(数百ページ以上)ではメモリ消費量が増大し、若干のパフォーマンス低下を招く場合がある。
しかし、現代のPCスペックにおいて、数千箇所の置換程度であればメリット(ユーザーの利便性)がデメリット(速度低下)を大きく上回る。
—
5. 結論:エンジニアが持つべき「Undoへの敬意」
自動化ツールは「動けばいい」のではない。
「間違えたときに安全に戻れる」という安心感を提供して初めて、現場で信頼されるツールとなる。
`Application.UndoRecord` を適切に実装することは、単なるテクニックではない。それは、あなたのコードを「ただのスクリプト」から「業務システム」へと昇華させる、プロフェッショナルとしての最低限の嗜みである。
次に置換マクロを書くときは、必ずこのトランザクション構造を組み込んでほしい。ユーザーがCtrl+Zを一度押したとき、あなたの配慮の深さが伝わるはずだ。
