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

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

ネットワークの戦場を制する:Windows APIによる排他制御を用いた、堅牢なるPowerPoint PDF一括変換アーキテクチャ

共有ネットワークドライブ上で動く自動化スクリプト。それは、多くのエンジニアにとって「いつ、誰の操作によってクラッシュするか分からない」という、時限爆弾のような存在だ。

「ファイルが開けません」「書き込み権限がありません」……。
標準的なVBAの `Dir` 関数や `FileSystemObject` (FSO) だけを用いたチェックでは、コンマ数秒の間に発生するレースコンディション(競合状態)を防ぐことはできない。ファイルが存在することを確認した直後、他ユーザーがファイルを開く。この絶望的な隙間を埋めるには、VBAの抽象化されたレイヤーを飛び越え、Windowsカーネルの排他制御に直接介入するしかない。

本稿では、エンタープライズ環境での運用に耐えうる、Windows APIとFSOを高度に融合させた「極限のバッチ変換スクリプト」の設計思想とその実装を詳解する。

1. なぜ「ファイル存在チェック」では不十分なのか

多くの開発者が陥る罠がある。それは、`If fso.FileExists(path) Then` という条件分岐を、ファイル操作の正当性の担保だと誤認することだ。

ファイルシステムにおける「存在」と「利用可能(Lock-free)」は全く別次元の話である。
共有環境において、対象のPPTXファイルが他者の編集プロセスに掴まれている場合、`Open` メソッドは即座に実行時エラーを吐き出し、あるいは最悪の場合、PowerPointプロセス自体をゾンビ化(バックグラウンドでメモリを食いつぶしながらハングアップ)させる。

我々が目指すべきは、「ファイルが物理的に存在し、かつ、他のプロセスによる共有モード(Share Mode)が許可されていない状態」を、OSレベルで確定させることだ。

2. Windows APIによる低レイヤーでの排他制御

この課題を解決するために、`CreateFile` APIを使用する。
`CreateFile` は、単にファイルを開くだけの関数ではない。第3引数の `dwShareMode` に `0` を渡すことで、「他のプロセスによる読み取り・書き込み・削除を一切許可しない」という、極めて強力な排他ロックを要求することができる。

もし、既に誰かがファイルを開いていれば、このAPIは `ERROR_SHARING_VIOLATION` を返してくる。これこそが、我々が求める「真実」だ。

API宣言と定数の定義

まず、64bit/32bit両環境に対応した宣言を定義する。現代のエンタープライズ環境では、Officeの64bit化は避けられないため、`PtrSafe` と `LongPtr` の使い分けは必須知識である。

Option Explicit

' --- Windows API Declarations ---

If VBA7 Then

Private Declare PtrSafe Function CreateFileW Lib "kernel32" ( _ ByVal lpFileName As LongPtr, _ 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 Declare PtrSafe Function GetLastError Lib "kernel32" () As Long

Else

Private Declare Function CreateFileW Lib "kernel32" ( _ ByVal lpFileName As String, _ ByVal dwDesiredAccess As Long, _ ByVal dwShareMode As Long, _ ByVal lpSecurityAttributes As Long, _ ByVal dwCreationDisposition As Long, _ ByVal dwFlagsAndAttributes As Long, _ ByVal hTemplateFile As Long) As Long Private Declare Function CloseHandle Lib "kernel32" ( _ ByVal hObject As Long) As Long Private Declare Function GetLastError Lib "kernel32" () As Long

End If

' --- Constants --- Private Const GENERIC_READ As Long = &H80000000 Private Const CREATE_ALWAYS As Long = 2 Private Const OPEN_EXISTING As Long = 3 Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80 Private Const ERROR_SHARING_VIOLATION As Long = 32 Private Const INVALID_HANDLE_VALUE As LongPtr = -1

3. 実装:堅牢なるPDF一括変換エンジン

以下に、FSOによるディレクトリ走査、APIによる排他制御、そしてPowerPointオブジェクトのメモリ管理を統合したクラスモジュールの核となるロジックを示す。

核心となるロック検知ロジック

''' <summary>
''' ファイルが他のプロセスによってロックされているかを確認する。
''' </summary>
''' <param name="filePath">対象ファイルのフルパス</param>
''' <returns>ロックされている場合はTrue</returns>
Private Function IsFileLocked(ByVal filePath As String) As Boolean
    #If VBA7 Then
        Dim hFile As LongPtr
    #Else
        Dim hFile As Long
    #End If
    
    ' dwShareMode を 0 に設定することで、排他的なアクセスを試みる
    ' 他のプロセスが既に開いている場合、ここで失敗する
    hFile = CreateFileW(StrPtr(filePath), GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
    
    If hFile = INVALID_HANDLE_VALUE Then
        If GetLastError() = ERROR_SHARING_VIOLATION Then
            IsFileLocked = True
            Exit Function
        End If
    End If
    
    ' 成功した場合は、即座にハンドルを閉じる
    If hFile <> INVALID_HANDLE_VALUE Then CloseHandle hFile
    IsFileLocked = False
End Function

メイン・バッチ処理プロセス

ここで重要なのは、PowerPoint Applicationオブジェクトのライフサイクル管理だ。ループ内で `New PowerPoint.Application` を繰り返すような愚行は、メモリリークとプロセス残存の元凶となる。Applicationオブジェクトは一度だけ生成し、処理終了時に確実に解放(`Set = Nothing`)しなければならない。

Public Sub BatchConvertPptxToPdf(ByVal targetFolder As String)
    Dim fso As Object
    Dim folder As Object
    Dim file As Object
    Dim pptApp As Object ' PowerPoint.Application
    Dim pres As Object   ' PowerPoint.Presentation
    Dim retryCount As Integer
    Const MAX_RETRIES As Integer = 3
    Const RETRY_DELAY_SEC As Double = 2#

    Set fso = CreateObject("Scripting.FileSystemObject")
    
    If Not fso.FolderExists(targetFolder) Then
        Err.Raise 53, , "Target folder not found: " & targetFolder
    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 ErrorHandler

    ' 表示を抑制してパフォーマンスを最適化
    pptApp.Visible = msoFalse 

    Set folder = fso.GetFolder(targetFolder)

    For Each file In folder.Files
        ' 拡張子チェック
        If LCase(fso.GetExtensionName(file.Path)) = "pptx" Then
            
            ' --- 排他制御フェーズ ---
            retryCount = 0
            Do While IsFileLocked(file.Path) And retryCount < MAX_RETRIES
                Debug.Print "File locked: " & file.Name & ". Retrying..."
                Sleep (RETRY_DELAY_SEC * 1000) ' Windows APIのSleepを使用推奨
                retryCount = retryCount + 1
            Loop

            If IsFileLocked(file.Path) Then
                Debug.Print "Skipping (Locked): " & file.Name
                GoTo NextFile
            End If

            ' --- 変換フェーズ ---
            On Error Resume Next
            ' ファイルを読み取り専用で開く(編集競合の防止)
            Set pres = pptApp.Presentations.Open(FileName:=file.Path, ReadOnly:=msoTrue, WithWindow:=msoFalse)
            
            If Err.Number = 0 Then
                Dim pdfPath As String
                pdfPath = fso.GetParentFolderName(file.Path) & "\" & fso.GetBaseName(file.Path) & ".pdf"
                
                ' PDFとして保存
                pres.SaveAs pdfPath, 32 ' 32 = ppSaveAsPDF
                Debug.Print "Success: " & file.Name
                
                ' オブジェクトの明示的解放(重要:メモリリーク防止)
                pres.Close
                Set pres = Nothing
            Else
                Debug.Print "Error opening " & file.Name & ": " & Err.Description
            End If
            On Error GoTo ErrorHandler
        End If

NextFile:
    Next file

CleanUp:
    ' リソースの完全解放
    On Error Resume Next
    If Not pres Is Nothing Then pres.Close: Set pres = Nothing
    If Not pptApp Is Nothing Then pptApp.Quit: Set pptApp = Nothing
    Set fso = Nothing
    Debug.Print "Process Completed."
    Exit Sub

ErrorHandler:
    MsgBox "Critical Error: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

4. アーキテクトの視点:パフォーマンスと堅牢性の極致

このスクリプトが、単なる「動くコード」ではなく「プロフェッショナルなツール」である理由は、以下の3点に集約される。

I. メモリ管理の厳格化

VBAにおいて、COMオブジェクト(PowerPointやFSO)は「参照カウント」によって管理されている。`Set pres = Nothing` を怠ることは、メモリ上に幽霊のようなプレゼンテーションオブジェクトを残留させ、長時間稼働するサーバー環境において、最終的に `Out of Memory` を引き起こす。本実装では、各ループの終わりに明示的な `Close` と `Nothing` を徹底している。

II. エラー・レジリエンス(回復力)

ネットワークの瞬断や、他ユーザーによるロックは「例外」ではなく「日常」である。`MAX_RETRIES` を設けた再試行ロジックと、`ReadOnly:=msoTrue` でのオープンにより、システムが停止することなく、可能な限りのタスクを完遂する設計にしている。

III. 低レイヤーへのアクセス

`CreateFileW` を用いたロック判定は、ファイルシステムが提供する最も信頼性の高いシグナルである。これにより、ファイルが存在するのに開けないという「中途半端なエラー状態」を事前に検知し、処理を安全にスキップ、あるいは待機させることが可能となる。

結語

自動化エンジニアの真価は、ハッピーパス(正常系)のコードを書くことではなく、エッジケース(異常系)をいかにエレガントに、かつ確実に制御するかで決まる。

Windows APIという強力な武器を、VBAという使い慣れた環境に組み込む。この「ハイブリッドなアプローチ」こそが、レガシーな資産を現代のエンタープライズ要件に適応させるための、唯一にして最強の解である。

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