【AutoCAD VBAを掌握する極限の知見】保存完了を検知する待機ループ:非同期書き込みによるエラーを防ぐ実装手法
AutoCAD VBAによる図面バッチ処理、あるいは外部システムとの連携において、最も看過されやすく、かつ致命的な障害を引き起こすのが「図面保存の非同期性(Async I/O)」である。
`ThisDrawing.SaveAs` や `SendCommand “_QSAVE”` を実行した瞬間、VBAの実行スレッドは「保存コマンドをAutoCADの実行キューに投げた」という事実だけで次の行へ進む。OSのファイルシステムレベル、あるいはネットワークストレージ(NAS)のバッファフラッシュが完了する前に次のファイルオープンやプロセス連携を行えば、「ファイルが使用中(Sharing Violation)」エラーや、0バイトの破損したDWGファイルが生成される地獄が待っている。
本稿では、レガシーな `DoEvents` ループの悪夢を捨て、Windows APIとメモリモデルの挙動を完全に制御した、真の「保存完了検知待機ループ」の実装アーキテクチャを提示する。
—
1. なぜ「同期」を信じてはいけないのか? AutoCADのI/Oモデル
AutoCADの内部アーキテクチャは、COM(Component Object Model)の境界を越えてホストアプリケーションと通信する。VBAから送出された保存命令は、ドキュメントマネージャーを介してグラフィックスサブシステムとファイルI/Oスレッドへ非同期にディスパッチされる。
特に近年の環境では、以下の要因が同期待ちを困難にしている。
- OSのライトバック・キャッシュ: ディスク書き込み完了の応答が、OSレベルで遅延される。
- ネットワークドライブ(SMBプロトコル)の遅延: WANやVPN経由での保存時、ファイルロックの解放タイミングが数秒単位でズレる。
- AutoCADのバックグラウンドパブリッシュ・保存: `BACKGROUNDSAVE` 変数が有効な場合、保存処理自体が別スレッドで実行され、COMオブジェクトの状態とファイルの物理的な実体にギャップが生じる。
これに対し、単なる `Application.Wait` や `Sleep` APIによる固定時間のウェイトは、巨大図面では不足し、小規模図面では無駄な待ち時間となる「悪手」である排他制御のアンチパターンだ。我々は、「ファイルが実際に排他ロックされ、かつ書き込みが完了した瞬間」を検知しなければならない。
—
2. 実装アーキテクチャ:ファイル排他制御とWindows APIの融合
完璧な待機ループを実現するためには、以下の2段階のガードが必要である。
1. AutoCAD側のアイドル状態検知: コマンドが完全に終了し、AutoCADが次の入力を受け付けられる状態であること。
2. ファイルシステム側の排他制御(Exclusive Lock)確認: 対象ファイルに対して、他のプロセス(=AutoCADのI/Oスレッド)による書き込みハンドルが解放されていること。
ここでは、Windows APIの `CreateFile` を用い、対象ファイルを「排他モード(共有なし)」でオープンできるかをポーリングする堅牢な関数を構築する。
実用VBAコード:非同期書き込み完全同期エンジン
以下のモジュールをAutoCADのVBAプロジェクトに組み込んでほしい。エラーハンドリングとオブジェクトのライフサイクル管理を極限まで高めた実装である。
Option Explicit
‘ ==============================================================================
‘ Windows API Declarations for File Locking Verification
‘ ==============================================================================
Private Declare PtrSafe Function CreateFile Lib “kernel32” Alias “CreateFileA” ( _
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 LongPtr
Private Declare PtrSafe Function CloseHandle Lib “kernel32” ( _
ByVal hObject As LongPtr) As Long
Private Const GENERIC_READ As Long = &H80000000
Private Const GENERIC_WRITE As Long = &H40000000
Private Const FILE_SHARE_READ As Long = &H1
Private Const FILE_SHARE_WRITE As Long = &H2
Private Const OPEN_EXISTING As Long = 3
Private Const INVALID_HANDLE_VALUE As LongPtr = -1
‘ ==============================================================================
‘ メインプロシージャ:安全な図面保存と完了待機
‘ ==============================================================================
Public Sub SafeSaveDrawing(ByVal targetDoc As AcadDocument, ByVal filePath As String)
On Error GoTo ErrorHandler
Dim fso As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 事前チェック:パスの正当性
If targetDoc Is Nothing Then Err.Raise 1001, , “ドキュメントオブジェクトが無効です。”
‘ 1. 保存実行(コマンドの非同期発行)
‘ ※ SaveAsを使用する場合、AutoCADの内部バッファに処理が渡る
targetDoc.SaveAs filePath
‘ 2. AutoCADのアイドル状態およびファイルI/O完了を監視するループ
If Not WaitForFileReady(filePath, 30000) Then
Err.Raise 1002, , “タイムアウト: ファイルの書き込み完了を確認できませんでした。”
End If
‘ オブジェクトの明示的解放(メモリリーク防止)
Set fso = Nothing
Exit Sub
ErrorHandler:
If Not fso Is Nothing Then Set fso = Nothing
MsgBox “Critical Error [SafeSaveDrawing]: ” & Err.Description, vbCritical
Err.Raise Err.Number, “SafeSaveDrawing”, Err.Description
End Sub
‘ ==============================================================================
‘ ファイルが完全に解放されるまでポーリングする待機ループ関数
‘ ==============================================================================
Private Function WaitForFileReady(ByVal filePath As String, ByVal maxTimeoutMs As Long) As Boolean
Dim startTime As Double
startTime = Timer
‘ 処理落ちを防ぎつつCPU負荷を抑えるための微小待機(ミリ秒単位の制御)
Do
‘ AutoCADのイベントキューを処理させつつ待機
DoEvents
‘ ファイルが排他制御なしでGENERIC_READ/WRITEでオープンできるかテスト
‘ 書き込み中のファイルはOSによりロックされているため、CreateFileが失敗する
If TestFileAccess(filePath) Then
WaitForFileReady = True
Exit Function
End If
‘ タイムアウト判定 (Timerは秒単位のためミリ秒に換算)
If ((Timer – startTime) 1000) > maxTimeoutMs Then
WaitForFileReady = False
Exit Function
End If
‘ 100msウェイト(CPU使用率の100%張り付きを防ぐ極意)
SleepByDoEvents 100
Loop
End Function
‘ ==============================================================================
‘ Windows APIを用いたファイルアクセステスト
‘ ==============================================================================
Private Function TestFileAccess(ByVal filePath As String) As Boolean
Dim hFile As LongPtr
‘ 共有モードを「0(共有なし)」にしてオープンを試みる
‘ 書き込み中であればエラー(INVALID_HANDLE_VALUE)を返す
hFile = CreateFile(filePath, _
GENERIC_READ Or GENERIC_WRITE, _
0, _
0, _
OPEN_EXISTING, _
0, 0)
If hFile <> INVALID_HANDLE_VALUE
