【テクニカル・上級編】【上級プロ】カスタムイベントとWindows APIの非同期タイマーを融合した、アセンブリ自動ビルド中のリアルタイム進捗&中断UIの実装 – SolidWorks VBA解析バイブル

スポンサーリンク

【上級プロ】カスタムイベントとWindows APIの非同期タイマーを融合した、アセンブリ自動ビルド中のリアルタイム進捗&中断UIの実装

SolidWorks VBAによる大規模アセンブリの自動生成(ボトムアップビルド)において、開発者が直面する最大の壁は「UIの完全なフリーズ」と「処理中断の不確実性」である。

数千点に及ぶ部品のロード、マトリクス計算、そして幾何学的拘束(Mate)の解決をループさせるとき、VBAのシングルスレッドモデルは容易に沈黙する。`DoEvents`を挟み込めば画面は描画されるが、ネストが深いループ内ではパフォーマンスが著しく低下し、最悪の場合はCOMオブジェクトのコンテキスト喪失による不意のクラッシュを誘発する。

本稿では、Windows APIのタイマー割り込みとカスタムクラスによるイベント駆動アーキテクチャを融合し、SolidWorksの重厚長大なCOM操作を阻害することなく、極めて滑らかなプログレスバーと確実なキャンセル機構を実装する極限の知見を公開する。

—

1. アーキテクチャの核心:なぜ通常の `DoEvents` では不十分なのか

VBAにおける標準的な処理中断手法は `DoEvents` 関数だが、これには致命的な欠陥がある。
1. メッセージポンプの過負荷: ループの度にOSのメッセージキューをポーリングするため、アセンブリの数学的計算リソースが奪われ、処理時間が1.5倍〜2倍に跳ね上がる。
2. スタックの深化: ユーザーがキャンセルボタン連打やウィンドウ移動を行った際、VBAのコールスタックが予期せぬ深さに達し、COM例外(RPC_E_CANTCALLOUT_IN_ASYNCCALLなど)を引き起こす。

これを解決するため、「Windows APIによる非同期タイマー(TimerProc)」と「カスタムクラスによる疎結合なイベント通知」を組み合わせる。SolidWorksのメインスレッドをブロックせず、一定時間ごとにメッセージを処理しつつ、アセンブリ生成の進捗状態を安全にUIスレッドへ伝播させる。

—

2. 実装コード:完全版モジュール群

以下のコードは、標準モジュール、クラスモジュール、そしてWindows API宣言を適切に配置することで、堅牢な非同期ビルド環境を構築する。

① Windows API およびタイマーコールバックの定義(標準モジュール: `mAsyncBuilder`)

Option Explicit

‘ — Windows API Declarations —
Private Declare PtrSafe Function SetTimer Lib “user32” (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib “user32” (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long

Private m_TimerID As LongPtr
Private m_IsCancelled As Boolean
Private m_ProgressController As clsProgressManager

Public Sub StartAsyncBuildProcess()
Dim swApp As Object
Set swApp = Application.SldWorks

‘ キャンセル フラグの初期化
m_IsCancelled = False

‘ プログレスマネージャーの初期化(カスタムイベント用)
Set m_ProgressController = New clsProgressManager

‘ ユーザーフォームの表示(モードレス)
Dim frmProgress As frmProgressUI
Set frmProgress = New frmProgressUI
frmProgress.Initialize m_ProgressController
frmProgress.Show vbModeless

‘ Windows API タイマーの起動 (50ms間隔でメッセージポンプを駆動)
m_TimerID = SetTimer(0, 0, 50, AddressOf TimerCallback)

‘ 実際の重いアセンブリビルド処理を実行
ExecuteHeavyAssemblyBuild swApp, m_ProgressController

‘ タイマーの破棄
If m_TimerID <> 0 Then
KillTimer 0, m_TimerID
m_TimerID = 0
End If

‘ 終了処理
Unload frmProgress
Set frmProgress = Nothing
Set m_ProgressController = Nothing

MsgBox “アセンブリの自動ビルドが完了しました。”, vbInformation
End Sub

‘ タイマー割り込み関数(Windows APIからコールバックされる)
Private Sub TimerCallback(ByVal hwnd As LongPtr, ByVal uMsg As LongPtr, ByVal idEvent As LongPtr, ByVal dwTime As LongPtr)
‘ この中で必要に応じた非同期ポーリングやUIの描画更新を行う
DoEvents
End Sub

Public Sub RequestCancel()
m_IsCancelled = True
End Sub

Public Function GetCancelState() As Boolean
GetCancelState = m_IsCancelled
End Function

② イベント発行用クラス(クラスモジュール: `clsProgressManager`)

Option Explicit

‘ UIへ進捗とステータスを通知するためのカスタムイベント
Public Event ProgressChanged(ByVal Current As Long, ByVal Total As Long, ByVal StatusText As String)
Public Event BuildCancelled()

Public Sub ReportProgress(ByVal currentStep As Long, ByVal totalSteps As Long, ByVal message As String)
‘ キャンセル要求の監視
If GetCancelState() Then
RaiseEvent BuildCancelled
Err.Raise 9999, “Builder”, “ユーザーによって処理がキャンセルされました。”
End If

‘ 進捗イベントの発火
RaiseEvent ProgressChanged(currentStep, totalSteps, message)

‘ 最小限のイベント処理を許可
DoEvents
End Sub

③ アセンブリ構築・合致(Mate)自動定義ロジック(標準モジュール内サブルーチン)

Private Sub ExecuteHeavyAssemblyBuild(ByVal swApp As Object, ByVal pm As clsProgressManager)
Dim swModel As Object
Dim swAssy As Object
Dim totalComponents As Long
Dim i As Long

totalComponents = 100 ‘ 例として100部品のビルドを想定

‘ 新規アセンブリの作成
Set swModel = swApp.NewDocument(“C:\ProgramData\SolidWorks\SolidWorks 2023\templates\Assembly.asmdt”, 0, 0, 0)
Set swAssy = swModel

On Error GoTo ErrorHandler

For i = 1 To totalComponents
‘ 進捗報告(ここでキャンセルチェックも行われる)
pm.ReportProgress i, totalComponents, “部品配置中: ID-” & i

‘ — 実際のSolidWorks API操作 —
‘ 例: swAssy.AddComponent5(…)
‘ 例: AddMate(…) などの重い処理

‘ パフォーマンス維持のためのウェイト(必要に応じて調整)
Dim startTimer As Double
startTimer = Timer
Do While Timer < startTimer + 0.02 ' 微小な遅延によるCPUスパイクの抑制 DoEvents Loop Next i Exit Sub ErrorHandler: If Err.Number = 9999 Then ' キャンセル時のクリーンアップ swApp.CloseDoc swModel.GetTitle MsgBox "処理は中断されました。", vbExclamation, "中断" Else MsgBox "エラーが発生しました: " & Err.Description, vbCritical End If End Sub ---

3. 現場で絶対に押さべきメモリ最適化とCOMのライフサイクル管理

SolidWorks VBAにおいて、メモリリークとCOMオブジェクトの解放漏れは、ExcelやSolidWorks自体の強制終了(クラッシュ)の主要な原因となる。

オブジェクトの明示的破棄(`Nothing`代入の作法)

ループ内でコンポーネントやマティカル・データ(MathUtilityなど)を生成・取得する場合、ローカル変数をそのまま放置してはならない。

Dim swComp As Object
Set swComp = swAssy.AddComponent5(…)
‘ — 処理 —
Set swComp = Nothing ‘ 毎回スコープごとに確実に参照を断つ

VBAのガベージコレクションは参照カウント方式(Reference Counting)に依存しているため、特にアセンブリ階層(Component2やFeature)を巡回する際は、変数の再利用を避け、使い捨ての変数をスコープ単位で `Nothing` に明示的解放することが鉄則である。

強制終了時の例外捕捉と安全なロールバック

大規模アセンブリの途中でユーザーがキャンセルした場合、中途半端に拘束(Mate)が付与された壊れたアセンブリファイルがメモリ上、あるいはディスク上に残る。
前述のコード例のように、`Err.Raise` を利用したカスタム例外をキャッチし、エラーハンドラー内で必ず `swApp.CloseDoc` によるドキュメントの破棄(変更保存なし)を行うこと。これにより、PDMシステムやローカルワークスペースをゴミデータで汚染するリスクを完全に排除できる。

—

4. チーフアーキテクトからの提言

「動けばいい」という妥協の産物であるスパゲッティコードのVBAマクロは、現場のエンジニアの時間を奪う最大の負債である。
Windows APIの非同期機構とVBAのクラスイベントを結合した本アーキテクチャを導入することで、何千部品をもつ複雑なアセンブリの自動化であっても、市販のデスクトップアプリケーションと同等の、滑らかでストレスのないUI体験を実現することが可能となる。

堅牢性、パフォーマンス、そしてオペレータビリティのすべてを妥協なく追求せよ。それがプロフェッショナルのコードベースである。

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