【上級プロフェッショナル向け】マルチインスタンス環境でのAcadApplication特定と対象ドキュメントのバインド制御
設計・開発プロジェクトのリーダー諸君、そして AutoCAD VBA による自動化で現場の生産性を劇的に向上させたいと志すプロフェッショナル諸君。
君たちが日々直面しているであろう、ある一つの課題。それは、複数の AutoCAD インスタンスが起動している環境下で、VBA が意図した正しい AutoCAD アプリケーションと、その中の特定の図面ファイル(ドキュメント)を確実に捕捉し、操作することだ。
「いや、普通に `ThisDrawing` でいいじゃないか」と思った君、それはまだ初段。あるいは、単一インスタンス環境での開発に留まっている証拠だ。しかし、一度でも、複数の AutoCAD が立ち上がり、意図しないインスタンスにコマンドが飛んでしまい、図面が破壊されたり、貴重な作業時間が無駄になった経験があるなら、この話の重要性が理解できるはずだ。
本記事では、単なるリファレンスの羅列や、表面的なコード例に終始することなく、オブジェクトのライフサイクル、パフォーマンスへの影響、そして何よりも「バグの起きない堅牢な設計」という、プロフェッショナルが最も重視すべき観点から、この複雑な問題を解き明かす。Win32 API を駆使し、マルチインスタンス環境における `AcadApplication` の特定と、対象ドキュメントのバインド制御を、魂を込めて伝授しよう。
なぜ `ThisDrawing` だけでは不十分なのか?
まず、なぜ我々が `ThisDrawing` に頼るだけでは、マルチインスタンス環境で足元をすくわれるのかを明確にしよう。
- `ThisDrawing` のスコープ: VBA における `ThisDrawing` は、VBA マクロが現在実行されている AutoCAD インスタンスの、アクティブな図面ドキュメントを指す。これは非常に便利だが、あくまで「実行されているインスタンス」という文脈に限定される。
- マルチインスタンスの現実: ユーザーは、複数の図面を並行して作業するために、意図的に複数の AutoCAD インスタンスを起動することがある。例えば、設計レビューのために参照図面を開いたまま、自身の担当図面を編集するといったシナリオだ。
- 意図しない操作のリスク: ここで、もし君が、ある特定の図面(例えば、共有サーバー上のマスター図面)に対して、バックグラウンドで何らかの処理を実行する VBA マクロを開発したとしよう。そのマクロが、たまたまアクティブだった別のインスタンスの、全く関係のない図面に対して処理を実行してしまったら? 結果は火を見るより明らかだ。データ破損、意図しない変更、そして何よりも、デバッグに費やされる膨大な時間と、失われる信頼。
これは、単なる「ちょっとした不便」ではない。プロダクションコードとして、あるいは基幹業務の自動化ツールとして、絶対に回避しなければならないリスクなのだ。
Win32 API による `AcadApplication` 特定の鍵
では、このリスクを回避し、目的の `AcadApplication` インスタンスを確実に捕捉するにはどうすれば良いか。ここで登場するのが、Windows の低レベル API、すなわち Win32 API の活用だ。
AutoCAD は COM (Component Object Model) オブジェクトモデルを通じて VBA に機能を提供しているが、その背後には Windows のプロセスとして動作している。Win32 API を使うことで、このプロセスレベルでの情報にアクセスし、より確実な特定が可能になる。
1. プロセスID (PID) の取得と列挙
Windows 上で実行されている各プログラムは、一意のプロセスID (PID) を持つ。AutoCAD の各インスタンスも、それぞれ異なる PID を持つ。この PID を取得し、列挙することで、実行中の AutoCAD プロセスを特定する第一歩となる。
`EnumProcesses` 関数や `CreateToolhelp32Snapshot` / `Process32First` / `Process32Next` といった API を利用して、システム上の全プロセスIDを取得し、その中から AutoCAD に関連するもの(`acad.exe`)をフィルタリングする。
2. プロセスから `IAcadApplication` オブジェクトへのバインド
PID を特定しただけでは、まだ VBA から操作できる `AcadApplication` オブジェクトは得られない。ここからが本番だ。COM の世界では、`CoCreateInstance` や `GetActiveObject` といった関数が、実行中の COM サーバー(この場合は AutoCAD)のインスタンスを取得するために使われる。
しかし、ここで問題になるのが、複数の AutoCAD インスタンスが存在する場合、`GetActiveObject` は通常、最初に登録された、あるいは最もアクティブなインスタンスを返してしまうということだ。
そこで、PID を使って、より具体的に対象の `AcadApplication` インスタンスにアクセスする必要が出てくる。これは、一般的に以下のステップで行われる。
1. プロセスハンドル (Process Handle) の取得: 特定した PID を用いて、`OpenProcess` API を呼び出し、そのプロセスのハンドルを取得する。
2. プロセス内の COM オブジェクトへのアクセス: プロセスハンドルと、AutoCAD が使用する COM インターフェース (CLSID) を利用して、そのプロセス内で動作している `IAcadApplication` インスタンスへのポインタを取得する。この部分で `IUnknown` インターフェースを介したやり取りや、Marshal Assemby (MTA) / Single Threaded Apartment (STA) の違いなどを考慮する必要が出てくる場合があるが、VBA から直接これらの低レベルな操作を行うのは難易度が高い。
3. VBA からの現実的なアプローチ:`GetObject` と `CreateObject` の賢い使い方
Win32 API を直接 VBA から呼び出して、プロセス内の COM オブジェクトにアクセスするのは、非常に高度で、かつ VB6 や VB.NET のような環境であれば容易だが、VBA ではAPI呼び出しのラッパーなどを自作する必要があり、実装のハードルが格段に上がる。
そこで、VBA から現実的かつ堅牢にマルチインスタンス環境で `AcadApplication` を特定するためには、以下の戦略が有効だ。
- `GetObject` を利用した既存インスタンスの検索: `GetObject` 関数は、実行中の COM オブジェクトを取得するために使用できる。ここで重要なのは、`GetObject` に渡すプログラムID(ProgID)に、単に `”AutoCAD.Application”` ではなく、特定のインプロセスインスタンスを識別するための情報(例えば、起動時に登録したユニークなキーなど)を付与するという高度なテクニックだ。
- Registry/WMI の活用(より高度なシナリオ): 完全に低レベルなアプローチとしては、Windows Registry や WMI (Windows Management Instrumentation) を利用して、実行中の AutoCAD プロセスとその COM インターフェースへのポインタを間接的に取得する方法もある。しかし、これはさらに実装の複雑さを増す。
より実践的なアプローチとして、ここでは `GetObject` と、起動時の「登録」という概念を組み合わせた方法に焦点を当てる。
実践コード例:マルチインスタンス環境での `AcadApplication` 特定とバインド
以下に、複数の AutoCAD インスタンスが起動している環境で、特定のインスタンスを名前で識別し、そのインスタンスのアクティブなドキュメントを操作する VBA コード例を示す。
このコードは、「各 AutoCAD インスタンスが起動する際に、自分自身を特定の名前で COM サーバーとして登録する」という前提に立っている。この登録は、VBA から直接行うのは難しいが、もし外部(例えば、VB.NET で作成された起動スクリプトなど)から AutoCAD を起動する際に、この登録処理を組み込めるのであれば、以下の VBA コードがそのインスタンスを確実に捕捉できるようになる。
前提:起動時の登録処理(外部スクリプト例:VB.NET)
まず、VBA 側で対象のインスタンスを特定するための「登録」処理を、VB.NET で行う例を示す。これは VBA コードそのものではないが、VBA コードが正しく動作するための「仕掛け」として理解してほしい。
.net
‘ VB.NET コード例 (AutoCAD 起動時に実行される想定)
‘ このコードは、AutoCAD のインスタンスを起動し、
‘ 特定の名前 (例: “MyAutoCAD_Instance_1″) で COM サーバーとして登録します。
Imports Autodesk.AutoCAD.Interop
Imports System.Runtime.InteropServices
Public Class AutoCADLauncher
Public Sub LaunchAndRegister(instanceName As String)
Dim acadApp As AcadApplication = Nothing
Dim runningObjectTable As IRunningObjectTable = Nothing
Dim moniker As IMoniker = Nothing
Dim bh As Integer = 0 ‘ Bind Handle
Try
‘ AutoCAD が既に起動しているか確認し、存在しなければ起動する
Try
‘ 登録された名前で GetObject を試みる
acadApp = CType(GetObject(Nothing, instanceName), AcadApplication)
Console.WriteLine($”‘{instanceName}’ として既存の AutoCAD インスタンスが見つかりました。”)
Catch ex As Exception
‘ 起動していない場合は新規に起動
Console.WriteLine($”‘{instanceName}’ として AutoCAD を起動します。”)
Dim acadType As Type = Type.GetTypeFromProgID(“AutoCAD.Application”)
acadApp = CType(Activator.CreateInstance(acadType), AcadApplication)
acadApp.Visible = True ‘ 必要に応じて可視化
End Try
‘ 実行オブジェクトテーブル (ROT) を取得
runningObjectTable = GetRunningObjectTable(0)
‘ 新しいモニカーを作成 (インスタンス名をキーとする)
Dim progId As String = GetProgID(acadApp) ‘ 例: “AutoCAD.Application.24″
Dim bindCtx As IBindCtx = Nothing
CreateBindCtx(0, bindCtx)
‘ プログラムIDとインスタンス名を組み合わせたモニカーを作成
‘ ここが重要: インスタンス名で一意に識別できるようにする
Dim instanceMoniker As IMoniker = GetMoniker(progId, instanceName)
runningObjectີble.RegisterObjectInstance(instanceMoniker, acadApp, bh)
Console.WriteLine($”AutoCAD インスタンス ‘{instanceName}’ を COM ROT に登録しました。”)
Catch ex As Exception
Console.WriteLine($”エラーが発生しました: {ex.Message}”)
Finally
‘ クリーンアップ (省略)
End Try
End Sub
‘ GetObject のためのヘルパー関数 (実際には System.Runtime.InteropServices.Marshal を利用)
Private Function GetObject(server As Object, path As String) As Object
Dim result As Object = Nothing
Dim obj As Object = Nothing
Dim bindCtx As IBindCtx = Nothing
Dim moniker As IMoniker = Nothing
Dim enumMoniker As IEnumMoniker = Nothing
Dim currentMoniker As IMoniker = Nothing
Dim fetched As Integer = 0
Try
CreateBindCtx(0, bindCtx)
Dim progId As String = If(server Is Nothing, path, GetProgID(server))
‘ ProgID からモニカーを作成
Dim clsid As Guid
GetClassID(progId, clsid)
CreateClassMoniker(clsid, moniker)
‘ 実行中のオブジェクトテーブルから enumerator を取得
Dim rot As IRunningObjectTable = GetRunningObjectTable(0)
rot.EnumRunning(enumMoniker)
‘ 全ての実行中オブジェクトを列挙し、一致するものを探す
While enumMoniker.Next(1, currentMoniker, fetched) = 0 AndAlso fetched = 1
Dim name As String = “”
currentMoniker.GetDisplayName(bindCtx, Nothing, name)
‘ インスタンス名で比較 (ここが鍵)
If name.Contains(path) Then
obj = rot.GetObject(currentMoniker)
If TypeOf obj Is AcadApplication Then
‘ 取得したオブジェクトが目的の型か確認
result = obj
Exit While
End If
End If
End While
If result Is Nothing Then
Throw New Exception(“指定された名前の AutoCAD インスタンスは見つかりませんでした。”)
End If
Catch ex As Exception
Throw ex
Finally
‘ クリーンアップ (省略)
End Try
Return result
End Function
‘ COM オブジェクトの ProgID を取得するヘルパー
Private Function GetProgID(obj As Object) As String
Dim progId As String = “”
Dim clsid As Guid = obj.GetType().GUID
Dim hr As Integer = GetProgIDOfClass(clsid, progId)
If hr <> 0 Then Throw Marshal.GetExceptionForHR(hr)
Return progId
End Function
‘ インスタンス名をキーとしたモニカーを作成するヘルパー
Private Function GetMoniker(progId As String, instanceName As String) As IMoniker
Dim clsId As Guid
GetClassID(progId, clsId)
Dim moniker As IMoniker
CreateClassMoniker(clsId, moniker)
Dim bindCtx As IBindCtx = Nothing
CreateBindCtx(0, bindCtx)
‘ プログラムIDとインスタンス名を組み合わせた文字列を作成
‘ COM の規約に従って、ROT で一意に識別できるような文字列にする
Dim rotEntryName As String = $”AutoCAD.Application/{instanceName}” ‘ 例
Dim compositeMoniker As IMoniker = Nothing
‘ 文字列モニカーを作成し、クラスモニカーと合成する
Dim strMoniker As IMoniker = Nothing
MkParseDisplayName(bindCtx, rotEntryName, Nothing, strMoniker)
moniker.ComposeWith(strMoniker, New VariantWrapper(1), compositeMoniker) ‘ 1はコンポジションフラグ
Return compositeMoniker
End Function
‘ ROT 関連の Win32 API 宣言
Private Shared Function GetRunningObjectTable(ByVal reserved As Integer, ByRef prot As IRunningObjectTable) As Integer
End Function
Private Shared Function CreateBindCtx(ByVal reserved As Integer, ByRef ppbc As IBindCtx) As Integer
End Function
Private Shared Function CreateClassMoniker(ByRef rclsid As Guid, ByRef ppmk As IMoniker) As Integer
End Function
Private Shared Function MkParseDisplayName(ByVal pbc As IBindCtx, ByVal szDisplayName As String, ByVal pmkContext As IMoniker, ByRef ppmk As IMoniker) As Integer
End Function
Private Shared Function GetProgIDOfClass(ByRef clsid As Guid, ByRef lpszProgID As String) As Integer
End Function
Private Shared Function GetClassID(ByVal lpszProgID As String, ByRef pclsid As Guid) As Integer
End Function
‘ COM インターフェースの宣言 (省略)
‘ IRunningObjectTable, IMoniker, IBindCtx, IEnumMoniker など
‘ これらは .NET Framework の Marshal クラスで利用可能だが、
‘ より詳細な制御のために定義が必要になる場合がある。
‘ ここでは、概念を示すため省略する。
‘ 実際には、System.Runtime.InteropServices 名前空間のクラスを利用するか、
‘ Interop.dll から取得する必要がある。
End Class
‘ — VBA からの呼び出し例 —
‘ Sub Main()
‘ Dim launcher As New AutoCADLauncher
‘ launcher.LaunchAndRegister(“MyAutoCAD_Instance_1”)
‘ launcher.LaunchAndRegister(“MyAutoCAD_Instance_2”)
‘ End Sub
補足: 上記 VB.NET コードは、`GetObject` の実装や ROT への登録方法など、COM の低レベルな部分に触れています。VBA から直接 `GetObject` を使って名前でインスタンスを取得するには、この「登録」が鍵となります。VB.NET でこのような外部ツールを作成し、VBA から `GetObject(“AutoCAD.Application.XX”, “MyAutoCAD_Instance_XX”)` のように呼び出すのが、現実的な方法です。
VBA コード例:特定の `AcadApplication` インスタンスの特定とドキュメント操作
次に、上記のような「登録」が行われた `AcadApplication` インスタンスを、VBA から特定し、その中の特定の図面ドキュメントを操作するコードを示します。
Option Explicit
‘——————————————————————————-
‘ @Sub: GetSpecificAcadApplication
‘ @Desc: 指定された名前で登録された AutoCAD インスタンスを取得する。
‘ この関数は、外部 (例: VB.NET) によって実行オブジェクトテーブル (ROT) に
‘ 登録されたインスタンスを特定するために使用される。
‘ @Param: targetInstanceName (String): 取得したい AutoCAD インスタンスの名前 (ROT登録名)。
‘ @Return: AcadApplication: 正常に取得できた AcadApplication オブジェクト。
‘ 失敗した場合は Nothing を返す。
‘ @Usage: Set acadApp = GetSpecificAcadApplication(“MyAutoCAD_Instance_1”)
‘——————————————————————————-
Function GetSpecificAcadApplication(ByVal targetInstanceName As String) As AcadApplication
On Error Resume Next ‘ エラー発生時も処理を継続
Dim acadApp As AcadApplication = Nothing
Dim runningObjectTable As Object = Nothing
Dim enumMoniker As Object = Nothing
Dim currentMoniker As Object = Nothing
Dim fetched As Integer = 0
Dim bindCtx As Object = Nothing
Dim progId As String = “”
Dim clsId As Guid
Dim moniker As Object = Nothing
‘ Windows API 宣言 (VBA で COM オブジェクトを扱うためのヘルパー)
‘ これらは、VBA から直接 Win32 API を呼び出すのではなく、
‘ COM オブジェクトのインターフェースを介して間接的に利用する。
‘ 実際には、VBA から直接これらの API を呼び出すのは困難なため、
‘ Type Library の参照設定や、外部 DLL の利用が必要になる。
‘ ここでは、概念的な理解のために、COM の標準的なインターフェース操作として記述する。
‘ 実行オブジェクトテーブル (ROT) を取得
‘ VBA から直接 Win32 API の GetRunningObjectTable を呼ぶのは難しい。
‘ 通常は、Type Library (例: Microsoft ActiveX Data Objects X.X Library)
‘ や、外部の COM ライブラリを利用する。
‘ ここでは、PowerShell など外部ツールから AutoCAD を起動し、
‘ ROT に登録させることを前提とする。
‘ 試行錯誤の結果、VBA から GetObject を直接利用し、
‘ ProgID + インスタンス名で特定するのが最も現実的。
‘ ただし、ProgID のバージョン指定は重要。
‘ 例: “AutoCAD.Application.24” (AutoCAD 2020 の場合)
Dim acadVersionProgID As String = “”
Dim i As Integer
‘ 複数の AutoCAD バージョンがインストールされている可能性を考慮
Dim possibleProgIDs() As String = {“AutoCAD.Application.24”, “AutoCAD.Application.23”, “AutoCAD.Application.22”, “AutoCAD.Application.21”, “AutoCAD.Application.20”, “AutoCAD.Application.19”, “AutoCAD.Application.18”, “AutoCAD.Application.17”, “AutoCAD.Application.16”, “AutoCAD.Application”}
For i = 0 To UBound(possibleProgIDs)
acadVersionProgID = possibleProgIDs(i)
‘ GetObject で、ProgID とインスタンス名を指定して取得を試みる
‘ 注意: GetObject の第2引数は、外部プロセスに渡すためのインターフェース名や
‘ ROT で登録されたオブジェクトを特定するためのキーになる。
‘ ここで指定する “targetInstanceName” は、外部で登録された際の名前と一致する必要がある。
On Error Resume Next ‘ GetObject が失敗しても次の ProgID で試す
Set acadApp = GetObject(acadVersionProgID, targetInstanceName)
On Error GoTo 0 ‘ エラーハンドリングを元に戻す
If Not acadApp Is Nothing Then
‘ 取得成功!
Exit For
End If
Next i
‘ 最終的な確認
If acadApp Is Nothing Then
Debug.Print “エラー: 指定された名前 ‘” & targetInstanceName & “‘ の AutoCAD インスタンスが見つかりませんでした。”
Set GetSpecificAcadApplication = Nothing
Exit Function
End If
‘ 取得した AcadApplication オブジェクトの確認
‘ 必要であれば、さらにプロパティなどで詳細な検証を行う
If Not acadApp.Caption Like ” – ” Then ‘ 仮のチェック: タイトルに “-” が含まれていない場合など
‘ 予期しないインスタンスが取得された可能性
Debug.Print “警告: ‘” & targetInstanceName & “‘ で取得しましたが、想定外のインスタンスかもしれません。”
‘ ここで、さらに詳細な検証 (例: 特定のレジストリキーの存在確認など) を追加することも可能
End If
Set GetSpecificAcadApplication = acadApp
On Error GoTo 0 ‘ エラーハンドリングをリセット
End Function
‘——————————————————————————-
‘ @Sub: BindToDocumentAndProcess
‘ @Desc: 特定の AcadApplication インスタンス内の、指定された名前の図面ファイルに
‘ バインドし、処理を実行する。
‘ @Param: acadApp (AcadApplication): 対象の AutoCAD アプリケーションオブジェクト。
‘ @Param: documentName (String): 対象の図面ファイル名 (フルパス)。
‘ @Param: targetInstanceName (String): デバッグ用。対象インスタンスの名前。
‘ @Usage: BindToDocumentAndProcess acadApp, “C:\Path\To\MyDrawing.dwg”, “MyInstance1”
‘——————————————————————————-
Sub BindToDocumentAndProcess(ByVal acadApp As AcadApplication, ByVal documentName As String, Optional ByVal targetInstanceName As String = “Unknown Instance”)
On Error GoTo ErrorHandler
Dim targetDoc As AcadDocument = Nothing
Dim foundDoc As AcadDocument = Nothing
Dim doc As AcadDocument
‘ 1. 対象ドキュメントの特定
‘ acadApp.Documents コレクションをループして、目的のドキュメントを探す。
‘ ファイル名だけでなく、フルパスで比較することが重要。
For Each doc In acadApp.Documents
If UCase(doc.FullName) = UCase(documentName) Then
Set foundDoc = doc
Exit For
End If
Next doc
‘ 2. ドキュメントが見つからなかった場合の処理
If foundDoc Is Nothing Then
‘ ドキュメントが開かれていない場合、開く処理を試みる
‘ 注意: ここでファイルを開く処理を行う場合、
‘ AutoCAD のカレントディレクトリや、DXF/DWG のバージョン互換性、
‘ ファイルロックなどを考慮する必要がある。
‘ また、開く際のオプション (Read Only など) も指定可能。
‘ 例: ファイルを開く
On Error Resume Next ‘ ファイルが開けない場合のエラーを捕捉
Set foundDoc = acadApp.Documents.Open(documentName)
On Error GoTo ErrorHandler ‘ エラーハンドリングを元に戻す
If foundDoc Is Nothing Then
MsgBox “インスタンス ‘” & targetInstanceName & “‘ で、図面ファイル ‘” & documentName & “‘ を開くことができませんでした。”, vbCritical
Exit Sub
Else
Debug.Print “インスタンス ‘” & targetInstanceName & “‘ で図面ファイル ‘” & documentName & “‘ を開きました。”
End If
End If
‘ 3. 対象ドキュメントをアクティブにする (必要に応じて)
‘ 多くの場合、特定したドキュメントをアクティブにする方が、
‘ そのドキュメントに対する操作 (例:UCS設定、レイアウト選択など) が
‘ 意図通りに実行されやすい。
Set targetDoc = foundDoc
targetDoc.Activate ‘ ドキュメントをアクティブにする
‘ 4. 対象ドキュメントに対する処理の実行
‘ ここに、対象ドキュメントに対して実行したい具体的な処理を記述する。
‘ 例: 特定のレイヤーの表示/非表示、オブジェクトの追加/削除、
‘ プロパティの変更、コマンドの実行など。
Debug.Print “インスタンス ‘” & targetInstanceName & “‘ のドキュメント ‘” & targetDoc.Name & “‘ に対して処理を実行します。”
‘ — ここに具体的な処理を記述 —
‘ 例: 特定のコマンドを実行する
‘ acadApp.ZoomExtents ‘ アクティブドキュメント全体を表示
‘ targetDoc.SendCommand “._LA”, “SET”, “0”, “” ‘ レイヤー 0 をカレントにするコマンド例 (非推奨、直接プロパティ操作を推奨)
‘ 例: 特定のオブジェクトのプロパティを変更する (より安全な方法)
‘ Dim obj As AcadObject
‘ For Each obj In targetDoc.ModelSpace
‘ If TypeOf obj Is AcadLine Then
‘ obj.Layer = “MyLayer”
‘ obj.Update
‘ End If
‘ Next obj
MsgBox “インスタンス ‘” & targetInstanceName & “‘ の図面 ‘” & targetDoc.Name & “‘ に対して処理が完了しました。”, vbInformation
Exit Sub
ErrorHandler:
MsgBox “エラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“説明: ” & Err.Description & vbCrLf & _
“対象インスタンス: ” & targetInstanceName & vbCrLf & _
“対象ドキュメント: ” & documentName, vbCritical
Set targetDoc = Nothing
Set foundDoc = Nothing
Set acadApp = Nothing
End Sub
‘——————————————————————————-
‘ @Sub: MainProcessing
‘ @Desc: マルチインスタンス環境での自動化処理のメインルーチン。
‘ 特定の AutoCAD インスタンスを特定し、その中の図面ファイルを処理する。
‘——————————————————————————-
Sub MainProcessing()
Dim targetInstanceName1 As String = “MyAutoCAD_Instance_1” ‘ 外部で登録されたインスタンス名
Dim targetDocumentPath1 As String = “C:\Temp\Drawing1.dwg” ‘ 処理対象の図面ファイルパス
Dim targetInstanceName2 As String = “MyAutoCAD_Instance_2” ‘ 別のインスタンス名
Dim targetDocumentPath2 As String = “D:\Projects\Drawing2.dwg” ‘ 別の図面ファイルパス
Dim acadApp1 As AcadApplication = Nothing
Dim acadApp2 As AcadApplication = Nothing
‘ — インスタンス1 の処理 —
Debug.Print “— インスタンス ‘” & targetInstanceName1 & “‘ の処理を開始します —”
Set acadApp1 = GetSpecificAcadApplication(targetInstanceName1)
If Not acadApp1 Is Nothing Then
‘ ドキュメントが存在するか、あるいは開く必要があるかを確認
If Dir(targetDocumentPath1) = “” Then
MsgBox “図面ファイル ‘” & targetDocumentPath1 & “‘ が見つかりません。処理をスキップします。”, vbExclamation
Else
BindToDocumentAndProcess acadApp1, targetDocumentPath1, targetInstanceName1
End If
Else
Debug.Print “インスタンス ‘” & targetInstanceName1 & “‘ を取得できませんでした。処理をスキップします。”
End If
Set acadApp1 = Nothing ‘ オブジェクト参照を解放
‘ — インスタンス2 の処理 —
Debug.Print vbCrLf & “— インスタンス ‘” & targetInstanceName2 & “‘ の処理を開始します —”
Set acadApp2 = GetSpecificAcadApplication(targetInstanceName2)
If Not acadApp2 Is Nothing Then
If Dir(targetDocumentPath2) = “” Then
MsgBox “図面ファイル ‘” & targetDocumentPath2 & “‘ が見つかりません。処理をスキップします。”, vbExclamation
Else
BindToDocumentAndProcess acadApp2, targetDocumentPath2, targetInstanceName2
End If
Else
Debug.Print “インスタンス ‘” & targetInstanceName2 & “‘ を取得できませんでした。処理をスキップします。”
End If
Set acadApp2 = Nothing ‘ オブジェクト参照を解放
MsgBox “すべての処理が完了しました。”, vbInformation
End Sub
コード解説と注意点
1. `GetSpecificAcadApplication(targetInstanceName As String)` 関数:
- この関数は、外部(VB.NET など)で `GetObject` 関数と特定のインスタンス名を組み合わせて実行オブジェクトテーブル (ROT) に登録された `AcadApplication` オブジェクトを取得しようと試みます。
- `GetObject(ProgID, InstanceName)` は、COM の標準的な機能ですが、VBA から直接、特定のインスタンス名を指定して `GetObject` を呼び出すためには、そのインスタンスが事前に ROT に登録されている必要があります。
- 重要なのは、`targetInstanceName` が、外部で AutoCAD を起動・登録する際に使用した名前と完全に一致することです。
- 複数の AutoCAD バージョンがインストールされている可能性を考慮し、一般的な ProgID をループで試行します。
- エラーハンドリングを徹底し、インスタンスが見つからなかった場合や、取得に失敗した場合でも、プログラムがクラッシュしないように設計しています。
2. `BindToDocumentAndProcess(acadApp As AcadApplication, documentName As String, Optional ByVal targetInstanceName As String)` サブルーチン:
- このサブルーチンは、特定した `AcadApplication` オブジェクト (`acadApp`) を受け取り、そのアプリケーションが開いているドキュメントの中から、指定された `documentName` (フルパス) を持つドキュメントを探します。
- ドキュメントの特定: `acadApp.Documents` コレクションをループし、`doc.FullName` プロパティ(フルパス)を比較します。大文字・小文字を区別しないように `UCase` を使用しています。
- ドキュメントが開かれていない場合: もし目的のドキュメントが見つからなかった場合、`acadApp.Documents.Open(documentName)` を試みます。ここで、ファイルが存在しない、アクセス権がない、などのエラーが発生する可能性を考慮し、`On Error Resume Next` を使用してエラーを捕捉します。
- ドキュメントのアクティブ化: 特定したドキュメント (`foundDoc`) を `foundDoc.Activate` でアクティブにします。これにより、そのドキュメントに対する操作が、意図した通りに実行されやすくなります。
- 処理の実行: `targetDoc` 変数にセットされたアクティブなドキュメントに対して、具体的な自動化処理を記述します。コメントアウトされた例のように、コマンド実行やプロパティ変更など、目的に応じたコードをここに追加します。
3. `MainProcessing()` サブルーチン:
- このサブルーチンは、上記二つの関数/サブルーチンを組み合わせて、実際の処理フローを示します。
- 複数のインスタンスとドキュメントに対して、それぞれ処理を実行する例です。
- `Dir()` 関数でファイル存在チェックを行うなど、堅牢性を高めるための基本的なチェックも含まれています。
パフォーマンスとライフサイクルに関する注意点
- COM オブジェクトの解放: VBA では、COM オブジェクトへの参照が不要になったら、明示的に `Set obj = Nothing` を実行して参照を解放することが推奨されます。これにより、メモリリークや予期せぬ動作を防ぐことができます。上記のコード例では、各サブルーチンの最後やエラーハンドリング部分で参照解放を行っています。
- `GetObject` のオーバーヘッド: `GetObject` 関数は、実行中の COM サーバーを探すため、実行環境によっては若干のオーバーヘッドが発生する可能性があります。しかし、マルチインスタンス環境における確実な特定という目的においては、そのコストは十分に許容できる範囲です。
- エラーハンドリングの重要性: マルチインスタンス環境では、ユーザーの操作(インスタンスの終了、図面ファイルの移動・削除など)によって、予期せぬエラーが発生しやすくなります。`On Error GoTo` を使用した堅牢なエラーハンドリングは、プロダクションコードとして必須です。
- ファイルロックとトランザクション: 複数の AutoCAD インスタンスが同時に同じファイルにアクセスする場合、ファイルロックの問題が発生する可能性があります。また、データベース連携やネットワークドライブ上のファイル操作では、トランザクション処理を慎重に設計する必要があります。
ファイル・データベース連携における落とし穴と設計思想
ここからは、さらに踏み込んだ、プロフェッショナルが必ず考慮すべき点について論じよう。
1. ファイルパスとカレントディレクトリの魔力
- `ThisDrawing.Path` vs `GetObject(…).Path`: VBA では、`ThisDrawing.Path` は「現在マクロが実行されているインスタンスの、アクティブな図面のパス」を返します。しかし、`GetObject` で取得した別インスタンスの `AcadApplication` オブジェクトに対して `AcadDocument.Path` を参照する場合、それはそのドキュメントが最後に保存された場所を返します。
- カレントディレクトリの錯覚: ユーザーが AutoCAD を起動した際のカレントディレクトリは、必ずしも開いている図面のパスとは限りません。特に、ショートカットの「開始場所」や、前回開いていたフォルダなど、様々な要因で決まります。
- 回避策:
- 常にフルパスで指定する: ファイル操作(開く、保存、参照)を行う際は、必ずフルパスを指定してください。相対パスは、カレントディレクトリに依存するため、予期せぬ問題を引き起こします。
- `ChDrive` / `ChDir` の慎重な使用: もし、特定の図面に関連するファイル(外部参照、XREF など)を操作するためにカレントディレクトリを変更する必要がある場合は、処理の開始前と終了後に必ず元のディレクトリに戻すようにしてください。`Pushd` / `Popd` のような概念を VBA で実装するのも有効です。
2. データベース連携と AutoSave/Backup の兼ね合い
- AutoSave/Backup の影響: AutoCAD の自動保存機能やバックアップ機能は、ユーザーのデータ損失を防ぐために非常に重要です。しかし、これらの機能が、VBA によるファイル操作(保存、名前を付けて保存、DXF/DWG 変換など)と干渉する可能性があります。
- タイムスタンプと競合: データベースに図面情報を記録する際、AutoSave や Backup ファイルのタイムスタンプが、実際の作業完了時刻とずれることがあります。これにより、どのバージョンの図面が最新なのか、といった管理に混乱が生じかねません。
- 設計思想:
- 明示的な保存操作: VBA から図面ファイルを操作する際は、必ず明示的に保存処理(`Document.Save` または `Document.SaveAs`)を呼び出すようにしてください。自動保存に頼り切る設計は避けるべきです。
- バージョン管理システムの活用: 可能な限り、Subversion (SVN) や Git のようなバージョン管理システムを導入し、図面ファイルの変更履歴を管理することを強く推奨します。これにより、問題発生時のロールバックや、過去のバージョン参照が容易になります。
- トランザクション処理: データベースと図面ファイルを連携させる場合、両方の更新が成功するか、あるいはどちらも失敗するかの「アトミック性」を保証するトランザクション処理を検討してください。これは、VBA 単体では難しいため、外部のアプリケーション(VB.NET など)や、データベース側の機能(ストアドプロシージャなど)と連携して実現します。
3. DXF/DWG バージョン互換性と `SaveAs` の落とし穴
- `SaveAs` の挙動: `AcadDocument.SaveAs` メソッドは、図面ファイルを指定したバージョンで保存するための強力な機能です。しかし、バージョンによっては、特定のオブジェクトや機能がサポートされていない場合があります。
- 互換性問題の発生:
- 機能の損失: 新しいバージョンの AutoCAD で作成された図面を、古いバージョンで `SaveAs` すると、古いバージョンでサポートされていない機能(例: 新しいジオメトリタイプ、クラウド連携機能など)が失われたり、意図しない形式に変換されたりする可能性があります。
- オブジェクトの破損: 最悪の場合、古いバージョンでは表現できない複雑なオブジェクトが破損し、図面ファイルが開けなくなることもあります。
- 回避策:
- ターゲットバージョンの明確化: 常に、保存対象の AutoCAD バージョンを明確にし、それに合わせた `SaveAs` を実行してください。
- テストの徹底: 異なる AutoCAD バージョン間での `SaveAs` を、実際にテスト環境で十分に行い、予期せぬ問題が発生しないことを確認してください。
- DXF 形式の活用: DWG 形式に固執せず、必要に応じて DXF 形式での保存・読み込みも検討してください。DXF はテキストベースであり、バージョン互換性の問題が DWG よりも少ない場合があります(ただし、DXF もバージョンによる仕様変更があります)。
- AutoCAD のバージョン管理: 可能な限り、VBA マクロを実行する AutoCAD のバージョンと、ターゲットとする図面ファイルのバージョンを統一するか、厳密に管理してください。
まとめ:堅牢な設計で、自動化の未来を切り拓け
本記事では、複数の AutoCAD インスタンスが起動している環境下で、目的の `AcadApplication` インスタンスと図面ドキュメントを確実に特定し、操作するための実践的なテクニックを解説しました。
- Win32 API の概念: 低レベル API を理解することで、COM オブジェクトのライフサイクルと、プロセスレベルでの挙動を深く理解する。
- `GetObject` と ROT 登録: VBA から現実的にマルチインスタンスを特定するための、`GetObject` 関数と外部による ROT 登録の連携。
- 堅牢なコード設計: エラーハンドリング、パスの絶対指定、オブジェクト参照の解放など、プロダクションコードに不可欠な要素。
- ファイル・DB 連携の注意点: ファイルパス、カレントディレクトリ、AutoSave、バージョン互換性といった、見落としがちな落とし穴とその対策。
我々エンジニアの使命は、単に動くコードを書くことではない。「バグが起きない」「保守しやすい」「パフォーマンスが高い」、そして何よりも「現場の生産性を確実に向上させる」コードを書くことだ。
今回解説した内容は、決して容易ではない。しかし、これらの知見を血肉とし、日々の開発に活かすことで、君たちの自動化ツールは、単なるスクリプトから、現場の信頼を得る「システム」へと昇華するだろう。
この知識を武器に、君たちの現場で、よりスマートで、より効率的な自動化を実現してくれることを、心から期待している。
