【実務・中級編】【上級プロフェッショナル向け】大量図面処理における「GDIリソース」監視と、リソース枯渇前のAcadApplication自動再起動 – AutoCAD VBA解析バイブル

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

限界を超えろ:大量図面処理におけるGDIリソース枯渇への対抗策と、自己修復型オートメーション設計

数百、時には数千のDWGファイルを一括処理するツールを開発する際、多くのエンジニアが陥る「見えない壁」がある。

「最初は完璧に動いていた。しかし、200枚を超えたあたりから動作が重くなり、最終的にはAutoCADが『メモリ不足』あるいは『予期せぬエラー』でクラッシュする。」

もし君がこのような現象に直面しているなら、君はまだ「コードを書くこと」に終始しており、「システムのライフサイクルを管理すること」ができていない。原因は単純なメモリ(RAM)不足ではない。GDI(Graphics Device Interface)リソースの枯渇だ。

本稿では、プロフェッショナルとして、単なる「図面処理スクリプト」を「堅牢な業務基盤」へと昇華させるための、極限の設計思想と実装手法を伝授する。

1. なぜ「メモリ」ではなく「GDI」が死を招くのか

多くの開発者は、メモリ使用量(Working Set)だけを監視すれば良いと考えている。しかし、AutoCADのような高度なグラフィックエンジンを搭載したアプリケーションにおいて、真のボトルネックとなるのはGDIハンドルである。

GDIリソースとは

Windowsが描画(フォント、ペン、ブラシ、ビットマップなど)を行うために使用するシステムリソースだ。AutoCADが図面を開き、オブジェクトを描画し、レイヤーやスタイルを操作するたびに、これらGDIハンドルは消費される。

致命的な誤解:COMオブジェクトの解放

VBAで `Set obj = Nothing` と書けば、オブジェクトは解放される。しかし、それはあくまでCOMの参照カウントを減らすだけであり、Windows OSレベルで確保されたGDIハンドルが即座に、かつ確実に返却される保証はない。

大量の図面を連続で処理すると、メモリは空いているように見えても、OSのGDIハンドル上限(プロセスあたり通常10,000個程度)に達し、AutoCADは描画不能に陥り、沈黙する。これが「リソースリークによるクラッシュ」の正体だ。

2. 堅牢な設計思想:コントローラー・ワーカー・パターン

単一のVBAマクロ内で全てのループを完結させる設計は、プロフェッショナルとしては「脆弱」と断じる。なぜなら、一度クラッシュすれば、どこまで処理が進んだかのコンテキストを失うからだ。

我々が採用すべきは、「コントローラー(管理責任者)」と「ワーカー(実行部隊)」を分離したアーキテクチャである。

1. Controller (Excel VBA or VBScript):

  • 処理すべき図面リストの管理(DBやCSVから取得)。
  • GDIリソースの監視。
  • AutoCADプロセスの起動・終了の制御。
  • 進捗の永続化(どこまで終わったかをログに記録)。

2. Worker (AutoCAD VBA):

  • 単一の図面を開き、処理を行い、保存して閉じる。
  • 「一回限りのミッション」に特化する。

この分離により、リソースが限界に達した際、ControllerはWorker(AutoCAD)を安全に終了させ、新しいプロセスを立ち上げて、中断した箇所から再開することができる。

3. 実装:GDI監視と自己修復ロジック

以下に、Windows APIを用いたGDIリソースの監視と、それに基づいた制御ロジックのプロトタイプを示す。ここでは、Controller側(Excel等)で動作することを想定した設計とする。

Windows APIによるリソース監視モジュール

' --- Module: Win32API_Monitor ---
Option Explicit

If VBA7 Then

' 64bit版AutoCAD/Office対応 Declare PtrSafe Function GetGuiResources Lib "user32" (ByVal hdc As LongPtr) As Long

Else

' 32bit版対応 Declare Function GetGuiResources Lib "user32" (ByVal hdc As Long) As Long

End If

' GDIリソースの現在の使用量を取得する ' hdc には 0 (GetDC(0)) を渡すことでシステム全体のコンテキストを取得可能だが、 ' プロセス単位の監視が基本となる。 Public Function GetCurrentGDICount() As Long ' 0を指定することで、呼び出しプロセス(この場合はExcel/Controller)のGDIハンドル数を取得 GetCurrentGDICount = GetGuiResources(0) End Function

プロフェッショナル・メインループ(Controller側)

このコードは、単なるループではない。「限界を察知し、次世代へバトンを渡す」ためのロジックだ。

' --- Module: MainController ---
Option Explicit

Private Const GDI_THRESHOLD As Long = 8000 ' 警告レベル(OS上限の手前で設定)
Private Const LOG_FILE As String = "C:\Temp\ProcessingLog.txt"

Sub ExecuteMassProcessing()
    Dim drawingList As Collection
    Set drawingList = LoadDrawingList() ' 未処理の図面リストを取得
    
    Dim acadApp As Object
    Dim currentDWG As Variant
    
    Do While drawingList.Count > 0
        ' 1. AutoCADの起動・接続
        Set acadApp = GetOrStartAutoCAD()
        
        If acadApp Is Nothing Then
            MsgBox "AutoCADの起動に失敗しました。", vbCritical
            Exit Sub
        End If

        ' 2. 図面処理ループ
        On Error Resume Next
        Do While drawingList.Count > 0
            ' --- 【極めて重要】リソース監視 ---
            If GetCurrentGDICount() > GDI_THRESHOLD Then
                Debug.Print "GDIリソース限界を検知。セッションを再起動します..."
                Exit Do ' 内側のループを抜け、AutoCADを再起動させる
            End If
            
            currentDWG = drawingList(1)
            
            ' Worker(AutoCAD内マクロ)を呼び出し
            ' 引数として図面パスを渡す
            Call RunWorkerMacro(acadApp, CStr(currentDWG))
            
            ' 処理完了後の後処理
            If Err.Number = 0 Then
                ' 成功したらリストから削除し、ログに記録
                UpdateProgressLog currentDWG, "SUCCESS"
                drawingList.Remove 1
            Else
                ' 失敗した場合はログにエラーを記録して次へ(または中断)
                UpdateProgressLog currentDWG, "ERROR: " & Err.Description
                drawingList.Remove 1
                Err.Clear
            End If
        Loop
        On Error GoTo 0
        
        ' 3. リソース枯渇によるループ脱出時、AutoCADを安全に終了させる
        Debug.Print "AutoCADセッションをクリーンアップします。"
        Call SafeTerminateAutoCAD(acadApp)
        Set acadApp = Nothing
        
        ' 次のセッションへ(drawingListが空なら終了)
    Loop
    
    MsgBox "全工程が完了しました。", vbInformation
End Sub

' AutoCADインスタンスの取得または生成
Private Function GetOrStartAutoCAD() As Object
    On Error Resume Next
    ' 既存のインスタンスを探す
    Set GetOrStartAutoCAD = GetObject(, "AutoCAD.Application")
    
    ' なければ新規作成
    If Err.Number <> 0 Then
        Err.Clear
        Set GetOrStartAutoCAD = CreateObject("AutoCAD.Application")
        GetOrStartAutoCAD.Visible = True
    End If
    On Error GoTo 0
End Function

' AutoCADを強制終了させず、安全に閉じる
Private Sub SafeTerminateAutoCAD(ByRef app As Object)
    On Error Resume Next
    ' 図面が開いていればすべて閉じる
    Dim doc As Object
    For Each doc In app.Documents
        doc.Close False ' 保存せずに閉じる(進捗はログで管理しているため)
    Next doc
    app.Quit
    Set app = Nothing
End Sub

4. 実務における「落とし穴」と回避策

データベース・ログ連携の重要性

上記のコードでは `UpdateProgressLog` という関数を仮定している。これは、単なるテキストファイルではなく、「処理済みフラグ」を管理するデータベース(SQLiteやAccess、あるいは単純なCSV)であるべきだ。
なぜなら、ハードウェアの不具合や停電でマシンが落ちた際、Controller自体が死ぬ可能性があるからだ。再起動した際、そのログを見て「どこから再開すべきか」を判断できなければ、二重処理(図面への書き込み重複)を招く。

バージョン互換性の制御

大量処理において、古いバージョンのDWGを扱う場合は、Worker側で `SaveAs` を使用して、明示的に新しいバージョンへ変換するロジックを組み込むこと。

' Worker側での例
doc.SaveAs "C:\Path\To\NewFile.dwg", acNative // 常に最新形式で保存

タイムアウト設計

GDIリソースが枯渇し始めると、AutoCADの応答が極端に遅くなる。`GetObject` や `CreateObject` がハングアップすることを見越し、Controller側には必ず「一定時間応答がない場合はプロセスを強制終了(`taskkill`)する」といった、より強硬なタイムアウト処理を検討すべきだ。

結論

プロフェッショナルな自動化エンジニアとは、コードが「正常に動く時」のことだけを考えている者ではない。「システムが限界に達した時、いかにエレガントに、かつ確実に業務を継続させるか」を設計できる者のことだ。

GDIリソースの監視、Controller-Worker分離、そして永続的な進捗管理。これらを実装に組み込むことで、君のツールは単なるスクリプトから、プロフェッショナルの現場に耐えうる「信頼のインフラ」へと進化する。

「止まらないシステム」を作れ。それが、我々エンジニアの責務である。

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