【実務・中級編】【上級者】Windowsの排他制御APIとFSOを組み合わせ、ネットワーク上の共有PPTXファイルを安全にロック取得した上で、他ユーザーの編集競合を回避しながらバックグラウンドでPDF一括変換を行う堅牢スクリプト – PowerPoint VBA解析バイブル

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

「ファイルは存在するか」ではない。「ファイルは占有可能か」だ。

ネットワーク共有環境におけるPowerPoint VBAの極限設計

業務自動化の現場において、ローカル環境で完璧に動作したスクリプトが、共有サーバー(NASやファイルサーバー)に投入された瞬間に無残なエラーを吐き出し、処理を停止させる――。これは、自動化エンジニアが最初に直面する、そして最も回避すべき「プロフェッショナルへの壁」だ。

多くの開発者は、`FileSystemObject`(FSO)の `FileExists` メソッドでファイルの存在を確認し、そのまま `Presentations.Open` を実行する。しかし、これはあまりにも楽観的すぎる。マルチユーザー環境において、「ファイルが存在すること」と「そのファイルが編集可能な状態であること」は全く別次元の概念だからだ。

誰かがそのファイルを開いている最中に、バックグラウンド処理が強引にアクセスを試みれば、ファイルは破損するか、あるいはVBAのランタイムエラーを引き起こしてプロセスを強制終了させる。

本稿では、Windows APIを用いた排他制御(Exclusive Lock)を導入し、ネットワーク上の競合を完全に回避しながら、堅牢にPDF一括変換を行う「エンタープライズ仕様」の設計思想を伝授する。

1. なぜ FSO だけでは不十分なのか?

`FileSystemObject` や VBA標準の `Dir` 関数は、ファイルシステムの「メタデータ」を読み取るには適している。しかし、「プロセスによるファイルハンドル」の状況までは把握できない。

Windows OSにおいて、ファイルは開かれると、そのプロセスによって「共有モード(Share Mode)」が設定される。

  • 他のユーザーが「読み取り専用」で開いているのか?
  • それとも「書き込み権限」を持って排他的に開いているのか?

これを判別せずに `Presentations.Open` を叩くのは、ブレーキのない車を高速道路に走らせるようなものだ。真に堅牢なツールを作るには、OSのカーネルレベルに近い Windows API を叩き、ファイルに対して「自分が独占的にアクセスできるか」を直接問い合わせる必要がある。

2. 設計思想:ロック取得とリトライ戦略

我々が実装すべきは、単なる「エラー回避」ではない。以下の3層構造を持つ「防御型バッチエンジン」だ。

1. Lock Check Layer (Windows API): `CreateFile` 関数を用い、`dwShareMode` を `0`(排他)に設定してファイルを開こうと試みる。成功すれば「安全」、失敗すれば「誰かが使用中」と判定する。
2. Retry & Backoff Layer: 競合が発生した場合、即座にエラーにするのではなく、数秒の待機(Exponential Backoff)を挟んで再試行する。これにより、一時的な保存動作によるロックを回避する。
3. Isolation Layer: PowerPointのインスタンスを適切に管理し、変換処理中に他のプロセスに影響を与えないよう、エラーハンドリングを徹底する。

3. 実装:プロダクション・グレードのコード例

以下に、そのまま現場に投入できるレベルのコード構造を示す。ここでは、API宣言、ロック判定ロジック、そしてメインの変換エンジンを一つのモジュールとして構成している。

Option Explicit

' --- Windows API Declarations ---
' ファイルの排他制御を行うための核心的なAPI
Private Declare PtrSafe Function CreateFile Lib "kernel32" Alias "CreateFileA" ( _
    ByVal lpFileName As String, _
    ByVal dwDesiredAccess As Long, _
    ByVal dwShareMode As Long, _
    ByVal lpSecurityAttributes As LongPtr, _
    ByVal dwCreationDisposition As Long, _
    ByVal dwFlagsAndAttributes As Long, _
    ByVal hTemplateFile As LongPtr) As LongPtr

Private Declare PtrSafe Function CloseHandle Lib "kernel32" (ByVal hObject As LongPtr) As Long

' 定数の定義
Private Const GENERIC_READ As Long = &H80000000
Private Const FILE_SHARE_NONE As Long = 0 ' 排他制御の要
Private Const OPEN_EXISTING As Long = 3
Private Const INVALID_HANDLE_VALUE As LongPtr = -1

' --- Core Logic ---

''' <summary>
''' 指定されたファイルが他のプロセスによってロックされているかを確認する
''' </summary>
''' <param name="filePath">対象ファイルのフルパス</param>
''' <returns>True: ロックされている(使用中), False: 使用可能</returns>
Public Function IsFileLocked(ByVal filePath As String) As Boolean
    Dim hFile As LongPtr
    
    ' CreateFileを「共有モードなし(FILE_SHARE_NONE)」で呼び出す
    ' これにより、他の誰かが少しでもファイルを開いていれば失敗する
    hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_NONE, 0, OPEN_EXISTING, 0, 0)
    
    If hFile = INVALID_HANDLE_VALUE Then
        ' ハンドル取得失敗 = 他のプロセスが掴んでいる
        IsFileLocked = True
    Else
        ' 取得成功 = 独占可能。即座にハンドルを解放する
        CloseHandle hFile
        IsFileLocked = False
    End If
End Function

''' <summary>
''' 堅牢なPDF一括変換メインエンジン
''' </summary>
Public Sub RobustBatchConvertPDF(ByVal targetFolder As String)
    Dim fso As Object
    Dim folder As Object
    Dim file As Object
    Dim pptApp As Object
    Dim pres As Object
    Dim retryCount As Integer
    Const MAX_RETRIES As Integer = 3
    Const RETRY_DELAY_SEC As Integer = 2
    
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    ' フォルダの存在確認
    If Not fso.FolderExists(targetFolder) Then
        MsgBox "Target folder not found.", vbCritical
        Exit Sub
    End If
    
    ' PowerPointのインスタンス管理(既存または新規)
    On Error Resume Next
    Set pptApp = GetObject(Class:="PowerPoint.Application")
    If pptApp Is Nothing Then
        Set pptApp = CreateObject("PowerPoint.Application")
    End If
    On Error GoTo 0
    
    Set folder = fso.GetFolder(targetFolder)
    
    ' メインループ
    For Each file In folder.Files
        ' 拡張子チェック (.pptx)
        If LCase(fso.GetExtensionName(file.Path)) = "pptx" Then
            
            Dim locked As Boolean
            Dim success As Boolean
            retryCount = 0
            success = False
            
            ' --- Retry Logic (Exponential Backoff) ---
            Do While retryCount < MAX_RETRIES
                locked = IsFileLocked(file.Path)
                
                If Not locked Then
                    success = True
                    Exit Do
                Else
                    ' ロックされている場合は待機してリトライ
                    retryCount = retryCount + 1
                    Debug.Print "File locked, retrying... (" & retryCount & "/" & MAX_RETRIES & "): " & file.Name
                    ' Windows APIのSleepではなく、VBAのWaitで実装(簡便性のため)
                    Dim startTime As Double
                    startTime = Timer
                    Do While Timer < startTime + (RETRY_DELAY_SEC * retryCount)
                        DoEvents
                    Loop
                End If
            Loop
            
            ' --- Execution Phase ---
            If success Then
                On Error GoTo FileError
                ' ファイルを開く(バックグラウンド処理を想定し、Windowは表示しない設定も検討可)
                Set pres = pptApp.Presentations.Open(file.Path, WithWindow:=msoFalse)
                
                ' PDFとして保存
                Dim pdfPath As String
                pdfPath = fso.GetParentFolderName(file.Path) & "\" & _
                          fso.GetBaseName(file.Path) & ".pdf"
                
                ' 32 = ppSaveAsPDF
                pres.SaveAs pdfPath, 32
                pres.Close
                Debug.Print "Successfully converted: " & file.Name
            Else
                Debug.Print "SKIPPED (Max retries reached): " & file.Name
            End If
        End If
        
NextFile:
    Next file
    
    MsgBox "Batch process completed. Check Debug window for logs.", vbInformation
    Exit Sub

FileError:
    Debug.Print "ERROR processing " & file.Name & ": " & Err.Description
    If Not pres Is Nothing Then pres.Close
    Resume NextFile
End Sub

4. アーキテクトの視点:運用における注意点

このスクリプトをプロダクション環境へデプロイする際、以下の3点に留意せよ。

① ログの重要性

`Debug.Print` は開発用だ。実運用では、変換に失敗したファイルの一覧をCSVやテキストファイルに書き出す「エラーログ出力機能」を実装せよ。ユーザーは「なぜ処理されなかったのか」を常に知りたがっている。

② PowerPointのメモリリーク対策

大量のファイルを処理する場合、PowerPointのインスタンスがメモリを食いつぶすことがある。`WithWindow:=msoFalse` を指定してバックグラウンドで処理を行うことは、パフォーマンスだけでなく、UIスレッドのハングアップを防ぐ意味でも極めて重要だ。

③ ネットワーク遅延(Latency)の考慮

ネットワーク越しにAPIを叩く場合、`IsFileLocked` の判定自体に時間がかかることがある。リトライ回数(`MAX_RETRIES`)と待機時間(`RETRY_DELAY_SEC`)のバランスは、対象となるサーバーの応答性能に基づいてチューニングが必要だ。

結論

「動けばいい」コードは、開発者の手元では動く。しかし、「業務を止めない」コードは、現場の混沌とした環境、つまり競合、遅延、不完全な操作を前提に設計されている。

Windows APIを用いた排他制御をマスターすることは、単なるテクニックの習得ではない。それは、システムの決定論的な挙動を保証し、エンジニアとしての信頼を勝ち取るための必須条件である。

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