Wordの「ストーリー」を完全掌握せよ:ヘッダー・フッター・注釈を漏らさず駆け抜ける極限の一括置換アーキテクチャ
Word VBAを使った業務自動化ツールを開発する際、多くのエンジニアが最初にぶつかり、そして自覚のないまま放置しがちな「致命的なバグ」が存在します。
それが、「本文の置換は成功したのに、ヘッダーやフッター、テキストボックス、脚注の文字が置き換わっていない」という問題です。
「`ActiveDocument.Content.Find.Execute Replace:=wdReplaceAll` を書いたから全置換は完了した」と思い込んで納品し、本番環境で顧客のヘッダーに残った旧社名や旧プロジェクトコードを見て青ざめる……。このような障害事故を、私はこれまで何度も目にしてきました。
Wordの内部構造は、Excelのような単純な二次元セル配列ではありません。Word文書は「Story(ストーリー)」と呼ばれる独立した複数のテキスト領域(キャンバス)の集合体で構成されています。
この記事では、Wordのオブジェクトモデルの神髄である「ストーリー」の仕組みを解剖し、1文字の漏れすら許さない、本番環境で耐えうる堅牢な一括置換モジュールの設計と実装を伝授します。
—
1. なぜ「本文の全置換」では不十分なのか?ストーリーの正体
Word文書は、目に見える1枚の紙のように見えて、実は裏で階層化された複数の「ストーリー領域」を持っています。
VBAの `Document.Content` は、あくまで「本文領域(wdMainTextStory)」のRangeオブジェクトを返すに過ぎません。ヘッダーやフッター、脚注、テキストボックスは、まったく別のストーリー領域に隔離されて存在しています。
代表的なストーリータイプ(WdStoryType)
Word内部には、主に以下のような `WdStoryType` 定義が存在します。
| 定数 | 説明 |
| :— | :— |
| `wdMainTextStory` | 文書の本文領域 |
| `wdPrimaryHeaderStory` | 主ヘッダー(奇数ページまたは基本ヘッダー) |
| `wdPrimaryFooterStory` | 主フッター |
| `wdFirstPageHeaderStory` | 1ページ目のみ異なる場合のヘッダー |
| `wdFirstPageFooterStory` | 1ページ目のみ異なる場合のフッター |
| `wdEvenPagesHeaderStory` | 偶数ページヘッダー |
| `wdEvenPagesFooterStory` | 偶数ページフッター |
| `wdFootnotesStory` | 脚注(Footnotes) |
| `wdEndnotesStory` | 文末脚注(Endnotes) |
| `wdCommentsStory` | コメント |
| `wdTextFrameStory` | テキストボックス(図形内のテキスト) |
これらの領域は、`Document.StoryRanges` コレクションとして管理されています。全領域を走査(トラバース)するには、この `StoryRanges` を巡回しなければなりません。
—
2. 最も根深い罠:`NextStoryRange` を手繰り寄せなければ「漏れ」が生じる
ここからが、一般的なVBAリファレンスには書かれていない、真のノウハウです。
「`For Each rng In Document.StoryRanges` でループを回せば解決するのではないか?」と考えた方は甘いと言わざるを得ません。それだけでは不十分です。
Wordの仕様上、`Document.StoryRanges` コレクションを `For Each` で回したときに取得できるのは、各ストーリータイプの「第1セクションの最初の要素」だけです。
例えば、文書が複数のセクションに分かれており、ヘッダーの「前と同じヘッダー/フッター(LinkToPrevious)」が解除されている場合、2つ目以降のセクションのヘッダーは `For Each` の要素としては出現しません。それらは、最初の要素の `NextStoryRange` プロパティ を手繰り寄せる(単方向リンク式ポインタを辿る)ことでしかアクセスできないのです。
ストーリー走査の正解アルゴリズム
[ StoryRanges コレクション ] (For Each で巡回)
│
├── wdMainTextStory ──(NextStoryRange)──> [ なし ]
│
└── wdPrimaryHeaderStory ──(NextStoryRange)──> [ セクション2の独立ヘッダー ] ──(NextStoryRange)──> [ セクション3の独立ヘッダー ]
つまり、ストーリーを全網羅するためには、「StoryRangesのFor Eachループ」の内部で「NextStoryRangeが存在する限りのDo Whileループ」を回す二重ループ構造が絶対に不可欠となります。
—
3. アンチパターン vs 堅牢な設計
設計の差がパフォーマンスと安定性にどう影響するかを整理しましょう。
【アンチパターン】`Selection` オブジェクトと単層ループの愚
- `Selection.Find` を使用して画面をスクロールさせながら置換する。
- 弊害: 画面描画のオーバーヘッドで極めて遅い。また、フォーカスが外れると失敗し、ヘッダー/フッターにアクセスできない。
- `For Each rng In Doc.StoryRanges` のみで置換を行う。
- 弊害: 複数セクションでヘッダーのリンクが切れている場合、セクション2以降のヘッダー置換が完全に漏れる。
【堅牢な設計】`Range` オブジェクト直接操作 + ポインタトラバース
- 画面非表示(非同期的なメモリ内操作)で処理を実行。
- `StoryRanges` と `NextStoryRange` を完全トラバース。
- 画面描画停止(`ScreenUpdating = False`)とイベント停止を徹底し、ミリ秒単位で処理を完結させる。
- エラーハンドリングを組み込み、万が一の例外発生時にも画面更新フラグを確実に復旧させる。
—
4. プロダクション環境で使える完全版VBAコード
以下に、そのまま実務のバッチ処理や業務自動化ツールに組み込めるプロダクションコードを示します。
標準モジュール(例: `M_TextReplacer`)に配置して使用してください。
Option Explicit
‘ ==============================================================================
‘ モジュール名: M_TextReplacer
‘ 概要 : Word文書内の全ストーリー(本文・ヘッダー・フッター・注釈・テキストボックス等)
‘ を網羅的に走査し、完全な一括置換を行う堅牢なエンジンモジュール。
‘ ==============================================================================
”’
”’
Public Sub Execute_ReplaceAllStories_Sample()
Dim targetDoc As Document
Set targetDoc = ActiveDocument
Dim success As Boolean
‘ 実行例: 旧社名を新社名に置換
success = ReplaceTextInAllStories(targetDoc, “株式会社旧社名”, “株式会社新社名”)
If success Then
MsgBox “文書全体の置換処理が正常に完了しました。”, vbInformation, “処理完了”
Else
MsgBox “置換処理中にエラーが発生しました。ログを確認してください。”, vbCritical, “処理失敗”
End If
End Sub
”’
”’
”’ 対象のDocumentオブジェクト
”’ 検索文字列
”’ 置換文字列
”’
Public Function ReplaceTextInAllStories( _
ByVal pDoc As Document, _
ByVal pFindText As String, _
ByVal pReplaceText As String) As Boolean
On Error GoTo ErrorHandler
‘ パラメータのバリデーション
If pDoc Is Nothing Then Err.Raise 5, , “DocumentオブジェクトがNullです。”
If Len(pFindText) = 0 Then Err.Raise 5, , “検索文字列が空です。”
‘ パフォーマンス最適化のため描画・制御を一時停止
Application.ScreenUpdating = False
Dim currentStoryRange As Range
‘ 1. Document.StoryRanges コレクションを外部ループで走査
For Each currentStoryRange In pDoc.StoryRanges
‘ 2. NextStoryRange ポインタが存在する限り内部ループで深さ優先走査
Dim targetRange As Range
Set targetRange = currentStoryRange
Do While Not targetRange Is Nothing
‘ Rangeオブジェクトに対して直接Findを実行(Selectionは絶対に使用しない)
Call ExecuteReplaceOnRange(targetRange, pFindText, pReplaceText)
‘ 次のストーリーセグメント(例: 次のセクションの独立ヘッダー)へポインタを移動
On Error Resume Next ‘ 破損したストーリーポインタによる例外を安全に回避
Set targetRange = targetRange.NextStoryRange
On Error GoTo ErrorHandler
Loop
Next currentStoryRange
ReplaceTextInAllStories = True
CleanUp:
‘ 後処理: 描画機能を確実に再開
Application.ScreenUpdating = True
Exit Function
ErrorHandler:
‘ 業務ツールとしてのログ記録処理(実務ではログファイル出力等を推奨)
Debug.Print “[ERROR] ReplaceTextInAllStories: ” & Err.Number & ” – ” & Err.Description
ReplaceTextInAllStories = False
Resume CleanUp
End Function
”’
”’
Private Sub ExecuteReplaceOnRange( _
ByVal pRange As Range, _
ByVal pFindText As String, _
ByVal pReplaceText As String)
With pRange.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = pFindText
.Replacement.Text = pReplaceText
.Forward = True
.Wrap = wdFindStop ‘ Rangeの境界を超えて無限ループするのを防ぐ
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = False
‘ 該当領域の全一致箇所を一括置換
.Execute Replace:=wdReplaceAll
End With
End Sub
—
5. 実務で直面するエッジケースとアーキテクチャ上の注意点
このモジュールをさらに大規模な自動化システム(数千枚のWordファイルを一括処理するバッチシステムや、データベース連携ツール)に組み込む場合、以下の3点に注意してください。
① 大量ファイル処理時のメモリリークとオブジェクト解放
VB.NETやC#などの外部プロセス、あるいはVBAからのフォルダ一括処理ループ内でWordを操作する場合、`Document.Close SaveChanges:=wdSaveChanges` を呼び出した後、オブジェクト変数に `Set pDoc = Nothing` を明示的に行う必要があります。
特に `StoryRanges` の巡回処理は、内部的にWordのアンマネージオブジェクトを多数作成するため、明示的な解放を行わないと、「処理が100件を超えたあたりでWordが応答なしになる」という現象を引き起こします。
② フィールド構造(Field)の破壊に注意
検索対象の文字列が、Wordの「フィールド」(例: `FILENAME`, `DATE`, `NUMPAGES` など)の内部テキストや、自動生成された目次(TOC)と重なっている場合、`Find.Execute Replace:=wdReplaceAll` を実行するとフィールド構造そのものが破棄され、プレーンテキストに置き換わってしまうことがあります。
これを防ぐためには、事前に `pRange.Fields.Count` を確認するか、置換後に必要に応じて `pDoc.Fields.Update` を呼び出してドキュメントの整合性を保つ設計にしてください。
③ ネストされた図形(Shape)内のテキストフレーム
Word 2010以降の高度な描画オブジェクト(`Shape` / `InlineShape`)の内部にあるテキストは、稀に `StoryRanges` の走査だけでは捕捉しきれないケース(グループ化された図形内部のテキストなど)が存在します。
完全性を極限まで高めるのであれば、上記のストーリー走査に加え、以下のように `Doc.Shapes` コレクションを走査し、`Shape.GroupItems` を再帰的にチェックするサブモジュールを併用するのが最高峰のアーキテクチャです。
‘ (概念コード)Shape内のテキストフレーム走査
Dim shp As Shape
For Each shp In pDoc.Shapes
If shp.TextFrame.HasText Then
Call ExecuteReplaceOnRange(shp.TextFrame.TextRange, pFindText, pReplaceText)
End If
Next shp
—
まとめ:正しいオブジェクトモデルの理解が、無欠のコードを生む
Word VBA開発において、「動いたからよし」とする素人コードと、プロフェッショナルが書くコードの差は、「オブジェクトの隠されたライフサイクルと構造を理解しているか」に集約されます。
- `Document.Content` だけの置換は失格。
- `StoryRanges` を回すだけでも不十分。
- `NextStoryRange` を追いかけて初めて、Word文書を「完全に掌握した」と言える。
この二重ループ構造によるストーリー走査アーキテクチャを自身のライブラリ(共通モジュール)として保有しておけば、今後どのようなWord自動化案件が持ち込まれても、置換漏れの障害におびえることは一切なくなります。
ぜひ、あなたのプロジェクトのコードベースにこの堅牢な設計を取り入れ、泥臭い手作業からチームを解放してください。
