目录

  • 创建工程,并增加COM模板代码
  • 启动Illustrator.Application
    • newApp函数声明
    • 实现newApp方法
    • 代码分析
      • CLSID
      • C/C++ IDispatch与Java Dispatch
  • 创建文稿Document
    • 声明callAndToDispatch函数
    • 实现callAndToDispatch函数
    • 代码分析
    • 照葫芦画瓢,增加文本框
  • 设置文本框内容
    • 声明put方法
    • 实现put方法
    • 代码分析
  • 总结

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 AddRefreturn 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

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

  1. COM组件浅析(二) - 使用C#操作Adobe Illustrator

    目录 Adobe 官方手册 使用C#编写Demo 1. 创建工程 2. 添加引用 3. 添加样例代码 4. 运行样例代码 尝试看得更清晰 在上一篇中, COM组件浅析(一) - 使用Java操作Ado ...

  2. COM组件浅析(一) - 使用Java操作Adobe Illustrator

    目录 一.下载Jacob 二.创建JacobDemo maven工程,引入jacob依赖 1. 创建工程 2. 引入jacob依赖 3. 在pom.xml中引入jacob.jar 三.在代码中操作Ad ...

  3. 六十、深入理解Vue组件,使用组件的三个细节点

    @Author:Runsen @Data:2020/10/16 文章目录 is的使用 组件中的data必须是方法 ref 引用 Vue中如何操作dom 实现计算器中的功能 后言 备战前端.大四加油.下 ...

  4. 深入解析React创建组件的三种方式

    eact创建组件的三种方式: 1.函数式无状态组件 2.es5方式React.createClass组件 3.es6方式extends React.Component 三种创建方式的异同 1.函数式无 ...

  5. 从Element ui看开发公共组件的三种方式

    在日常的开发工作中,封装自己的公共组件是很常见的需求,但想要组件更加优雅和通用也是一门学问.恰好前段时间用过Element ui,于是想学习这种库是如何封装插件的,这篇文章就是我的一点理解. 从入口文 ...

  6. vue单文件props写法_详解Vue 单文件组件的三种写法

    详解Vue 单文件组件的三种写法 JS构造选项写法 export defaul { data, methods, ...} JS class写法 @Component export default c ...

  7. SQL2K数据库开发三十之存储过程操作删除存储过程

            1.在要删除的存储过程上右击鼠标,在弹出的菜单中选择"删除"命令.         2.在"除去对象"对话框中显示了即将删除的对象,点击&quo ...

  8. loading怎么关闭 vant_vant-ui组件调用Dialog弹窗异步关闭操作

    需求描述: 需求描述:官方文档又是组件调用方式,又是函数调用方式. 我就需要一个很简单的:点击操作弹窗显示后,我填写一个表单,表单校验通过后,再调用API接口,返回成功后,关闭弹窗. 一个很简单的东西 ...

  9. jQuery——入门(三)JQuery DOM操作(核心处理和文档处理)

    jQuery--入门(三)JQuery DOM操作(核心处理和文档处理) 一.核心处理(JQuery对象访问) 1.页面加载检测函数:$(document).ready(function(){}); ...

最新文章

  1. 中国知名企业ERP失败案例深入剖析
  2. Andorid SQLite数据库开发基础教程(2)
  3. 【深度学习】基于深度神经网络进行权重剪枝的算法(一)
  4. css层叠上下文详解,CSS定位(层叠上下文)
  5. Cookie 学习案例之三天免登录
  6. vue插件:vue-resource的使用笔记
  7. 单片机小白学步系列(三) 偶遇51单片机
  8. 我的世界java版月步教程_《我的世界》月步?幻影剑?大神才会的骚操作 第一个我就跪了!...
  9. spring3 常见异常解决
  10. linux命令收集录
  11. android 开发规范
  12. ggplot2作图4
  13. 响应式网站设计 - 最佳实践
  14. Android连点器(adb)
  15. iOS 中可能用到的数学公式(绝对值、平方、取整、正余弦)
  16. 计算机职业道德核心价值观,信息技术学科的核心价值观再认识
  17. 淘宝SDK高级模板,设计师模块开放接口详解
  18. turtle的setheading函数详解
  19. python、java、ruby、node等如何提取office文档中的内容?
  20. 流形上的预积分(上)

热门文章

  1. ​探讨AI+新模式,百度大脑提供纺织企业数字化转型新路径
  2. 指南:在 linux 下的 Oracle Database 11g 中安装 Oracle Enterprise Manager 10g Grid Control 第 5 版
  3. 电商大数据 API接口 数据挖掘 淘宝拼多多京东1688数据抓取
  4. Office 佳能MP259打印EXCEL线条歪曲,字迹模糊怎么办
  5. eb8000软件怎样上传_EB8000软件使用介绍幻灯片
  6. 佳木斯计算机编程学校,佳木斯公立数控培训,靠谱电气编程培训价格
  7. html和vue混合开发
  8. 小程序开发框架WePY和mpvue使用感受
  9. 软件测试工程师职业发展路线简介
  10. 构建中国云生态 | 华云数据与杉岩数据完成产品兼容互认证 携手满足更多用户海量数据的管理需求