【テクニカル・上級編】【実務中級】AcadDocument.Utility.GetEntityで選択ミスを許容する「リトライ・ループ」の実装:実務で使える堅牢な選択ツール – AutoCAD VBA解析バイブル

スポンサーリンク

【実務中級】AcadDocument.Utility.GetEntityで選択ミスを許容する「リトライ・ループ」の実装:実務で使える堅牢な選択ツール

AutoCAD VBAを用いた開発において、誰もが最初に実装し、そして最も多くのバグとユーザーからのクレームを誘発するのがユーザーによる図形選択(インタラクティブ・セレクション)です。

入門書に書かれているような、単純に `AcadDocument.Utility.GetEntity` を呼び出すだけのコードは、実務の過酷な環境では全く役に立ちません。ユーザーは図形のない「空域」を平気でクリックしますし、選択の途中で気が変わり、Escキーを押して処理を中断しようとします。

これらの操作に対して、システムが「実行時エラー」を吐いて強制終了するか、あるいは「意図しない無限ループ」に陥ってAutoCADプロセスをフリーズさせるか――。本稿では、この極めて古典的でありながら、未だに多くのエンジニアを悩ませる「図形選択の堅牢化」について、COMオブジェクトのライフサイクル管理、Win32 APIによるハードウェア割り込み検知、そして完璧な例外処理を組み合わせた「実戦型リトライ・ループ」の極限の知見を共有します。

1. なぜ、標準の `GetEntity` は実務で崩壊するのか?

AutoCAD ActiveX オブジェクトモデルにおける `Utility.GetEntity` メソッドは、同期処理でユーザーの入力を待ち受けるブロック関数です。

‘ 入門書レベルの危険なコード
Dim returnObj As AcadEntity
Dim basePoint As Variant
ThisDrawing.Utility.GetEntity returnObj, basePoint, “図形を選択してください: ”

このコードが内包する、実務上の致命的な脆弱性は以下の3点に集約されます。

1. 空クリック(ミス選択)による例外崩壊
ユーザーが図形のない空間をクリックした場合、AutoCADは「選択失敗」として実行時エラー(HRESULT: `0x80200010` など)をスローします。これを適切にトラップしない限り、VBAマクロは即座にクラッシュします。
2. Escキー(ユーザーキャンセル)とミス選択の混同
ユーザーが「処理を中断したい」と思ってEscキーを押した場合と、単に「クリック位置がずれて図形を掴み損ねた」場合は、どちらも同じ「エラー」としてVBA側に通知されます。これらを識別できなければ、「Escキーを押しているのに、何度も選択を強要されるゾンビUI」が誕生します。
3. COM参照のゴースト化とメモリリーク
ループ内で `GetEntity` を繰り返す際、エラー発生時に内部的なCOMポインタが適切に初期化されないと、AutoCADのプロセス空間に解放されないメモリ(ゴーストオブジェクト)が蓄積され、最終的に図面データの破損や強制終了(Fatal Error)を引き起こします。

我々プロフェッショナルが目指すべきは、「空クリックは優しく無視して再試行を促し、Escキーによるキャンセルは即座に検知して安全にロールバックする」極限の堅牢性です。

2. 物理検知とHRESULTの解析:Escキーをいかに見極めるか

VBAの `Err.Number` だけでは、AutoCADが返すエラーの「真の理由」を特定できないケースがあります。特に、日本語OS環境やAutoCADのバージョン(LISPエンジンとの兼ね合い)によって、エラーメッセージの文字列やエラー番号が微妙に変動するためです。

そこで、本アーキテクチャでは以下のダブルチェック機構を採用します。

1. HRESULT(エラーコード)の厳密な判定
AutoCADが返すエラーコードのうち、ユーザーキャンセルは一般的に `-2147352567` (DISP_E_EXCEPTION) または `-2147467259` (E_FAIL) として現れ、`Err.Description` に「入力が取り消されました」や「User input is keyword」が格納されます。
2. Win32 API `GetAsyncKeyState` による物理キー検知
エラー発生の瞬間、物理的に Escキー(VK_ESCAPE: `0x1B`) が押されていたかどうかをWindows APIで直接問い合わせます。これにより、COMのエラーハンドリングだけに依存しない、100%確実な「ユーザーの意志による中断」を捕捉します。

3. 実践:堅牢極まる `SafeGetEntity` ラッパー関数

以下に、実務でそのまま再利用可能な、極めて堅牢な図形選択用ラッパー関数を示します。この関数は、指定されたオブジェクトタイプ(例: `AcDbLine`, `AcDbPolyline` 等)のフィルタリング機能も備えています。

標準モジュール:`ModSafeSelection`

Option Explicit

‘ ==============================================================================
‘ Windows API 定義
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function GetAsyncKeyState Lib “user32” (ByVal vKey As Long) As Integer
Else
Private Declare Function GetAsyncKeyState Lib “user32″ (ByVal vKey As Long) As Integer
End If

‘ 仮想キーコード (Escキー)
Private Const VK_ESCAPE As Long = &H1B

‘ ==============================================================================
‘ 構造体・定数定義
‘ ==============================================================================
Public Const ERR_USER_CANCEL As Long = vbObjectError + 51001

”’

”’ ユーザーが正しい図形を選択するまで、エラーを許容しながら入力を促し続ける堅牢な関数。
”’

”’ 選択されたエンティティを返す参照引数 ”’ 選択された座標(3D WCS)を返す参照引数 ”’ コマンドラインに表示するプロンプト ”’ 許可する図形タイプ(カンマ区切り、空なら全図形を許容。例: “AcDbLine,AcDbPolyline”) ”’ True: 正常選択、False: ユーザーキャンセル
Public Function SafeGetEntity( _
ByRef outEntity As AcadEntity, _
ByRef outPoint As Variant, _
ByVal promptMessage As String, _
Optional ByVal allowedTypes As String = “” _
) As Boolean

Dim acadDoc As AcadDocument
Set acadDoc = ThisDrawing

‘ 出力変数の初期化
Set outEntity = Nothing
outPoint = Empty

Dim isSelected As Boolean
isSelected = False

Dim tryCount As Long
tryCount = 0
Const MAX_TRIES As Long = 1000 ‘ 無限ループ防止のセーフティネット

‘ フィルター用配列の作成
Dim filterList() As String
Dim hasFilter As Boolean
If Trim(allowedTypes) <> “” Then
filterList = Split(LCase(Replace(allowedTypes, ” “, “”)), “,”)
hasFilter = True
Else
hasFilter = False
End If

‘ コマンドプロンプトのフォーカスを確実にする
acadDoc.Utility.Prompt vbCrLf

Do While Not isSelected
tryCount = tryCount + 1
If tryCount > MAX_TRIES Then
MsgBox “選択試行回数の上限に達しました。処理を中断します。”, vbCritical, “システムエラー”
SafeGetEntity = False
Exit Function
End If

‘ エラーハンドリングの開始
On Error Resume Next

Dim tempEnt As AcadEntity
Dim tempPt As Variant
Set tempEnt = Nothing
tempPt = Empty

‘ 選択の実行(ブロッキングコール)
acadDoc.Utility.GetEntity tempEnt, tempPt, promptMessage

‘ エラー発生時の処理
If Err.Number <> 0 Then
Dim errNum As Long
Dim errDesc As String
errNum = Err.Number
errDesc = Err.Description

On Error GoTo 0 ‘ ハンドラーの一時解除

‘ 1. Escキーが物理的に押されているか、または特定のキャンセルエラーコードか
If IsEscKeyPressed() Or _
InStr(errDesc, “入力が取り消されました”) > 0 Or _
InStr(errDesc, “User input is keyword”) > 0 Or _
errNum = -2147352567 Then

‘ ユーザーによる明示的なキャンセル
acadDoc.Utility.Prompt vbCrLf & “キャンセル” & vbCrLf
SafeGetEntity = False
Exit Function
End If

‘ 2. 単なる空クリック(ミス)の場合はループを継続
‘ メモリリーク防止のためCOM参照を明示的に解放
Set tempEnt = Nothing
Err.Clear

‘ ユーザーに注意喚起し、再入力を促す
acadDoc.Utility.Prompt vbCrLf & “【警告】空域がクリックされました。もう一度図形を選択してください。” & vbCrLf
GoTo ContinueLoop
End If

On Error GoTo 0 ‘ ハンドラーの復旧

‘ オブジェクトが正しく取得できた場合の検証
If Not tempEnt Is Nothing Then
Dim isValidType As Boolean
isValidType = True

‘ 型フィルターが指定されている場合、ObjectNameを検証
If hasFilter Then
isValidType = IsAllowedType(tempEnt.ObjectName, filterList)
End If

If isValidType Then
‘ すべての条件をクリア
Set outEntity = tempEnt
outPoint = tempPt
isSelected = True
Else
‘ 異なるオブジェクトタイプが選択された場合
acadDoc.Utility.Prompt vbCrLf & “【エラー】選択されたオブジェクトの種類 [” & tempEnt.ObjectName & “] は許可されていません。” & vbCrLf
Set tempEnt = Nothing
End If
End If

ContinueLoop:
‘ OSに制御を戻し、UIのフリーズやハングアップを防ぐ
DoEvents
Loop

SafeGetEntity = True
End Function

‘ ==============================================================================
‘ ヘルパー関数群
‘ ==============================================================================

”’

”’ Escキーが物理的に押されているか判定する
”’

Private Function IsEscKeyPressed() As Boolean
‘ GetAsyncKeyStateの最上位ビットが1の場合、キーが押されている
If (GetAsyncKeyState(VK_ESCAPE) And &H8000) <> 0 Then
IsEscKeyPressed = True
Else
IsEscKeyPressed = False
End If
End Function

”’

”’ 取得したオブジェクト名が許可リストに含まれているか検証する
”’

Private Function IsAllowedType(ByVal objectName As String, ByRef filterList() As String) As Boolean
Dim target As String
target = LCase(Trim(objectName))

Dim i As Long
For i = LBound(filterList) To UBound(filterList)
If target = filterList(i) Then
IsAllowedType = True
Exit Function
End If
Next i

IsAllowedType = False
End Function

4. この実装が極めて堅牢である理由

このコードには、長年の現場でのクラッシュ、トラブル対応から得られた「実践的な防御策」が組み込まれています。

① `DoEvents` によるUIデッドロックの回避

`Do…While` による同期ループは、一歩間違えるとAutoCAD全体の描画スレッドをロックし、プロセスを「応答なし」にします。ループの最後に `DoEvents` を挟むことで、Windowsメッセージキューが適切に処理され、AutoCADがOSから「フリーズしたプロセス」と誤認されるのを防ぎます。

② 明示的なCOM参照の破棄 (`Set tempEnt = Nothing`)

VBAのガベージコレクションは非決定的(Deterministicではない)です。ループの内部で、エラー時に `tempEnt` を明示的に `Nothing` に落とさないと、内部のC++オブジェクトに対するCOMの参照カウント(Reference Count)がデクリメントされず、図面を閉じるまでメモリ上にゾンビとして残り続けます。これが「数千回マクロを動かすとAutoCADが突然落ちる」という怪奇現象の真犯人です。

③ 物理キーボード状態のポーリングによる、多言語・複数バージョン対応

AutoCADのバージョンや、インストールされている業種別製品(AutoCAD ArchitectureやCivil 3Dなど)によっては、エラーの内部文字列が「入力が取り消されました」ではなく、英語表記や異なる文字列になることがあります。
Win32 APIの `GetAsyncKeyState` は、これらの環境差異を完全に無視し、「ユーザーがEscキーを叩いたという物理的事実」のみを根拠に中断処理を決定するため、グローバル展開する社内システムでも極めて高いポータビリティを誇ります。

5. 実戦での使用例:特定の図形(LINE/POLYLINE)のみを選択させる

以下は、作成した `SafeGetEntity` ラッパーを使用して、図面内から「線分(Line)」または「ポリライン(LightweightPolyline)」のみをユーザーに選択させる実装例です。

Sub ExecSelectionTool()
Dim selectedObj As AcadEntity
Dim pickPoint As Variant
Dim success As Boolean

‘ 選択可能なオブジェクトクラスを指定(AutoCADの内部オブジェクト名)
‘ Line = AcDbLine
‘ Polyline = AcDbPolyline, AcDb2dPolyline
Dim targetTypes As String
targetTypes = “AcDbLine,AcDbPolyline,AcDb2dPolyline”

Dim promptMsg As String
promptMsg = vbCrLf & “処理対象の線分、またはポリラインを選択してください: ”

‘ 堅牢な選択処理の呼び出し
success = SafeGetEntity(selectedObj, pickPoint, promptMsg, targetTypes)

If success Then
‘ 正常に取得できた場合の処理
ThisDrawing.Utility.Prompt vbCrLf & “【システム】正常に選択されました。” & vbCrLf
ThisDrawing.Utility.Prompt “オブジェクト名: ” & selectedObj.ObjectName & vbCrLf
ThisDrawing.Utility.Prompt “選択座標: X=” & Format(pickPoint(0), “0.00”) & ” Y=” & Format(pickPoint(1), “0.00”) & vbCrLf

‘ ここに対象オブジェクトに対する本来のビジネスロジックを記述する
selectedObj.Highlight True
selectedObj.Update
Else
‘ ユーザーキャンセル時の処理
MsgBox “ユーザーによって処理が取り消されました。”, vbInformation, “処理中断”
End If

‘ 最後に必ず参照をクリーンアップ
Set selectedObj = Nothing
End Sub

6. アーキテクトの視点:レガシーを「枯れた超兵器」に変える

AutoCAD VBAはレガシーな技術として扱われがちですが、適切に設計されたVBAコードは、.NET(ObjectARX / AutoCAD .NET API)で書かれた重厚なアドインと同等か、それ以上の軽快さと堅牢性を発揮します。

実務で求められるのは、最新の技術を追うことではなく、「目の前で動いているシステムが、何があっても途中で落ちないこと」です。今回紹介した「リトライ・ループ」は、まさにその思想を具現化したものです。

ユーザーのどのような予期せぬ操作(ミス、連打、乱暴なキャンセル)をも受け止め、システムをエレガントに制御し続ける。この「泥臭くも圧倒的に堅牢な設計」こそが、エンタープライズ開発における最高峰の知見です。あなたのコードを、ただ「動く」レベルから、「誰も壊せない」レベルへと引き上げてください。

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