【実務・中級編】【中級者】スピーカーノートに特定の制御タグ(例:[Skip])があるページを自動除外して、公開用のクリーンなプレゼンテーションを新しく作成・保存するフィルタリングマクロ – PowerPoint VBA解析バイブル

スポンサーリンク
スポンサーリンク

破壊的変更を避け、純粋な「抽出」を設計せよ:スライド・フィルタリングのアーキテクチャ

業務自動化の現場において、最もリスクが高いのは「既存のデータを書き換える」操作だ。

特に、社内向けの機密情報(スピーカーノートに書かれた非公開コメントや、検討中のメタデータ)が含まれたプレゼンテーションから、外部公開用の「クリーンなスライド」を切り出す作業。これを「元のファイルから不要なスライドを消していく」というアプローチで実装しようとする者がいる。

それは、エンジニアとして致命的な設計ミスだ。

スライドを削除していくループ処理は、インデックスのズレという古典的なバグを誘発するだけでなく、万が一の実行エラー時に「元データが破損した状態」で残るリスクを孕んでいる。プロフェッショナルが構築すべきは、元のソースを一切汚染せず、条件に合致するものだけを「新しい器」へと転送する「抽出型(Extraction-based)アーキテクチャ」である。

本稿では、スピーカーノート内の制御タグ(例:`[Skip]`)を検知し、安全かつ高速にクリーンなプレゼンテーションを生成する、プロダクションレベルのVBA実装を伝授する。

1. 設計思想:Mutation(変異)ではなく Extraction(抽出)

中級者が陥る罠は、`Slides(i).Delete` を使った破壊的処理だ。これを避けるべき理由は3つある。

1. インデックスの不整合: ループ中にオブジェクトを削除すると、残されたオブジェクトのインデックスが動的に変化し、スライドの読み飛ばしが発生する。
2. データの不可逆性: 処理中にエラーが発生した場合、どのスライドが削除され、どのスライドが残ったのかの整合性を保つことが困難になる。
3. メタデータの残留: スライドを消しても、スライドマスターやノートの残骸がファイル内に潜伏し続けるリスクがある。

我々が目指すべきは、「新しいプレゼンテーション・インスタンスを生成し、合格したスライドのみをコピーして集める」という、不変性(Immutability)を重視した設計だ。

2. 実装:高効率・堅牢なフィルタリングエンジン

以下のコードは、単なるコピペ用スクリプトではない。エラーハンドリング、パフォーマンス(クリップボード操作の最小化)、および保守性を考慮した、実務投入可能な設計となっている。

Option Explicit

''' <summary>
''' スピーカーノートに特定のタグがあるスライドを除外した、
''' クリーンなプレゼンテーションを新規作成する。
''' </summary>
Public Sub CreateCleanPresentation()
    ' --- 設定領域 ---
    Const SKIP_TAG As String = "[Skip]"
    Dim sourcePres As Presentation
    Dim targetPres As Presentation
    Dim slideIdx As Long
    Dim validIndices() As Long
    Dim validCount As Long
    Dim savePath As String
    
    ' エラーハンドリングの開始
    On Error GoTo ErrorHandler
    
    ' 1. ソースプレゼンテーションの取得
    Set sourcePres = ActivePresentation
    If sourcePres Is Nothing Then
        MsgBox "ソースとなるプレゼンテーションが開かれていません。", vbCritical
        Exit Sub
    End If
    
    ' 2. フィルタリングロジック(インデックスの収集)
    ' スライドを直接コピーするのではなく、まず「残すべきインデックス」をメモリ上で特定する。
    ' これにより、クリップボードへのアクセス回数を最小限に抑える。
    validCount = 0
    For slideIdx = 1 To sourcePres.Slides.Count
        If Not IsSlideSkipped(sourcePres.Slides(slideIdx), SKIP_TAG) Then
            validCount = validCount + 1
            ReDim Preserve validIndices(1 To validCount)
            validIndices(validCount) = slideIdx
        End If
    Next slideIdx
    
    If validCount = 0 Then
        MsgBox "抽出対象のスライドが見つかりませんでした。", vbExclamation
        Exit Sub
    End If
    
    ' 3. 新規プレゼンテーションの生成とスライドの複製
    ' 新しいプレゼンテーションを作成
    Set targetPres = Presentations.Add(WithWindow:=msoTrue)
    
    ' 効率化の鍵:スライドを一つずつコピーするのではなく、
    ' スライド範囲(SlideRange)として一括コピーを試みる設計が理想だが、
    ' PowerPoint VBAの制約上、ここでは確実に型を維持するためにループ内でコピーを行う。
    ' ただし、インデックスは常に「ソース」を参照しているため安全。
    For slideIdx = 1 To validCount
        sourcePres.Slides(validIndices(slideIdx)).Copy
        targetPres.Slides.Paste
    Next slideIdx
    
    ' 4. 保存処理
    savePath = Replace(sourcePres.FullName, ".pptx", "_Clean.pptx")
    ' 同名ファイルが存在する場合の対策
    If Dir(savePath) <> "" Then
        savePath = Replace(savePath, ".pptx", "_" & Format(Now, "yyyymmdd_hhnnss") & ".pptx")
    End If
    
    targetPres.SaveAs savePath
    
    MsgBox "クリーンなプレゼンテーションを作成しました:" & vbCrLf & savePath, vbInformation, "完了"
    
ExitSub:
    ' オブジェクトの解放(メモリ管理)
    Set targetPres = Nothing
    Set sourcePres = Nothing
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical, "実行エラー"
    Resume ExitSub
End Sub

''' <summary>
''' スライドのスピーカーノートに特定のタグが含まれているか判定する。
''' </summary>
''' <param name="targetSlide">判定対象のスライド</param>
''' <param name="tag">検索するタグ(例: [Skip])</param>
''' <returns>スキップすべき場合はTrue</returns>
Private Function IsSlideSkipped(ByRef targetSlide As Slide, ByVal tag As String) As Boolean
    Dim noteText As String
    
    ' スピーカーノートが存在しない場合のエラーを回避
    On Error Resume Next
    noteText = targetSlide.NotesPage.Shapes.Placeholders(2).TextFrame.TextRange.Text
    On Error GoTo 0
    
    ' タグが含まれているか判定 (大文字小文字を区別しない)
    If InStr(1, noteText, tag, vbTextCompare) > 0 Then
        IsSlideSkipped = True
    Else
        IsSlideSkipped = False
    End If
End Function

3. アーキテクトの視点:なぜこのコードは「強い」のか

① メモリ効率とインデックスの分離

`validIndices()` という動的配列を使用している点が肝だ。スライドの判定(ロジック)と、スライドの操作(アクション)を完全に分離している。これにより、スライドを削除したことによる「インデックスのズレ」という低レベルなバグを構造的に排除している。

② `IsSlideSkipped` の堅牢性

スピーカーノートへのアクセスは、実は非常に脆い。ノートが存在しないスライドや、プレースホルダーが壊れているスライドに対してアクセスすると、VBAは即座にランタイムエラーを吐く。`On Error Resume Next` を局所的に使い、ノートのテキスト取得を「防衛的」に行うことで、処理の継続性を担保している。

③ クリップボード・オーバーヘッドへの配慮

スライドのコピー&ペーストは、OSのクリップボードを介するため、非常に重い操作だ。本来であれば `SlideRange.Copy` で一括コピーするのが最速だが、スライドの選択順序によってはフォーマットが崩れるリスクがある。本コードでは、あえて「判定済みのインデックスのみをループで回す」ことで、不必要なコピー操作を最小限に抑えつつ、確実に目的のスライドだけを転送している。

4. 実務における注意点:さらなる高みへ

このツールをプロダクション環境(実務の自動化ライン)に組み込む際は、以下の点に留意せよ。

  • スライドマスターの継承: `Slides.Paste` を使用した場合、コピー先にはソースのスライドマスターが持ち込まれる。これにより、見た目は完璧に維持されるが、ファイルサイズが肥大化することがある。配布用として極限まで軽量化したい場合は、マスターを再定義する処理を追加検討せよ。
  • ファイルI/Oの競合: `SaveAs` を実行する際、同名のファイルが既に開かれているとエラーになる。本コードではタイムスタンプを付与する回避策を入れているが、システム連携する場合は、ファイルロックのチェックを先行して行うべきだ。
  • データベース連携への拡張: もし「どのスライドがスキップされたか」のログをDBに記録したい場合は、`IsSlideSkipped` が `True` を返した瞬間に、スライド番号とタイムスタンプをログテーブルへ `INSERT` する処理を差し込め。それが「監査証跡(Audit Trail)」として機能する。

結論

自動化エンジニアの仕事は、単に「動くもの」を作ることではない。「壊れないもの」を、「予測可能な方法で」作ることだ。

今回提示した「抽出型」の設計思想をマスターすれば、君の書くコードは単なるスクリプトから、信頼に値する「ソフトウェア」へと昇華するだろう。

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