目录
2021年04月04日17:34:57
在上一讲COM组件浅析(二) - 使用C#操作Adobe Illustrator中,我们使用C#来操作Adobe Illustrator,感觉十分顺畅,AI的所有类和方法尽收眼底。但另一方面,这也导致了我们没法深入了解COM组件的实现机制,以及,Jacob中Dispatch、Variant等概念。因此,在这一篇中,我们尝试使用C/C++来操作Adobe Illustrator。
创建工程,并增加COM模板代码
我们仍旧使用VS 2017来创建工程。点击菜单:文件->新建->项目…,左侧点击Visual C++,右侧选择控制台应用。项目名称设置为JacobDemoConsoleApplication。点击确定即可。
在新建的JacobDemoConsoleApplication.cpp文件中,删除所有默认生成的代码,替换为如下C/C++代码。
// JacobDemoConsoleApplication.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
#include <stdio.h>
#include <objbase.h>
// CComBSTR
#include <atlbase.h>
int main(){
CoInitialize(NULL);
// 功能代码
CoUninitialize();
return 0;
}
使用C/C++对COM组件进行操作前,我们需要调用CoInitialize
方法进行初始化,并在结束时调用CoUninitialize
以让系统做一些收尾工作。我们后续的代码将在两个方法之间编写。点击“运行”,如果没有问题,那么会看到一个控制台窗口,如下图所示。
启动Illustrator.Application
newApp函数声明
使用C/C++来创建Illustrator.Application就不能像C#中一样直接new了,由于步骤较为复杂,我们把代码放入到一个函数中。首先在main函数前声明一下这个函数newApp。此方法需要传入一个字符串,并返回一个IDispatch对象指针,后续我们可以根据该对象进一步操作,如创建Documents对象等。
IDispatch *newApp(LPOLESTR progId);
而main函数中,增加调用代码。调用参数是Illustrator.Application
。
int main(){
CoInitialize(NULL);
// 功能代码 - 增加了下面两行代码
IDispatch *pApp = newApp((LPOLESTR)L"Illustrator.Application");
pApp->Release();
CoUninitialize();
return 0;
}
实现newApp方法
如何实现newApp方法,拿到一个IDispatch对象指针呢?需要3步。
第一步,根据传入的字符串获取到指定的类。确切地说,是类的唯一标识符,即CLSID。我们只需要调用CLSIDFromProgID,并传入类名称字符串,以及CLSID接收地址即可。
第二步,根据类唯一标识符CLSID,创建一个表示这个类的对象,该对象存储了类的信息。系统提供了CoCreateInstance函数用于获取类对象。
第三步,根据类对象获取到IDispatch对象指针。这一步对应的API是IUnknown::QueryInterface
。
Talk is cheap. Show me the code
,在main函数后面,实现newApp函数:
IDispatch *newApp(LPOLESTR progId) {
// 第一步,根据类名称字符串获取到类id(CLSID)
CLSID clsid;
CLSIDFromProgID(progId, &clsid); // 该函数返回HRESULT,为了演示需要,暂不判断返回值
IUnknown *pUnknown = NULL;
CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER,
IID_IUnknown, (void **)&pUnknown); // 该函数返回HRESULT,为了演示需要,暂不判断返回值
IDispatch *pApp;
pUnknown->QueryInterface(IID_IDispatch, (void **)&pApp); // 该函数返回HRESULT,为了演示需要,暂不判断返回值
pUnknown->Release();// CoCreateInstance called AddRef
return pApp;
}
点击“运行”,我们可以看到Adobe Illustrator自动启动了(如果已经启动则没有任何变化)。
代码分析
CLSID
COM组件中,每个类都有一个惟一的标识符(CLSID),形如:{D4480A50-BA28-11d1-8E75-00C04FA31A86}。这是一个128bits的数字,一共16字节。如果我们在刚才的代码的CLSIDFromProgID后面打印一下clsid变量,就可以看到Illustrator.Application的CLSID为:0fa36670-f0bc-48c0-ad256cf62cad3a31。
printf("%08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x\n", clsid.Data1, clsid.Data2, clsid.Data3,
clsid.Data4[0], clsid.Data4[1], clsid.Data4[2], clsid.Data4[3],
clsid.Data4[4], clsid.Data4[5], clsid.Data4[6], clsid.Data4[7]);
C/C++ IDispatch与Java Dispatch
我们通过QueryInterface方法得到了一个IDispatch指针,我们猜测这个IDispatch即对应Java代码中的Dispatch。我们查看Jacob的源码可以发现,Dispatch类中就有一个long类型的成员变量m_pDispatch。
// Dispatch.java
public class Dispatch extends JacobObject {
// ...省略无关代码
public long m_pDispatch;
}
在Java代码中,我们通过这个Dispatch变量可以得到Documents对象,那么如果使用C/C++来实现,怎么获取到Documents对象呢?
创建文稿Document
声明callAndToDispatch函数
使用C/C++来得到Documents对象不是一行代码的事情,这里我们创建一个新的函数来获取Documents对象:callAndToDispatch。下面是这个函数的原型,我们放在声明newApp的后面。
IDispatch *callAndToDispatch(IDispatch* pIDispatch, LPOLESTR name);
该函数接收1个IDispatch指针对象,以及一个字符串name,表示获取(或调用)pIDispatch的name属性(方法)。这个方法跟Java中的Dispatch.get方法有异曲同工之妙。在main函数中使用方式如下。
int main(){
CoInitialize(NULL);
// 功能代码
IDispatch *pApp = newApp((LPOLESTR)L"Illustrator.Application");
// 增加了如下两行代码
IDispatch *pDocs = callAndToDispatch(pApp, (LPOLESTR)L"Documents");
IDispatch *pDoc = callAndToDispatch(pDocs, (LPOLESTR)L"Add");
pApp->Release();
CoUninitialize();
return 0;
}
从上面的使用可以看出,callAndToDispatch的返回值,可以作为callAndToDispatch的参数继续调用,所以,我们可以简单理解为,IDispatch就是一个调用器指针,在调用任何方法前,都需要提供一个IDispatch变量。
实现callAndToDispatch函数
callAndToDispatch,从名字就可以看出来,这个函数会做两件事情:call和toDispatch。以下是代码实现。
static UINT NAME_COUNT = 1;
VARIANT *_call(IDispatch* pIDispatch, LPOLESTR name);
IDispatch *callAndToDispatch(IDispatch* pIDispatch, LPOLESTR name) {
VARIANT *varReturn = _call(pIDispatch, name);
IDispatch *pReturn = varReturn->pdispVal;
delete varReturn;
return pReturn;
}
VARIANT *_call(IDispatch* pIDispatch, LPOLESTR name) {
DISPID dispid = 0;
// 1. 获取方法名对应的的方法id(或者说dispatch id)
pIDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&name, NAME_COUNT,
LOCALE_SYSTEM_DEFAULT,
&dispid); // 该函数返回HRESULT,为了演示需要,暂不判断返回值
VARIANT *varReturn = new VARIANT();
// 设置【调用参数dispparams】为全空,因为我们不需要
DISPPARAMS dispparams = { 0 };
// 2. 根据方法id(或者)、调用参数执行方法,将返回值存到varReturn中
pIDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, (WORD)DISPATCH_PROPERTYGET,
&dispparams, varReturn, NULL, NULL);// 该函数返回HRESULT,为了演示需要,暂不判断返回值
return varReturn;
}
点击“运行”,可以看到Adobe Illustrator打开之后,新建了一个文稿。当然,文稿是空白的,因为我们没有添加任何元素(如文本框)到文稿中。
代码分析
从上面的代码可以看出,callAndToDispatch先调用_call得到一个VARIANT指针变量,然后从这个指针变量中解析出一个IDispatch指针变量,最后返回。
我们在前面已经知道,每个类都有一个唯一的标识符CLSID。那么对于方法而言,也有一个唯一的id(在这个类内唯一,不同类可能会重复),这个id就是DISPID
。我们需要通过GetIDsOfNames
得到DISPID,才能够调用这个方法。所以在_call函数中的第一步,就是通过这个API来获取到参数name对应的DISPID。
_call方法的第二步,就是通过IDispatch的Invoke方法来调用DISPID。而一个方法(method),它可以有返回值,也可以没有返回值。对于有返回值的方法,返回值可以是简单的数据类型,比如int,double等,也可以是较为复杂的类型,比如指针。所以,Invoke API封装了方法的返回值到VARIANT变量中。如果我们知道返回值是一个基本数据类型,那么可以将VARIANT变量转为对应的基本数据类型。如果我们知道返回值是一个IDispatch指针对象,那么可以通过VARIANT的pdispVal成员变量得到这个指针。而"Documents"
是一个IDispatch对象,因此,在callAndToDispatch中,我们提取了这个指针,并返回。
由于Documents.Add返回的是新建出来的Document,所以也是一个IDispatch对象。我们使用
callAndToDispatch(pDocs, (LPOLESTR)L"Add")
调用即可创建一个新文稿。
照葫芦画瓢,增加文本框
按照上述分析,我们可以调用callAndToDispatch
创建一个文本框。在main中增加两行代码,增加后main函数如下。
int main(){
CoInitialize(NULL);
// 功能代码
IDispatch *pApp = newApp((LPOLESTR)L"Illustrator.Application");
IDispatch *pDocs = callAndToDispatch(pApp, (LPOLESTR)L"Documents");
IDispatch *pDoc = callAndToDispatch(pDocs, (LPOLESTR)L"Add");
// 增加了如下两行代码
IDispatch *pTextFrames = callAndToDispatch(pDoc, (LPOLESTR)L"TextFrames");
IDispatch *pTextFrame = callAndToDispatch(pTextFrames, (LPOLESTR)L"Add");
pApp->Release();
CoUninitialize();
return 0;
}
点击“运行”。没有报错,可正常执行。但是,文稿仍旧是空白,什么都没看到。当然,因为我们没有给这个文本框填充内容。
不过,我们可以在图层面板看到新建的文本框。(如果你没有图层面板,可以点击Adobe Illustrator的菜单:窗口->图层,快捷键F7,将图层面板呼出来)
如果在图层面板中能够看到一个叫<文字>的元素,表示代码执行成功。
设置文本框内容
声明put方法
想必大家都已经猜到了,要给设置文本框内容单独写一个方法。没错,就是put方法。不过这里,我们需要声明两个put方法,因为我们不仅要设置文本框内容,还要设置它的偏移量(不记得了?请复习COM组件浅析(一) - 使用Java操作Adobe Illustrator的第一个Demo)。
void putInt(IDispatch* pIDispatch, LPOLESTR propName, double value);
void putString(IDispatch* pIDispatch, LPOLESTR propName, LPOLESTR value);
以下是main使用方式
int main(){
CoInitialize(NULL);
// 功能代码
IDispatch *pApp = newApp((LPOLESTR)L"Illustrator.Application");
IDispatch *pDocs = callAndToDispatch(pApp, (LPOLESTR)L"Documents");
IDispatch *pDoc = callAndToDispatch(pDocs, (LPOLESTR)L"Add");
IDispatch *pTextFrames = callAndToDispatch(pDoc, (LPOLESTR)L"TextFrames");
IDispatch *pTextFrame = callAndToDispatch(pTextFrames, (LPOLESTR)L"Add");
// 增加了下面3行代码
putInt(pTextFrame, (LPOLESTR)L"Top", 600.0);
putInt(pTextFrame, (LPOLESTR)L"Left", 100.0);
putString(pTextFrame, (LPOLESTR)L"Contents", (LPOLESTR)L"hello, illustrator");
pApp->Release();
CoUninitialize();
return 0;
}
实现put方法
void _put(IDispatch* pIDispatch, LPOLESTR propName, VARIANT *variant);
void putInt(IDispatch* pIDispatch, LPOLESTR propName, double value) {
VARIANT *variant = new VARIANT();
variant->vt = VT_R4;
variant->fltVal = value;
_put(pIDispatch, propName, variant);
delete variant;
}
void putString(IDispatch* pIDispatch, LPOLESTR propName, LPOLESTR value) {
VARIANT *variant = new VARIANT();
variant->vt = VT_BSTR; // 将变量设置为BSTR类型
variant->bstrVal = CComBSTR(wcslen(value), value); // 初始化变量的值
_put(pIDispatch, propName, variant);
delete variant;
}
void _put(IDispatch* pIDispatch, LPOLESTR propName, VARIANT *variant) {
DISPID dispid = 0;
pIDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&propName, NAME_COUNT,
LOCALE_SYSTEM_DEFAULT, &dispid); // 该函数返回HRESULT,为了演示需要,暂不判断返回值
DISPID dispidPropertyPut = DISPID_PROPERTYPUT;
DISPPARAMS dispparams; // = { 0 };
dispparams.cArgs = 1;
dispparams.rgvarg = variant;
dispparams.cNamedArgs = 1;
dispparams.rgdispidNamedArgs = &dispidPropertyPut;
VARIANT *varReturn = new VARIANT();
pIDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, (WORD)DISPATCH_PROPERTYPUT, &dispparams, varReturn, NULL, NULL);
delete varReturn;
}
点击“运行”,可以看到,新文稿中看到了久违的“hello, illustrator”。
代码分析
putInt和putString其实都调用了_put方法。_put方法接受一个IDispatch指针,LPOLESTR属性名字符串,以及一个VARIANT指针变量,表示要设置pIDispatch的propName等于variant。
对于VARIANT而言,我们在上一小节已经知道,其实就是一个变量的封装,上一小节是封装了返回值,而这里,封装了参数。如何将double以及字符串包装为VARIANT变量已经在putInt和putString中体现,这里不再赘述。
而_put方法的实现步骤,与_call基本相同,只在调用参数dispparams以及调用Invoke的第4个参数有所不同。以下是它们的区别,具体操作可查阅MSDN。
//
// 调用参数dispparams
//
// _call中
DISPPARAMS dispparams = { 0 };
// _put中
DISPID dispidPropertyPut = DISPID_PROPERTYPUT;
DISPPARAMS dispparams;
dispparams.cArgs = 1;
dispparams.rgvarg = variant;
dispparams.cNamedArgs = 1;
dispparams.rgdispidNamedArgs = &dispidPropertyPut;
//
// Invoke
//
// _call中
pIDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, (WORD)DISPATCH_PROPERTYGET,
&dispparams, varReturn, NULL, NULL);
// _put中
pIDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, (WORD)DISPATCH_PROPERTYPUT,
&dispparams, varReturn, NULL, NULL);
总结
至此,相信读者能够明白,Dispatch、Variant这些概念了。
系列文章
第一篇 COM组件浅析(一) - 使用Java操作Adobe Illustrator
第二篇 COM组件浅析(二) - 使用C#操作Adobe Illustrator
第三篇 COM组件浅析(三)- 使用C/C++操作Adobe Illustrator