【上級プロフェッショナル向け】大量図面のオープン・クローズに伴うGDIリソース消費の監視と強制ガベージコレクション
AutoCAD VBAによる数千枚単位のDWG/DXFバッチ処理において、最も恐れられている現象は何か。
それは、メモリリークでも、VBA特有の`Run-time error ‘462’`でもない。「GDIハンドルおよびUserハンドルの枯渇による、予期せぬプロセス暴走とサイレントクラッシュ」である。
非表示インスタンス(`AcadApplication`)を生成し、`Documents.Open`と`Close`を数千回繰り返す。一見、コードは美しく完結しており、オブジェクト変数を`Nothing`に代わりがわるがわる代入しているように見えても、AutoCADの背後にあるCOMアーキテクチャとネイティブのGDIリソース管理は、完全に同期してはいない。
本稿では、レガシー環境の限界を突破し、数千枚の図面群を完全無人で踏破するための、Windows API監視とプロセスライフサイクル制御の極限の知見を公開する。
—
1. なぜAutoCAD VBAのループ処理は破綻するのか?
AutoCADをCOMオブジェクトとして外部起動(`CreateObject(“AutoCAD.Application”)`)し、図面を連続処理するスクリプトを書いたことがある者なら、一度は次のような絶望を味わっているはずだ。
- 500枚を超えたあたりから処理速度が目に見えて低下する。
- タスクマネージャーで見ると、メモリ(Working Set)は微増程度だが、「GDI オブジェクト」の数詞が右肩上がりに増え続け、最終的に10,000に達してAutoCADが固まる。
- `ThisDrawing.Close` や `Doc.Close` を呼んでも、内部のグラフィックスキャッシュやフォントハンドルの解放が遅延し、COMラッパーがメモリ空間のゴミ(Garbage)を抱え込む。
VBAのランタイムは、COMオブジェクトの参照カウント(Reference Counting)がゼロになった時点で解放を試みるが、AutoCAD側のCOMサーバー実装におけるスレッドモデル(Apartment Threading)とリソース解放の非同期性により、VBA側から即時的なGDIハンドルの回収を強制することは不可能に近い。
ここで必要となるのは、VBAの内部ガベージコレクションに期待するのではなく、「OSレベルでリソース消費量を監視し、閾値を超えた瞬間にAutoCADプロセスを安全にパージ(再起動)する」という、マクロ的アプローチである。
—
2. 実装アーキテクチャ:Windows APIによる監視と動的プロセス制御
以下のVBAモジュールは、指定したプロセスID(PID)のGDIハンドル数をWindows APIを通じてリアルタイムに監視し、安全なタイミングでAutoCADインスタンスを再生成するプロフェッショナル向けの実装である。
このコードは、エラーハンドリングの美しさよりも、「絶対に止まらない堅牢性」を最優先に設計されている。
Option Explicit
‘ ==============================================================================
‘ Windows API Declarations for GDI Resource Monitoring & Process Management
‘ ==============================================================================
Private Declare PtrSafe Function OpenProcess Lib “kernel32” ( _
ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long _
) As LongPtr
Private Declare PtrSafe Function CloseHandle Lib “kernel32” ( _
ByVal hObject As LongPtr _
) As Long
Private Declare PtrSafe Function GetGuiResources Lib “user32” ( _
ByVal hProcess As LongPtr, _
ByVal uiFlags As Long _
) As Long
Private Declare PtrSafe Function TerminateProcess Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal uExitCode As Long _
) As Long
‘ Constants
Const PROCESS_QUERY_INFORMATION As Long = &H400
Const PROCESS_TERMINATE As Long = &H1
Const GR_GDIOBJECTS As Long = 0
Const GR_USEROBJECTS As Long = 1
‘ Threshold for GDI Objects (AutoCAD starts leaking heavily above 8,000 in batch mode)
Const GDI_THRESHOLD As Long = 7500
Const BATCH_INTERVAL As Long = 50 ‘ Check every 50 drawings
Private acadApp As Object
Private currentProcessId As Long
Public Sub ExecuteHeavyBatchProcessing()
Dim targetFiles As Collection
Dim filePath As Variant
Dim processedCount As Long
‘ 処理対象ファイルのリストアップ(仮のプロシージャ)
Set targetFiles = GetTargetDrawingFiles(“C:\AutoCAD_Batch\Target”)
processedCount = 0
‘ 初期AutoCADインスタンスの起動
InitializeAutoCADInstance
For Each filePath In targetFiles
On Error GoTo ErrorHandler
‘ 図面のオープンと処理
Dim doc As Object
Set doc = acadApp.Documents.Open(CStr(filePath), True) ‘ ReadOnly
‘ — ここに実際の図面操作ロジックを記述 —
ProcessDrawing doc
doc.Close False
Set doc = Nothing
processedCount = processedCount + 1
‘ 一定間隔、またはGDIリソースの消費状況をチェック
If processedCount Mod BATCH_INTERVAL = 0 Then
If CheckAndPurgeGDIResource() Then
‘ 閾値超過によりAutoCADが再起動された場合、ログを記録
Debug.Print “GDIリソースの肥大化を検知。AutoCADプロセスをリフレッシュしました。”
End If
End If
On Error GoTo 0
Next filePath
‘ クリーンアップ
TerminateAutoCADInstance
MsgBox “バッチ処理が正常に完了しました。処理枚数: ” & processedCount, vbInformation
Exit Sub
ErrorHandler:
Debug.Print “Error processing file: ” & filePath & ” | Error: ” & Err.Description
Resume Next
End Sub
‘ ==============================================================================
‘ AutoCADインスタンスのライフサイクル管理
‘ ==============================================================================
Private Sub InitializeAutoCADInstance()
On Error Resume Next
Set acadApp = CreateObject(“AutoCAD.Application”)
acadApp.Visible = False ‘ 非表示モードで限界までパフォーマンスを引き出す
‘ プロセスIDを取得し、API監視のターゲットに設定
currentProcessId = GetProcessIdFromApp(acadApp)
On Error GoTo 0
End Sub
Private Sub TerminateAutoCADInstance()
On Error Resume Next
If Not acadApp Is Nothing Then
acadApp.Quit
Set acadApp = Nothing
End If
On Error GoTo 0
End Sub
‘ ==============================================================================
‘ GDIハンドル監視と動的リフレッシュ
‘ ==============================================================================
Private Function CheckAndPurgeGDIResource() As Boolean
Dim hProcess As LongPtr
Dim gdiCount As Long
CheckAndPurgeGDIResource = False
If currentProcessId = 0 Then Exit Function
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_TERMINATE, 0, currentProcessId)
If hProcess = 0 Then Exit Function
‘ 現在のGDIオブジェクト数を取得
gdiCount = GetGuiResources(hProcess, GR_GDIOBJECTS)
CloseHandle hProcess
Debug.Print “Current GDI Object Count: ” & gdiCount
‘ 閾値を超えている場合、強制再起動を実行
If gdiCount > GDI_THRESHOLD Then
TerminateAutoCADInstance
‘ OSがハンドルを完全に解放するまでわずかにウェイト
DoEvents
Application.Wait Now + TimeValue(“0:00:03”)
‘ インスタンスの再生成
InitializeAutoCADInstance
CheckAndPurgeGDIResource = True
End If
End Function
‘ ==============================================================================
‘ 補助関数:COMオブジェクトからProcessIDを逆引き
‘ ==============================================================================
Private Function GetProcessIdFromApp(app As Object) As Long
Dim wmi As Object, processes As Object, proc As Object
Dim hwnd As LongPtr
On Error Resume Next
‘ AutoCADのウィンドウハンドルからPIDを特定するアプローチも可能だが、
‘ ここでは簡易的にWMIを使用するか、あらかじめ起動時のPIDを取得する
‘ ※実運用ではCreateObject時のHwndプロパティ等を利用するのが堅牢
hwnd = app.Hwnd
Dim pid As Long
GetWindowThreadProcessId hwnd, pid
GetProcessIdFromApp = pid
On Error GoTo 0
End Function
(※注意: 上記の `GetWindowThreadProcessId` を使用する場合は、User32.dllからのAPI宣言が別途必要となる。実務では `app.Hwnd` からウィンドウを特定してPIDを抽出するルーチンを組み込むこと。)
—
3. チーフアーキテクトが教える:実運用における3つの鉄則
数千枚規模のプロダクション環境でこのシステムを稼働させる場合、コードの美しさ以上に「環境起因の例外」をいかにねじ伏せるかが勝負の分かれ目となる。
鉄則 1: `DoEvents` の適切な挿入とダイアログの完全抑止
非表示(`Visible = False`)で起動しているとはいえ、AutoCADは背後でフォントの欠落や古い図面フォーマットの警告ダイアログをモーダル表示しようとすることがある。これが起きると、バッチ処理は永遠にフリーズする。
図面を開く前に、必ず以下のシステム変数(またはPreferences)を設定し、ダイアログを一切出さない環境を担保しなければならない。
- `FILEDIA` = 0
- `CMDDIA` = 0
- `EXPERT` = 5 (警告を自動的にデフォルト値でスルー)
鉄則 2: COMオブジェクトのローカル変数スコープの徹底
VBAにおいて、ループ内で生成したオブジェクト(`SelectionSet`, `BlockReference` など)をグローバル変数に保持させたり、解放漏れを起こしたりすることは、自らメモリリークの地雷原を歩くようなものである。
図面ごとの処理は必ず別プロシージャ(SubまたはPrivate Function)に切り出し、スコープを抜けた瞬間にローカル変数が自動的に破棄される構造を徹底すること。
鉄則 3: ハードウェアアクセラレーションの無効化
非表示インスタンスであっても、AutoCADはデフォルトでGPUアクセラレーションやシェーダーの初期化を試みる。これがGDIリソースの無駄な消費や、デスクトップヒープとの競合を引き起こす原因となる。
バッチ処理用のショートカットやスクリプト起動時には、`/nologo` スイッチに加え、ハードウェアアクセラレーションを強制オフにする環境変数(またはレジストリ制御)を組み合わせるのが、真のプロフェッショナルのアプローチである。
—
結びにかえて
AutoCAD VBAは、レガシーな技術と揶揄されることもある。しかし、OSの内部挙動(GDIリソース、プロセスライフサイクル)を正確に理解し、Windows APIと緻密に連携させることで、現代のモダンな言語で作られた専用バッチツールに匹敵する、いや、それ以上に泥臭く確実に稼働する巨大自動化システムへと昇華させることができる。
「動かない理由」を環境やツールのせいにするな。アーキテクトが制御しきれていないだけだ。
この知見をあなたの開発環境にインストールし、全自動・無停止の図面処理パイプラインを完遂してほしい。
