----哆啦刘小洋 原创,转载需说明出处 2022-12-29
SegeX Automation:Surfer自动化(Automation)失败原因
- 1 简介
- 2 初始化Surfer对象不成功
- 2.1 一般代码
- 2.1 改进代码
- 3 Windows系统原因
- 4 Surfer原因
- 5 其他问题
1 简介
在工程领域,Golden Surfer普遍使用,为了提高成图效率,经常使用自动化(Automation)技术调用Surfer完成自动绘制。但网上一般提供的调用方式会有一定机会失败,从而大大影响了用户的体验并给工作带来困难。本文来源于SegeX Automation组件中的技术,旨在分析Surfer调用失败的原因,并给出解决办法。本文中的一些方法也适用于其他软件如AutoCAD、Office等软件的Automation使用。本文的代码基于VC2012及以上。
2 初始化Surfer对象不成功
2.1 一般代码
VC中初始化Surfer对象的一般代码如下(这里使用的是Automation包装类的方法,用接口的调用方法也是一样的。另外里面返回的错误号是SegeX内定,看的时候可直接忽略):
...
#include "CSurferApplication.h"
long InitSurfer()
{
CoInitialize(NULL);
CSurferApplication SurfApp;
CLSID clsid;
HRESULT h=NOERROR;
IUnknown* pUnknown;
LPDISPATCH pDispatch;
BOOL bSuccessget_Object = FALSE;
//获取Surfer的Class ID
h = ::CLSIDFromProgID(OLESTR("Surfer.Application"), &clsid);
if(!SUCCEEDED(h))
return ERR_AUTOMATION_CAN_NOT_GET_CLSID;
//获取正在运行的Surfer对象
h = ::GetActiveObject(clsid, NULL, &pUnknown);
if(SUCCEEDED(h))
{
h = pUnknown->QueryInterface(IID_IDispatch, (void**)&pDispatch);
if(SUCCEEDED(h))
{
bSuccessget_Object = TRUE;
pDispatch->Release();
SurfApp.AttachDispatch(pDispatch);
}
}
//没有正在运行的Surfer对象,就创建一个
if(!bSuccessget_Object)
h=SurfApp.CreateDispatch(clsid);
if(!SUCCEEDED(h) || SurfApp.m_lpDispatch == NULL)
return ERR_AUTOMATION_CAN_NOT_CREATE_OBJECT;
}
这里使用的是Automation包装类的方法,用接口的调用方法也是一样的(关于两种方法详解,可参考我的另一篇文章“SegeX Automation:VC调用AutoCAD自动化的两种方法(包装类、接口)使用详解”)。CSurferApplication是Surfer Automation包装类CApplication重新改了个名字,至于如何得到Surfer Automation的包装类,网上有很多文章,这里不赘述了。运气好的话,上述代码就可以获取或打开Surfer,后面就好办了。但是!往往这里就会失败。咋办?听我道来。
2.1 改进代码
很多时候(在不同的计算机、不同的Windows版本,表现是不一样),我们在获取Surfer的Class ID就失败了!这是失败的一大主因,但问题是不同的计算机反应不同,所以很多时候我们编程人员不知道是哪里出了问题,因为在编程的计算机上是没有问题的。这是微软自动化技术很大的一个弱点,不强壮!不强壮!(重要的事情说两遍),专业术语讲,不鲁棒!别以为我瞎说,作为SegeX的开发人员,任何一句话、一个细节都是很认真的。言归正传,这里失败的原因出在Windows系统函数::CLSIDFromProgID身上。这个函数我认为不完善,为什么这么讲,必须要多啰嗦两句。
首先,你换个平台,别在VC上折腾,先打开Surfer(切记!),然后用Excel的Vba,你试试Excel中的代码,在Excel中随便写个宏,如果没有Excel,用Word、AutoCAD…,实在不行,就用Surfer中的VbScript也行。如下:
Sub a()
set app = GetObject(, "Surfer.Application")
End Sub
调试运行,我哇!竟然成功(严格讲是大概率会成功,或者说成功率大大高于上面VC中的代码)!为什么?我们MFC洋洋洒洒数十行比不上人家一行,MFC不没落才怪。所以我说::CLSIDFromProgID是有缺陷的,到了这一步,也可以肯定的讲Vba中绝不是简单的使用::CLSIDFromProgID来获取对象CLSID的。
翻开注册表吧!regedit,点开HKEY_CLASSES_ROOT,直接快速打两个字母“su”,会跳到入下图所示的位置:
看到了吗?明明有Surfer.Application对应的CLSID。可::CLSIDFromProgID就是视而不见,非要到后面很远的地方去读,不知道微软怎么弄的,咱也不敢乱说。解决方法也呼之欲出了,改进代码如下(为节省篇幅,只写::CLSIDFromProgID相关的地方):
h = ::CLSIDFromProgID(OLESTR("Surfer.Application"), &clsid);
if(!SUCCEEDED(h))
{
//尝试从注册表读入
HKEY hStartKey = HKEY_CLASSES_ROOT;
TCHAR buf[256] = { 0 };
BOOL b = GetRegKeyValue(hStartKey, _T("Surfer.Application\\CLSID"), NULL, REG_SZ, (LPBYTE)buf, 256, NULL);
if (b)
h = CLSIDFromString(buf, &clsid);
else
{
hStartKey = HKEY_CURRENT_USER;
b = GetRegKeyValue(hStartKey, _T("SOFTWARE\\Classes\\Surfer.Application\\CLSID"), NULL, REG_SZ, (LPBYTE)buf, 256, NULL);
if (b)
h = CLSIDFromString(buf, &clsid);
}
if (h != S_OK)
return ERR_AUTOMATION_CAN_NOT_GET_CLSID;
}
代码里有个函数GetRegKeyValue,是专门用来读取注册表的,代码如下:
BOOL IsWow64()
{
BOOL bIsWow64 = FALSE;
typedef BOOL(WINAPI* LPFN_ISWOW64PROCESS) (HANDLE, PBOOL);
LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process");
if (NULL != fnIsWow64Process)
{
fnIsWow64Process(GetCurrentProcess(), &bIsWow64);
}
return bIsWow64;
}
BOOL GetRegKeyValue(HKEY hKey, LPCTSTR lpszSubKey, LPCTSTR lpszItem, DWORD dwType, LPBYTE pRetData, UINT uRetDataBufSize, LPDWORD pRetSize)
{
HKEY hKeyResult;
LONG retval;
if (IsWow64())
retval = RegOpenKeyEx(hKey, lpszSubKey, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKeyResult);
else
retval = RegOpenKeyEx(hKey, lpszSubKey, 0, KEY_QUERY_VALUE, &hKeyResult);
if (retval == ERROR_SUCCESS || retval == ERROR_MORE_DATA)
{
DWORD size = uRetDataBufSize;
retval = RegQueryValueEx(hKeyResult, lpszItem, 0, &dwType, pRetData, &size);
if (retval == ERROR_SUCCESS || retval == ERROR_MORE_DATA)
{
if (pRetSize != NULL)
*pRetSize = size;
if (dwType == REG_SZ)
pRetData[size] = 0;
}
RegCloseKey(hKeyResult);
return (retval == ERROR_SUCCESS || retval == ERROR_MORE_DATA);
}
return (retval == ERROR_SUCCESS || retval == ERROR_MORE_DATA);
}
3 Windows系统原因
我们下了这么多功夫,应该没问题了吧!呵呵,那你太小看MFC了,它总是会时不时的跳起来,突然出其不意的给你一击。“MFC,永远在路上!”,我给MFC的墓志铭。
不多说了,首先你的程序最好设置为系统管理员权限。在哪里设置?呃。。。,我找找,唉,实在是找不到!还有一句想送的话:”永远别低估VC的难度!“。好了,在”链接器/清单文件/UAC执行级别“,选则:“requireAdministrator”。
这下有预感了吧!事情没有结束,这时我已经没有什么别的好办法了,让用户先打开Surfer,打开方式尝试两种:普通运行和管理员身份运行。可能总有一种适合他的计算机。
4 Surfer原因
事情是没有结束。我们一味指责MFC、Windows也不全对,因为Surfer本身也有问题。不然,为什么你用自动化技术打开Excel、AutoCAD很少出问题呢?第一个问题就是Surfer在注册自动化组件时不完善,也就是注册表中关于自动化项不完善,导致VC调用失败。但这里要说的不是这个。而是你不要安装多个不同的Surfer版本。安装了不同版本之后,可能时之间有冲突,也会导致失败。这时候,请你请求你的用户,让他们删除多余的Surfer,留一个就够了。
这时候就又双叒引申了新的Surfer问题,Surfer删除不干净!也就是说,用户只留了一个Surfer也仍然不行了。这时候已经不是代码问题了, 也不是VC、VB或C#能解决的问题了。请让用户不要嫌麻烦,也不要怪罪开发人员,直接按下面方法干就完了:
1)删除所有的Surfer,重新启动计算机。
2)打开注册表,搜索“Surfer.Application”,见一个删一个,看都不要看。
3)打开Execel的Vba开发代码窗口,别的微软的Vba开发代码窗口也行,在菜单“工具/引用”弹出的对话框中,看看有没有Surfer的影子:
如果有,看一下下面它定位的位置,比如“C:\Program Files\Golden Software\Surfer 12\Surfer.exe”。回到注册表,从头开始,搜索这个字符串,还是见一个删一个。
4)重新安装一个Surfer。
如果又双叒叕不成功,最后的最后,去建议用户安装一个相对低版本的Surfer,我的建议是11~14。
5 其他问题
在使用surfer接口函数时,也要注意:
1)要考虑到中英文版的区别。这个Surfer也是奇葩,汉化的时候,内核也全部汉化了!!简直就是简直了!内核你去汉化它是个什么鬼思路,想让用户看你里面穿的什么马甲吗?关键是汉化之后,开发人员麻烦了,比如预设的颜色值红色,本来英文版就是“red”,代码中也用字符串“red”读取,但汉化后,这个常量变成“红色”了,也就是你代码中要放两套,我去!而且,而且还有不同的汉化版,一个颜色的汉语两个汉化版用的不是一个词语!更别说还有湾湾的繁体字!坚决让你的用户用英文版!
2)要考虑后续Surfer版本接口函数的参数变化以及执行的差别,否则会导致程序崩溃或调用提前终止。
不说了,如果想更详细了解Surfer自动化处理的细节代码,请看我的另一篇文章“SegeX Automation:VC VB(VBA) C# 调用Golden Surfer自动化(Automation)方法详解”,当然这篇文章还没有写,希望你看这篇文章的时候,那篇文章也写好了。