在CSDN上看到有不少人问怎样才能象MS Office一样支持VBA,偶也很感兴趣,可惜Google后发现要支持VBA得付钱给M$才行,象我们这种无产阶级自然是只有想的份啦。
不过MS还是给了我们另外一个选择——ActiveX Scripting(后面简称[AS])。
简单的说[AS]就是:MS来帮我们解析脚本里的基本语句(如if、for、表态式、赋值等),我们负责解释、执行它所不认识的函数,对象。
下面再简单说一下操作[AS]的流程:
- 实例化一个脚本对象(IActiveScript),一般装了IE的电脑上应该都有JScript和VBScript。
- 告诉IActiveScript谁来解释脚本中的对象(IActiveScript ::SetScriptSite)
- 告诉IActiveScript脚本里会用到哪些对象(用IActiveScript ::AddNamedItem)
- 装入脚本(JScript或VBScript代码,UNICODE格式)
- 运行脚本(通过设置IActiveScript::SetScriptState实现)
- [AS]在运行脚本过程中如果遇到第3步里告诉它的对象,它就会向我们要此对象的接口以便继续执行(它会调用- IActiveScriptSite::GetItemInfo,第2步里告诉它的)。
- 打完收工。当然也可以强制停下运行中的脚本(比如不小心编了一个死循环的脚本)。也是通过设置IActiveScript::SetScriptState实现。
从上面可以看出,我们的主要工作是实现脚本里的对象的解释工作。在COM编程中,毫无悬念地,这个光荣而又艰巨的任务就又落到了IDispatch身上。IDispatch的生平事迹咱就不介绍了,不明白的去问明白的,都不明白的去Google,心急的可以看后面的示例代码。
对我们编程的来说,说再多也不如源代码来得直接有效,下面我们就来做一个支持脚本的小程序。这里我们使用BCB来做,其它如VC,GCC当然也行,不过对于快速原型开发方面,BCB绝对是不二选择(广告时间)。
先看偶写的一个JScript脚本:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | var bForward = true;for(var i=0;i<ScreenWidth-200;i+=100)
 {
 for(var j=0;j<ScreenHeight-200;j+=10)
 {
 var x = i;
 var y = bForward? j : (ScreenHeight-200-j)
 MyWin.MoveTo(x,y);
 MyWin.Caption = "X:" + x + " Y:"+y;
 MyWin.Color = (x<<16|y)&0xffffff;
 Sleep(10);
 }
 bForward = !bForward;
 }
 
 | 
此脚本的目的是让一个叫MyWin的窗口从左到右地上下移动,同时改变标题和颜色。
前面说了MS只帮我们解决脚本语言上的问题,我们来处理对象和函数。在这个脚本里,我们的任务有:ScreenWidth、ScreenHeight、Sleep、MyWin对象以及它的方法属性。
这里还得说一下IDispatch的调用过程:比如当[AS]执行到MyWin.MoveTo(x,y);时,它先得到MyWin的IDispatch接口(它是怎么得到的?后面会讲先按下不提),然后调用IDispatch的GetIDsOfNames(riid,L"MoveTo",1,lcid,&rgDispId)获得MoveTo对应的”Member Id”(rgDispId参数)。再用这个”Member Id”去调用Invoke(rgDispId,...)。所以我们只需关注GetIDsOfNames和Invoke两个方法即可。
先编写一个TMyGlobalFunc来处理ScreenWidth、ScreenHeight、Sleep这三个全局函数:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 
 | struct TMyGlobalFunc : TDispatch{enum {itemScreenWidth,itemScreenHeight,itemSleep};
 static wchar_t Name[];
 STDMETHOD(GetIDsOfNames)(REFIID riid,LPOLESTR *rgszNames,UINT cNames,LCID lcid,
 DISPID *rgDispId){
 if(lstrcmpW(rgszNames[0],L"ScreenWidth") == 0){
 *rgDispId = itemScreenWidth;
 }
 else if(lstrcmpW(rgszNames[0],L"ScreenHeight") == 0) {
 *rgDispId = itemScreenHeight;
 }
 else if(lstrcmpW(rgszNames[0],L"Sleep") == 0) {
 *rgDispId = itemSleep;
 }
 else
 return E_NOTIMPL;
 return S_OK;
 }
 
 STDMETHOD(Invoke)(DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,
 DISPPARAMS *pDispParams,
 VARIANT *pVarResult,EXCEPINFO *pExcepInfo,UINT *puArgErr){
 switch(dispIdMember)
 {
 case itemScreenWidth:
 pVarResult->vt=VT_I4;
 pVarResult->intVal = Screen->Width;
 break;
 case itemScreenHeight:
 pVarResult->vt=VT_I4;
 pVarResult->intVal = Screen->Height;
 break;
 case itemSleep:
 if(pDispParams->cArgs!=1) return DISP_E_BADPARAMCOUNT;
 if(pDispParams->rgvarg[0].vt != VT_I4) {
 *puArgErr = 0;
 return DISP_E_TYPEMISMATCH;
 }
 Application->ProcessMessages();
 Sleep(pDispParams->rgvarg[0].intVal);
 break;
 default:
 return DISP_E_MEMBERNOTFOUND;
 }
 return S_OK;
 }
 };
 wchar_t TMyGlobalFunc::Name[]=L"MyGlobalFunc";
 
 | 
再写个TMyWin来处理MyWin对象的方法和属性,和上面一样
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 
 | struct TMyWin : TDispatch{enum {itemCaption,itemColor,itemMoveTo};
 static wchar_t Name[];
 
 STDMETHOD(GetIDsOfNames)(REFIID riid,LPOLESTR *rgszNames,UINT cNames,LCID lcid,
 DISPID *rgDispId){
 if(lstrcmpW(rgszNames[0],L"Caption") == 0) {
 *rgDispId = itemCaption;
 }
 else if(lstrcmpW(rgszNames[0],L"Color") == 0) {
 *rgDispId = itemColor;
 }
 else if(lstrcmpW(rgszNames[0],L"MoveTo") == 0) {
 *rgDispId = itemMoveTo;
 }
 else
 return E_NOTIMPL;
 return S_OK;
 }
 
 STDMETHOD(Invoke)(DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,
 DISPPARAMS *pDispParams,
 VARIANT *pVarResult,EXCEPINFO *pExcepInfo,UINT *puArgErr){
 switch(dispIdMember)
 {
 case itemCaption:
 {
 if(wFlags==DISPATCH_PROPERTYGET)
 {
 pVarResult->vt = VT_BSTR;
 pVarResult->bstrVal = GetCaption();
 }
 else if(wFlags==DISPATCH_PROPERTYPUT)
 {
 if(pDispParams->cArgs!=1) return DISP_E_BADPARAMCOUNT;
 if(pDispParams->rgvarg[0].vt != VT_BSTR) {
 *puArgErr = 0;
 return DISP_E_TYPEMISMATCH;
 }
 SetCaption(pDispParams->rgvarg[0].bstrVal);
 }
 else
 return DISP_E_MEMBERNOTFOUND;
 break;
 }
 case itemColor:
 {
 if(wFlags==DISPATCH_PROPERTYGET)
 {
 pVarResult->vt = VT_I4;
 pVarResult->intVal = GetColor();
 }
 else if(wFlags==DISPATCH_PROPERTYPUT)
 {
 if(pDispParams->cArgs!=1) return DISP_E_BADPARAMCOUNT;
 if(pDispParams->rgvarg[0].vt != VT_I4) {
 *puArgErr = 0;
 return DISP_E_TYPEMISMATCH;
 }
 SetColor(pDispParams->rgvarg[0].intVal);
 }
 else
 return DISP_E_MEMBERNOTFOUND;
 break;
 }
 case itemMoveTo:
 {
 if(wFlags==DISPATCH_METHOD)
 {
 if(pDispParams->cArgs!=2) return DISP_E_BADPARAMCOUNT;
 if(pDispParams->rgvarg[0].vt != VT_I4) {
 *puArgErr = 0;
 return DISP_E_TYPEMISMATCH;
 }
 else if(pDispParams->rgvarg[1].vt != VT_I4){
 *puArgErr = 1;
 return DISP_E_TYPEMISMATCH;
 }
 MoveTo( pDispParams->rgvarg[1].intVal,
 pDispParams->rgvarg[0].intVal);
 }
 else
 return DISP_E_MEMBERNOTFOUND;
 break;
 }
 default:
 return DISP_E_MEMBERNOTFOUND;
 }
 return S_OK;
 }
 
 
 TMyWin(TForm *fm_Opt) : TDispatch(),m_Form(fm_Opt){ ; }
 
 BSTR GetCaption(){
 return WideString(m_Form->Caption).Detach();
 }
 void SetCaption(BSTR bstrCaption){
 m_Form->Caption = bstrCaption;
 }
 int GetColor(){
 return (int)m_Form->Color;
 }
 void SetColor(int iColor){
 m_Form->Color = TColor(iColor);
 }
 
 void MoveTo(int X,int Y){
 m_Form->Left=X;
 m_Form->Top=Y;
 }
 
 private:
 TForm *m_Form;
 };
 wchar_t TMyWin::Name[]=L"MyWin";
 
 | 
主要任务完成,接着我们要实现IActiveScriptSite,它用于上面所说的第2步和第6步。[AS]解析上面的脚本时遇到MyWin及ScreenWidth、ScreenHeight、Sleep时会通过GetItemInfo向它要接口,它则负责把我们刚才写的IDispatch喂给[AS]。
这个IActiveScriptSite要包含头文件#include <activscp.h>
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 
 | struct TMyActiveScriptSite: IActiveScriptSite
 {
 TMyActiveScriptSite(TMyWin *pMyWin,TMyGlobalFunc *pMyGlobalFunc)
 : m_iRefCount(1),m_pMyWin(pMyWin),m_pMyGlobalFunc(pMyGlobalFunc){
 }
 
 
 HRESULT __stdcall QueryInterface(REFIID iid, void **ppv){
 if(iid==IID_IUnknown||iid==IID_IActiveScriptSite)
 {
 *ppv=this;
 AddRef();
 return S_OK;
 }
 *ppv=NULL;
 return E_NOINTERFACE;
 }
 ULONG __stdcall AddRef(void) {
 return ++m_iRefCount;
 }
 ULONG __stdcall Release(void) {
 if(--m_iRefCount==0){
 delete this;
 return 0;
 }
 return m_iRefCount;
 }
 
 
 STDMETHOD(GetLCID)(LCID* ) {
 return E_NOTIMPL;
 }
 
 
 STDMETHOD(GetItemInfo)(LPCOLESTR pstrName,
 DWORD dwReturnMask,
 IUnknown** ppiunkItem,
 ITypeInfo** ppti) {
 if( (dwReturnMask & SCRIPTINFO_ITYPEINFO)!=0 ){
 *ppti = NULL;
 return E_FAIL;
 }
 if( (dwReturnMask & SCRIPTINFO_IUNKNOWN)==0 ) return E_FAIL;
 if( ppiunkItem==NULL ) return E_POINTER;
 *ppiunkItem = NULL;
 if( lstrcmpW( pstrName, TMyWin::Name )==0 ) {
 
 m_pMyWin->AddRef();
 *ppiunkItem = m_pMyWin;
 return S_OK;
 }
 else if( lstrcmpW( pstrName, TMyGlobalFunc::Name )==0 ) {
 
 m_pMyGlobalFunc->AddRef();
 *ppiunkItem = m_pMyGlobalFunc;
 return S_OK;
 }
 return E_FAIL;
 }
 
 STDMETHOD(GetDocVersionString)(BSTR* pbstrVersion) {
 if( pbstrVersion==NULL ) return E_POINTER;
 *pbstrVersion = ::SysAllocString(OLESTR("Script 1.0"));
 return S_OK;
 }
 
 STDMETHOD(OnScriptTerminate)(
 const VARIANT* ,
 const EXCEPINFO* ) {
 return S_OK;
 }
 
 STDMETHOD(OnStateChange)(SCRIPTSTATE ) {
 return S_OK;
 }
 
 STDMETHOD(OnScriptError)(IActiveScriptError* pScriptError) {
 EXCEPINFO e;
 DWORD dwContext;
 ULONG ulLine;
 LONG lPos;
 pScriptError->GetExceptionInfo(&e);
 pScriptError->GetSourcePosition(&dwContext, &ulLine, &lPos);
 char *pstrFormat = "An error occured while parsing script:"
 " Source: %ws Error: %08X Description: %ws Line: %d";
 char pstrStr[1024];
 ::wsprintf( pstrStr, pstrFormat,
 e.bstrSource,
 e.scode,
 e.bstrDescription,
 ulLine+1);
 ::MessageBox(::GetActiveWindow(), pstrStr,
 _T("Compile Error"), MB_OK | MB_ICONEXCLAMATION | MB_SETFOREGROUND);
 return S_OK;
 }
 
 STDMETHOD(OnEnterScript)() {
 return S_OK;
 }
 
 STDMETHOD(OnLeaveScript)() {
 return S_OK;
 }
 private:
 TMyWin *m_pMyWin;
 TMyGlobalFunc *m_pMyGlobalFunc;
 int m_iRefCount;
 };
 
 | 
万事俱备,就等执行了:
打开BCB,新建一VCL Form Application。
在默认的Form1上加入一个TMemo改名为:mmoScript;加入一个TButton改名为btnRun。btnRun->OnClick代码:
偷懒用到了TComInterface(对标VC里的CComPtr),要包含: #include <utilcls.h>
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 
 | void __fastcall TForm1::btnRunClick(TObject *Sender){
 TComInterface<IActiveScript> pAS;
 
 pAS.CreateInstance(L"JScript");
 if(!pAS) return;
 
 TComInterface<IActiveScriptParse> pASP(pAS);
 if(!pASP) return;
 
 pASP->InitNew();
 
 
 TComInterface<TMyWin> pDispatch_MyWin = new TMyWin(this);
 TComInterface<TMyGlobalFunc> pDispatch_MyGlobalFunc = new TMyGlobalFunc;
 TComInterface<TMyActiveScriptSite> pActiveScriptSite_Mine =
 new TMyActiveScriptSite(pDispatch_MyWin,pDispatch_MyGlobalFunc);
 
 
 pAS->SetScriptSite(pActiveScriptSite_Mine);
 
 
 pAS->AddNamedItem(TMyWin::Name,SCRIPTITEM_ISVISIBLE);
 
 pAS->AddNamedItem(TMyGlobalFunc::Name,
 SCRIPTITEM_ISVISIBLE|SCRIPTITEM_GLOBALMEMBERS);
 
 
 pASP->ParseScriptText(WideString(mmoScript->Lines->Text),
 NULL,
 NULL,
 NULL,
 0,
 0,
 0,
 NULL,
 NULL);
 
 
 pAS->SetScriptState(SCRIPTSTATE_STARTED);
 
 pAS->Close();
 }
 
 | 
编译,运行。
把写的Jscript代码拷贝到mmoScript里然后按btnRun运行可以看到效果。当然也可以修改脚本弄点更好玩的花样出来,呵呵。
最后还要多说一句关于ParseScriptText,用好它后面的几个参数。看MSDN,修改其中一个参数这段代码就可以用作表态式解析了。