【上級プロフェッショナル向け】マルチインスタンス環境でのAcadApplication特定と対象ドキュメントのバインド制御:伝説のアーキテクトが語るAutoCAD VBAの深淵
長年、CADシステムの自動化とレガシーアーキテクチャの維持・発展に携わってきた者として、AutoCAD VBAの奥深さ、そしてそのポテンシャルを最大限に引き出すための技術的探求は、今もなお私の情熱の源泉です。特に、複数のAutoCADインスタンスが並存する環境下で、目的のアプリケーションと図面ファイルを確実に特定し、意図した操作をバインドさせるという課題は、多くの現場で直面する、しかしながら奥深く、かつ重要度の高いテーマと言えるでしょう。
本稿では、単なるリファレンスの羅列に終始することなく、長年の経験とAPI仕様の深い理解に基づき、この難題を解決するためのWin32 API活用術を、伝説のチーフアーキテクトの視点から、技術至上主義のスタイルで詳解していきます。読者の皆様が、日々の開発現場で直面するであろう課題、特にレガシー環境の保守、システム間連携、そしてメモリ管理といった、システム全体の安定稼働に不可欠な要素に焦点を当て、実践的な知見を提供することを目指します。
1. マルチインスタンス環境の現実:なぜ「特定のインスタンス」が重要なのか
Modernな開発環境では、単一のアプリケーションインスタンスを前提とするケースが多く見られます。しかし、AutoCADの世界、特に大規模な設計組織や、異なるプロジェクトが同時並行で進行する環境においては、複数のAutoCADインスタンスが起動している状況は決して珍しくありません。
- ユーザーの利便性: 複数の図面を同時に開いて比較・参照する。
- システム統合: 既存のバッチ処理システムや、他アプリケーションとの連携。
- バージョン管理: 異なるバージョンのAutoCADを併用する必要がある場合。
- リソース制約: パフォーマンス向上のため、あるいは特定の機能のために、意図的にインスタンスを分離する。
このような環境下で、VBAマクロが「ただちに利用可能なAutoCADインスタンス」に闇雲にアクセスすると、意図しないインスタンスに対して操作が実行され、データ破壊や予期せぬ動作を引き起こすリスクが極めて高まります。これは、バッチ処理の失敗、重要な設計データの誤った上書き、あるいはシステム全体の不安定化に直結しかねません。
だからこそ、我々は「対象となる正確なAutoCADインスタンス」を特定し、そのインスタンスに「操作対象の図面ファイル」を紐づける(バインドする)ための、確実なメカニズムを確立する必要があるのです。
2. Win32 APIを駆使したAcadApplicationの特定:COMオブジェクトの深層へ
VBAからAutoCADを操作する際、我々は通常 `GetObject` 関数や `CreateObject` 関数、あるいは `Application` オブジェクトを通じてAutoCADのCOMインターフェースにアクセスします。しかし、これらの標準的な方法だけでは、複数のインスタンスが存在する場合に、どのインスタンスが返されるかを制御することは困難です。
ここで、我々の強力な味方となるのが、Windowsの低レベルAPI、すなわちWin32 APIです。これにより、プロセスの列挙、ウィンドウハンドルの取得、そしてCOMオブジェクトへのアクセスをより細かく制御することが可能になります。
2.1. プロセス列挙とAutoCADプロセスの特定
まずは、実行中のプロセスの中からAutoCADのプロセスを特定する必要があります。これは、`EnumProcesses`、`EnumProcessModules`、`GetModuleFileNameEx` といったAPIを組み合わせることで実現できます。
‘ VBA Code Example: Enumerating AutoCAD Processes
‘ Author: Legendary Chief Architect
‘ Date: 2023-10-27
‘ Description: This function enumerates running processes and identifies AutoCAD processes.
‘ Declare necessary Win32 API functions
If VBA7 Then ‘ 64-bit VBA
Private Declare PtrSafe Function EnumProcesses Lib “psapi.dll” ( _
ByRef lpidProcess As LongPtr, _
ByVal cb ByVal, _
ByRef cbNeeded As Long _
) As Long
Private Declare PtrSafe Function OpenProcess Lib “kernel32” ( _
ByVal dwFlags As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long _
) As LongPtr
Private Declare PtrSafe Function EnumProcessModules Lib “psapi.dll” ( _
ByVal hProcess As LongPtr, _
ByRef lphModule As LongPtr, _
ByVal cb ByVal, _
ByRef lpcbNeeded As Long _
) As Long
Private Declare PtrSafe Function GetModuleFileNameExA Lib “psapi.dll” ( _
ByVal hProcess As LongPtr, _
ByVal hModule As LongPtr, _
ByVal lpBaseName As String, _
ByVal nSize As Long _
) As Long
Private Declare PtrSafe Function CloseHandle Lib “kernel32” ( _
ByVal hObject As LongPtr _
) As Long
Else ‘ 32-bit VBA
Private Declare Function EnumProcesses Lib “psapi.dll” ( _
ByRef lpidProcess As Long, _
ByVal cb As Long, _
ByRef cbNeeded As Long _
) As Long
Private Declare Function OpenProcess Lib “kernel32” ( _
ByVal dwFlags As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long _
) As Long
Private Declare Function EnumProcessModules Lib “psapi.dll” ( _
ByVal hProcess As Long, _
ByRef lphModule As Long, _
ByVal cb As Long, _
ByRef lpcbNeeded As Long _
) As Long
Private Declare Function GetModuleFileNameExA Lib “psapi.dll” ( _
ByVal hProcess As Long, _
ByVal hModule As Long, _
ByVal lpBaseName As String, _
ByVal nSize As Long _
) As Long
Private Declare Function CloseHandle Lib “kernel32” ( _
ByVal hObject As Long _
) As Long
End If
‘ Constants for API calls
Private Const PROCESS_QUERY_INFORMATION As Long = &H400
Private Declare Function GetTickCount Lib “kernel32” () As Long ‘ To check if API calls are valid
Public Function FindAcadProcessIds() As Variant
‘ This function returns an array of Process IDs for AutoCAD applications.
‘ It uses Win32 API calls to enumerate processes and check their executable names.
Dim processIDs() As Long ‘ Array to hold process IDs
Dim processIDsPtr As LongPtr ‘ Pointer for 64-bit compatibility
Dim cbNeeded As Long
Dim bufferSize As Long
Dim numProcesses As Long
Dim i As Long
Dim hProcess As LongPtr
Dim hModule As LongPtr
Dim moduleFileName As String
Dim acadProcessIds As Variant
Dim acadCount As Long
‘ Initialize the result array
ReDim acadProcessIds(0)
acadCount = 0
‘ First, determine the required buffer size for process IDs
If EnumProcesses(0, 0, cbNeeded) = 0 Then
‘ Error handling: Could not enumerate processes.
‘ In a production environment, log this error.
FindAcadProcessIds = Array() ‘ Return empty array on error
Exit Function
End If
‘ Allocate buffer for process IDs
If VBA7 Then
bufferSize = cbNeeded
ReDim processIDs(0 To (bufferSize \ 4) – 1) As LongPtr ‘ Use LongPtr for 64-bit
processIDsPtr = VarPtr(processIDs(0))
Else
bufferSize = cbNeeded
ReDim processIDs(0 To (bufferSize \ 4) – 1) As Long
processIDsPtr = VarPtr(processIDs(0))
End If
‘ Enumerate processes
If EnumProcesses(processIDsPtr, bufferSize, cbNeeded) = 0 Then
‘ Error handling: Could not enumerate processes.
FindAcadProcessIds = Array()
Exit Function
End If
‘ Calculate the actual number of processes found
numProcesses = cbNeeded \ IIf(VBA7, Len(CLngPtr(0)), Len(CLng(0)))
‘ Iterate through each process
For i = 0 To numProcesses – 1
Dim currentProcessId As Long
If VBA7 Then
currentProcessId = CLngPtr(processIDs(i)) ‘ Cast to Long for comparison
Else
currentProcessId = processIDs(i)
End If
‘ Open the process with sufficient privileges
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, currentProcessId)
If hProcess <> 0 Then
‘ Enumerate modules for the process
Dim hModules(0 To 1023) As LongPtr ‘ Array to hold module handles
Dim cbModulesNeeded As Long
Dim numModules As Long
If EnumProcessModules(hProcess, hModules(0), LenB(hModules), cbModulesNeeded) <> 0 Then
numModules = cbModulesNeeded \ Len(hModules(0))
‘ Get the full path of the main module (executable)
Dim lpBaseName As String
Dim nSize As Long
nSize = 1024 ‘ Maximum path length
lpBaseName = String$(nSize, vbNullChar)
If GetModuleFileNameExA(hProcess, hModules(0), lpBaseName, nSize) > 0 Then
‘ Trim null characters and check if it’s AutoCAD
lpBaseName = Left$(lpBaseName, InStr(lpBaseName, vbNullChar) – 1)
If LCase$(Right$(lpBaseName, Len(“acad.exe”))) = “acad.exe” Then
‘ Found an AutoCAD process
ReDim Preserve acadProcessIds(acadCount)
acadProcessIds(acadCount) = currentProcessId
acadCount = acadCount + 1
End If
End If
End If
‘ Close the process handle
CloseHandle hProcess
End If
Next i
‘ If no AutoCAD processes were found, return an empty array.
If acadCount = 0 Then
FindAcadProcessIds = Array()
Else
FindAcadProcessIds = acadProcessIds
End If
End Function
‘ — Example Usage in another Subroutine —
Sub TestFindAcadProcesses()
Dim acadPIDs As Variant
acadPIDs = FindAcadProcessIds()
If UBound(acadPIDs) < LBound(acadPIDs) Then ' Check if array is empty MsgBox "AutoCADプロセスが見つかりませんでした。", vbInformation Else Dim msg As String Dim i As Long msg = "検出されたAutoCADプロセスID:" & vbCrLf For i = LBound(acadPIDs) To UBound(acadPIDs) msg = msg & acadPIDs(i) & vbCrLf Next i MsgBox msg, vbInformation End If End Sub 【解説】
- `EnumProcesses`: システム上の全プロセスのプロセスID(PID)を取得します。メモリバッファのサイズを事前に確認し、動的に確保することが重要です。
- `OpenProcess`: 特定のPIDを持つプロセスハンドルを取得します。`PROCESS_QUERY_INFORMATION` フラグを指定することで、プロセスの情報を取得する権限を得ます。
- `EnumProcessModules`: プロセスのロードしているモジュール(DLLやEXE)のハンドルを取得します。
- `GetModuleFileNameExA`: モジュールハンドルから、そのモジュールのフルパス名を取得します。ここで、実行ファイル名が `acad.exe` であることを確認します。
- `CloseHandle`: 取得したプロセスハンドルを解放します。これはリソースリークを防ぐための極めて重要なステップです。
【パフォーマンスとメモリ最適化の観点】
- バッファサイズの最適化: `EnumProcesses` の最初の呼び出しで `cbNeeded` を取得し、それに基づいてメモリバッファを確保することで、不要なメモリ割り当てを防ぎます。
- ハンドル解放の徹底: `OpenProcess` で取得したハンドルは、不要になったら必ず `CloseHandle` で解放します。これは、システムリソースの枯渇を防ぐための基本中の基本です。
- 64bit VBAへの対応: `PtrSafe` キーワードと `LongPtr` 型を使用することで、32bit版・64bit版のVBA環境の両方で互換性のあるコードを提供します。
2.2. プロセスIDからCOMインスタンスへのバインド
AutoCADのプロセスIDが特定できたら、次はそのプロセスに関連付けられたCOMアプリケーションオブジェクトを取得する必要があります。ここで威力を発揮するのが、`GetRunningObjectTable`、`EnumMoniker`、そして`BindToMoniker` といったCOM APIです。
VBAで直接これらのAPIを呼び出すのはやや煩雑なため、VB.NETなどのより高度な言語でヘルパーDLLを作成し、VBAから呼び出すというアーキテクチャも強力な選択肢となります。しかし、VBA単体でも実装は可能です。
ここでは、VBAで直接実装するアプローチを、より理解しやすいように概念的に説明します。実際の実装は、`IMoniker`、`IRunningObjectTable` といったCOMインターフェースを駆使することになります。
概念的なアプローチ(VBAでの直接実装は高度なCOM知識を要します):
1. Running Object Table (ROT) の取得:
`GetRunningObjectTable(0, rot)` API を使用して、システム上の実行中オブジェクトテーブルを取得します。
2. ROT の列挙:
ROT に登録されているオブジェクトの `EnumMoniker` を取得し、各オブジェクトのmoniker(オブジェクトを識別するためのポインタのようなもの)を列挙します。
3. monikerからのオブジェクト識別:
列挙されたmonikerから、対象のAutoCADアプリケーションオブジェクト (`AcadApplication`) を識別します。識別には、オブジェクトのクラス名や、関連付けられたプロセスIDなどが利用されます。
4. バインド:
識別されたmonikerに対して `BindToMoniker` を実行し、実際の `AcadApplication` オブジェクトへの参照を取得します。
より実践的なアプローチ:VB.NETヘルパーDLLの利用
VBAのCOM呼び出しの複雑さを回避し、保守性を高めるために、VB.NETでCOM可視なクラスライブラリ(DLL)を作成し、VBAからそれを参照して利用する手法は、現代のシステム連携において非常に有効です。
VB.NET ヘルパーDLL(例)
.net
‘ VB.NET Code Example: Helper DLL for AutoCAD Instance Management
‘ Author: Legendary Chief Architect
‘ Date: 2023-10-27
‘ Description: Provides a COM-visible class to get running AutoCAD instances.
Imports System
Imports System.Runtime.InteropServices
Imports System.Diagnostics
Imports Microsoft.VisualBasic.ComServices
Namespace AutoCADInstanceManager
Public Interface IAcadInstanceManager
Function GetAcadApplications() As Object()
Function GetAcadApplicationByProcessId(processId As Integer) As Object
End Function
Public Class AcadInstanceManager
Implements IAcadInstanceManager
Private Const PROCESS_QUERY_INFORMATION As Integer = &H400
Private Const MEM_COMMIT As Integer = &H1000
Private Const PAGE_READWRITE As Integer = &H4
Private Const PROCESS_VM_OPERATION As Integer = &H8
Private Const PROCESS_VM_READ As Integer = &H10
Private Const PROCESS_VM_WRITE As Integer = &H20
‘ Helper to get executable path from Process ID
Private Function GetProcessExecutablePath(processId As Integer) As String
Dim process As Process = Nothing
Try
process = Process.GetProcessById(processId)
Return process.MainModule.FileName
Catch ex As Exception
Return String.Empty
Finally
If process IsNot Nothing Then process.Dispose()
End Try
End Function
Public Function GetAcadApplications() As Object() Implements IAcadInstanceManager.GetAcadApplications
Dim runningObjects As Object = GetRunningObjectTable(0)
Dim rotEnumerator As IEnumMoniker
Dim moniker As IMoniker
Dim count As Integer = 0
Dim applications As New List(Of Object)
runningObjects.EnumRunning(rotEnumerator)
rotEnumerator.Next(1, moniker, count)
While count > 0
Dim nameTable As IRunningObjectTable = Nothing
Dim runningObj As Object = Nothing
Dim bindCtx As IBindCtx = Nothing
Dim displayName As String = Nothing
Dim objType As System.Type = Nothing
try
CreateBindCtx(0, bindCtx)
moniker.GetDisplayName(bindCtx, Nothing, displayName)
‘ Check if it’s an AutoCAD application
If displayName.ToLowerInvariant().Contains(“autocad”) Then
runningObjects.GetObject(moniker, runningObj)
‘ Check if the object is an AcadApplication
objType = runningObj.GetType()
If objType.FullName.EndsWith(“Application”) Then
‘ Further check: Ensure it’s a genuine AutoCAD process
Dim processId As Integer = GetProcessIdFromComObject(runningObj) ‘ Custom implementation needed
If processId > 0 AndAlso GetProcessExecutablePath(processId).ToLowerInvariant().EndsWith(“acad.exe”) Then
applications.Add(runningObj)
End If
End If
End If
Catch ex As Exception
‘ Ignore errors for objects that cannot be enumerated or bound
Finally
If runningObj IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(runningObj)
If moniker IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(moniker)
If bindCtx IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(bindCtx)
End Try
rotEnumerator.Next(1, moniker, count)
End While
Return applications.ToArray()
End Function
‘ IMPORTANT: GetProcessIdFromComObject is a complex function
‘ It typically involves using `GetProcessId` with a process handle obtained from the COM object.
‘ This often requires P/Invoke and careful handling of COM object lifetimes.
‘ For brevity and focus, a simplified placeholder is used here.
‘ A robust implementation is crucial for production systems.
Private Function GetProcessIdFromComObject(comObject As Object) As Integer
Try
‘ This is a placeholder. Actual implementation requires P/Invoke to kernel32.dll’s
‘ GetProcessId, which needs a process handle. Obtaining a process handle
‘ from a generic COM object is non-trivial and depends on the COM object’s
‘ underlying implementation (e.g., IUnknown->QueryInterface->specific interfaces).
‘ In many cases, you might need to rely on external information or
‘ specific AutoCAD COM interfaces if available to link back to the process.
‘ For a real-world scenario, you might pass the ProcessId when creating
‘ the COM object, or use a more advanced technique like debugging APIs.
‘ A common workaround is to iterate through running processes and check
‘ their COM-related properties if exposed, or by matching window titles.
‘ Example (highly simplified, NOT reliable for all COM objects):
‘ If you know the COM object has a ProcessId property directly:
‘ Return CInt(comObject.GetType().GetProperty(“ProcessId”)?.GetValue(comObject))
‘ For AutoCAD, a common approach is to use IDispatch::Invoke to call
‘ a method on the AutoCAD object that returns its process ID.
‘ This requires a deep understanding of IDispatch and AutoCAD’s COM model.
‘ Alternatively, use the Process class and try to match the COM object’s GUID
‘ or ProgID, but this is also not guaranteed.
‘ Advanced technique example (using Debugger.Launch and then getting PID):
‘ This is for illustration; not for production code.
‘ Debugger.Launch() ‘ This would break execution.
‘ Then you’d inspect the debugger.
‘ A more feasible approach for AutoCAD:
‘ Iterate through all running AutoCAD processes.
‘ For each process, try to get its AcadApplication object.
‘ Compare the obtained object with the input COM object.
‘ This comparison often involves checking if they are the same instance.
‘ A simple reference equality check might work if the COM object is already valid.
Dim currentAcadApps As New List(Of Object)
Dim runningObjects As Object = GetRunningObjectTable(0)
Dim rotEnumerator As IEnumMoniker
Dim moniker As IMoniker
Dim count As Integer = 0
Dim bindCtx As IBindCtx = Nothing
System.Runtime.InteropServices.Marshal.ReleaseComObject(runningObjects) ‘ Release previous if held
runningObjects = GetRunningObjectTable(0)
runningObjects.EnumRunning(rotEnumerator)
rotEnumerator.Next(1, moniker, count)
While count > 0
Dim obj As Object = Nothing
Dim dispName As String = Nothing
Try
CreateBindCtx(0, bindCtx)
moniker.GetDisplayName(bindCtx, Nothing, dispName)
If dispName.ToLowerInvariant().Contains(“autocad”) Then
runningObjects.GetObject(moniker, obj)
Dim objType As System.Type = obj.GetType()
If objType.FullName.EndsWith(“Application”) Then
‘ Compare the obtained object with the target COM object
If System.Runtime.InteropServices.Marshal.AreComObjectsEqual(obj, comObject) Then
Dim proc As Process = Nothing
Try
proc = Process.GetProcessById(obj.Hwnd) ‘ Assuming AutoCAD exposes Hwnd
Return proc.Id
Catch
‘ Fallback if Hwnd isn’t directly accessible or reliable
‘ This part is tricky and might require more advanced COM introspection.
Finally
If proc IsNot Nothing Then proc.Dispose()
End Try
End If
End If
End If
Catch
‘ Ignore errors
Finally
If obj IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
If moniker IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(moniker)
If bindCtx IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(bindCtx)
End Try
rotEnumerator.Next(1, moniker, count)
End While
Return -1 ‘ Not found or unable to determine
Catch ex As Exception
‘ Log the exception for debugging purposes
System.Diagnostics.Debug.WriteLine($”Error in GetProcessIdFromComObject: {ex.Message}”)
Return -1 ‘ Indicate an error
End Try
End Function
‘ Helper to get Process ID from a COM object (requires specific interfaces)
‘ This is a simplified placeholder and may not work for all COM objects.
‘ A robust implementation often requires P/Invoke and knowledge of the COM object’s structure.
‘ For AutoCAD, you might need to access its HWND and then use the Win32 API.
Private Function GetProcessIdFromComObject_Simple(comObject As Object) As Integer
Dim processId As Integer = -1
Dim process As Process = Nothing
Try
‘ Attempt to access the window handle if exposed by the COM object
Dim appHandle As IntPtr = IntPtr.Zero
‘ Example: If comObject.HWND exists and is an IntPtr
‘ appHandle = CType(comObject.GetType().InvokeMember(“HWND”, System.Reflection.BindingFlags.GetProperty, Nothing, comObject, Nothing), IntPtr)
‘ A more common and reliable way for AutoCAD is to use its documented COM interfaces
‘ and potentially P/Invoke to get the process ID.
‘ If the COM object is an AcadApplication, you might be able to get its HWND.
‘ For example, if you have an IAcadApplication interface:
‘ Dim acadApp As IAcadApplication = DirectCast(comObject, IAcadApplication)
‘ appHandle = acadApp.HWND ‘ Assuming HWND is a property
‘ For demonstration, let’s assume we can get the HWND somehow.
‘ This is the most critical and complex part.
‘ A practical approach might be to iterate through processes and match them.
‘ Let’s use a more direct approach by matching running AutoCAD processes.
Dim acadProcesses() As Process = Process.GetProcessesByName(“acad”)
For Each proc As Process In acadProcesses
‘ Try to get the AcadApplication object for this process and compare
‘ This is computationally expensive and might require multiple attempts.
‘ A more efficient method would involve directly obtaining the process ID
‘ if the COM object provides a direct link.
‘ For simplicity, let’s assume we can directly get the process ID from the object
‘ if it’s available via reflection (not always the case).
‘ Example:
Try
Dim pidProperty As System.Reflection.PropertyInfo = comObject.GetType().GetProperty(“ProcessID”) ‘ Hypothetical property
If pidProperty IsNot Nothing Then
processId = CInt(pidProperty.GetValue(comObject, Nothing))
If processId = proc.Id Then
Return processId
End If
End If
Catch
‘ Property not found or access error
End Try
‘ If no direct property, we might need to try getting the COM object from the process’s ROT
‘ This becomes circular. The best approach is to get the PID when you FIRST get the COM object.
Next
‘ If the above fails, try to get PID from a known property like HWND (if available)
‘ This is a very common pattern for Win32-interop COM objects.
Try
Dim acadApp As Object = comObject
Dim hwndProperty As System.Reflection.PropertyInfo = acadApp.GetType().GetProperty(“HWND”)
If hwndProperty IsNot Nothing Then
appHandle = CType(hwndProperty.GetValue(acadApp, Nothing), IntPtr)
If appHandle <> IntPtr.Zero Then
Dim procId As UInteger
Dim threadId As UInteger = GetWindowThreadProcessId(appHandle, procId)
processId = CInt(procId)
Return processId
End If
End If
Catch ex As Exception
‘ HWND property not found or access error.
System.Diagnostics.Debug.WriteLine($”Error accessing HWND for COM object: {ex.Message}”)
End Try
Return -1 ‘ Unable to determine process ID
Catch ex As Exception
System.Diagnostics.Debug.WriteLine($”Error in GetProcessIdFromComObject_Simple: {ex.Message}”)
Return -1
Finally
‘ No need to dispose of Process objects here if they were obtained via GetProcessById and not kept.
End Try
End Function
‘ Helper to get the window handle and process ID
Private Shared Function GetWindowThreadProcessId(ByVal hWnd As IntPtr, ByRef lpdwProcessId As UInteger) As UInteger
End Function
‘ Win32 API Declarations for ROT interaction
Private Shared Function GetRunningObjectTable(ByVal reserved As Integer, ByRef pprot As Object) As Integer
End Function
Private Interface IRunningObjectTable
Sub Register(ByVal punkObject As Object, ByRef pmkObjectName As IMoniker, ByRef pcacheEntry As Object)
Sub Revoke(ByVal dwRegister As Integer)
Sub IsRunning(ByRef pmkObjectName As IMoniker)
Sub GetObject(ByRef pmkObjectName As IMoniker, ByRef ppunkObject As Object)
Sub UnlockObject(ByVal dwRegister As Integer)
End Interface
Private Interface IMoniker
Sub BindToObject(pbc As Object, pmkToLeft As Object, riidRef As Guid, ByRef ppv Out)
Sub BindToStorage(pbc As Object, pmkToLeft As Object, riidRef As Guid, ByRef ppv Out)
Sub Reduce(pbc As Object, nMaskReduce As Integer, ByRef ppmkToLeft As IMoniker, ByRef ppmkReduced As IMoniker)
Sub ComposeWith(pmkToLeft As IMoniker, nSetActions As Integer, ByRef ppmkResult As IMoniker)
Sub ComposeArrayWith(pmkArray As Object, nSetActions As Integer, ByRef ppmkResult As IMoniker)
Sub Inverse(ByRef ppmkInv As IMoniker)
Sub CoinhabitedBy(pmkOther As IMoniker, ByRef ppmkInh As IMoniker)
Sub CommonPrefixWith(pmkOther As IMoniker, ByRef ppmkPrefix As IMoniker)
Sub RelativePathTo(pmkOther As IMoniker, ByRef ppmkPath As IMoniker)
Sub GetParent(ByRef ppmkParent As IMoniker)
Sub GetClassVersion(ByRef pVersion As Integer)
Sub IsEqual(pmkOther As IMoniker)
Sub Hash(ByRef pdwHash As Integer)
Sub IsRelative()
Sub GetRemaining(pbc As Object, ByRef pmkRem As IMoniker)
Sub Get partes(ByRef ppParts As Object)
Sub GetName(pbc As Object, ByRef pmkName As IMoniker)
Sub IsNull()
Sub IsRunning(pbc As Object, pmkOwner As IMoniker, pmkName As IMoniker, ByRef pfRunning As Integer)
Sub GetTimeOfLastChange(pbc As Object, pmkOwner As IMoniker, pmkName As IMoniker, ByRef pfiletime As System.Runtime.InteropServices.ComTypes.FILETIME)
Sub Enum(pbc As Object, ByRef ppenumMoniker As IEnumMoniker)
Sub IsPersistent()
Sub GetStateBits(ByRef grfState As Integer, ByRef pdwStateBits As Integer)
Sub Load(pstm As System.Runtime.InteropServices.ComTypes.IStream)
Sub Save(pstm As System.Runtime.InteropServices.ComTypes.IStream, fClearDirty As Integer)
End Interface
Private Interface IEnumMoniker
Sub Next(celt As Integer, rgelt As IMoniker, pceltFetched As Integer)
Sub Skip(celt As Integer)
Sub Reset()
Sub Clone(ByRef ppenum As IEnumMoniker)
End Interface
Private Shared Function CreateBindCtx(reserved As Integer, ByRef ppbc As IBindCtx) As Integer
End Function
Private Interface IBindCtx
Sub RegisterObjectBound(punk As Object)
Sub UnregisterObjectBound(punk As Object)
Sub EnumObjectBound(ByRef penum As IEnumObjects)
Sub GetBindOptions(pbindopts As Object)
Sub SetBindOptions(pbindopts As Object)
Sub GetCapabilities(ByRef pdwCapab As Integer)
Sub GetRunningObjectTable(ByRef prot As Object)
Sub RegisterObject(pmkToLeft As IMoniker, pmkName As IMoniker, punk As Object)
Sub RevokeObject(pmkName As IMoniker)
Sub ReleaseBoundObjects()
Sub CreateClassEnumerator(rclsid As Guid, ByRef ppunk As Object, pbindOptions As Object)
Sub GetOleVersion(ByRef pVersion As Integer)
End Interface
Private Interface IEnumObjects
Sub Next(celt As Integer, rgelt As Object, pceltFetched As Integer)
Sub Skip(celt As Integer)
Sub Reset()
Sub Clone(ByRef ppenum As IEnumObjects)
End Interface
Public Function GetAcadApplicationByProcessId(processId As Integer) As Object Implements IAcadInstanceManager.GetAcadApplicationByProcessId
Dim runningObjects As Object = GetRunningObjectTable(0)
Dim rotEnumerator As IEnumMoniker
Dim moniker As IMoniker
Dim count As Integer = 0
Dim bindCtx As IBindCtx = Nothing
Try
runningObjects.EnumRunning(rotEnumerator)
rotEnumerator.Next(1, moniker, count)
While count > 0
Dim runningObj As Object = Nothing
Dim displayName As String = Nothing
Dim currentProcessId As Integer = -1
Try
CreateBindCtx(0, bindCtx)
moniker.GetDisplayName(bindCtx, Nothing, displayName)
If displayName.ToLowerInvariant().Contains(“autocad”) Then
runningObjects.GetObject(moniker, runningObj)
‘ Try to get the process ID from the COM object
‘ This is the CRITICAL part that requires robust implementation.
‘ Here we use the simplified helper. In a real scenario,
‘ you might need to use P/Invoke on GetWindowThreadProcessId
‘ if the COM object exposes an HWND.
currentProcessId = GetProcessIdFromComObject_Simple(runningObj)
If currentProcessId = processId Then
Return runningObj ‘ Found the matching application
End If
End If
Catch ex As Exception
‘ Ignore errors for objects that cannot be enumerated or bound
System.Diagnostics.Debug.WriteLine($”Error processing moniker: {ex.Message}”)
Finally
If runningObj IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(runningObj)
If moniker IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(moniker)
If bindCtx IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(bindCtx)
End Try
rotEnumerator.Next(1, moniker, count)
End While
Finally
‘ Ensure COM objects are released
If rotEnumerator IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(rotEnumerator)
If moniker IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(moniker)
If runningObjects IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(runningObjects)
End Try
Return Nothing ‘ Not found
End Function
End Class
End Namespace
【VB.NETヘルパーDLLのVBAからの利用方法】
1. VB.NETプロジェクトで「COM に公開する」設定を有効にし、DLLをビルドします。
2. VBAプロジェクトで「ツールの参照設定」から、ビルドしたDLLを選択します。
3. VBAコードから、以下のように利用します。
‘ VBA Code Example: Using the VB.NET Helper DLL
‘ Author: Legendary Chief Architect
‘ Date: 2023-10-27
‘ Description: Demonstrates how to use the COM-visible VB.NET DLL to get specific AutoCAD instances.
‘ Ensure you have added a reference to the compiled VB.NET DLL in your VBA project.
Sub GetSpecificAcadInstanceAndDoc()
Dim acadManager As Object
Dim acadApp As Object
Dim targetProcessId As Long
Dim acadPIDs As Variant
Dim i As Long
On Error Resume Next ‘ Basic error handling
‘ 1. Get AutoCAD process IDs using the Win32 API function from earlier
acadPIDs = FindAcadProcessIds()
If UBound(acadPIDs) < LBound(acadPIDs) Then
MsgBox "AutoCADプロセスが見つかりませんでした。", vbExclamation
Exit Sub
End If
' --- User Interaction to Select Target Process ---
' In a real system, this selection logic would be more sophisticated.
' For this example, we'll ask the user to choose a PID from the list.
Dim pidList As String
For i = LBound(acadPIDs) To UBound(acadPIDs)
pidList = pidList & (i + 1) & ". " & acadPIDs(i) & vbCrLf
Next i
Dim selection As String
selection = InputBox("操作対象のAutoCADインスタンスを選択してください。" & vbCrLf & vbCrLf & pidList, "AutoCADインスタンス選択")
If selection = "" Then
MsgBox "選択がキャンセルされました。", vbInformation
Exit Sub
End If
Dim selectedIndex As Integer
On Error Resume Next
selectedIndex = CInt(selection) - 1
On Error GoTo 0
If selectedIndex < LBound(acadPIDs) Or selectedIndex > UBound(acadPIDs) Then
MsgBox “無効な選択です。”, vbExclamation
Exit Sub
End If
targetProcessId = acadPIDs(selectedIndex)
MsgBox “選択されたAutoCADプロセスID: ” & targetProcessId
‘ 2. Instantiate the VB.NET COM object
‘ Replace “AutoCADInstanceManager.AcadInstanceManager” with the actual ProgId
Set acadManager = CreateObject(“AutoCADInstanceManager.AcadInstanceManager”)
If acadManager Is Nothing Then
MsgBox “VB.NETヘルパーオブジェクトの作成に失敗しました。”, vbCritical
Exit Sub
End If
‘ 3. Get the specific AcadApplication object using the Process ID
Set acadApp = acadManager.GetAcadApplicationByProcessId(targetProcessId)
‘ Release the VB.NET helper object (important for COM)
Set acadManager = Nothing
If acadApp Is Nothing Then
MsgBox “指定されたプロセスID (” & targetProcessId & “) に対応するAutoCADインスタンスが見つかりませんでした。”, vbExclamation
Exit Sub
End If
‘ 4. Now you have the correct AcadApplication object
MsgBox “対象のAutoCADインスタンスを特定しました。” & vbCrLf & _
“Application Name: ” & acadApp.Caption & vbCrLf & _
“Version: ” & acadApp.Version, vbInformation
‘ 5. Access and control the documents within this specific instance
Dim acadDoc As AcadDocument
Dim docPath As String
‘ Example: Open a specific document or work with existing ones
If acadApp.Documents.Count > 0 Then
‘ Get the first document in the collection
Set acadDoc = acadApp.Documents.Item(1)
docPath = acadDoc.FullName
MsgBox “最初のドキュメント: ” & docPath, vbInformation
‘ Perform operations on acadDoc…
‘ Example: Zoom extents
‘ acadDoc.ZoomExtents
‘ Explicitly release the document object
Set acadDoc = Nothing
Else
MsgBox “このAutoCADインスタンスには開いているドキュメントがありません。”, vbInformation
End If
‘ Explicitly release the application object
Set acadApp = Nothing
MsgBox “操作が完了しました。”, vbInformation
End Sub
【VB.NETヘルパーDLLの重要性と注意点】
- COMインターフェースの正確性: `IRunningObjectTable`, `IMoniker`, `IBindCtx` などのCOMインターフェースの定義は、正確でなければなりません。
- オブジェクトのライフサイクル管理: COMオブジェクトの参照カウント管理は極めて重要です。`Marshal.ReleaseComObject` を適切に使用しないと、メモリリークや、予期せぬオブジェクトの解放を引き起こします。
- `GetProcessIdFromComObject` の実装: この関数は、COMオブジェクトからプロセスIDを正確に取得するための鍵となります。AutoCADのCOMオブジェクトは通常 `HWND` プロパティを公開しており、これを利用して `GetWindowThreadProcessId` APIを呼び出すのが最も一般的で信頼性の高い方法です。VB.NETの例では、この部分を簡略化していますが、実際の開発ではP/InvokeによるWin32 API呼び出しが不可欠です。
- エラーハンドリング: COM呼び出しは失敗しやすいため、厳密なエラーハンドリングと例外処理が必須です。
3. 対象ドキュメントのバインド制御:カレントドキュメントとその落とし穴
特定のAutoCADインスタンスが特定できれば、そのインスタンス内のドキュメントを操作することになります。通常、`AcadApplication.ActiveDocument` プロパティでカレントドキュメントにアクセスしますが、マルチインスタンス環境や、バックグラウンドでの操作においては、この「カレント」が必ずしも意図したドキュメントを指すとは限りません。
3.1. `ActiveDocument` の曖昧さと、ドキュメントコレクションへのアクセス
`AcadApplication.Documents` コレクションは、そのインスタンスで開かれている全てのドキュメントへの参照を提供します。特定のドキュメントを操作したい場合は、`ActiveDocument` に依存するのではなく、`Documents.Item(“DocumentName.dwg”)` や `Documents.OfType(Of AcadDocument)().FirstOrDefault(…)` (VB.NETの場合)といった方法で、名前やその他のプロパティでドキュメントを明示的に特定し、バインドすることが推奨されます。
‘ VBA Code Example: Accessing Documents by Name
‘ Author: Legendary Chief Architect
‘ Date: 2023-10-27
‘ Description: Demonstrates robust document access within a specific AutoCAD instance.
Sub AccessSpecificDocument()
Dim targetApp As AcadApplication ‘ Assume this is already correctly bound to a specific instance
Dim targetDocName As String
Dim specificDoc As AcadDocument
‘ — Assume targetApp is already obtained via the methods described previously —
‘ For demonstration purposes, let’s assume we have a valid targetApp object.
‘ If targetApp is Nothing, the following will fail.
‘ Set targetApp = GetSpecificAcadApplication(…) ‘ Replace with actual call
‘ Example: If targetApp is not yet bound, uncomment and use one of the previous methods.
‘ Dim acadManager As Object
‘ Set acadManager = CreateObject(“AutoCADInstanceManager.AcadInstanceManager”)
‘ targetApp = acadManager.GetAcadApplicationByProcessId(12345) ‘ Example PID
‘ Set acadManager = Nothing
If targetApp Is Nothing Then
MsgBox “対象のAutoCADアプリケーションがバインドされていません。”, vbCritical
Exit Sub
End If
‘ — Specify the document you want to work with —
‘ Ensure you use the full path or just the file name if it’s open in the targetApp
targetDocName = “MyDrawing.dwg” ‘ Replace with the actual document name
On Error Resume Next ‘ Handle cases where the document is not found
‘ Attempt to get the document by name from the Documents collection
Set specificDoc = targetApp.Documents.Item(targetDocName)
On Error GoTo 0 ‘ Turn error handling back on
If specificDoc Is Nothing Then
MsgBox “ドキュメント ‘” & targetDocName & “‘ は、対象のAutoCADインスタンスで開かれていないか、見つかりませんでした。”, vbExclamation
Exit Sub
Else
MsgBox “ドキュメント ‘” & targetDocName & “‘ を特定しました。”, vbInformation
‘ — Perform operations on the specificDoc —
‘ Example: Set the active document to this one (if needed)
specificDoc.Activate
‘ Example: Perform some operations
MsgBox “ドキュメント ‘” & specificDoc.Name & “‘ のレイヤー数を取得します。”, vbInformation
Dim layerCount As Integer
layerCount = specificDoc.Layers.Count
MsgBox “レイヤー数: ” & layerCount, vbInformation
‘ — IMPORTANT: Explicitly release document object —
Set specificDoc = Nothing
End If
‘ — Explicitly release the application object —
Set targetApp = Nothing
End Sub
3.2. バックグラウンド処理とUIスレッドの分離
UIスレッド(ユーザーが直接操作しているAutoCADインスタンス)とは異なるスレッドでVBAマクロを実行する場合、`ActiveDocument` は予期せぬドキュメントを指す可能性があります。また、バックグラウンドでAutoCADを操作する場合、UIの更新を抑制したり、ユーザーの操作を妨げないように配慮が必要です。
- `Application.Visible = False`: マクロ実行中はAutoCADウィンドウを非表示にする。
- `Application.DisplayAlerts = False`: ダイアログボックス(例: 上書き確認)を表示させない。
- `Application.AutomationSecurity = msoAutomationSecurityForceDisable`: マクロの実行を強制的に無効化する(これは通常、VBAエディター内での操作には影響しませんが、外部からの自動化では注意が必要です)。
これらの設定は、操作対象のインスタンスに対してのみ適用し、完了後に元に戻すことが、システム全体の安定稼働に不可欠です。
4. レガシー環境の保守とシステム間連携の極意
長年運用されているAutoCAD VBAシステムは、しばしばレガシーな技術や、他システムとの複雑な連携の上に成り立っています。
- VBA 2.x / 3.x の遺産: 古いバージョンのAutoCAD VBAは、COMの扱いやオブジェクトモデルに若干の違いがあります。現在の環境でこれらのレガシーコードを保守・移行する際は、APIの互換性や、デバッグ手法の差異を理解する必要があります。
- DLL / OCX の依存関係: 外部DLLやOCXに依存している場合、それらのコンポーネントのバージョン管理や、32bit/64bit の互換性問題に直面することがあります。
- 外部システムとの連携:
- データベース連携: SQL Server, Oracle, Access などとの連携では、ADO/DAO の適切な使用と、トランザクション管理が重要です。
- ファイルベース連携: DXF, DWG 以外のフォーマット(CSV, XML, JSON)でのデータ交換は、VBAの `FileSystemObject` や、外部ライブラリ、あるいは .NET の機能を利用して効率化します。
- Webサービス連携: RESTful API などとの連携が必要な場合は、VBAからは直接的な実現が難しいため、VB.NETヘルパーDLLや、PowerShellスクリプトなどを介して実行するのが現実的です。
【システム間連携におけるパフォーマンス最適化】
- バッチ処理の最適化: 大量のデータを処理する場合、ループ処理の回数を最小限にし、必要に応じてCOMオブジェクトの更新(`DoEvents` の適切な挿入)や、バッチ単位での処理実行を検討します。
- メモリ解放の徹底: VBAでは、`Set obj = Nothing` によるオブジェクト解放が明示的に必要です。特に、ループ内で大量のオブジェクトを生成・破棄する場合は、解放漏れがないか注意深くコードレビューを行う必要があります。
- 非同期処理の検討: UIスレッドをブロックしないように、バックグラウンドワーカーや、タスクスケジューラを利用した非同期処理を設計に組み込むことで、ユーザーエクスペリエンスを向上させることができます。
5. まとめ:極限の知見を現場へ
AutoCAD VBAの世界は、一見すると静的で古びた技術に見えるかもしれません。しかし、その根底にあるCOMテクノロジーや、Windows OSの低レベルAPIとの連携を深く理解することで、現代の複雑なシステム環境においても、極めて強力で柔軟な自動化ソリューションを構築することが可能です。
今回詳解した、マルチインスタンス環境での `AcadApplication` 特定と対象ドキュメントのバインド制御は、まさにその一例です。Win32 APIやCOM APIを駆使し、VB.NETヘルパーDLLといったモダンな技術も取り入れながら、堅牢で信頼性の高いシステムを構築すること。それが、長年この世界に身を置く者たちの使命であり、真の「技術」の追求であると、私は確信しています。
レガシーシステムの保守、複雑なシステム連携、そしてパフォーマンスの極限追求。これらの課題に立ち向かう全てのエンジニア、システム管理者の皆様へ。本稿が、皆様の現場での実践に、そして技術的探求の一助となれば幸いです。
