COM組件開發實踐(七)---多線程ActiveX控件和自動調整ActiveX控件大小(上)
聲明:本文代碼基于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類中加入兩個變量:

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

2,重載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 = S_OK;
goto cleanup;
}
hr = isp2->QueryService(SID_SWebBrowserApp, IID_IWebBrowser2, reinterpret_cast<void **>(&pWebBrowser)); //查詢IE瀏覽器接口
if (FAILED(hr))
{
hr = S_OK;
goto cleanup;
}
hr = pWebBrowser->get_Document((IDispatch**)&pHTMLDocument); //查詢Web頁面接口
if(FAILED(hr))
{
hr = S_OK;
goto cleanup;
}
cleanup:
// Free resources.
COMRELEASE(isp);
COMRELEASE(isp2);
}
}

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

{
CHECK_POINTER(pHTMLDocument);
HRESULT hr = pHTMLDocument->get_Script(&spDisp);
ATLASSERT(SUCCEEDED(hr));
return SUCCEEDED(hr);
}
bool CMyActiveXCtrl::GetJScripts(CComPtr<IHTMLElementCollection>& spColl)
{
CHECK_POINTER(pHTMLDocument);
HRESULT hr = pHTMLDocument->get_scripts(&spColl);
ATLASSERT(SUCCEEDED(hr));
return SUCCEEDED(hr);
}
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)
{
CStringArray paramArray;
paramArray.Add(strArg1);
return CallJScript(strFunc,paramArray,pVarResult);
}
bool CMyActiveXCtrl::CallJScript(const CString strFunc,const CString strArg1,const CString strArg2,CComVariant* pVarResult)
{
CStringArray paramArray;
paramArray.Add(strArg1);
paramArray.Add(strArg2);
return CallJScript(strFunc,paramArray,pVarResult);
}
bool CMyActiveXCtrl::CallJScript(const CString strFunc,const CString strArg1,const CString strArg2,const CString strArg3,CComVariant* pVarResult)
{
CStringArray paramArray;
paramArray.Add(strArg1);
paramArray.Add(strArg2);
paramArray.Add(strArg3);
return CallJScript(strFunc,paramArray,pVarResult);
}
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));
return false;
}
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(FAILED(hr))
{
//ShowError(GetSystemErrorMessage(hr));
return false;
}
if(pVarResult)
{
*pVarResult = vaResult;
}
return true;
}

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

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

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

{
alert("phinecos");
}

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

{
public:
virtual void OnSuccesful() = 0;//操作成功
virtual void OnFailed() = 0;//操作失敗
};

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

{
public:
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_hThread )
{
//嘗試"溫柔地"結束線程
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;
}
private:
HANDLE m_hThread; //線程句柄
DWORD m_dwTID; //線程ID
LPVOID m_pParent; //this pointer of the parent CThread object
DWORD m_dwExitCode; //線程退出碼
protected:
LPTHREAD_START_ROUTINE m_pThreadFunction; //工作線程指針
BOOL m_runthread; //線程是否繼續運行的標志
ICallBack* pCallBack; //主線程的回調函數接口
};

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

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

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

{//操作成功
//FireParameterLoaded();
CString strOnLoaded("OnLoaded");
this->CallJScript(strOnLoaded);
}

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