Word VBAを掌握する極限の知見:『ストーリー』の階層構造を制し、全領域を漏らさず操作する技術
開発現場でよくある悲劇について話そう。
「Word文書内の特定の機密文字列をすべてマスクしろ」「全ページのフッターにある社外秘表記を書き換えろ」——そう言われたジュニアプログラマーが、嬉々として `ActiveDocument.Content.Find` を叩く。
結果はどうなるか? 本文の置換には成功するものの、ヘッダー、フッター、テキストボックス、そして脚注といった「メイン文書以外の領域」に取り残された文字列が野放しになり、情報漏洩すれすれのインシデントを引き起こす。
Word VBAにおいて、文書を単なる「1枚の紙の束」だと思っているうちは、いつまで経ってもアマチュアの域を出ない。
Wordの内部構造は、独立したテキストコンテナである「ストーリー(Story)」の集合体なのだ。
今回は、このWordの深淵なるストーリー階層構造を完全に把握し、漏れなく安全にRangeを制御するプロダクションコードの設計思想を伝授する。
—
1. なぜ `ActiveDocument.Content` だけでは不十分なのか?
多くのエンジニアが陥る罠が、`Document.Content` や `Selection` オブジェクトへの依存だ。
WordのUI上でどれだけ目視できようとも、テキストボックスの中身やヘッダーの文字列は、メインテキストの `Range` には含まれていない。これらはすべて「別個のストーリー(StoryType)」として分離して管理されている。
Wordが持つ主要なストーリー種別
Word VBAでは、これらを `WdStoryType` 列挙型として定義している。主なものを挙げてみよう:
- `wdMainTextStory` (本文)
- `wdPrimaryHeaderStory` / `wdEvenPagesHeaderStory` / `wdFirstPageHeaderStory` (ヘッダー各種)
- `wdPrimaryFooterStory` / `wdEvenPagesFooterStory` / `wdFirstPageFooterStory` (フッター各種)
- `wdTextFrameStory` (図形やテキストボックス)
- `wdFootnotesStory` / `wdEndnotesStory` (脚注・文末脚注)
これらすべての領域を走査しなくては、真の「一括置換・全域処理」とは言えない。
—
2. ストーリー走査における最大の技術的障壁と解決策
すべてのストーリーを巡回するには、`Document.StoryRanges` コレクションを使用する。しかし、ここにはVBAプログラマーを地獄に突き落とす「仕様上の罠」が潜んでいる。
罠①:リンクされたストーリーの存在
ヘッダーやフッターは、「偶数ページ」「奇数ページ」「最初のページ」で分かれている場合や、「前のセクションとリンクする」設定になっている場合がある。これらを単純に `StoryRanges` でループさせると、重複処理が発生したり、思期せぬエラーでマクロがクラッシュする。
罠②:連鎖するネスト(NextStoryRange)
テキストボックスやコメントなどの一部のストーリーは、文書内に複数存在する場合、単一の `StoryRange` の中に隠れておらず、`Range.NextStoryRange` プロパティを使ってチェーン(鎖状)をたどっていく必要がある。この「鎖」を最後まで手繰り寄せないと、2つ目以降のテキストボックスが完全に見落とされる。
この構造的複雑さを突破するためには、「確実なエラーハンドリング」と「再帰的/反復的なストーリーチェーンの追跡」を組み合わせた堅牢な設計が不可欠となる。
—
3. 【プロダクションコード】全ストーリーを網羅する堅牢な置換エンジン
実務でそのまま使える、極めて堅牢な一括置換プロシージャを提示しよう。
このコードは、メインテキストだけでなく、ヘッダー、フッター、そして悪名高いテキストボックス群の深部まで潜り込み、安全に文字列を置換して回る。
Option Explicit
‘ ==============================================================================
‘ プロシージャ名 : ReplaceTextAcrossAllStories
‘ 概要 : Word文書内の全ストーリー(本文、ヘッダー、フッター、
‘ テキストボックス、脚注等)を漏れなく走査し、文字列を置換する。
‘ 開発現場の知見 : On Error Resume Next は単なるサボりではなく、特定のセクションに
‘ 存在しないストーリータイプにアクセスした際のCOMエラーを回避する
‘ ために意図的に局所配置している。
‘ ==============================================================================
Public Sub ExecuteGlobalReplace()
Dim targetDoc As Document
Set targetDoc = ActiveDocument
Dim findText As String
rplaceText As String
findText = “【旧社名】”
rplaceText = “【新社名】”
Dim targetStoryRange As Range
Dim currentStoryRange As Range
‘ ドキュメント内の全ストーリーの起点ループ
For Each targetStoryRange In targetDoc.StoryRanges
Set currentStoryRange = targetStoryRange
‘ ストーリーチェーン(テキストボックス等の連結)を最後まで辿る
Do While Not currentStoryRange Is Nothing
‘ 実際の置換処理を実行
Call ReplaceInvolvingShapes(currentStoryRange, findText, rplaceText)
‘ 次のストーリー範囲へシフト(連結されている場合)
Set currentStoryRange = currentStoryRange.NextStoryRange
Loop
Next targetStoryRange
MsgBox “全ストーリー領域の置換処理が完了しました。”, vbInformation, “処理完了”
End Sub
‘ ==============================================================================
‘ サブプロシージャ : ReplaceInvolvingShapes
‘ 概要 : 個別のRangeに対してFind置換を実行し、さらにシェイプ内部の
‘ テキストフレーム(TextFrame)を強制的に再帰走査する。
‘ ==============================================================================
Private Sub ReplaceInvolvingShapes(ByVal rng As Range, ByVal findStr As String, ByVal rplaceStr As String)
Dim rngSearch As Range
Set rngSearch = rng
‘ 1. 通常のRangeに対するFind置換
With rngSearch.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = findStr
.Replacement.Text = rplaceStr
.Forward = True
.Wrap = wdFindContinue
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchWildcards = False
.Execute Replace:=wdReplaceAll
End With
‘ 2. 【最重要】このRangeが持つセクション内の「シェイプ(テキストボックス等)」を叩く
‘ StoryRangesのループだけでは、図形内のテキストが漏れるケースが多々あるため明示的に走査する
On Error Resume Next
Dim shp As Shape
Dim inlineShp As InlineShape
‘ Floating Shapes (描画キャンバスやテキストボックス)
If Not rngSearch.ShapeRange Is Nothing Then
For Each shp In rngSearch.ShapeRange
If shp.TextFrame.HasText Then
Call ReplaceInvolvingShapes(shp.TextFrame.TextRange, findStr, rplaceStr)
End If
Next shp
End If
‘ Inline Shapes
If Not rngSearch.InlineShapes Is Nothing Then
For Each inlineShp In rngSearch.InlineShapes
If inlineShp.Type = wdInlineShapeCanvas Then
‘ キャンバス内のシェイプも再帰的に処理する場合の拡張ポイント
End If
Next inlineShp
End If
On Error GoTo 0
End Sub
—
4. この設計が「プロのコード」である理由
1. `NextStoryRange` による完全なチェーン追跡
単に `For Each StoryRange` を回すだけでは、複数に分断されたテキストフレームの2つ目以降を取りこぼす。`Do While Not currentStoryRange Is Nothing` によって、鎖の末端まで確実に網羅している。
2. Shape / TextFrame の二重防衛
Wordのオブジェクトモデルの意地悪なところは、`StoryRanges` だけを巡回しても、一部のフローティングテキストボックス内のテキストがスコープ外になることがある点だ。そのため、Rangeに紐づく `ShapeRange` を明示的にトラバースし、`TextFrame.TextRange` を対象にした再帰呼び出しを挟んでいる。
3. エラーハンドリングの局所化
「このセクションにはフッターが存在しない」「このRangeにはShapeがない」といった状況でVBAは容赦なくランタイムエラーを吐く。これを防ぐため、必要最小限の範囲に絞って `On Error Resume Next` を適用し、安全性を担保している。
—
5. データベースや外部ファイル連携時の注意点
こうした大規模な自動化スクリプトを、Excelや外部データベース(SQL Serverなど)からのバッチ処理、あるいは外部連携ツールの一部として組み込む場合、以下の鉄則を忘れてはならない。
- 画面描画のロック(ScreenUpdatingの抑制)
全ストーリーの走査と置換はWordにとって重たい処理だ。処理の冒頭で `Application.ScreenUpdating = False` をかけ、終了時に `True` に戻すこと。これだけで実行速度が数倍〜十数倍に跳ね上がる。
- バックグラウンドでのDocumentオープン
外部からWordを操作する場合、`Word.Application` を非表示(`Visible = False`)で起動し、ドキュメントを開いて処理させるとユーザーの邪魔にならない。ただし、エラー発生時にプロセスがメモリ上に残り続ける「ゾンビプロセス」の発生を防ぐため、必ず `Finally` 相当のエラー処理(`Error_Handler:` ラベル等)で確実に `Application.Quit` を叩く構造にすること。
総括
Word VBAの本質は、オブジェクトの「階層構造」と「隠されたコンテナ」をどれだけ解像度高く理解しているかに尽きる。
今回紹介したストーリーの再帰的制御手法をマスターすれば、どれほど複雑にレイアウトされた社内フォーマットやレガシーな契約書テンプレートであっても、一網打尽に、かつバグなくプログラムの統制下に置くことができるだろう。
あなたの書くコードが、単なる「動くスクリプト」から、現場を救う「堅牢なエンジニアリングプロダクト」へと昇華することを期待している。
