AutoCAD VBAを掌握する極限の知見:図面ファイルに宿る機密
図面管理システムと外部データベース(SQL Server)の融合。これは製造業やプラントエンジニアリングの現場において、避けて通れない聖域である。
図面(DWG)を開いた瞬間に、背後にある巨大なDBから部品表、工程進捗、品質データを動的に引き込む――。このアーキテクチャを実現する際、最大のボトルネックとなるのが「接続情報の保持とセキュリティ」だ。
接続文字列(Connection String)を平文でソースコードにハードコーディングする、あるいは外部のINIファイルやレジストリに依存する。このような設計は、配布時のパス整合性の崩壊や、認証情報の露出という致命的なセキュリティホールを生む。
答えは一つしかない。「図面ファイル(AcadDocument)のライフサイクルの中に、暗号化された接続情報を完全に封じ込める」ことだ。
今回は、AutoCAD VBAのオブジェクトモデルの深層とWindows API、そして`ExtensionDictionary`(拡張ディクショナリ)を極限まで駆使し、図面とセキュアに一体化するデータベース連携のアーキテクチャを解説する。
—
1. アーキテクチャの全体像:なぜExtensionDictionaryなのか?
AutoCADの図面ファイル(`AcadDocument`)は、単なる図形のコンテナではない。COMのレイヤーにおいて、それは高度にカプセル化されたデータベースそのものだ。
図面固有のメタデータを保持する手段として、古くは「Xデータ(Extended Data)」が存在した。しかし、Xデータは容量制限(16KB)が厳しく、バイナリや構造化されたデータの保持には耐えられない。ここで登場するのが、オブジェクト指向の極みである「拡張ディクショナリ(Extension Dictionary)」だ。
- 図面との運命共同体: 図面を別名保存(`SaveAs`)しようとも、クラウドやメールで共有されようとも、`AcadDocument.ExtensionDictionary`に格納されたデータは図面ストリームの一部として不可分に帯同する。
- 名前空間の隔離: 独自のディクショナリキー(例: `”EnterpriseDB_Config”`)を占有することで、他のアドインやAutoCAD本体のデータと衝突しない。
ここに、Windows標準の暗号化API(DPAPI)を組み合わせることで、「その図面を開く権限を持つユーザー/環境でのみ復元可能な接続文字列」を構築する。
—
2. Windows APIによる強力な暗号化(DPAPI)の導入
VBA単体で強固な暗号化を実装しようとすると、レガシーなXOR演算や脆弱なアルゴリズムに頼りがちになる。しかし、我々にはWindows OSが標準提供するDPAPI(Data Protection API)がある。外部DLLのインポート不要で、カレントユーザーのコンテキストに依存した堅牢な暗号化・復号化をノーコストで実現できる。
以下のコードは、暗号化と拡張ディクショナリへの永続化を司るコアエンジンである。
Option Explicit
‘ =================================================================
‘ 拡張ディクショナリ & DPAPI 暗号化管理モジュール
‘ =================================================================
Private Const DICTIONARY_NAME As String = “SecuredDBConfig”
Private Const RECORD_KEY As String = “EncryptedConnectionString”
‘ Windows API for DPAPI (CryptProtectData / CryptUnprotectData)
If VBA7 Then
Private Declare PtrSafe Function CryptProtectData Lib “crypt32.dll” ( _
pDataIn As DATA_BLOB, ByVal szDataDescr As LongPtr, pEntropy As Any, _
ByVal pvReserved As LongPtr, pPromptStruct As Any, ByVal dwFlags As Long, pDataOut As DATA_BLOB) As Long
Private Declare PtrSafe Function CryptUnprotectData Lib “crypt32.dll” ( _
pDataIn As DATA_BLOB, szDataDescr As LongPtr, pEntropy As Any, _
ByVal pvReserved As LongPtr, pPromptStruct As Any, ByVal dwFlags As Long, pDataOut As DATA_BLOB) As Long
Private Declare PtrSafe Sub LocalFree Lib “kernel32” (ByVal hMem As LongPtr)
Private Declare PtrSafe Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As Long)
Else
‘ 32bit環境への配慮(レガシー保守用)
Private Declare Function CryptProtectData Lib “crypt32.dll” ( _
pDataIn As DATA_BLOB, ByVal szDataDescr As Long, pEntropy As Any, _
ByVal pvReserved As Long, pPromptStruct As Any, ByVal dwFlags As Long, pDataOut As DATA_BLOB) As Long
Private Declare Function CryptUnprotectData Lib “crypt32.dll” ( _
pDataIn As DATA_BLOB, szDataDescr As Long, pEntropy As Any, _
ByVal pvReserved As Long, pPromptStruct As Any, ByVal dwFlags As Long, pDataOut As DATA_BLOB) As Long
Private Declare Sub LocalFree Lib “kernel32” (ByVal hMem As Long)
Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As Long)
End If
Private Type DATA_BLOB
cbData As Long
#If VBA7 Then
pbData As LongPtr
#Else
pbData As Long
#End If
End Type
‘ —————————————————————–
‘ 公開メソッド: 現在の図面にSQL Serverの接続文字列を暗号化して保存
‘ —————————————————————–
Public Sub SaveEncryptedConnectionString(ByVal connString As String)
On Error GoTo ErrorHandler
Dim doc As AcadDocument
Set doc = ThisDrawing
‘ 1. DPAPIによる文字列のバイト配列暗号化
Dim encBytes() As Byte
If Not EncryptString(connString, encBytes) Then
Err.Raise 9999, “DPAPI”, “接続文字列の暗号化に失敗しました。”
End If
‘ 2. AcadDocumentの拡張ディクショナリを取得または作成
Dim extDict As AcadDictionary
Set extDict = GetOrCreateExtensionDictionary(doc)
‘ 3. 既存のXRecordがあれば削除(クリーンナップ)
On Error Resume Next
extDict.RemoveItem RECORD_KEY
On Error GoTo ErrorHandler
‘ 4. XRecord(拡張レコード)の生成とバイナリデータの格納
Dim xRec As AcadXRecord
Set xRec = extDict.AddXRecord(RECORD_KEY)
‘ XRecordに格納するための型コードと値の配列を構築
Dim dataType(0) As Integer
Dim dataValue(0) As Variant
dataType(0) = 302 ‘ DXFGroupCode: 文字列 または バイナリ用(今回はバリアント配列経由でByte配列を格納)
‘ VBAのVariant配列にByte配列を渡す
dataValue(0) = encBytes
xRec.SetXRecordData dataType, dataValue
‘ 5. 図面のデータベース変更フラグを立てる(保存促すため)
doc.SetVariable “FILEDIA”, doc.GetVariable(“FILEDIA”) ‘ ダミー操作または明示的な保存処理
MsgBox “接続文字列を図面にセキュアに埋め込みました。”, vbInformation, “Architecture Notice”
Exit Sub
ErrorHandler:
MsgBox “エラー発生: ” & Err.Description, vbCritical
End Sub
‘ —————————————————————–
‘ 公開メソッド: 図面から暗号化された接続文字列を復元して取得
‘ —————————————————————–
Public Function LoadDecryptedConnectionString() As String
On Error GoTo ErrorHandler
Dim doc As AcadDocument
Set doc = ThisDrawing
‘ 1. 拡張ディクショナリの存在確認
On Error Resume Next
Dim extDict As AcadDictionary
Set extDict = doc.ExtensionDictionary
If Err.Number <> 0 Or extDict Is Nothing Then
Err.Raise 10001, “DBConfig”, “この図面には拡張ディクショナリが存在しません。”
End If
‘ 2. XRecordの取得
Dim xRec As AcadXRecord
Set xRec = extDict.Item(RECORD_KEY)
If Err.Number <> 0 Or xRec Is Nothing Then
Err.Raise 10002, “DBConfig”, “接続情報レコードが見つかりません。”
End If
On Error GoTo ErrorHandler
‘ 3. XRecordからデータの抽出
Dim dataType As Variant
Dim dataValue As Variant
xRec.GetXRecordData dataType, dataValue
Dim encBytes() As Byte
encBytes = dataValue(0)
‘ 4. DPAPIによる復号化
Dim plainStr As String
If Not DecryptBytes(encBytes, plainStr) Then
Err.Raise 10003, “DPAPI”, “接続文字列の復号化に失敗しました(権限または環境の不一致)。”
End If
LoadDecryptedConnectionString = plainStr
Exit Function
ErrorHandler:
‘ 異常時は空文字を返すかログを残す
LoadDecryptedConnectionString = “”
‘ Debug.Print “Load Error: ” & Err.Description
End Function
‘ =================================================================
‘ 内部ヘルパー関数群(メモリ管理とDPAPIラップ)
‘ =================================================================
Private Function GetOrCreateExtensionDictionary(ByVal obj As AcadObject) As AcadDictionary
On Error Resume Next
Dim dict As AcadDictionary
Set dict = obj.ExtensionDictionary
If dict Is Nothing Then
Set dict = obj.CreateExtensionDictionary()
End If
On Error GoTo 0
Set GetOrCreateExtensionDictionary = dict
End Function
Private Function EncryptString(ByVal plainText As String, ByRef outBytes() As Byte) As Boolean
Dim bIn() As Byte
bIn = plainText & vbNullChar ‘ ヌル終端
Dim blobIn As DATA_BLOB
Dim blobOut As DATA_BLOB
blobIn.cbData = UBound(bIn) – LBound(bIn) + 1
blobIn.pbData = VarPtr(bIn(0))
If CryptProtectData(blobIn, 0, ByVal 0&, 0, ByVal 0&, 0, blobOut) <> 0 Then
ReDim outBytes(blobOut.cbData – 1)
CopyMemory outBytes(0), ByVal blobOut.pbData, blobOut.cbData
LocalFree blobOut.pbData
EncryptString = True
Else
EncryptString = False
End If
End Function
Private Function DecryptBytes(ByRef inBytes() As Byte, ByRef outText As String) As Boolean
Dim blobIn As DATA_BLOB
Dim blobOut As DATA_BLOB
blobIn.cbData = UBound(inBytes) – LBound(inBytes) + 1
blobIn.pbData = VarPtr(inBytes(0))
If CryptUnprotectData(blobIn, 0, ByVal 0&, 0, ByVal 0&, 0, blobOut) <> 0 Then
Dim bOut() As Byte
ReDim bOut(blobOut.cbData – 1)
CopyMemory bOut(0), ByVal blobOut.pbData, blobOut.cbData
LocalFree blobOut.pbData
outText = CStr(bOut)
‘ ヌル文字の除去
If InStr(outText, vbNullChar) > 0 Then
outText = Left(outText, InStr(outText, vbNullChar) – 1)
End If
DecryptBytes = True
Else
DecryptBytes = False
End If
End Function
—
3. シニアエンジニアが知るべき「メモリ最適化」と「オブジェクトライフサイクル」の罠
AutoCAD VBAにおいて、COMオブジェクトの解放を怠ることは「メモリリークの温床」を作るに等しい。特に`AcadDocument`や`AcadDictionary`、`AcadXRecord`といった階層構造を走査・操作する際、背後ではARX(AutoCAD Runtime Extension)のC++オブジェクトとCOMラッパーの間で参照カウンタが厳密に管理されている。
1. ローカル変数の明示的解放 (`Set … = Nothing`)
上記のコードブロックを見てほしい。オブジェクト変数(`doc`, `extDict`, `xRec`)は、プロシージャの終了時にスコープを抜けることで自動的に解放されるように見える。しかし、VBAのガベージコレクションやCOMの参照カウンタの解放タイミングは決定論的ではない。
大量の図面をバッチ処理するツール(例えば、ドキュメント管理サーバーからの自動インポートバッチなど)を構築する場合、明示的な `Set obj = Nothing` の記述がシステムの寿命を左右する。
2. 多重参照の排除
`ThisDrawing`というグローバルショートカットは便利だが、大規模システムではアンチパターンとなることがある。マルチドキュメント環境(SDI/MDI)において、ユーザーが複数の図面を開閉する最中に非同期処理やイベントが走ると、`ThisDrawing`が指す実体が予期せず変化する。
「操作対象のドキュメントは必ずローカル変数にキャプチャし、そのスコープ内で完結させる」――これがレガシー環境を生き抜く鉄則だ。
—
4. SQL Server連携の実践:図面からトランザクションへ
暗号化された接続文字列の復元ができれば、あとは通常のADO(ActiveX Data Objects)を用いた堅牢なSQL Server連携を行うだけだ。
以下は、図面に埋め込まれた接続情報を用いて、図面内の特定の属性ブロック値からSQL Server上のマスターデータを検索・同期する実務的なプロシージャの断片である。
Public Sub SyncDrawingWithSQLServer()
‘ 1. 図面から接続文字列を復元
Dim connStr As String
connStr = LoadDecryptedConnectionString()
If connStr = “” Then
MsgBox “有効なデータベース接続情報がこの図面に存在しません。”, vbExclamation
Exit Sub
End If
‘ 2. ADOによる高速な接続と処理
Dim conn As Object
Dim cmd As Object
Set conn = CreateObject(“ADODB.Connection”)
Set cmd = CreateObject(“ADODB.Command”)
On Error GoTo DBError
conn.ConnectionString = connStr
conn.Open
Set cmd.ActiveConnection = conn
cmd.CommandText = “UPDATE DrawingMaster SET LastAccessDate = GETDATE() WHERE DrawingName = ?”
‘ パラメータ化クエリによるSQLインジェクション対策(エンジニアの基本)
‘ ※実際の業務では図面名や管理IDをDrawing変数から取得してバインドする
conn.Close
MsgBox “データベースとの同期が完了しました。”, vbInformation
Exit Sub
DBError:
MsgBox “DB通信エラー: ” & Err.Description, vbCritical
If Not conn is Nothing Then
If conn.State = 1 Then conn.Close
End If
End Sub
—
5. 結び:技術の真髄は「自己完結性」に宿る
外部ファイルへの依存は、システムを脆弱にする。
「図面ファイルがそこにある限り、必要なインフラストラクチャの文脈もすべてその中に内包されていること」。
今回紹介した `AcadDocument.ExtensionDictionary` と `DPAPI` を組み合わせたセキュアな設計手法は、単なるコードのテクニックにとどまらず、配布・運用・セキュリティのすべてのジレンマを解決するプロフェッショナルのアプローチである。
レガシーとモダンが交差するAutoCAD VBAの世界において、コードの美しさと強靭さを両立させるのは、いつだって細部へのこだわりを捨てないエンジニアの矜持に他ならない。
