「ファイルは存在するか」ではない。「ファイルは占有可能か」だ。
ネットワーク共有環境における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を用いた排他制御をマスターすることは、単なるテクニックの習得ではない。それは、システムの決定論的な挙動を保証し、エンジニアとしての信頼を勝ち取るための必須条件である。
