【実務・中級編】【中級者】複数のプレゼンテーションを一括オープンし、指定した文字列を一括置換した上で、元のフォーマットを保持したまま一斉に上書き保存する検索置換ツール – PowerPoint VBA解析バイブル

スポンサーリンク

複数のPowerPointファイルを一括走査する「極限の検索置換ツール」の極意

開発プロジェクトを率いるリーダー、あるいは社内ツールの技術選定を行うアーキテクトの諸氏。

「フォルダ内にある数百のPowerPointファイルから、旧社名や古いブランド名を一括で置換してほしい」
このようなオーダーを受けた際、あなたならどう設計するだろうか。

ネットに転がっている「とりあえず動く」レベルのVBAコードをコピペして使えば、十中八九、以下の惨劇に見舞われることになる。

  • フォーマット崩れの悲劇: `TextRange.Text = Replace(…)` を実行した瞬間、テキスト部分に施されていた「部分的なフォント色」「太字」「下線」などのリッチテキスト情報が完全に消失し、一様で無機質なデフォルト書式に初期化される。
  • 探索漏れの致命傷: グループ化された図形、表(Table)、スマートアート(SmartArt)、さらにはスライドマスターやノートページに埋め込まれたテキストが一切置換されずに取り残される。
  • 処理停止の悪夢: 大量のファイルを処理する途中で、1ファイルだけ破損していたり、パスワード保護されていたり、読み取り専用になっていたりしただけでマクロがクラッシュし、どこまで処理が進んだか分からなくなる。

本稿では、これらの課題をすべて解決し、プロダクション環境(実業務の最前線)で耐えうる「極めて堅牢で、フォーマットを維持し、走査漏れのない」PowerPoint一括検索置換ツールの設計思想と実装コードを伝授する。

—

1. 堅牢な一括処理システムを構築するための「3つの設計哲学」

単なるスクリプトから「エンタープライズ対応のツール」へ昇華させるには、以下の3つの設計哲学が必要不可欠である。

① テキストコンテナの完全網羅(再帰的探索)

PowerPointのスライド構造は、ExcelやWordに比べてオブジェクトモデルが極めて複雑である。スライド上のテキストは、単一の `Shape` だけでなく、何重にもネストされた構造の中に隠蔽されている。

  • Group Shape(グループ化された図形): 再帰的に内部の `GroupItems` を掘り下げる必要がある。
  • Table(表): 各行・各列の `Cell` を走査し、その中の `TextFrame` を抽出する必要がある。
  • SmartArt: `SmartArt.AllNodes` を走査し、各ノードのテキストを処理する必要がある。
  • スライドマスター / レイアウト: スライドそのものだけでなく、背景テンプレートに埋め込まれた共通テキストもターゲットにしなければ片手落ちとなる。
  • ノート(NotesPage): プレゼンター用のメモ領域も、コンプライアンスやブランド管理の観点から置換対象に含めるべきである。

② フォーマットの完全維持(TextRange.Replaceの正しいハック)

最もやってはならないのが、VBAの組み込み関数 `Replace(TextRange.Text, …)` を使用することだ。これはString型(文字列のみ)を返すため、オブジェクト内の書式情報を破壊する。

フォーマットを維持するためには、PowerPointのオブジェクト自体が持つ `TextRange.Replace` メソッドを使用しなければならない。しかし、このメソッドは「1回の実行で最初に見つかった1箇所しか置換しない」という厄介な仕様を持つ。
かつ、置換後の文字列が検索文字列を含んでいる場合(例:「API」を「WebAPI」に置換する等)、単純なループを回すと無限ループに陥る。

これに対処するため、「置換が成功した位置の『次の文字位置』から再検索をかける」というインデックス制御の実装が必須となる。

③ メモリリークとファイルロックの徹底排除

数百のファイルを開閉する場合、描画処理のオーバーヘッドは無視できない。
`Presentations.Open` の引数 `WithWindow` を `msoFalse` に設定し、バックグラウンド(非表示)で処理を行うことで、速度を劇的に向上させるとともに、ユーザー画面のちらつきを完全に排除する。

また、処理中に予期せぬエラーが発生した場合でも、開いたファイルを確実に閉じ、リソースを解放する `Try – Catch – Finally` 相当の構造化エラーハンドリングを構築する。

—

2. 完全実用版プロダクションコード(PowerPoint VBA)

以下のコードは、前述の設計哲学を具現化したものである。
ExcelのVBAエディタ、あるいはPowerPointの標準モジュールにそのまま貼り付けて使用できる。

※ 参照設定として `Microsoft Scripting Runtime` (FSO用) を有効にしておくと便利だが、今回はポータビリティを最優先し、後期バインディング(CreateObject)で実装している。

Option Explicit

‘ ==============================================================================
‘ ■ PowerPoint一括検索置換ツール(プロダクション・グレード)
‘ ==============================================================================

Public Sub BatchReplaceInPresentations()
Dim fso As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ ————————————————————————–
‘ 1. 設定値の定義(実務に合わせてここを変更、またはUIから取得する)
‘ ————————————————————————–
Dim targetFolderPath As String
targetFolderPath = “C:\Temp\PPT_Files” ‘ 処理対象フォルダ(末尾のスラッシュは不要)

Dim findText As String
Dim replaceText As String
findText = “旧社名株式会社”
replaceText = “新社名株式会社”

‘ ————————————————————————–
‘ 2. バリデーション
‘ ————————————————————————–
If Not fso.FolderExists(targetFolderPath) Then
MsgBox “指定されたフォルダが存在しません: ” & targetFolderPath, vbCritical, “エラー”
Exit Sub
End If

If findText = “” Then
MsgBox “検索文字列が空です。”, vbExclamation, “警告”
Exit Sub
End If

‘ 描画と警告の制御(ExcelからPowerPointを制御する場合はAppオブジェクトに対して行う)
‘ PowerPoint自身で実行する場合は、以下のように警告を抑制する
Application.DisplayAlerts = ppAlertsNone

Dim targetFolder As Object
Set targetFolder = fso.GetFolder(targetFolderPath)

Dim fileItem As Object
Dim processedCount As Long
Dim successCount As Long
Dim errorCount As Long

Debug.Print “=== 一括置換処理を開始します ===”
Debug.Print “対象フォルダ: ” & targetFolderPath
Debug.Print “検索: [” & findText & “] -> 置換: [” & replaceText & “]”

‘ ————————————————————————–
‘ 3. ファイル走査ループ
‘ ————————————————————————–
For Each fileItem In targetFolder.Files
Dim ext As String
ext = LCase(fso.GetExtensionName(fileItem.Name))

‘ 処理対象は PowerPointプレゼンテーション(マクロ有効、テンプレート含む)
If ext = “pptx” Or ext = “ppt” Or ext = “pptm” Then
processedCount = processedCount + 1

Dim isSuccess As Boolean
isSuccess = ProcessSinglePresentation(fileItem.Path, findText, replaceText)

If isSuccess Then
successCount = successCount + 1
Debug.Print “【成功】: ” & fileItem.Name
Else
errorCount = errorCount + 1
Debug.Print “【失敗】: ” & fileItem.Name
End If
End If
Next fileItem

‘ ————————————————————————–
‘ 4. 処理結果の通知
‘ ————————————————————————–
Application.DisplayAlerts = ppAlertsAll

Dim msg As String
msg = “処理が完了しました。” & vbCrLf & _
“検出ファイル数: ” & processedCount & vbCrLf & _
“置換成功: ” & successCount & ” 件” & vbCrLf & _
“エラー発生: ” & errorCount & ” 件”

MsgBox msg, IIf(errorCount = 0, vbInformation, vbExclamation), “処理完了”
End Sub

‘ ==============================================================================
‘ ■ 単一プレゼンテーションの処理(エラーハンドリングとファイルクローズを保証)
‘ ==============================================================================
Private Function ProcessSinglePresentation(ByVal filePath As String, ByVal findText As String, ByVal replaceText As String) As Boolean
ProcessSinglePresentation = False

Dim activePres As Presentation
On Error GoTo ErrBlock

‘ WithWindow:=msoFalse により、非表示(バックグラウンド)で高速処理
‘ ReadOnly:=msoFalse で上書き保存可能な状態で開く
Set activePres = Presentations.Open(FileName:=filePath, _
ReadOnly:=msoFalse, _
Untitled:=msoFalse, _
WithWindow:=msoFalse)

‘ プレゼンテーションが読み取り専用で開かれてしまった場合のハンドリング
If activePres.ReadOnly = msoTrue Then
Debug.Print ” -> 警告: ファイルが読み取り専用です。書き込みができません。”
GoTo CleanUp
End If

Dim totalReplacements As Long
totalReplacements = 0

‘ — A. 通常スライドの走査 —
Dim sld As Slide
For Each sld In activePres.Slides
totalReplacements = totalReplacements + ProcessShapesCollection(sld.Shapes, findText, replaceText)

‘ ノートページの走査
If sld.HasNotesPage Then
totalReplacements = totalReplacements + ProcessShapesCollection(sld.NotesPage.Shapes, findText, replaceText)
End If
Next sld

‘ — B. スライドマスターおよびレイアウトの走査 —
Dim master As Design
For Each master In activePres.Designs
totalReplacements = totalReplacements + ProcessShapesCollection(master.SlideMaster.Shapes, findText, replaceText)

Dim layout As CustomLayout
For Each layout In master.SlideMaster.CustomLayouts
totalReplacements = totalReplacements + ProcessShapesCollection(layout.Shapes, findText, replaceText)
Next layout
Next master

‘ 変更があった場合のみ保存
If totalReplacements > 0 Then
activePres.Save
Debug.Print ” -> 置換実行数: ” & totalReplacements & ” 箇所 (保存完了)”
Else
Debug.Print ” -> 置換対象なし”
End If

ProcessSinglePresentation = True

CleanUp:
On Error Resume Next
If Not activePres Is Nothing Then
activePres.Close
Set activePres = Nothing
End If
Exit Function

ErrBlock:
Debug.Print ” -> エラー発生: ” & Err.Description & ” (Code: ” & Err.Number & “)”
Resume CleanUp
End Function

‘ ==============================================================================
‘ ■ シェイプコレクションの再帰走査(グループ、テーブル、スマートアート対応)
‘ ==============================================================================
Private Function ProcessShapesCollection(ByRef shps As Shapes, ByVal findText As String, ByVal replaceText As String) As Long
Dim count As Long
count = 0

If shps Is Nothing Then Exit Function
If shps.count = 0 Then Exit Function

Dim shp As Shape
For Each shp In shps
‘ — 1. グループ化されたシェイプ(再帰処理) —
If shp.Type = msoGroup Then
count = count + ProcessShapesCollection(shp.GroupItems, findText, replaceText)

‘ — 2. テーブル(表)の走査 —
ElseIf shp.HasTable = msoTrue Then
Dim tbl As Table
Set tbl = shp.Table
Dim r As Long, c As Long
For r = 1 To tbl.Rows.count
For c = 1 To tbl.Columns.count
Dim cellTxtRange As TextRange
Set cellTxtRange = tbl.Cell(r, c).Shape.TextFrame.TextRange
count = count + SafeReplaceText(cellTxtRange, findText, replaceText)
Next c
Next r

‘ — 3. スマートアートの走査 —
ElseIf shp.HasSmartArt = msoTrue Then
Dim node As SmartArtNode
For Each node In shp.SmartArt.AllNodes
If node.TextFrame2.HasText Then
‘ TextFrame2からTextRangeを取得(SmartArtはTextFrame2構造を持つ)
‘ ※VBAの互換性維持のため、TextFrame2.TextRangeを仲介
Dim saTxtRange As TextRange
Set saTxtRange = node.TextFrame2.TextRange
count = count + SafeReplaceText(saTxtRange, findText, replaceText)
End If
Next node

‘ — 4. 通常のテキストボックス / シェイプ —
ElseIf shp.HasTextFrame = msoTrue Then
If shp.TextFrame.HasText = msoTrue Then
count = count + SafeReplaceText(shp.TextFrame.TextRange, findText, replaceText)
End If
End If
Next shp

ProcessShapesCollection = count
End Function

‘ ==============================================================================
‘ ■ 安全な文字列置換(フォーマット維持 & 無限ループ防止)
‘ ==============================================================================
Private Function SafeReplaceText(ByRef rng As TextRange, ByVal findText As String, ByVal replaceText As String) As Long
Dim localCount As Long
localCount = 0

If rng Is Nothing Then Exit Function
If rng.Length = 0 Then Exit Function

Dim startPos As Long
startPos = 1

Dim foundRange As TextRange

Do
‘ MatchCase:=msoTrue (大文字小文字を区別する)
‘ WholeWords:=msoFalse (完全一致ではなく、部分一致で検索する)
Set foundRange = rng.Replace(FindWhat:=findText, _
ReplaceWhat:=replaceText, _
Start:=startPos, _
MatchCase:=msoTrue, _
WholeWords:=msoFalse)

If foundRange Is Nothing Then Exit Do

localCount = localCount + 1

‘ 無限ループ防止策:
‘ 次の検索開始位置を「今回置換された文字列の直後」にオフセットする。
‘ これにより、”A” から “AA” への置換のように、検索文字列が置換後文字列に含まれていてもループが固まらない。
startPos = foundRange.Start + foundRange.Length

‘ 念のため、インデックスが全体の文字数を超えた場合はループを抜ける
If startPos > rng.Length Then Exit Do
Loop

SafeReplaceText = localCount
End Function

—

3. コードのディープダイブ&技術解説

このコードがなぜ「プロ仕様」なのか、核となる技術的アプローチを解説する。

1. `TextRange.Replace` の無限ループを回避するポインタ制御

多くの開発者が陥るのが、以下のような単純なループである。

‘ !!これは無限ループを引き起こす危険なコード!!
Do
Set foundRange = rng.Replace(findText, replaceText)
Loop While Not foundRange Is Nothing

もし、`findText` が `”設計”` で、`replaceText` が `”設計思想”` だった場合、置換するたびに毎回先頭の `”設計”` がヒットし続け、VBAはフリーズする。

プロダクションコードで採用した手法は、`Start` 引数の動的制御である。

startPos = foundRange.Start + foundRange.Length

`foundRange` には「置換された後の新しい文字列領域」が返される。その「開始位置(`Start`)」に「置換後の長さ(`Length`)」を加算した値を、次回の検索開始位置(`Start:=startPos`)に指定する。これにより、置換済み領域を完全にスキップして後方へと走査を進めることができる。

2. 徹底された「非表示(バックグラウンド)処理」

`Presentations.Open` の引数に `WithWindow:=msoFalse` を指定している。

Set activePres = Presentations.Open(FileName:=filePath, ReadOnly:=msoFalse, Untitled:=msoFalse, WithWindow:=msoFalse)

この一行がもたらす恩恵は大きい。PowerPointのGUI(ウィンドウ)を立ち上げずにメモリ上だけでファイルを展開するため、処理速度が約3〜5倍に向上し、画面のチラつきによるOS全体のパフォーマンス低下を防ぐ。

3. 多重ネスト(Group, Table, SmartArt)の再帰的走査

スライド内の構造は、入れ子(ネスト)構造になり得る。特に「グループ化された図形の中に、さらにグループ化された図形がある」といったケースに対応するため、`ProcessShapesCollection` 関数は、自身を再帰的に呼び出す(Recursive Call)設計になっている。

If shp.Type = msoGroup Then
count = count + ProcessShapesCollection(shp.GroupItems, findText, replaceText)

この設計により、ネストの深さに関わらず、すべての階層のテキストボックスを漏れなく走査可能にしている。

—

4. 実務運用における注意点とさらなる拡張

① 実行前の「バックアップ強制」は絶対ルール

このマクロはファイルを直接「上書き保存(`Save`)」する。
VBAによる変更は 「元に戻す(Ctrl + Z)」が効かない。万が一、検索・置換の条件設定を誤った場合(例: 空白に置換してしまった、など)、元ファイルを破壊することになる。
ツールを運用する際は、事前にフォルダごとバックアップを生成するロジックを組み込むか、ユーザーにバックアップを強く促すプロンプトを挟むのがプロの仕事である。

② Excelから制御する場合の「早期バインディング vs 後期バインディング」

上記のコードはPowerPoint内部のVBA(`pptm`)で走らせることを想定しているが、実務では「Excelにファイルリストを書き出し、ExcelからPowerPointをコントロールしたい」という要望が多い。

Excel VBAに移植する場合は、以下の点に注意されたい。

  • 参照設定(早期バインディング): ExcelのVBA画面で `Tools -> References` から `Microsoft PowerPoint XX.O Object Library` にチェックを入れる。
  • 定数の解決: `ppAlertsNone` などのPowerPoint固有の定数は、Excelからは見えない。参照設定を行わない(後期バインディング)場合は、これらを実数(`ppAlertsNone` = `1`)に置き換える必要がある。

—

5. まとめ

VBAは手軽に書けるがゆえに、スパゲティコードや考慮不足のツールが量産されがちな言語である。しかし、オブジェクトモデルを深く理解し、適切なエラーハンドリングと再帰設計を行えば、C#やPythonで作成したツールに匹敵する、極めて堅牢なエンタープライズツールを構築できる。

今回の検索置換コードをベースに、ログ出力機能(どのファイルの、どのスライドで、何箇所置換したかをExcelシートに吐き出す等)を拡張すれば、そのまま全社展開できるレベルの社内インフラツールが完成する。

ぜひ、この「妥協のない設計思想」を、貴方の開発現場でも役立ててほしい。

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