在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脚本:
1 2 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
这三个全局函数:
1 2 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对象的方法和属性,和上面一样
1 2 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>
1 2 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>
1 2 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,修改其中一个参数这段代码就可以用作表态式解析了。