【実務・中級編】Wordの『ストーリー』を理解する:ヘッダー・フッター・本文を横断する一括置換 – Word VBA解析バイブル

スポンサーリンク

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

”’

”’ 指定されたWord文書のすべてのストーリー(NextStoryRange含む)を走査し、文字列を置換する
”’

”’ 対象のDocumentオブジェクト ”’ 検索文字列 ”’ 置換文字列 ”’ 成功時True、失敗時False
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

”’

”’ 単一のRangeオブジェクトに対して Find.Execute を実行する内部ヘルパー関数
”’

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自動化案件が持ち込まれても、置換漏れの障害におびえることは一切なくなります。

ぜひ、あなたのプロジェクトのコードベースにこの堅牢な設計を取り入れ、泥臭い手作業からチームを解放してください。

タイトルとURLをコピーしました