【厳密なドキュメント制御】ActiveDocに頼らないSldWorks.ActivateDoc3とDocumentCollectionによる安全な文書切り替え
CADオートメーションにおける「魔が差す瞬間」――それは、あなたが作成した完璧なロジックが、ユーザーの気まぐれなクリック一つ、あるいはバックグラウンドで実行されたPLM(製品ライフサイクル管理)システムとの非同期通信によって、一瞬にして崩壊する瞬間である。
SolidWorks VBAの開発において、最も安易に使われ、かつ最も重大なバグの温床となっているAPI、それが `SldWorks.ActiveDoc` である。
本稿では、複数ドキュメントが同時に開かれるマルチタスク環境において、堅牢極まるドキュメント制御を確立するための技術論を展開する。`ActiveDoc` の使用を完全に禁止し、`SldWorks.ActivateDoc3` とドキュメントトラバースによる「確実なオブジェクトハンドリング」を実装するための極限の知見をここに共有する。
—
1. `SldWorks.ActiveDoc` という名の時限爆弾
なぜ `ActiveDoc` を本番環境のコードで使用してはならないのか。その理由は、このAPIが「現在のWindows UI上でフォーカスを持っているドキュメント」という、極めて不安定な状態に依存しているからである。
脅威のメカニズム
1. ユーザーの割り込み操作:
マクロがバッチ処理(100個のパーツを順次オープンしてエクスポートするなど)を行っている最中、ユーザーが画面上をシングルクリックしただけで、アクティブなウィンドウは切り替わる。その瞬間、`ActiveDoc` は書き換え対象外のドキュメントを指し示し、データ破損や異常終了を引き起こす。
2. 非同期イベントの干渉:
アドインや外部連携ツールがバックグラウンドで再構築(Rebuild)や属性同期を実行した際、一時的にアクティブドキュメントのポインタが奪取される。
3. MDI(マルチ・ドキュメント・インターフェース)の罠:
アセンブリを開くと、その構成部品(パーツ)もメモリ上にロードされる。この時、画面上に表示されていなくても、内部的なフォーカス遷移が発生し、`ActiveDoc` が意図しないオブジェクトを返すことがある。
プロフェッショナルな設計において、操作対象のドキュメントは「アクティブなもの」ではなく、「メモリ上にロードされ、一意に特定されたインスタンス(ポインタ)」でなければならない。
—
2. ドキュメント・アイデンティティの確立:トラバースによる厳密な探索
`ActiveDoc` に頼るのをやめる第一歩は、現在SolidWorksのメモリ上に展開されている全ドキュメントを走査(トラバース)し、目的のファイルを「ファイルパス(絶対パス)」によって厳密に特定することである。
SolidWorks APIには、メモリ上のドキュメントコレクションを走査するために `SldWorks.GetFirstDoc` と `ModelDoc2.GetNext` が用意されている。これらをカプセル化し、目的のドキュメント(`ModelDoc2`)を安全に奪取するエンジンを構築する。
なぜファイル名(タイトル)ではなく「フルパス」なのか?
SolidWorksのウィンドウタイトルは、システム設定(拡張子の表示有無、フルパス表示オプションなど)に依存するため、`ModelDoc2.GetTitle` による単純一致判定は極めて脆弱である。一意性を保証できるのは `ModelDoc2.GetPathName`(またはサードパーティ製ストレージへの一時保存パス)のみである。
—
3. `ActivateDoc3` の仕様とハッキング
どうしてもユーザーインターフェース(UI)上で特定のドキュメントを前面に表示し、アクティブ化しなければならないケースがある(例:図面出力の直前処理、ユーザーへの確認プロンプト表示など)。
その際に使用するのが `SldWorks.ActivateDoc3` である。このAPIは、単にドキュメントをアクティブにするだけでなく、アクティベーション後の `ModelDoc2` のインスタンスを戻り値として返すという重要な特性を持つ。
SldWorks.ActivateDoc3 のシグネチャ
Set ModelDoc2 = SldWorks.ActivateDoc3 ( Name, Option, RebuildOnActivation, Errors )
- `Name`:
アクティブにするドキュメントの名前。ここには通常、「拡張子を含むファイル名」(例: `”Part1.sldprt”`)を渡す。フルパスではない点に注意が必要である。
- `Option`:
`swActivateDocOptions_e` 列挙型を指定する。通常は、ユーザーの意思決定を介在させずサイレントに処理を進めるため `swUserDecision`(ユーザーへの確認を促す)を避け、`swRebuildOnActivation`(アクティベート時に再構築する)または `0`(デフォルト)を指定する。
- `RebuildOnActivation`:
アクティベート時にモデルを強制再構築するかどうかのブーリアン値。パフォーマンスを最優先する場合は `False` を指定し、形状の最新化を保証する場合は `True` を指定する。
- `Errors`:
エラーコードを受け取る `Long` 型変数。`swActivateDocError_e` 列挙型によって、失敗理由(ファイルが見つからない、ロードされていない等)を特定できる。
—
4. 極限のドキュメント・マネジメント・コード
以下に、実戦でそのまま使用できる堅牢なドキュメント制御モジュールを示す。
このコードは、以下のプロフェッショナル要件を満たしている。
1. 画面描画の凍結 (Win32 API): ドキュメント切り替え時の画面のチラつき(フリッカー)を極限まで抑え、処理速度を向上させる。
2. 厳密なトラバース: メモリ上のドキュメントからターゲットパスを探索。
3. 安全なアクティベーション: `ActivateDoc3` の戻り値検証とエラーハンドリング。
4. COM参照の厳密な管理: 参照カウンタを意識したオブジェクトの解放。
Option Explicit
‘ ==============================================================================
‘ Win32 API Definitions for Performance and UI Control
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hwndLock As LongPtr) As Long
Else
Private Declare Function LockWindowUpdate Lib “user32″ (ByVal hwndLock As Long) As Long
End If
‘ SolidWorks API Errors
Private Const swDocTemplateNotSpecified As Long = 1
Private Const swGenericError As Long = 1
”’
”’
Public Function SafeActivateDocument(ByVal swApp As SldWorks.SldWorks, ByVal targetFullPath As String) As SldWorks.ModelDoc2
Dim targetDoc As SldWorks.ModelDoc2
Set targetDoc = FindOpenedDocument(swApp, targetFullPath)
If targetDoc Is Nothing Then
‘ メモリ上に存在しない場合は、ここでオープン処理を挟むか、エラーとする
Err.Raise vbObjectError + 513, “SafeActivateDocument”, “指定されたドキュメントはメモリ上にロードされていません: ” & targetFullPath
End If
‘ ドキュメント名(拡張子付きファイル名)の抽出
Dim fileName As String
fileName = Mid(targetFullPath, InStrRev(targetFullPath, “\”) + 1)
‘ Win32 APIによる描画停止(パフォーマンス向上とチラつき防止)
Dim swFrame As SldWorks.Frame
Set swFrame = swApp.Frame
#If VBA7 Then
Dim hwnd As LongPtr
#Else
Dim hwnd As Long
#End If
hwnd = swFrame.GetHWnd
Call LockWindowUpdate(hwnd)
On Error GoTo CleanUp
Dim errors As Long
Dim activatedDoc As SldWorks.ModelDoc2
‘ ActivateDoc3の実行
‘ オプション: 0 (swActivateDocOptions_e.swUserDecision を避ける)
‘ 再構築: False (パフォーマンス重視。必要に応じてTrueに変更)
Set activatedDoc = swApp.ActivateDoc3(fileName, 0, False, errors)
If activatedDoc Is Nothing Or errors <> 0 Then
Err.Raise vbObjectError + 514, “SafeActivateDocument”, “ドキュメントのアクティベーションに失敗しました。Error Code: ” & errors
End If
‘ 整合性の検証(ポインタレベルでの検証)
If Not activatedDoc Is targetDoc Then
‘ 万が一、アクティベートされたドキュメントが意図したものと異なる場合
Err.Raise vbObjectError + 515, “SafeActivateDocument”, “ドキュメントの整合性検証に失敗しました。ターゲットが一致しません。”
End If
Set SafeActivateDocument = activatedDoc
CleanUp:
‘ 描画ロックの解除(必ず実行すること)
Call LockWindowUpdate(0)
‘ COMオブジェクトの明示的解放
Set swFrame = Nothing
Set targetDoc = Nothing
‘ エラーの再スロー
If Err.Number <> 0 Then
Dim errDesc As String
errDesc = Err.Description
On Error GoTo 0
Err.Raise Err.Number, “SafeActivateDocument”, errDesc
End If
End Function
”’
”’
Public Function FindOpenedDocument(ByVal swApp As SldWorks.SldWorks, ByVal targetFullPath As String) As SldWorks.ModelDoc2
Dim currentDoc As SldWorks.ModelDoc2
Set currentDoc = swApp.GetFirstDoc
Dim normalizedTarget As String
normalizedTarget = UCase(Trim(targetFullPath))
Do While Not currentDoc Is Nothing
Dim currentPath As String
currentPath = UCase(Trim(currentDoc.GetPathName))
‘ パスの一致検証(新規未保存ドキュメントはGetPathNameが空になるため除外)
If currentPath <> “” Then
If currentPath = normalizedTarget Then
Set FindOpenedDocument = currentDoc
Exit Function
End If
End If
‘ 次のドキュメントへ移動
Dim nextDoc As SldWorks.ModelDoc2
Set nextDoc = currentDoc.GetNext
Set currentDoc = nextDoc
Loop
‘ 見つからなかった場合はNothingを返す
Set FindOpenedDocument = Nothing
End Function
—
5. メモリ最適化とCOM参照ライフサイクルの真実
SolidWorks VBA(およびVB.NET / C#による外部連携)において、最も開発者を悩ませるのが「マクロ終了後にSolidWorksのプロセス(`SLDWORKS.exe`)がゾンビ化して残る」現象や、メモリリークである。
ドキュメントの切り替えやトラバースを行うコードは、大量のCOMオブジェクト(`ModelDoc2`)への参照を生成する。VBAはガベージコレクション(GC)を持たず、参照カウンタ方式でメモリを管理しているため、以下のルールを鉄則として守らなければならない。
COMオブジェクト解放の鉄則
1. `Set obj = Nothing` の徹底:
プロシージャのスコープを抜ければ自動的に解放されるというのは、COMの世界、特にSolidWorks APIにおいては半分迷信である。特に、ループ処理の内部で一時変数に格納する `ModelDoc2` や `Component2` は、次の要素に代入する前に必ず `Set nextDoc = Nothing` で参照を明示的に外す必要がある。上記の `FindOpenedDocument` 内のトラバースコード:
Dim nextDoc As SldWorks.ModelDoc2
Set nextDoc = currentDoc.GetNext
Set currentDoc = nextDoc
これは、参照の「数珠つなぎ」を防ぎ、古い `currentDoc` がメモリに保持され続けるのを防ぐ設計になっている。
2. Win32 APIのロック解除保証:
`LockWindowUpdate` を呼び出した場合、如何なるエラーが発生しても必ず `LockWindowUpdate(0)` を実行して描画ロックを解除しなければならない。これを行わずにプロシージャを異常終了させると、SolidWorksのUI全体がフリーズし、タスクマネージャーから強制終了するしかなくなる。そのため、エラーハンドラ(`GoTo CleanUp`)の配置は必須である。
—
6. アーキテクトの思想:防衛的プログラミングの確立
本稿で紹介した手法は、単に「バグを減らす」ためのものではない。数万点の部品で構成される大規模アセンブリを対象に、深夜のバッチ処理で何百もの図面PDFを自動生成するような、「絶対に止まることが許されないシステム」を稼働させるための必須要件である。
`ActiveDoc` をコードから完全に排除すること。それは、ユーザーの介在という「不確定要素」をシステムから完全に排除することを意味する。
あなたの書くコードが、真の意味でエンタープライズクラスの信頼性を獲得するために。今日からすべての `SldWorks.ActiveDoc` を、厳密なポインタトラバースと `ActivateDoc3` による安全なハンドリングへとリファクタリングしていただきたい。それが、システムを掌握するということである。
