初次发贴,还请各位多多关照。
有一些软件会把一部分算法以脚本的形式实现,放在程序的外面,当这部分算法需要变更的时候,只需修改脚本即可,从而避免了整个项目的重新编译。虽然使用脚本在效率上会有一定的损失,但如果不是用于大量计算等一些耗时操作的话,还是值得推荐的。下面是用
VC
实现对脚本(
VBS
,
JavaScript
)支持的主要步骤,以及我对这部分功能的封装。
一、首先要实现自己的
IActiveScriptSite
接口。
class
CAxScriptSite : public IActiveScriptSite
{
protected:
DWORD m_dwRef; //
用于引用计数
std::map<_bstr_t, ObjPtr_TypePtr_Pair> m_mapScriptObj; //
用于管理多个顶级对象
CAxScriptHostWindow m_xScriptSiteWindow; //
一个实现了
IActiveScriptSiteWindow
接口的对象,用于与
UI
交互
public:
ScriptError m_err;
public:
CAxScriptSite() : m_dwRef(1)
{
m_err.m_bIsErr = false;
m_err.m_bstrDescription = L"";
m_err.m_bstrSource = L"";
m_err.m_lChar = 0;
m_err.m_scCode = 0;
m_err.m_ulRow = 0;
}
// IUnknown methods...
virtual HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObject);
virtual ULONG _stdcall AddRef(void)
{
return ++m_dwRef;
}
virtual ULONG _stdcall Release(void)
{
if(--m_dwRef == 0)
return 0;
return m_dwRef;
}
// IActiveScriptSite methods...
virtual HRESULT __stdcall GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown **ppunkItem, ITypeInfo **ppti);
virtual HRESULT __stdcall GetLCID(LCID *plcid)
{
return S_OK;
}
virtual HRESULT __stdcall GetDocVersionString(BSTR *pbstrVersion)
{
return S_OK;
}
virtual HRESULT __stdcall OnScriptTerminate(const VARIANT *pvarResult, const EXCEPINFO *pexcepInfo)
{
return S_OK;
}
virtual HRESULT __stdcall OnStateChange(SCRIPTSTATE ssScriptState)
{
return S_OK;
}
virtual HRESULT __stdcall OnScriptError(IActiveScriptError *pse)
{
EXCEPINFO ei;
pse->GetExceptionInfo(&ei);
m_err.m_bstrDescription = ei.bstrDescription;
m_err.m_bstrSource = ei.bstrSource;
m_err.m_scCode = ei.scode;
pse->GetSourcePosition(NULL, &m_err.m_ulRow, &m_err.m_lChar);
m_err.m_bIsErr = true;
return S_OK;
}
virtual HRESULT __stdcall OnEnterScript(void)
{
m_err.m_bstrDescription = L"";
m_err.m_bIsErr = false;
m_err.m_lChar = 0;
m_err.m_ulRow = 0;
return S_OK;
}
virtual HRESULT __stdcall OnLeaveScript(void)
{
return S_OK;
}
};
这里被重写的函数,除了错误处理,大部分只需返回
S_OK
就可以了,具体含义详见
MSDN
,下面来看一下上面未给出实现的两个函数:
HRESULT __stdcall CAxScriptSite::QueryInterface(REFIID riid, void **ppvObject)
{
if(riid == IID_IActiveScriptSiteWindow )
{
*ppvObject = &m_xScriptSiteWindow;
return S_OK;
}
*ppvObject = NULL;
return E_NOTIMPL;
}
这里注意对
IID_IActiveScriptSiteWindow
的判断,如果没有这部分,程序将无法与窗体交互,
VBS
里
MsgBox
(或
JavaScrip
里的
alert
)这样的语句将无法执行,提示没有权限
HRESULT __stdcall CAxScriptSite::GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown **ppunkItem, ITypeInfo **ppti)
{
_bstr_t bstrKey((wchar_t*)pstrName);
// Is it expecting an ITypeInfo?
if(ppti)
{
// Default to NULL.
*ppti = NULL;
// See if asking about ITypeInfo...
if(dwReturnMask & SCRIPTINFO_ITYPEINFO)
{
//get type infomation according to namespace
*ppti = m_mapScriptObj[bstrKey].second;
}
}
// Is the engine passing an IUnknown buffer?
if(ppunkItem)
{
// Default to NULL.
*ppunkItem = NULL;
// Is Script Engine looking for an IUnknown for our object?
if(dwReturnMask & SCRIPTINFO_IUNKNOWN)
{
//find host object according to namespace
IUnknown *pUnkScriptObject = m_mapScriptObj[bstrKey].first;
*ppunkItem = pUnkScriptObject;
pUnkScriptObject->AddRef();
}
}
return S_OK;
}
当脚本中出现名字空间时(比如脚本中出现
“MyObject.funXXX”
),会调用该函数使该名字空间与主程序中的组件对象建立关联:其中
pstrName
保存的是这个名字空间的名称(上例中的
”MyObject”
),整个过程就是将这个名字空间所对应的组件对象和类型信息返回到
*ppunkItem
和
*ppti
中去,这样,脚本就可以与主程序交互了。为了以后能在脚本中使用多个名字空间,这里使用了
std::map
。
二、为了让
ScriptSite
能与
UI
交互,要实现
IActiveScriptSiteWindow
接口,具体原因已经在第一步中讲过了,就是为了能让类似
VBS
中
MsgBox
这样的函数能用,实现起来也很简单:
class
CAxScriptHostWindow : public IActiveScriptSiteWindow
{
protected:
DWORD m_dwRef; //
引用计数
public:
HWND m_hWnd; //
可供交互的窗体句柄
public:
CAxScriptHostWindow() : m_dwRef(1), m_hWnd(0)
{}
// IUnknown methods...
virtual HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObject)
{
if(riid == IID_IActiveScriptSiteWindow)
*ppvObject = this;
else
*ppvObject = NULL;
return
S_OK;
}
virtual ULONG _stdcall AddRef(void)
{
return ++m_dwRef;
}
virtual ULONG _stdcall Release(void)
{
if(--m_dwRef == 0)
return 0;
return m_dwRef;
}
virtual HRESULT __stdcall GetWindow(HWND *phwnd)
{
*phwnd = m_hWnd;
return S_OK;
}
virtual HRESULT __stdcall EnableModeless(BOOL fEnable)
{
return S_OK;
}
};
三、
为了使用方便以及复用,我对上述功能进行了封装(脚本宿主)
class CAxScriptHost : public CAxScriptSite
{
private:
bool m_bInit;
IActiveScript *m_pAS;
IActiveScriptParse *m_pASP;
public:
CAxScriptHost() :
m_bInit(false), m_pAS(NULL), m_pASP(NULL)
{}
~CAxScriptHost()
{
CloseEngine();
}
public:
//---------------------------------------
//DESC :
为脚本宿主添加根对象
//
//T : COM
对象指针或与之兼容的
COM
指针对象类型
//
//bstrAxHostLib :
所在类型库
//clsidAxHostObj :
类型标识
//pHostObj : COM
对象指针或与之兼容的
COM
指针对象
(
必需可以使用
p->QueryInterface()
函数
)
//bstrAxHostNamespace :
分配的名字空间
//
//RETURN :
//---------------------------------------
template<class T>
bool AddHostObject(BSTR bstrAxHostLib, CLSID clsidAxHostObj, T pHostObj, BSTR bstrAxHostNamespace)
{
HRESULT hr;
ITypeInfo *pTypeInfo;
IUnknown *pUnkScriptObject;
// Register your type-library
ITypeLib *ptLib = 0;
hr = LoadTypeLib((LPCWSTR)bstrAxHostLib, &ptLib);
FAIL_RETURN(hr);
// Initialize your IActiveScriptSite implementation with your
// object's ITypeInfo...
hr = ptLib->GetTypeInfoOfGuid(clsidAxHostObj, &pTypeInfo);
ptLib->Release();
FAIL_RETURN(hr);
//Get interface of the com object
pHostObj->QueryInterface(IID_IUnknown, (void **)&pUnkScriptObject);
FAIL_RETURN(hr);
// Add a root-level item to the engine's name space...
hr = m_pAS->AddNamedItem((LPCWSTR)bstrAxHostNamespace, SCRIPTITEM_ISVISIBLE | SCRIPTITEM_ISSOURCE);
FAIL_RETURN(hr);
//Add com object and type info to map
m_mapScriptObj.insert(pair<_bstr_t, ObjPtr_TypePtr_Pair>(bstrAxHostNamespace, ObjPtr_TypePtr_Pair(pUnkScriptObject, pTypeInfo)));
return
true;
}
//---------------------------------------
//DESC :
设置脚本
//
//lpScript :
环境脚本
//
//RETURN :
//---------------------------------------
bool SetEngineScript(LPCWSTR lpScript);
//---------------------------------------
//DESC :
脚本引擎初始化
//
//lpLanguage :
使用的脚本语言
ProgID
//hSiteWindow :
与
UI
交互所用窗体句柄
//
//RETURN :
//---------------------------------------
bool InitEngine(LPCWSTR lpLanguage, const HWND& hSiteWindow);
//---------------------------------------
//DESC :
关闭脚本引擎
//
//RETURN :
//---------------------------------------
bool CloseEngine();
//---------------------------------------
//DESC :
调用脚本中的函数
//
//lpFunc :
函数名
//pParam :
参数
(
注意参数的顺序是倒置的
)
//pvarResult :
返回值
//pEx :
返回执行错误
//pArgErr :
返回参数错误
//
//RETURN :
//---------------------------------------
bool ExecuteFunction(LPCWSTR lpFunc, DISPPARAMS *pParam, VARIANT *pvarResult = NULL, EXCEPINFO *pEx = NULL, UINT *pArgErr = NULL);
//---------------------------------------
//DESC :
指定脚本函数是否存在
//
//lpFunc :
函数名
//
//RETURN :
//---------------------------------------
bool FunctionExist(LPCWSTR lpFunc);
};
经过封装以后,要在程序中调用脚本语言,可以按以下步骤完成:
1
创建一个
CAxScriptHost
对象
,
以及要添加到宿主根名字空间的
COM
对象(比如
CComPtr, CComObject
)
2
初始化宿主对象,设置要使用的语言
3
将组件对象挂接到根名字空间下
4
设置脚本(可能会调用主程序中的组件对象)
5
运行脚本中的方法(可能会调用主程序中的组件对象)
6
使用结束,关闭引擎,并销毁
COM
对象
下面是示例程序的一些截图:
加载脚本后
调用函数
fun
调用名字空间
HostObjectDemo
调用名字空间
HostObject2