【テクニカル・上級編】【上級プロ】カスタムイベントハンドラ(SldWorksEvents)の導入:アセンブリへの部品追加・削除をVBAでリアルタイム検知 – SolidWorks VBA解析バイブル

スポンサーリンク

1. はじめに:ポーリングという悪夢からの脱却

アセンブリ構築の自動化において、多くのエンジニアが陥る罠がある。それは「タイマー(`Application.OnTime` や `SetTimer` API)を用いて、一定周期でアセンブリのツリー構成をループ走査し、部品の増減をチェックする」というポーリング(監視型ループ)実装だ。

このアプローチは、CPUリソースの無駄飯食いであるだけでなく、SolidWorksのユーザー操作に対するレイテンシを生み、最悪の場合はソリッドモデルのジオメトリ計算中にスレッドが交錯してSolidWorksを強制終了(クラッシュ)させる。

プロフェッショナルが選択すべき唯一の正解は、COMのコネクションポイント(Connection Point)機構を利用した「イベント駆動(Event-Driven)アーキテクチャ」である。

本稿では、SolidWorks VBAの限界を超え、アセンブリ内でのコンポーネント追加・削除・置換をミリ秒単位でリアルタイムに検知・フックする「カスタムイベントハンドラ」の完全な実装パターンを解説する。COMオブジェクトのライフサイクル管理、参照カウントの制御、Windows APIを用いた低レイヤデバッグまで、現場の最前線で培われた極限の知見を明かす。

—

2. SolidWorks COMイベントアーキテクチャの真実

SolidWorks APIは、内部的に標準的なWindows COM(Component Object Model)の `IConnectionPointContainer` および `IConnectionPoint` インターフェース群によって構築されている。

VBAからこれを利用する場合、`WithEvents` キーワードを用いてSolidWorksのディスパッチインターフェース(`IDispatch`)にイベントシンクをバインドする。しかし、ここに大きな罠が存在する。

+————————————————————————–+
| VBA Host Engine |
| |
| +——————————————————————–+ |
| | clsAssemblyEventHook (Class Instance) | |
| | | |
| | [Private WithEvents swApp As SldWorks.SldWorks] | |
| | [Private WithEvents swAssy As SldWorks.AssemblyDoc] | |
| +——-^——————————–^—————————+ |
| | (Event Sink) | (Event Sink) |
+———-|——————————–|——————————+
| |
===========|================================|===============================
| (Connection Point) | (Connection Point)
+———-v——————————–v——————————+
| SolidWorks Core Process (SLDWORKS.exe) |
| |
| +————————–+ +——————————+ |
| | SldWorks API Server | | AssemblyDoc Target Instance | |
| +————————–+ +——————————+ |
+————————————————————————–+

ライフサイクルと参照カウントの「恐怖」

VBAのクラスモジュール内で `WithEvents` を宣言すると、VBAインタープリタは暗黙的にターゲットのCOMオブジェクトに対する参照カウント(Reference Count)を増やす。

もし、ユーザーがGUI側でアセンブリウィンドウを閉じたとしても、VBA側で適切に `Set swAssy = Nothing` を呼び出してイベントフックを解除(Unhook)しない限り、SolidWorks内部でドキュメントオブジェクトがメモリ上に残留し続ける。これが、「SolidWorksを終了したはずなのに `SLDWORKS.exe` プロセスがタスクマネージャーに残り続ける」という深刻なメモリリーク現象の根本原因である。

—

3. 実装の設計指針と設計パターン

イベント検知システムを構築するにあたり、以下の3つのコンポーネントを分離設計する。

1. `clsAssemblyEventHook`(クラスモジュール)

  • SolidWorksアプリケーション(`SldWorks`)およびアセンブリドキュメント(`AssemblyDoc`)のイベントを直接受託するシンククラス。

2. `mdlMain`(標準モジュール)

  • イベントフックの生成・破棄、グローバル変数の管理、およびWindows APIの呼び出しを担当する制御タワー。

3. Windows APIによるデバッグ出力

  • VBAの `Debug.Print` はイミディエイトウィンドウへの同期書き込みにより、高速なイベント連鎖時にタイトなUIスレッドをブロックする危険がある。そのため、`OutputDebugStringA` APIを用いてカーネルレベルのデバッグバッファへ非同期出力を行う。

—

4. 完全実装コード

以下に、実運用に耐えうる堅牢な完全実装コードを示す。

1. `clsAssemblyEventHook.cls` (クラスモジュール)

> 注意: クラスモジュールのオブジェクト名は必ず `clsAssemblyEventHook` に変更すること。

VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “clsAssemblyEventHook”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

‘ ==============================================================================
‘ クラス名: clsAssemblyEventHook
‘ 概要 : SolidWorks アセンブリイベントをリアルタイムフックするクラス
‘ 責務 : イベントの受信、パラメータの解析、および安全なメモリ解放
‘ ==============================================================================

‘ — イベントソースの定義 —
Private WithEvents swApp As SldWorks.SldWorks
Private WithEvents swAssy As SldWorks.AssemblyDoc

‘ — フラグ管理 —
Private m_isHooked As Boolean

‘ ——————————————————————————
‘ フックの初期化メソッド
‘ ——————————————————————————
Public Sub AttachHook(ByRef appInstance As SldWorks.SldWorks, ByRef assyInstance As SldWorks.AssemblyDoc)
On Error GoTo ErrorHandler

If appInstance Is Nothing Or assyInstance Is Nothing Then
Call OutputDebugLog(“[SW_HOOK_ERR] Invalid object reference passed to AttachHook.”)
Exit Sub
End If

‘ イベントソースの割り当て(COM接続点の確立)
Set swApp = appInstance
Set swAssy = assyInstance

m_isHooked = True
Call OutputDebugLog(“[SW_HOOK_INFO] Assembly Event Hook successfully attached. Thread ID: ” & GetCurrentThreadId())
Exit Sub

ErrorHandler:
Call OutputDebugLog(“[SW_HOOK_FATAL] Exception in AttachHook: ” & Err.Description)
End Sub

‘ ——————————————————————————
‘ フックの明示的解除メソッド
‘ ——————————————————————————
Public Sub DetachHook()
On Error Resume Next

If m_isHooked Then
‘ イベントソースのクリア(COM参照カウントのデクリメント)
Set swAssy = Nothing
Set swApp = Nothing
m_isHooked = False
Call OutputDebugLog(“[SW_HOOK_INFO] Assembly Event Hook detached cleanly.”)
End If

On Error GoTo 0
End Sub

‘ ——————————————————————————
‘ [Event Handler] アセンブリに要素(コンポーネント含む)が追加された瞬間のフック
‘ ——————————————————————————
Private Function swAssy_AddItemNotify(ByVal EntityType As Long, ByVal EntityName As String) As Long
On Error GoTo ErrorHandler

‘ swNotifyItemType_e の定義値に基づく判定
‘ swNotifyComponent = 1 (コンポーネントの追加)
If EntityType = 1 Then
Call OutputDebugLog(“[EVENT_ADD_COMP] Component Added: ” & EntityName)

‘ — 独自のビジネスロジック呼び出し(例:属性自動付与、パーツナンバー検証)—
Call OnComponentAddedProcess(EntityName)
End If

‘ 0を返却することでSolidWorks側に処理の継続を伝える (S_OK)
swAssy_AddItemNotify = 0
Exit Function

ErrorHandler:
Call OutputDebugLog(“[SW_HOOK_ERR] Error in AddItemNotify: ” & Err.Description)
swAssy_AddItemNotify = 0
End Function

‘ ——————————————————————————
‘ [Event Handler] アセンブリから要素が削除される直前/直後のフック
‘ ——————————————————————————
Private Function swAssy_DestroyNotify2(ByVal DestroyType As Long) As Long
On Error GoTo ErrorHandler

‘ DestroyType: 1 = Component
Call OutputDebugLog(“[EVENT_DESTROY] Element Destroy Notification. Type: ” & DestroyType)

‘ ドキュメント自体が閉じられる場合の自動自己解放
If DestroyType = 0 Then ‘ Document level destroy
Call OutputDebugLog(“[SW_HOOK_INFO] Target Assembly Document is closing. Detaching hook…”)
Call DetachHook
End If

swAssy_DestroyNotify2 = 0
Exit Function

ErrorHandler:
Call OutputDebugLog(“[SW_HOOK_ERR] Error in DestroyNotify2: ” & Err.Description)
swAssy_DestroyNotify2 = 0
End Function

‘ ——————————————————————————
‘ [Event Handler] アクティブドキュメント切替時のフェイルセーフ
‘ ——————————————————————————
Private Function swApp_ActiveDocChangeNotify() As Long
‘ アクティブモデルが変更された場合の監視・制御が必要な場合はここに実装
swApp_ActiveDocChangeNotify = 0
End Function

‘ ——————————————————————————
‘ 内部処理用ビジネスロジック(スタブ)
‘ ——————————————————————————
Private Sub OnComponentAddedProcess(ByRef compName As String)
‘ ※注意: イベントハンドラ内からSolidWorksのモデルを大々的に変更する操作は
‘ COMの再帰呼び出し制限に抵触するリスクがあるため、ログ記録や非同期フラグ設定に留めるのが鉄則。
Call OutputDebugLog(“[SW_LOGIC] Processing new component entry: ” & compName)
End Sub

‘ ——————————————————————————
‘ クラス破棄イベント
‘ ——————————————————————————
Private Sub Class_Terminate()
Call DetachHook
End Sub

—

2. `mdlMain.bas` (標準モジュール)

Option Explicit

‘ ==============================================================================
‘ モジュール名: mdlMain
‘ 概要 : システムのエントリーポイント及びWin32 API宣言、グローバル状態管理
‘ ==============================================================================

‘ — Win32 API 宣言(64bit / 32bit 互換性確保) —
If VBA7 Then
Public Declare PtrSafe Sub OutputDebugStringA Lib “kernel32” (ByVal lpOutputString As String)
Public Declare PtrSafe Function GetCurrentThreadId Lib “kernel32” () As Long
Else
Public Declare Sub OutputDebugStringA Lib “kernel32” (ByVal lpOutputString As String)
Public Declare Function GetCurrentThreadId Lib “kernel32” () As Long
End If

‘ — グローバル参照維持用変数 —
‘ ※VBA環境では、この参照が失われるとクラスインスタンスがGC(参照カウント0)で消滅し、イベントが停止する。
Private g_EventHook As clsAssemblyEventHook
Private g_SwApp As SldWorks.SldWorks

‘ ——————————————————————————
‘ 監視開始ルーチン(ユーザー実行エントリーポイント)
‘ ——————————————————————————
Public Sub StartAssemblyMonitoring()
Dim swModel As SldWorks.ModelDoc2
Dim swAssy As SldWorks.AssemblyDoc

‘ 既存フックの安全な解放
Call StopAssemblyMonitoring

‘ SolidWorks インスタンス取得
On Error Resume Next
Set g_SwApp = Application.SldWorks
On Error GoTo 0

If g_SwApp Is Nothing Then
MsgBox “SolidWorksのアプリケーションインスタンスを取得できませんでした。”, vbCritical, “エラー”
Exit Sub
End If

‘ アクティブドキュメントの検証
Set swModel = g_SwApp.ActiveDoc
If swModel Is Nothing Then
MsgBox “アセンブリドキュメントが開かれていません。”, vbExclamation, “警告”
Exit Sub
End If

If swModel.GetType <> swDocASSEMBLY Then
MsgBox “現在アクティブなドキュメントはアセンブリではありません。”, vbExclamation, “警告”
Exit Sub
End If

Set swAssy = swModel

‘ フッククラスのインスタンス化とアタッチ
Set g_EventHook = New clsAssemblyEventHook
g_EventHook.AttachHook g_SwApp, swAssy

MsgBox “アセンブリのリアルタイム監視を開始しました。” & vbCrLf & _
“Sysinternals DebugView 等で API ログを確認できます。”, vbInformation, “監視開始”
End Sub

‘ ——————————————————————————
‘ 監視停止ルーチン(明示的クリーンアップ)
‘ ——————————————————————————
Public Sub StopAssemblyMonitoring()
If Not g_EventHook Is Nothing Then
g_EventHook.DetachHook
Set g_EventHook = Nothing
OutputDebugLog “[SW_MAIN] Global Event Hook destroyed.”
End If

Set g_SwApp = Nothing
End Sub

‘ ——————————————————————————
‘ カーネルレベルデバッグログ出力ヘルパー
‘ ——————————————————————————
Public Sub OutputDebugLog(ByVal msg As String)
Dim formattedMsg As String
formattedMsg = “[SW_VBA_HOOK] [” & Format$(Now, “hh:nn:ss”) & “] ” & msg & vbNullChar
OutputDebugStringA formattedMsg
Debug.Print formattedMsg
End Sub

—

5. シニアエンジニアが押さえるべき「本番運用の罠と極意」

上記の実装でイベントのリアルタイム検知は完成するが、これを企業の基幹システムやCAD/CAM連携ソリューションとして本番投入する場合、以下のアーキテクチャ上の制約を完全に理解していなければならない。

① 「再帰呼び出し(Re-entrancy)」によるSolidWorksのデッドロック

最も恐ろしいアンチパターンは、`swAssy_AddItemNotify` のイベントハンドラの中で、直接 `swModel.AddMate5` や `swModel.EditRebuild3` などのモデル変更APIを叩くことである。

SolidWorksが部品追加の内部処理(トランザクション)を実行している最中に、VBA側から再度SolidWorksのAPIを呼んでモデルを変更しようとすると、COMスレッドのリエントラント(再入可能性)の制限に抵触する。結果として、SolidWorksは内部状態の不整合を起こし、無応答(フリーズ)するかサイレントクラッシュする。

【解決策】コンテキストの分離

イベントハンドラ内では「追加されたコンポーネントのポインタまたは名前」をグローバルなキュー構造(`Collection` や `Dictionary`)にスタックするにとどめ、実際の処理は `Application.OnTime`(非同期タイマー)等でUIスレッドに制御が戻った後(=SolidWorksのトランザクション完了後)に遅延実行させる設計をとる。

② VBAの実行ポーズ問題とグローバル変数の「蒸発」

VBA環境特有の重大な弱点として、コード内で未ハンドルの実行時エラーが発生した場合や、開発者がVBAエディタで「リセット」ボタンを押した場合、すべてのグローバル変数(`g_EventHook`)が消滅(蒸発)する。

参照が切れた時点でCOMのイベントフックは静かに死滅し、ユーザーが部品を追加しても二度とイベントは発火しなくなる。

社内システム管理者として運用ツールを提供する場合は、本記事のコードのように必ず標準モジュール側で生存確認を行うラッパーを作るか、長期安定稼働が求められるシステムであれば .NET(C# / VB.NET)を用いたCOM Add-in(アドイン)構造へリファクタリングする べきである。

③ 64bit環境(VBA7)におけるAPI型安全性の遵守

SolidWorksは完全に64bitアーキテクチャへ移行している。VBA7においては、Win32 APIのポインタやハンドルの受け渡しにおいて `LongPtr` を厳密に使用しなければならない。

‘ 厳密な64bit互換性の確保
If VBA7 Then
Public Declare PtrSafe Function GetCurrentThreadId Lib “kernel32” () As Long
Else
Public Declare Function GetCurrentThreadId Lib “kernel32” () As Long
End If

これを怠ると、メモリのアドレス空間が4GBを超えた瞬間に `Access Violation (0xC0000005)` が発生し、CADアプリケーションごとプロセスが消滅する。

—

6. アーキテクトからの結び

「SolidWorks VBAは限定的なマクロ記述言語に過ぎない」という誤解は、COMメカニズムの深層を知らない者の言葉だ。

`WithEvents` を介したイベント同期メカニズムと、Win32 APIによるスレッド境界の意識、そして厳密なCOM参照カウントの制御――これらを統括することで、VBAであってもエンタープライズレベルのリアルタイム自動化ソリューションを構築することが可能となる。

コードの全行に意図を持ち、オブジェクトの命運を完全にコントロールすること。それが、SolidWorksのシステムアーキテクチャを掌握するということである。

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