1. イントロダクション:なぜ「ブロック走査」がCADオートメーションの分水嶺なのか
AutoCAD VBAを用いた開発において、`AcadDocument.Blocks` コレクションの走査は、単なる「初歩的な要素一覧の取得」にとどまらない。それは図面データベース(DWG)の内部構造であるブロックテーブル(Block Table)に対するダイレクトなクエリであり、属性抽出、図面標準化チェック、部品表(BOM)自動生成、さらには外部データベース(RDB)連携に至るあらゆる大規模オートメーションの不可避な起点である。
しかし、市販のリファレンスや安易なチュートリアルを鵜呑みにして実装すると、実務の現場で必ず壁にぶち当たる。数十メガバイトに及ぶプラント図面や詳細建築図面を処理した瞬間、VBA処理は凍結し、最悪の場合はCOMオブジェクトの解放漏れによってAutoCADプロセスがメモリを食いつぶして異常終了する。
本稿では、`AcadDocument.Blocks` を効率的かつ安全に走査し、図面内で定義されているユーザーブロック名のみを正確・高速に重複なくリストアップする高度な実装手法を解説する。
—
2. AcadDocument.Blocksのアーキテクチャと回避すべき罠
2.1 ブロックテーブルの特異構造
AutoCADにおける `Blocks` コレクション(`AcadBlocks`)は、一般的なプログラミングにおける「ユーザーが作成した図形グループのリスト」ではない。DWG内部では、レイアウト空間自体も一つのブロック定義として管理されている。
- `Model_Space`:モデル空間の描画要素を保持するブロック定義
- `Paper_Space` / `Paper_Space0`:ペーパー空間(レイアウト)を保持するブロック定義
- `D…` や `X…`:寸法、ハッチング、動的ブロック(Dynamic Block)の変更状態を表す匿名ブロック(Anonymous Block)
単に `For Each objBlock In ThisDrawing.Blocks` を実行するだけでは、これらのシステム内部ブロックや一時的オブジェクトまで取得してしまい、データ汚染や集計エラーを引き起こす。
2.2 COM層におけるアクセスオーバーヘッド
AutoCAD VBAは内部的にCOM(Component Object Model)オートメーション経由でAutoCADコア(C++層)と通信している。
ループ内でプロパティ参照(例: `objBlock.Name`)を呼び出すごとに、境界を越えるプロセス間通信(またはモジュール間マーシャリング)が発生する。
1. 匿名ブロックの早期フィルタリング:不要なプロパティ取得を回避する。
2. ハッシュテーブル(Scripting.Dictionary)の活用:$O(1)$ の探索速度を維持し、重複排除をメモリ上で行う。
3. 明示的COMインスタンスの解放:VBAのガベージコレクションに依存せず、参照カウントを適正に制御する。
—
3. 実装:重複排除と高速化を両立したブロック名抽出コード
以下のコードは、単にブロック名をリストアップするだけでなく、実務での運用に耐えうるエラーハンドリング、匿名ブロックの除外、大文字小文字の表記揺れ吸収、精密なパフォーマンス計測(Windows API利用)、および厳密なオブジェクト破棄を組み込んだ完全な実装である。
Option Explicit
‘ ==============================================================================
‘ 高精度タイムスタンプ計測用 Windows API
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function GetTickCount Lib “kernel32” () As Long
Else
Private Declare Function GetTickCount Lib “kernel32” () As Long
End If
‘ ==============================================================================
‘ メインプロシージャ: エントリポイント
‘ ==============================================================================
Public Sub ExtractUniqueBlockNames()
On Error GoTo ErrorHandler
Dim startTime As Long
startTime = GetTickCount()
‘ 1. Dictionaryオブジェクトの初期化 (参照設定不要の遅延バインディング)
Dim dictBlockNames As Object
Set dictBlockNames = CreateObject(“Scripting.Dictionary”)
‘ 文字列比較モードを「テキスト比較 (大文字・小文字を区別しない)」に設定
dictBlockNames.CompareMode = 1 ‘ 1 = vbTextCompare
‘ 2. 対象ドキュメントおよびコレクションのキャッシング
Dim doc As AcadDocument
Set doc = ThisDrawing
Dim blkCollection As AcadBlocks
Set blkCollection = doc.Blocks
Dim i As Long
Dim totalCount As Long
totalCount = blkCollection.Count
Dim blk As AcadBlock
Dim blkName As String
‘ 3. コレクションの高速走査
‘ ※ For Each よりも インデックス参照の方がCOMインターフェースのオーバーヘッドが少ない場合がある
For i = 0 To totalCount – 1
Set blk = blkCollection.Item(i)
‘ 4. フィルタリング条件の判定
‘ – IsLayout: モデル空間/ペーパー空間ブロックを除外
‘ – IsXRef: 外部参照ブロックを除外 (要件に応じて調整)
‘ – 名前頭文字 “”: 匿名ブロック (寸法・ハッチング・動的ブロック内部体) を除外
If Not blk.IsLayout And Not blk.IsXRef Then
blkName = blk.Name
‘ 匿名ブロック記号の判定
If Left$(blkName, 1) <> “” Then
‘ 重複チェックを行わずにKeyとして追加 (Dictionaryの性質を利用)
If Not dictBlockNames.Exists(blkName) Then
dictBlockNames.Add blkName, True
End If
End If
End If
‘ ループ内COMオブジェクトの明示的解放
Set blk = Nothing
Next i
‘ 5. 結果の出力 (イミディエイトウィンドウへの吐き出し)
Debug.Print “==========================================”
Debug.Print ” 抽出ブロック定義数: ” & dictBlockNames.Count
Debug.Print ” 処理時間: ” & (GetTickCount() – startTime) & ” ms”
Debug.Print “==========================================”
Dim varKey As Variant
For Each varKey In dictBlockNames.Keys
Debug.Print “Block Definition: ” & CStr(varKey)
Next varKey
CleanUp:
‘ 6. メモリの完全解放 (COMオブジェクト参照破棄)
If Not dictBlockNames Is Nothing Then
dictBlockNames.RemoveAll
Set dictBlockNames = Nothing
End If
Set blkCollection = Nothing
Set doc = Nothing
Exit Sub
ErrorHandler:
MsgBox “エラーが発生しました: ” & Err.Number & vbCrLf & _
“説明: ” & Err.Description, vbCritical, “Block Extraction Error”
Resume CleanUp
End Sub
—
4. プログラミングのディープ・ダイブ(極限の解説)
4.1 `Scripting.Dictionary` を選択すべき真の理由
VBA標準の `Collection` オブジェクトは、キーの存在確認メソッド(`Exists`)を持たない。重複を回避するために `On Error Resume Next` で強引にキー追加を試みる手法が散見されるが、これは例外処理フレームワークを無駄に駆動させ、パフォーマンスを著しく低下させる。
`Scripting.Dictionary` はハッシュテーブル構造を採用しているため、`Exists` メソッドによるキー検索の文脈複雑度は $O(1)$ である。数千種類に及ぶブロックが登録されている巨大な図面であっても、探索時間は一定であり、線形探索($O(N)$)のような速度低下を起こさない。
さらに、`CompareMode = 1 (vbTextCompare)` を明示指定することで、AutoCADのシンボル名規則における「大文字・小文字の区別なし(`Door` と `DOOR` を同一視する)」という仕様を正しく実装レベルでハンドリングできる。
4.2 COMオブジェクトのライフサイクル制御
VBAは参照カウント方式のガベージコレクション(GC)を採用している。しかし、`For Each` ループ内でCOMオブジェクトを変数に代入して使い回す場合、ループの切り替わり時に内部参照が即座に解放されず、ラッパーオブジェクトがメモリに残留することがある。
‘ 危険なパターン:暗黙の参照が残り、メモリ圧迫の原因となる
For Each blk In ThisDrawing.Blocks
‘ … 処理 …
Next blk
‘ 堅牢なパターン:明示的なスコープ制御と破棄
For i = 0 To totalCount – 1
Set blk = blkCollection.Item(i)
‘ … 処理 …
Set blk = Nothing ‘ 明示的に参照カウントを減算
Next i
特に10万要素を超えるエンタープライズレベルのDWGデータを一括処理する場合、この `Set blk = Nothing` の1行の有無が、VBAプロセスのクラッシュを防ぐ絶対的な防壁となる。
—
5. 将来の拡張とレガシー資産のモダナイゼーション
現在、AutoCADの開発環境はVBA(32bit/64bit COM API)から、より高速かつ柔軟な AutoCAD .NET API(C# / VB.NET) へと完全にシフトしている。
本稿で解説した `AcadDocument.Blocks` の走査アルゴリズムは、AutoCAD .NET APIにおける `BlockTable` および `BlockTableRecord` の概念と完全に一対一で対応している。将来的にC#等へコードを移植・再構築する際も、以下の比較構造を理解していれば設計思想をそのまま適用可能である。
| 概念 | AutoCAD VBA (COM API) | AutoCAD .NET API (ObjectARX wrapper) |
| :— | :— | :— |
| テーブル構造 | `AcadDocument.Blocks` | `BlockTable` (SymbolTable) |
| 要素データ | `AcadBlock` | `BlockTableRecord` (SymbolTableRecord) |
| 重複チェック | `Scripting.Dictionary` | `System.Collections.Generic.HashSet
| 匿名判定 | `Left$(blk.Name, 1) = “”` | `BlockTableRecord.IsAnonymous` |
| レイアウト判定| `blk.IsLayout` | `BlockTableRecord.IsLayout` |
| トランザクション| 自動(プロパティアクセス毎) | 明示的 `TransactionManager.StartTransaction()` |
アーキテクトからの助言
業務自動化システムを設計する際、VBAを「単なる簡易スクリプト」と捉えて粗悪なコードを記述することは、将来のシステム移行(.NET化)における巨大な技術的負債となる。
COMオブジェクトの参照管理を徹底し、アルゴリズムレベルで計算量($O$記法)とメモリフットプリントを最適化する設計手法は、言語が変わっても不変のコア・コンピタンスである。まずは本稿のコードをテンプレートとし、貴社の図面管理システムの堅牢な第一歩として活用していただきたい。
