COM组件浅析(三)- 使用C/C++操作Adobe Illustrator

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值