从外部进程调用 SOLIDWORKS 插件 API 的内部调用
独立自动化 COM 基于应用程序自动化(包括 SOLIDWORKS)的主要限制之一是性能。
当需要从外部进程调用数百个 API 调用时,性能可能下降数百甚至数千倍,与内部调用相比。
同样的限制也适用于以下任何一种调用插件 API 的方法:通过插件对象,通过运行对象表等。
可以错误地认为插件内部的所有 SOLIDWORKS API 调用都是通过内部调用的,因为只有一个 API 函数从独立应用程序中调用。但实际上,SOLIDWORKS 插件中的所有 SOLIDWORKS API 调用都是作为外部调用进行的。这意味着调用插件 API 将导致与调用独立应用程序相同的性能损失。
然而,有一种方法可以最大限度地提高性能,并通过从外部进程应用程序调用此方法获得与内部调用相同的结果。
以下插件示例实现了一个函数,用于索引活动装配文档的所有面。
插件使用 SwEx.AddIn 框架 开发,但是相同的技术也适用于使用不同方法构建的插件。
它遍历所有组件、所有实体和所有面,并在跟踪窗口中输出有关面的一些信息。
插件具有一个菜单命令,允许在进程中调用其函数。
{ width=350 }
完成后,将显示带有结果的消息框。
{ width=300 }
FaceIndexer 插件
这是一个实现 SOLIDWORKS 插件和 API 对象接口的主要项目。
FaceIndexerAddIn.cs
插件类
using CodeStack.SwEx.AddIn;
using CodeStack.SwEx.AddIn.Attributes;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace CodeStack.FaceIndexer
{
[AutoRegister("FaceIndexer")]
[ComVisible(true)]
[Guid("D85E0EEB-87AA-48BE-8C8A-DFD71CF12525")]
[ProgId("CodeStack.FaceIndexer")]
public class FaceIndexerAddIn : SwAddInEx, IFaceIndexerAddIn
{
private readonly List<KeyValuePair<IAssemblyDoc, IFaceIndexerCallback>> m_ProcessingQueue
= new List<KeyValuePair<IAssemblyDoc, IFaceIndexerCallback>>();
[SwEx.Common.Attributes.Title("Face Indexer")]
public enum Commands_e
{
[CommandItemInfo(SwEx.AddIn.Enums.swWorkspaceTypes_e.Assembly)]
IndexFaces
}
public override bool OnConnect()
{
AddCommandGroup<Commands_e>(OnButtonClick);
(App as SldWorks).OnIdleNotify += OnIdleNotify;
return true;
}
public void BeginIndexFaces(IAssemblyDoc assm, IFaceIndexerCallback callback)
{
m_ProcessingQueue.Add(new KeyValuePair<IAssemblyDoc, IFaceIndexerCallback>(assm, callback));
}
private void OnButtonClick(Commands_e cmd)
{
switch (cmd)
{
case Commands_e.IndexFaces:
IndexFaces(App.IActiveDoc2 as IAssemblyDoc);
break;
}
}
public int IndexFaces(IAssemblyDoc assm)
{
var count = 0;
var start = DateTime.Now;
{
var comps = assm.GetComponents(false) as object[];
if (comps != null)
{
foreach (IComponent2 comp in comps)
{
object bodyInfo;
var bodies = comp.GetBodies3((int)swBodyType_e.swAllBodies, out bodyInfo) as object[];
if (bodies != null)
{
foreach (IBody2 body in bodies)
{
var faces = body.GetFaces() as object[];
if (faces != null)
{
foreach (IFace2 face in faces)
{
var surf = face.IGetSurface();
var type = (swSurfaceTypes_e)surf.Identity();
count++;
Trace.WriteLine($"Area: {face.GetArea()}. Type: {type}");
}
}
}
}
}
}
}
App.SendMsgToUser($"{count} face(s) of {(assm as IModelDoc2).GetTitle()} indexed in {DateTime.Now.Subtract(start).TotalSeconds} seconds");
return count;
}
private int OnIdleNotify()
{
const int S_OK = 0;
foreach (var assmInQueue in m_ProcessingQueue)
{
var count = IndexFaces(assmInQueue.Key);
assmInQueue.Value?.IndexFacesCompleted(assmInQueue.Key, count);
}
m_ProcessingQueue.Clear();
return S_OK;
}
}
}
FaceIndexerAddInApi.cs
API 对象定义。
using SolidWorks.Interop.sldworks;
using System.Runtime.InteropServices;
namespace CodeStack.FaceIndexer
{
[ComVisible(true)]
public interface IFaceIndexerCallback
{
void IndexFacesCompleted(IAssemblyDoc assm, int count);
}
[ComVisible(true)]
public interface IFaceIndexerAddIn
{
void BeginIndexFaces(IAssemblyDoc assm, IFaceIndexerCallback callback);
int IndexFaces(IAssemblyDoc assm);
}
}
此插件向第三方公开 API。IndexFaces 方法是一个外部进程的 API 调用,可以使用以下代码片段:
var count = addIn.IndexFaces(assm);
Console.WriteLine($"已索引 {count} 个面");
结果性能几乎下降了一百倍:
{ width=300 }
使用 ISldWorks::CommandInProgress SOLIDWORKS API 属性可以稍微改善一下,但与基准结果相比,性能仍然下降了超过 10 倍。
app.CommandInProgress = true;
var count = addIn.IndexFaces(assm);
app.CommandInProgress = false;
Console.WriteLine($"已索引 {count} 个面");
下面是结果的比较表。结果可能因装配的大小和使用的 API 调用而异。
环境 | 结果(秒) | 比率(%) |
---|---|---|
插件内部调用 | 2.63 | 1 |
独立应用程序 | 241.95 | 92 |
独立应用程序命令进行中 | 36.14 | 13.74 |
VBA 宏 | 2.57 | 0.98 |
VBA 宏内部调用 | 2.20 | 0.84 |
独立应用程序内部调用 | 1.77 | 0.67 |
最佳性能是通过从独立应用程序作为内部调用调用插件 API 获得的。通过提供延迟调用来索引面,可以实现此功能。此调用将请求放入队列并立即返回控制权。然后,请求将在插件中处理。可以使用 OnIdle SOLIDWORKS API 通知来处理队列。由于此事件在进程中处理,实际的 API 调用也将在进程中处理。
注册回调函数也很重要,插件可以调用它来通知独立应用程序操作已完成。
以下是在独立应用程序中调用插件 API 的示例。
独立应用程序
调用插件函数的 C# 应用程序。
FaceIndexerCallback.cs
回调函数,当内部调用完成时通知独立应用程序。这必须注册为 COM 对象。
using CodeStack.FaceIndexer;
using SolidWorks.Interop.sldworks;
using System;
using System.Runtime.InteropServices;
namespace StandAlone
{
[ComVisible(true)]
public class FaceIndexerCallback : IFaceIndexerCallback
{
public void IndexFacesCompleted(IAssemblyDoc assm, int count)
{
Console.WriteLine($"已在独立应用程序中完成 '{(assm as IModelDoc2).GetTitle()}' 的索引,共 {count} 个面");
}
}
}
Program.cs
调用内部调用插件 API 并在回调中等待结果的控制台应用程序。
using CodeStack.FaceIndexer;
using SolidWorks.Interop.sldworks;
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace StandAlone
{
class Program
{
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
static void Main(string[] args)
{
var app = GetSwAppFromProcess(Process.GetProcessesByName("SLDWORKS").First().Id);
var addIn = app.GetAddInObject("CodeStack.FaceIndexer") as IFaceIndexerAddIn;
var assm = app.IActiveDoc2 as IAssemblyDoc;
addIn.BeginIndexFaces(app.IActiveDoc2 as IAssemblyDoc, new FaceIndexerCallback());
Console.ReadLine();
}
private static ISldWorks GetSwAppFromProcess(int processId)
{
var monikerName = "SolidWorks_PID_" + processId.ToString();
IBindCtx context = null;
IRunningObjectTable rot = null;
IEnumMoniker monikers = null;
try
{
CreateBindCtx(0, out context);
context.GetRunningObjectTable(out rot);
rot.EnumRunning(out monikers);
var moniker = new IMoniker[1];
while (monikers.Next(1, moniker, IntPtr.Zero) == 0)
{
var curMoniker = moniker.First();
string name = null;
if (curMoniker != null)
{
try
{
curMoniker.GetDisplayName(context, null, out name);
}
catch (UnauthorizedAccessException)
{
}
}
if (string.Equals(monikerName,
name, StringComparison.CurrentCultureIgnoreCase))
{
object app;
rot.GetObject(curMoniker, out app);
return app as ISldWorks;
}
}
}
finally
{
if (monikers != null)
{
Marshal.ReleaseComObject(monikers);
}
if (rot != null)
{
Marshal.ReleaseComObject(rot);
}
if (context != null)
{
Marshal.ReleaseComObject(context);
}
}
return null;
}
}
}
它也可以从宏或任何其他类型的应用程序中调用。
VBA 宏
调用插件 API 的 VBA 宏。在此示例中,使用用户窗体使宏保持运行,直到调用回调函数。
{ width=250 }
宏模块
启动用户窗体的主模块
Sub main()
UserForm1.Show vbModeless
End Sub
FaceIndexerCallback 类模块
实现回调类以接收完成的通知
Implements IFaceIndexerCallback
Private Sub IFaceIndexerCallback_IndexFacesCompleted(ByVal assm As SldWorks.IAssemblyDoc, ByVal count As Long)
Debug.Print "已完成 " & count & " 个面的索引"
End Sub
Form1 窗体
用户窗体以连接到插件并调用其 API
Dim swFaceIndexer As IFaceIndexerAddIn
Private Sub UserForm_Initialize()
Dim swApp As SldWorks.SldWorks
Set swApp = Application.SldWorks
Set swFaceIndexer = swApp.GetAddInObject("CodeStack.FaceIndexer")
Dim swAssy As SldWorks.AssemblyDoc
Set swAssy = swApp.ActiveDoc
swFaceIndexer.BeginIndexFaces swAssy, New FaceIndexerCallback
End Sub
源代码可从 GitHub 下载