聲明:本文代碼基于CodeProject的文章《
A Complete ActiveX Web Control Tutorial》修改而來,是以同樣遵循
Code Project Open License (CPOL)。
最近遇到兩個需求:1)在ActiveX控件中使用工作線程來完成底層的硬體裝置掃描任務,并在工作線程中根據操作結果回調外部web頁面的JavaScript函數;2)能根據控件任務的不同自動調整控件大小。但在查閱了大量資料後,發現網上讨論ActiveX中多線程開發的文章基本沒有,最後在csdn論壇裡遇到一個高手幫忙後,摸索了幾天才解決這兩個問題,本文的目的就在于記錄下我解決這兩個問題的過程,也希望能幫助到以後有同樣需求的朋友。
簡單抽象下第一個任務的模型:在AcitveX控件中開啟一個工作線程去執行特點任務後,然後根據工作線程的執行結果中去通知外部的web頁面的JavaScript。在進入到多線程之前,先來介紹下ActiveX中調用外部web頁面的JavaScript函數的兩種方式。
ActiveX中調用JavaScript
第一種方式是使用事件,這是最簡單方法。在“類視圖”中,右鍵CMyActiveXCtrl ,選擇“添加事件”,這種方式就不贅述了。
第二種方式是利用IWebBrowser2和IHTMLDocument2這兩個COM元件來通路包含ActiveX控件的外部Web頁面上的所有元素。具體實作步驟如下:
1, 在CMyActiveXCtrl類中加入兩個變量:

public:
IWebBrowser2* pWebBrowser; //IE浏覽器
IHTMLDocument2* pHTMLDocument; //包含此控件的web頁面

2,重載OnSetClientSite函數。

void CMyActiveXCtrl::OnSetClientSite()
{
HRESULT hr = S_OK;
IServiceProvider *isp, *isp2 = NULL;
if (!m_pClientSite)
{
COMRELEASE(pWebBrowser);
}
else
hr = m_pClientSite->QueryInterface(IID_IServiceProvider, reinterpret_cast<void **>(&isp));
if (FAILED(hr))
{
hr = S_OK;
goto cleanup;
}
hr = isp->QueryService(SID_STopLevelBrowser, IID_IServiceProvider, reinterpret_cast<void **>(&isp2));
if (FAILED(hr))
hr = isp2->QueryService(SID_SWebBrowserApp, IID_IWebBrowser2, reinterpret_cast<void **>(&pWebBrowser)); //查詢IE浏覽器接口
hr = pWebBrowser->get_Document((IDispatch**)&pHTMLDocument); //查詢Web頁面接口
if(FAILED(hr))
{
}
cleanup:
// Free resources.
COMRELEASE(isp);
COMRELEASE(isp2);
}
}

3,控件在加載後會調用OnSetClientSite函數的,是以就會查詢到對應包含控件的Web頁面,有了這個頁面後,就可以使用下述函數來調用Web頁面中的JavaScript函數了。下述代碼來自CodeGuru 的文章《
JavaScript Calls from C++》,感興趣的話可以細讀。

bool CMyActiveXCtrl::GetJScript(CComPtr<IDispatch>& spDisp)
CHECK_POINTER(pHTMLDocument);
HRESULT hr = pHTMLDocument->get_Script(&spDisp);
ATLASSERT(SUCCEEDED(hr));
return SUCCEEDED(hr);
bool CMyActiveXCtrl::GetJScripts(CComPtr<IHTMLElementCollection>& spColl)
HRESULT hr = pHTMLDocument->get_scripts(&spColl);
bool CMyActiveXCtrl::CallJScript(const CString strFunc,CComVariant* pVarResult)
CStringArray paramArray;
return CallJScript(strFunc,paramArray,pVarResult);
bool CMyActiveXCtrl::CallJScript(const CString strFunc,const CString strArg1,CComVariant* pVarResult)
paramArray.Add(strArg1);
bool CMyActiveXCtrl::CallJScript(const CString strFunc,const CString strArg1,const CString strArg2,CComVariant* pVarResult)
paramArray.Add(strArg2);
bool CMyActiveXCtrl::CallJScript(const CString strFunc,const CString strArg1,const CString strArg2,const CString strArg3,CComVariant* pVarResult)
paramArray.Add(strArg3);
bool CMyActiveXCtrl::CallJScript(const CString strFunc, const CStringArray& paramArray,CComVariant* pVarResult)
CComPtr<IDispatch> spScript;
if(!GetJScript(spScript))
//ShowError("Cannot GetScript");
return false;
CComBSTR bstrMember(strFunc);
DISPID dispid = NULL;
HRESULT hr = spScript->GetIDsOfNames(IID_NULL,&bstrMember,1,
LOCALE_SYSTEM_DEFAULT,&dispid);
if(FAILED(hr))
//ShowError(GetSystemErrorMessage(hr));
const int arraySize = paramArray.GetSize();
DISPPARAMS dispparams;
memset(&dispparams, 0, sizeof dispparams);
dispparams.cArgs = arraySize;
dispparams.rgvarg = new VARIANT[dispparams.cArgs];
for( int i = 0; i < arraySize; i++)
CComBSTR bstr = paramArray.GetAt(arraySize - 1 - i); // back reading
bstr.CopyTo(&dispparams.rgvarg[i].bstrVal);
dispparams.rgvarg[i].vt = VT_BSTR;
dispparams.cNamedArgs = 0;
EXCEPINFO excepInfo;
memset(&excepInfo, 0, sizeof excepInfo);
CComVariant vaResult;
UINT nArgErr = (UINT)-1; // initialize to invalid arg
hr = spScript->Invoke(dispid,IID_NULL,0,
DISPATCH_METHOD,&dispparams,&vaResult,&excepInfo,&nArgErr);
delete [] dispparams.rgvarg;
if(pVarResult)
*pVarResult = vaResult;
return true;

4,現在就可以來測試上述兩種調用JavaScript函數的方式了,為了簡單起見,就在原文代碼的基礎上修改了下。

void CMyActiveXCtrl::LoadParameter(void)
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_OutputParameter = m_InputParameter;
// Fire an event to notify web page
FireParameterLoaded();
CString strOnLoaded("OnLoaded");
this->CallJScript(strOnLoaded);

并且在web頁面中加入了一個測試用的JavaScript函數

function OnLoaded()
alert("phinecos");

多線程ActiveX控件
有了上面調用JavaScript函數的基礎,現在就來為控件加入工作線程,然後線上程中根據任務執行結果來通知外部Web頁面做出應有的響應。
我的第一個思路就是在主線程中設定回調函數,等建立子線程時,讓子線程儲存主線程的指針,然後線上程執行過程中根據執行的結果去回調主線程的回調函數。這種思路看上去很不錯,就先按這步走。
首先建立一個回調函數接口,指明主線程應有的回調函數

class ICallBack
virtual void OnSuccesful() = 0;//操作成功
virtual void OnFailed() = 0;//操作失敗
};

然後讓CMyActiveXCtrl控件類繼承自這個虛基類,實作這些回調函數接口
class CMyActiveXCtrl : public COleControl,public ICallBack
線程基類
為了處理線程友善,本文使用了CodeProject上
《TrafficWatcher》這篇文章中的一個CThread類,稍作修改得到下面的CMyThread類,就是在其中加入了ICallBack* pCallBack這個主線程的回調函數接口。

class CMyThread
CMyThread()
{
m_pThreadFunction = CMyThread::EntryPoint;
m_runthread = FALSE;
virtual ~CMyThread()
if ( m_hThread )
Stop(true); //thread still running, so force the thread to stop!
DWORD Start(DWORD dwCreationFlags = 0)
m_runthread = true;
m_hThread = CreateThread(NULL, 0, m_pThreadFunction, this, dwCreationFlags,&m_dwTID);
m_dwExitCode = (DWORD)-1;
return GetLastError();
/**//**
* Stops the thread.
*
* @param bForceKill if true, the Thread is killed immediately
*/
DWORD Stop ( bool bForceKill = false )
//嘗試"溫柔地"結束線程
if (m_runthread == TRUE)
m_runthread = FALSE; //first, try to stop the thread nice
GetExitCodeThread(m_hThread, &m_dwExitCode);
if ( m_dwExitCode == STILL_ACTIVE && bForceKill )
{//強制殺死線程
TerminateThread(m_hThread, DWORD(-1));
m_hThread = NULL;
}
return m_dwExitCode;
* Stops the thread. first tell the thread to stop itself and wait for the thread to stop itself.
* if timeout occurs and the thread hasn't stopped yet, then the thread is killed.
* @param timeout milliseconds to wait for the thread to stop itself
DWORD Stop ( WORD timeout )
Stop(false);
WaitForSingleObject(m_hThread, timeout);//等待一段時間
return Stop(true);
* suspends the thread. i.e. the thread is halted but not killed. To start a suspended thread call Resume().
DWORD Suspend()
{//挂起線程
return SuspendThread(m_hThread);
/**//**
* resumes the thread. this method starts a created and suspended thread again.
DWORD Resume()
{//恢複線程
return ResumeThread(m_hThread);
* sets the priority of the thread.
* @param priority the priority. see SetThreadPriority() in windows sdk for possible values.
* @return true if successful
BOOL SetPriority(int priority)
{//設定線程優先級
return SetThreadPriority(m_hThread, priority);
* gets the current priority value of the thread.
* @return the current priority value
int GetPriority()
{//擷取線程優先級
return GetThreadPriority(m_hThread);
void SetICallBack(ICallBack* pCallBack)
this->pCallBack = pCallBack;
protected:
/**
* 子類應該重寫此方法,這個方法是實際的工作線程函數
virtual DWORD ThreadMethod() = 0;
private:
* DONT override this method.
*
* this method is the "function" used when creating the thread. it is static so that way
* a pointer to it is available inside the class. this method calls then the virtual
* method of the parent class.
static DWORD WINAPI EntryPoint( LPVOID pArg)
CMyThread *pParent = reinterpret_cast<CMyThread*>(pArg);
pParent->ThreadMethod();//多态性,調用子類的實際工作函數
return 0;
HANDLE m_hThread; //線程句柄
DWORD m_dwTID; //線程ID
LPVOID m_pParent; //this pointer of the parent CThread object
DWORD m_dwExitCode; //線程退出碼
LPTHREAD_START_ROUTINE m_pThreadFunction; //工作線程指針
BOOL m_runthread; //線程是否繼續運作的标志
ICallBack* pCallBack; //主線程的回調函數接口

具體的工作線程子類
具體的工作線程子類隻需要從CMyThread繼承下去,重載ThreadMethod方法即可,為了簡單起見,下面就隻模拟了操作裝置成功的情況,當然可以根據實際應用記入具體操作代碼。

class CMyTaskThread :public CMyThread
DWORD CMyTaskThread::ThreadMethod()
while(m_runthread)
{
this->pCallBack->OnSuccesful();//模拟操作成功,回調主線程
Sleep(5000); //休息會再模拟
}
return 0;

回調函數
按照最明顯的思路,結合第一部分的知識,很顯然回調函數應該是下面這樣,選擇事件或直接調用外部的JavaScript函數來通知外部web頁面響應。

void CMyActiveXCtrl::OnSuccesful()
{//操作成功
//FireParameterLoaded();
CString strOnLoaded("OnLoaded");
this->CallJScript(strOnLoaded);

但不幸的是,這樣做根本無效,外部的Web頁面無法收到響應,就這個問題折騰了好幾天,思路上看好像沒什麼錯呀,怎麼就回調不了呢?。。。
那麼正确的做法應該是怎樣的呢?限于本文篇幅,将在下一篇中給出解答,并放出完整源代碼。
作者:
phinecos(洞庭散人)出處:
http://phinecos.cnblogs.com/本文版權歸作者和部落格園共有,歡迎轉載,但請保留此段聲明,并在文章頁面明顯位置給出原文連接配接。