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 ,选择添加事件,这种方式就不赘述了。

      第二种方式是利用IWebBrowser2IHTMLDocument2这两个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 
=  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++》,感兴趣的话可以细读。

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)
{
    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函数的方式了,为了简单起见,就在原文代码的基础上修改了下。

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
{
public :
    
virtual   void  OnSuccesful()  =   0 ; // 操作成功
     virtual   void  OnFailed()  =   0 ; // 操作失败
};

      然后让CMyActiveXCtrl控件类继承自这个虚基类,实现这些回调函数接口

class CMyActiveXCtrl : public COleControl,public ICallBack

线程基类

       为了处理线程方便,本文使用了CodeProjectTrafficWatcher这篇文章中的一个CThread类,稍作修改得到下面的CMyThread类,就是在其中加入了ICallBack* pCallBack这个主线程的回调函数接口。

class  CMyThread
{
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方法即可,为了简单起见,下面就只模拟了操作设备成功的情况,当然可以根据实际应用记入具体操作代码。

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/
本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,并在文章页面明显位置给出原文连接。

 

你可能感兴趣的:(ActiveX)