Wordの深淵を覗く:`StoryRanges`を制して「完全なる一括置換」を実装する
Word VBAにおいて、多くの初学者が陥る罠がある。それは「`ActiveDocument.Content.Find`で十分だ」という過信だ。
残念ながら、Wordのオブジェクトモデルはそんなに単純ではない。Wordの文書構造は、本文(`wdMainTextStory`)だけではない。ヘッダー、フッター、脚注、文末脚注、テキストボックス……これらはすべて独立した「ストーリー(Story)」としてメモリ上に存在している。本文だけを操作しても、ドキュメントの隅々に潜むノイズは取り除けない。
今日は、Wordのアーキテクチャを深く理解し、メモリ効率を考慮した「真の一括置換」を実装する極限のコードを共有する。
—
1. なぜ、単純なループでは不十分なのか
Wordの`StoryRanges`は、アクセスするたびに新しいRangeオブジェクトを生成する。この挙動を理解せず、不用意に`For Each`を回すと、メモリリークの温床となるだけでなく、予期せぬエラー(特に複雑なテーブルを含む文書)を誘発する。
我々エンジニアが目指すべきは、「オブジェクトのライフサイクルを明確に制御し、再帰的なストーリー探索を行う」ことだ。
2. 「完全なる一括置換」を実現する最適解
以下のコードは、文書内のすべてのストーリーを網羅し、なおかつリンクされたテキストボックスをも再帰的に探索する設計となっている。
Option Explicit
‘ ———————————————————
‘ @brief 文書内の全ストーリーを横断し、置換処理を行う
‘ @param findText 検索対象文字列
‘ @param replaceText 置換後文字列
‘ ———————————————————
Public Sub GlobalReplace(ByVal findText As String, ByVal replaceText As String)
Dim rng As Range
Dim story As Range
‘ 画面更新を停止し、処理速度を最適化(必須)
Application.ScreenUpdating = False
‘ 文書内の全ストーリーを走査
For Each story In ActiveDocument.StoryRanges
Set rng = story
Do
‘ 検索と置換の実行
Call ExecuteReplace(rng, findText, replaceText)
‘ 次のリンクされたテキストボックスがある場合、Rangeを更新
Set rng = rng.NextStoryRange
Loop Until rng Is Nothing
Next story
‘ 終了処理
Application.ScreenUpdating = True
MsgBox “全ストーリーの置換が完了しました。”, vbInformation
End Sub
Private Sub ExecuteReplace(ByRef rng As Range, ByVal fText As String, ByVal rText As String)
With rng.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = fText
.Replacement.Text = rText
.Forward = True
.Wrap = wdFindContinue
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = False
‘ 置換実行(戻り値がBooleanであることを意識せよ)
.Execute Replace:=wdReplaceAll
End With
End Sub
—
3. チーフアーキテクトからの「極限の知見」
① メモリ管理とオブジェクト解放
VBAはガベージコレクションが脆弱だ。`Set rng = Nothing`を明示的に記述する文化は重要だが、それ以上に「ループ内でのオブジェクト生成を最小化する」ことが、大規模文書(数百ページの仕様書等)を扱う際の安定性を決定づける。
② `wdFindContinue`の罠
多くのコードでは`wdFindAsk`や`wdFindStop`が使われるが、自動化においては`wdFindContinue`一択だ。これを指定しない場合、ストーリーの途中で検索が停止し、一部のテキストが置換されないという致命的なバグを生む。
③ Windows APIとの連携(拡張性)
もし、この置換処理の最中に「処理が重すぎてWordがフリーズしているように見える」という課題があるなら、`DoEvents`を不用意に入れるのではなく、`Win32 API`の`Sleep`関数を呼び出してCPU負荷を制御するべきだ。
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
結論
Word VBAにおいて、「動けばいい」という考え方は、システムが肥大化した瞬間に崩壊する。
今回紹介した`StoryRanges`を網羅するロジックは、単なる置換処理以上の意味を持つ。それは、Wordというアプリケーションがどのような階層構造でテキストを保持しているのかを理解する第一歩だ。この基礎さえ押さえていれば、マクロの安定性は格段に向上する。
現場でトラブルが起きたとき、「どこまでが範囲か?」を即座に特定できる技術力こそが、伝説たる所以である。コードをただコピペするだけでなく、なぜWordがこのようなストーリー構造をとっているのか、その背景に想いを馳せてほしい。
