【テクニカル・上級編】【中級者向け】「テキストボックス」内の文字列を検索対象に含めるための再帰処理 – Word VBA解析バイブル

スポンサーリンク

【Word VBA】テキストボックスの闇を討つ:再帰的`Shape`巡回による完全検索・置換アーキテクチャ

Word VBAにおける`Find`オブジェクトは、長年多くの開発者を絶望の淵に追い込んできた。
「ドキュメント全体の検索・置換を実装したはずなのに、なぜか図形内の文字がヒットしない」
この仕様ともバグとも取れる挙動に直面し、泥臭いループ処理で力技を試みたものの、ネストが深くなるにつれてVBAランタイムが沈黙した経験はないだろうか。

結論から言おう。Wordの標準的な`Selection.Find`や`Range.Find`は、キャンバスやグループ化された図形、そしてテキストボックスの内部をデフォルトではインデックス化しない。ドキュメントのルートストーリー(Main Text Story)しか走査していないからだ。

シニアエンジニアたる者、このWordの構造的欠陥を嘆く暇があれば、オブジェクトモデルの階層を正確に把握し、メモリリークを完全に排除した再帰的アルゴリズムで制圧すべきだ。

今回は、あらゆる深さのネストに埋め込まれたテキストボックスを網羅し、極限まで最適化された検索・置換を実現するチーフアーキテクト流の設計思想とコードを公開する。

—

1. Wordオブジェクトモデルの構造的罠

Wordのドキュメントは、平坦なテキストの連続ではない。本文(Story)を基点として、ヘッダー、フッター、脚注、そして描画レイヤー(Shapes / InlineShapes)という複雑なツリー構造を持っている。

特に厄介なのが「グループ化」と「テキストボックスの入れ子」だ。
図形の中にグループ図形があり、その中にさらにテキストボックスが存在する場合、単なる1次元の`Shapes`コレクションのループでは確実に撃墜される。

[Document]
┗ [Stories]
┗ [Main Story]
┗ [Shapes / InlineShapes]
┣ [Simple Textbox] (直撃可能)
┗ [Group Shape]
┗ [Nested Shape]
┗ [Textbox] ──> 【通常検索では絶対に到達できない】

この深層へ到達するためには、再帰関数(Recursive Function)を用いたDFS(深さ優先探索)の構築が不可欠となる。

—

2. メモリ最適化とオブジェクト解放の鉄則

VBAの実行環境(COMコンポーネント)において、オブジェクトの参照を適切に解放しないことは、「時限爆弾を抱えてコードを走らせる行為」に等しい。特に再帰処理において、ループ内で生成される`Shape`や`Range`の参照がスタック上に残留すると、VBA特有のメモリリーク(Heap Fragmentation)を引き起こし、大規模な文書を処理した瞬間に「メモリ不足」でクラッシュする。

チーフアーキテクトとして、以下の鉄則をコードに厳格に実装する。
1. 変数のスコープを最小限にし、ループ内でのインスタンス生成・破棄を制御する。
2. `Set obj = Nothing`による明示的な参照の切断。
3. エラーハンドリング(`On Error Resume Next`の局所化)による堅牢性の確保。

—

3. 実装コード:完全網羅型・再帰的テキストボックス検索エンジン

以下のコードは、指定したキーワードを本文およびすべての階層のテキストボックスから探し出し、一括置換を行うプロダクション品質のVBAモジュールである。

Option Explicit

‘ ==============================================================================
‘ 処理名: 統括テキスト検索・置換エンジン(再帰対応版)
‘ 概要: 本文およびすべてのShape階層(グループ・入れ子テキストボックス)を走査する
‘ ==============================================================================
Public Sub ExecuteDeepSearchAndReplace()
Dim targetDoc As Document
Set targetDoc = ActiveDocument

Dim searchKeyword As String
Dim replaceKeyword As String

searchKeyword = “旧システム名”
replaceKeyword = “新基幹システム”

Dim startTime As Single
startTime = Timer

‘ 1. 通常の本文ストーリーに対する置換
Call ReplaceInStory(targetDoc.Content, searchKeyword, replaceKeyword)

‘ 2. シェイプ階層(テキストボックス等)に対する再帰的置換
Call ProcessShapesRecursive(targetDoc.Shapes, searchKeyword, replaceKeyword)

‘ 3. インラインシェイプ(行内配置の図形)に対する処理
Dim iShape As InlineShape
For Each iShape In targetDoc.InlineShapes
If iShape.Type = wdInlineShapeCanvas Then
‘ キャンバス内のシェイプ群を処理
Call ProcessShapesRecursive(iShape.CanvasShapes, searchKeyword, replaceKeyword)
ElseIf iShape.Type = wdInlineShapeLinkedOLEObject Or iShape.HasTextFrame Then
‘ テキストフレームを持つInlineShapeの処理
If iShape.AlternativeText <> “” Or True Then
On Error Resume Next
Call ReplaceInStory(iShape.Range, searchKeyword, replaceKeyword)
On Error GoTo 0
End If
End If
Next iShape

Debug.Print “処理完了: ” & Format(Timer – startTime, “0.00秒”)
End Sub

‘ ==============================================================================
‘ 内部プロシージャ: Shapeコレクションの再帰的巡回
‘ ==============================================================================
Private Sub ProcessShapesRecursive(ByVal targetShapes As Shapes, ByVal sKey As String, ByVal rKey As String)
Dim shp As Shape
Dim i As Long

‘ コレクションの逆順ループ(オブジェクト削除や動的操作に強い設計)
For i = targetShapes.Count To 1 Step -1
Set shp = targetShapes(i)

On Error Resume Next
‘ A. グループ図形の場合の再帰呼び出し
If shp.Type = msoGroup Then
Call ProcessShapesRecursive(shp.GroupItems, sKey, rKey)

‘ B. テキストフレームを持ち、テキストが存在する可能性のある場合
ElseIf shp.HasTextFrame Then
If shp.TextFrame.HasText Then
Call ReplaceInStory(shp.TextFrame.TextRange, sKey, rKey)
End If
End If

‘ C. SmartArtやその他のコンテナ内部のチェック(必要に応じて拡張)
If shp.HasCanvas Then
Call ProcessShapesRecursive(shp.CanvasShapes, sKey, rKey)
End If
On Error GoTo 0

‘ 厳格なメモリ解放
Set shp = Nothing
Next i

Set targetShapes = Nothing
End Sub

‘ ==============================================================================
‘ 内部プロシージャ: 実際のFind/Replace実行レイヤー
‘ ==============================================================================
Private Sub ReplaceInStory(ByVal targetRange As Range, ByVal sKey As String, ByVal rKey As String)
With targetRange.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = sKey
.Replacement.Text = rKey
.Forward = True
.Wrap = wdFindStop
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchWildcards = False
.MatchSoundsLike = False
.MatchAllWordForms = False

‘ 一括置換の実行
.Execute Replace:=wdReplaceAll
End With
End Sub

—

4. チーフアーキテクトの視点:コードの急所とパフォーマンスチューニング

このコードを実務の巨大なWord資産(数百ページの仕様書や契約書など)に適用する際、いくつかの「プロの作法」が組み込まれていることに気づくだろう。

逆順ループ (`For i = targetShapes.Count To 1 Step -1`) の採用

VBAでコレクションを操作する際、順方向(`1 To Count`)で回すと、要素の動的な変更や一部のWord内部キャッシュの競合によってインデックスがズレ、予期せぬスキップが発生するリスクがある。逆順ループを採用することで、メモリ上のポインタ変動に対する耐性を高めている。

`On Error Resume Next` の限定的運用

図形の種類によっては、`TextFrame`プロパティにアクセスしただけでエラーを吐く特殊なオブジェクト(例:一部のレガシーなOLEオブジェクトやアドイン由来の図形)が存在する。これらでマクロがストップしないよう、エラートラップを局所化しつつ、`On Error GoTo 0`で即座にデフォルト挙動へ復帰させている。

スクリーン更新の抑制(推奨追加設定)

もし数千個のオブジェクトを含む極端に重い文書を処理する場合は、プロシージャの最初と最後で以下の最適化を入れることで、描画処理のオーバーヘッドを完全に排除できる。

Application.ScreenUpdating = False
Application.Calculation = wdCalculationManual
‘ — 処理 —
Application.Calculation = wdCalculationAutomatic
Application.ScreenUpdating = True

—

総括

Word VBAにおけるテキストボックスの検索は、単なるプログラミングのテクニック論ではなく、Officeドキュメントの内部構造(Binary/OpenXML)に対する深い理解が求められる領域だ。

今回提示した再帰的アーキテクチャを導入すれば、どんなに複雑に入り組んだ図形・テキストボックスであっても、漏れなく高速に文字列を制圧できる。
保守性の低い場当たり的なコードを捨て、オブジェクトのライフサイクルと階層構造を支配する洗練されたコードベースへとアップデートしてほしい。それこそが、現場を救うエンジニアの流儀である。

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