用C#做CATIA二次开发(1)
CATIA默认的二次开发语言是CATScript,语法和VBA很像,是纯文本格式,用通用的文本编辑器就可以处理,不像VBA必须要专用编辑器才能打开。这种开发语言还可以在Unix下运行。没错,CATIA有Unix版本,只不过国内很少见。
理论上,C语言可以连通所有的计算机程序,COM也可以支持,但是C/C++需要额外的头文件.h、导出库.lib.a,这些都不在CATIA默认安装中,需要通过额外的二次开发包CAA RADE来支持。就是说开发者除了CATIA软件本体以外,还需要额外安装RADE,实际上很多高级的二次开发都是基于RADE的,而且很多都已经做到了商业化。但是RADE这个软件包市面上很少有流出。在没有RADE支持的情况下使用C/C++做CATIA二次开发就受到了严重的限制,理论上可以用dllexport直接读取CATIA的二进制文件,然后根据导出的符号来推断其函数名和参数,但是CATIA有大量的二进制文件,这工作量不仅超级大,而且读出来的很可能不是那种适合二次开发的接口,很可能是那种很难使用的内部接口。
在Windows平台,CATIA还支持VBA进行二次开发。CATIA的VBA支持是通过COM实现的,也就是说只要编程语言支持COM,其实都可以做CATIA二次开发,而C#也支持COM,所以C#也支持CATIA的二次开发。VBA只能用自带的VBA编辑器,语法高亮、自动提示等功能都没有,而且有些底层的VB函数可能支持的还不好。C#可以使用Visual Studio,语法高亮、自动提示、点击跳转……多种IDE支持都很好,而且其语法也比VBA好得多,标准库功能也更完善。VBA被逆向可是超级简单的,毕竟只有一个密码保护;虽然C#也很容易被逆向,不过至少还是二进制的,还有代码混淆等保护手段。
今天初步尝试了使用C#进行CATIA二次开发,记录下来以供将来回顾。
.net Framework的方法
入门第一步就是让C#能够连接到正在运行的CATIA程序。
在Visual Studio创建一个基于.net framework的程序,然后在“C#项目->引用”上点“右键->添加引用”,然后选择“COM”,把“CATIA”开头的都勾选上,另外还要勾选“CATStiWIPBridgeSurrogateCOMExe”。理论上讲,不同的CATIA功能只需要勾选对应的模块即可,ApplicationFrame就是CATIA基本框架,CATPartInterfaces是CAITA零件设计模块,CATAssemblyInterfaces是CATIA装配模块,Drafting2DLInterface是CATIA二维绘图模块……但是我懒得分辨,干脆全勾上,用到哪个算哪个。
程序代码如下
using INFITF; // CATIA V5 ApplicationFramework最基本的CATIA库
using MECMOD; // CATIA PartDesign模块
using System;namespace MyProgram
{/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>// INFITF也有Window子模块,所以这里必须要指明public partial class MainWindow : System.Windows.Window{private INFITF.Application mainInstance; // CATIA进程对象public MainWindow(){InitializeComponent();}private void MyButtonClick(object sender, RoutedEventArgs e){mainInstance = CatiaAutomation.GetCatia();MECMOD.PartDocument ActivePartDocument = (MECMOD.PartDocument)mainInstance.ActiveDocument;MECMOD.Part ActivePart = ActivePartDocument.Part;myButton.Content = ActivePart.get_Name();}}public static class CatiaAutomation{// 获取正在运行的CATIA应用实例public static INFITF.Application GetCatia(){try{// 尝试获取已运行的实例,并将其转化为CATIA实例类型INFITF.Applicationreturn (INFITF.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("CATIA.Application");}catch (COMException){// 如果未找到,抛出异常}}}
}
关键点在
System.Runtime.InteropServices.Marshal.GetActiveObject("CATIA.Application");
CATIA.Application是CATIA注册的COM类型标识,Marshal.GetActiveObject函数通过标识符就可以搜索这个类型,如果系统中有正在运行的CATIA实例就能找到,随后的操作自然是在CATIA实例中操作。如果找不到,那就是系统中没有正在运行的CATIA实例
https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.interopservices.marshal.getactiveobject?view=netframework-4.8.1
在界面的xaml中,添加一个Button,点击这个Button时会调用MyButtonClick函数,如果想要在C#中操作这个Button,它的关联名是myButton
<Button x:Name="myButton" Content="连接CATIA" Click="MyButtonClick"/>
在C#代码中可以看到MyButtonClick函数的内容
private void MyButtonClick(object sender, RoutedEventArgs e)
{mainInstance = CatiaAutomation.GetCatia();MECMOD.PartDocument ActivePartDocument = (MECMOD.PartDocument)mainInstance.ActiveDocument;MECMOD.Part ActivePart = ActivePartDocument.Part;myButton.Content = ActivePart.get_Name();
}
首先获取了当前正在运行的CATIA实例,然后通过MECMOD模块获取了当前实例中的当前文档,接着获取了当前文档中的活动零件,最后把myButton上的内容改为活动零件的名字。
为了保证这段代码能够正确运行,需要
- 启动CATIA
- 在启动的CATIA中新建或者打开一个零件
如果没有启动CATIA,这段C#代码找不到正在运行的CATIA实例,那么mainInstance就得不到
如果CATIA没有正在处理任何文件,那么ActivePartDocument就获取不到
如果CATIA当前正在处理的不是零件文件(而是装配或者其他类型的文件),那么ActivePart就得不到
其实这些代码如果想要更加健壮,完全可以增加其他的保护性代码,比如说对得不到的情况做各种判断。不过本文只是引领入门,这些意外情况暂不讨论。
也就是说在获得了CATIA运行实例后,就可以对这个实例进行各种操作,还可以对实例外的东西——比如说myButton——进行各种操作,这样就可以对CATIA做二次开发了。
从代码的适应性来看,如果CATIA实例没有运行,那么由C#代码直接启动一个CATIA实例似乎是可行的
public static class CatiaHelper
{/// <summary>/// 获取 CATIA 应用实例,如果未运行则启动/// </summary>/// <param name="makeVisible">是否使 CATIA 可见(新启动时有效)</param>/// <returns>CATIA 应用实例</returns>public static INFITF.Application GetOrStartCatia(bool makeVisible = true){try{// 首先尝试获取已运行的实例return (INFITF.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("CATIA.Application");}catch (COMException){// 如果未找到,创建新实例Type catiaType = Type.GetTypeFromProgID("CATIA.Application");INFITF.Application newApp = (INFITF.Application)System.Activator.CreateInstance(catiaType);if (makeVisible){newApp.Visible = true;//newApp.WindowState = CATWindowState.catWindowStateNormal; // 正常窗口状态}// 可选:给 CATIA 一点时间完成初始化System.Threading.Thread.Sleep(2000);return newApp;}}
}
找到CATIA对应的COM类型,然后创建一个新的实例。这是COM操作的标准处理方法,适用于一般的COM库。不过CATIA不是一般的COM库,在
https://blog.csdn.net/silent_missile/article/details/134646817?spm=1011.2415.3001.5331
介绍了CATIA的启动模式。
可以看到CATIA实例的启动其实需要很多额外的参数,简单的创建一个COM实例是无法启动CATIA的。
想要启动CATIA也很简单,直接用C#调用系统可执行文件,然后按照文章中所述的方式加上参数即可启动。
.net的方法
.net Framework只支持windows,为了真正跨平台,微软推出了.net core,随后又将其升级为.net,这个命名确实容易让人糊涂。不管怎么说,.net才是未来的发展趋势。
前面的代码中有一个问题,System.Runtime.InteropServices.Marshal.GetActiveObject是.net Framework中的函数,在.net中的Marshal类里没有GetActiveObject函数。
bing AI这样说
The Marshal.GetActiveObject method is not available in .NET Core. However, you can achieve similar functionality using P/Invoke to call the GetActiveObject function from the Windows API.
这里介绍一下处理方法。
在Visual Studio中创建一个.net的程序,并在依赖项中添加COM引用。
程序代码如下
using INFITF;
using MECMOD;
using PARTITF;
using ProductStructureTypeLib;
using System;namespace MyProgram
{/// <summary>/// Interaction logic for MainWindow.xaml/// </summary>public partial class MainWindow : System.Windows.Window{private INFITF.Application mainInstance;public MainWindow(){InitializeComponent();}public static INFITF.Application GetOrStartCatia(bool makeVisible = true){try{// 获取已运行的实例return (INFITF.Application)ComInterop.GetActiveObject("CATIA.Application");}catch (COMException){// 如果没有获取到,抛出异常}}private void MyButtonClick(object sender, RoutedEventArgs e){mainInstance = GetOrStartCatia();MECMOD.PartDocument ActivePartDocument = (MECMOD.PartDocument)mainInstance.ActiveDocument;MECMOD.Part ActivePart = ActivePartDocument.Part;myButton.Content = ActivePart.get_Name();}}public static class ComInterop{public static object GetActiveObject(string progId, bool throwOnError = false){if (progId == null)throw new ArgumentNullException(nameof(progId));var hr = CLSIDFromProgIDEx(progId, out var clsid);if (hr < 0){if (throwOnError)Marshal.ThrowExceptionForHR(hr);return null;}hr = GetActiveObject(clsid, IntPtr.Zero, out var obj);if (hr < 0){if (throwOnError)Marshal.ThrowExceptionForHR(hr);return null;}return obj;}[DllImport("ole32.dll")]private static extern int CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] string lpszProgID, out Guid lpclsid);[DllImport("oleaut32.dll")]private static extern int GetActiveObject([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);}
}
.net的Marshal类没带GetActiveObject函数没关系,我们自己创建一个GetActiveObject函数就可以了。
上面的代码由bing AI提供,创建了一个ComInterop类,里面只有一个函数GetActiveObject实现了.net Framework中Marshal.GetActiveObject的功能。
通过这个自己创建的ComInterop.GetActiveObject函数获得了正在运行的CATIA实例后,剩下的就一样了。
在
https://www.catiawidgets.net/2024/03/10/c-catia-connection/
也有一位老哥提供了类似的解决思路,他的代码比bing AI提供的代码更健壮。在正常情况下bing AI的代码足够用了。这里给出他的代码
using INFITF;
using MECMOD;
using PARTITF;
using ProductStructureTypeLib;
using System;namespace MyProgram
{/// <summary>/// Interaction logic for MainWindow.xaml/// </summary>public partial class MainWindow : System.Windows.Window{private INFITF.Application mainInstance;public MainWindow(){InitializeComponent();}public static INFITF.Application GetOrStartCatia(bool makeVisible = true){try{// 首先尝试获取已运行的实例return (INFITF.Application)CustomMarshal.GetActiveObject("CATIA.Application");}catch (COMException){// 没有找到正在运行的CATIA实例,抛出异常}}private void MyButtonClick(object sender, RoutedEventArgs e){mainInstance = GetOrStartCatia();MECMOD.PartDocument ActivePartDocument = (MECMOD.PartDocument)mainInstance.ActiveDocument;MECMOD.Part ActivePart = ActivePartDocument.Part;myButton.Content = ActivePart.get_Name();}}// https://www.catiawidgets.net/2024/03/10/c-catia-connection/public static class CustomMarshal{internal const String OLEAUT32 = "oleaut32.dll";internal const String OLE32 = "ole32.dll";[System.Security.SecurityCritical] // auto-generated_requiredpublic static Object GetActiveObject(String progID){Object? obj = null;Guid clsid;// Call CLSIDFromProgIDEx first then fall back on CLSIDFromProgID if// CLSIDFromProgIDEx doesn't exist.try{CLSIDFromProgIDEx(progID, out clsid);}// catchcatch (Exception){CLSIDFromProgID(progID, out clsid);}GetActiveObject(ref clsid, IntPtr.Zero, out obj);return obj;}//[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)][DllImport(OLE32, PreserveSig = false)][ResourceExposure(ResourceScope.None)][SuppressUnmanagedCodeSecurity][System.Security.SecurityCritical] // auto-generatedprivate static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);//[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)][DllImport(OLE32, PreserveSig = false)][ResourceExposure(ResourceScope.None)][SuppressUnmanagedCodeSecurity][System.Security.SecurityCritical] // auto-generatedprivate static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);//[DllImport(Microsoft.Win32.Win32Native.OLEAUT32, PreserveSig = false)][DllImport(OLEAUT32, PreserveSig = false)][ResourceExposure(ResourceScope.None)][SuppressUnmanagedCodeSecurity][System.Security.SecurityCritical] // auto-generatedprivate static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out Object ppunk);}
}
这两份代码都可以用,不出意外的话,运行速度也是一样的。如果出了意外,后者健壮性更好,不过对于真正的“意外”恐怕也没什么抵御能力,聊胜于无。重要的是:千万别用精简版的Windows,一定要用正常安装的Windows
到底应该选择.net Framework还是.net呢?
如果选择.net Framework,那么在编译生成的可执行文件中是不包含任何.net Framework的库文件的,这些库文件都要操作系统安装对应的.net Framework才有。而在开发时,必须要选定某一个版本的.net Framework,不同版本的.net Framework并不兼容,并不存在高版本兼容低版本,尤其是3.5版本之前的
win7自带的.net Framework是3.5,win10自带的.net Framework是4.7,而win11自带的.net Framework是4.8。如果想要在win10/win11启用.net Framework 3.5的话非常麻烦。
如果说终端机数量较多,分别运行着不同版本的操作系统,它们上面有其他正在运行的程序依赖某个特定版本的.net Framework,它们不可能为了一个新开发的程序而放弃正在运行的程序,而不同的终端机依赖的版本可能还不一样,那就会非常麻烦。如果某个终端机比较老旧,运行着win7+.net Framework 3.5,那就超级麻烦了。开发者必须要针对不同的.net Framework分别编译,然后分别分发,这还是在代码不需要修改的前提下,如果需要针对不同的.net Framework修改代码,那工作量还要大幅上升。开发者需要准备win7+.net Framework 3.5版、win10+.net Framework 4.7版、win11+.net Framework 4.8版至少3个版本。
不过好处也是很明显的,生成的可执行文件体积非常小,而且如果能依赖操作系统自带的.net Framework,运行速度还不错。
如果选择.net,那么在编译的时候,会把.net的库函数直接打包编译进可执行文件,这样不论在哪台终端机上运行都不需要担心库依赖的问题,一个版本就可以分发到所有的终端机。坏处就是生成的可执行文件体积会大——而且大很多,如果需要向很多台终端机部署,文件传输的耗时就会长很多。运行速度倒是不用担心,从.net7开始,运行速度优化了很多。