posts - 126,  comments - 73,  trackbacks - 0

 Preface

      因为项目需要,开始从事 ActiveX 方面的工作,看了一些资料,可惜都是些 COM 原理方面的,没有切合实际动手的东西,在 CodeProject 上读完 David Marcionek的 文章【1】后,收获良多,但也遇到一些恼人的小问题,因此在其基础上就一些易错点做些小注解。本文版权归 David Marcionek 所有。

简介 

本文目的在于让你快速掌握 ActiveX 控件开发技术,将会展示开发 ActiveX 应该知道的基本概念,如方法,属性和事件,以及如何在一个 ActiveX 控件和一个 web 页面之间进行通信

在本文中,我们将创建一个 ActiveX 控件,当加载控件时,它会显示一个动画进度条,以便向用户表明控件正在加载。此控件会包含展示如何在控件和 web 页面间传递信息的功能。下面我们会使用 VS2005 一步步进行开发的。

创建一个 ActiveX 控件

为了创建一个 ActiveX 控件,如下所示:

1, 创建一个 "MFC ActiveX Control" 项目,取名 MyActiveX,

2 ,在 "MFC ActiveX Control Wizard" 对话框中,选中 "Control Settings"

3, "Create control based on" 中选择 "STATIC". 我们将使用静态控件,因为我们只是显示从控件中获取的输出信息,并不接受输入信息。

4 ,在 "Additional features" 中,确保 "Activates when visible" "Flicker-free activation" 被选中, "Has an About box dialog" 不选中。

5 ,默认情况下, wizard 会创建一个项目,使其在一个共享 DLL 中使用 MFC. 我们必须更改这种情况,因为除非所需的 MFC DLL 都已经在系统中安装了,否则 ActiveX 控件 就不能运行。包含 ActiveX 控件的 Web 页面上出现红叉的一个原因就是此。在项目的属性中, "Configuration Properties"-->"General", “Use of MFC” 改为 “Use MFC in a Static Library”.

6 ,向导会创建如下几个类:

      1)CMyActiveXApp: 这是 ActiveX 应用程序类,从 COleControlModule 类继承下来的。它是 OLE 控件模块对象继承自的基类,包含了初始化 (InitInstance )和清理 (ExitInstance) 的代码

      2)CMyActiveXCtrl: COleControl 继承而来,这里是我们实现控件大部分功能的地方。

      3)CMyActiveXPropPage: COlePropertyPage 继承而来,用于管理控件的属性页对话框。向导已经为我们创建了一个默认的对话框来作为控件的属性页对话框。

增加动画 GIF 支持

      这里我们使用了一个 CPictureEx 类(具体代码见最后的 资源 部分), vs2005 增加一个动画 GIF 资源有一个 bug (其实在 vs2008 中也存在),我们可以使用下面这种技巧来回避它:

ProcessingProgressBar.gif 拷贝到项目文件夹下,然后更名为 ProcessingProgressBar.gaf 在资源视图中,右键资源文件 MyActiveX.rc, 选择 添加资源 。在 添加资源 对话框中,按下 导入 按钮,并选择 ProcessingProgressBar.gaf 文件。在 自定义资源类型 对话框中输入 “GIF” 作为资源类型。这就会将 GIF 图片文件导入项目中。然后将导入的图片 ID IDR_GIF1 改为 IDR_PROGRESSBAR .

现在开始着手恢复原状,首先,打开 MyActiveX.rc 的源文件,找到 IDR_PROGRESSBAR 的定义,将其文件名改为 ProcessingProgressBar.gif ”. 同样地,把项目文件夹下的图片文件名也改回为 “ProcessingProgressBar.gif”, 最后在 解决方案资源管理器 视图中,选中 ProcessingProgressBar.gaf ,在其 属性 中,修改 相对路径 ."ProcessingProgressBar.gif”.

增加对话框

      现在,我们为进度条图像增加一个对话框。

1,  资源 视图中,右键 对话框 ,选择 插入对话框 来创建一个默认的对话框。

2,  删除默认产生的 确定 取消 按钮,调整对话框大小为 230*40

3, 更改对话框 ID IDD_MAINDIALOG ,并修改对话框属性: Border—none, Style – Child, System Menu – False, Visible – True.

4, 在对话框中加入一个图片控件,调整其大小为 200*20 ,更改控件 ID IDC_PROGRESSBAR ,颜色为 “white”

5, 为对话框创建一个类,名为 CMainDialog,

 现在我们为类增加成员变量:

1, CMyActiveXCtrl 类增加一个变量 m_MainDialog ,类型为 CMainDialog

2,  CMainDialog 类增加一个变量 m_ProgressBar ,类型为 CPictureEx ,这里注意确保 控件变量 选中,并且对于的控件是 ”IDC_PROGRESSBAR”.

增加支持代码

好了,现在加入一些代码来绘制主对话框和进度条控件吧。

1 ,为 CMyActiveXCtrl 处理 WM_CREATE 事件的代码,在其中加入:

m_MainDialog.Create(IDD_MAINDIALOG,  this );

并在 OnDraw 函数中加入:

m_MainDialog.MoveWindow(rcBounds, TRUE);
CBrush brBackGnd(TranslateColor(AmbientBackColor()));
pdc
-> FillRect(rcBounds,  & brBackGnd);

     2. CMainDialog 类中,加入处理 WM_CREATE 事件的代码,在其中加入:

if (m_ProgressBar.Load(MAKEINTRESOURCE(IDR_PROGRESSBAR),_T( " GIF " )))
m_ProgressBar.Draw();

Ok, 一个简单的 ActiveX 控件已经开发完毕,设置编译模式为 “Release” 模式,并构建整个应用程序。

创建一个 Web 页面作为 ActiveX 控件容器

      可以使用微软的 ActiveX Control Pad 。要利用它在 Web 页面中插入一个 ActiveX 控件,在 <BODY> 标记中右键,选择 “Insert ActiveX Control”, 选择你需要的就可以了。

 直接打开Web页面或者放到IIS服务器上进行访问,一切顺利的话就可以看到下面的图像: 

注1:前面要求设置编译模式为“ Release” ,其实是为了避免运行时因为触及 Assert 出错而做的,否则会报错如下:

 跟踪调试后会发现:  

  可以看出是图片扩展控件加载时的顺序有些问题,但在浏览器中并不需要考虑如此多,因此这里忽略此Assert条件。

 2:作者在这里没有对MyActiveX.idl文件进行讲解,我认为是一个不小的失误,也正是因为如此,才会导致一个很容易犯错的地方,当我们按照他的教程,仿照他的代码一步步进行完后,却发现在ActiveX测试容器中是可以运行通过的,但到了浏览器中却死活都是红叉叉。。。,就是因为作者忽略了其对MyActiveX.idl接口定义文件的修改进行解释。

注3:VS2008中没有ActiveX控件测试容器了,VS05以上的数字签名工具也改变了,因此使用VS2005可能更好

     我按照教程一步步模仿着做的时候,在上面这两点上纠缠了3个多小时才发现问题的原因。

     在下一篇文章中,将介绍如何对 ActiveX 控件进行数字签名并使其自注册和销毁来确保其安全性,此外还会介绍如何在 ActiveX 控件和 Web 页面间进行数据通信。

假设需求如下:底层是一个数学运算库 DLL, 中间是 ActiveX 控件(它调用底层的数学运算库 DLL 来完成控制层),界面层在测试时可以是一个 exe 程序,最后发布到 IE 浏览器上测试。

 

数学运算库 DLL 的开发

      新建一个 Win32 DLL 项目,加入一个头文件 MyNum.h, 在其中声明所有的数学函数(为简单起见,本文只考虑加法运算),代码如下:

#ifndef MY_NUM_H
#define  MY_NUM_H
int  __stdcall AddNum( int , int );
#endif

请注意这里的方法声明为 __stdcall ,而 VC++ 默认的是 __cdecl ,由于组件的语言无关性要求调用和被调双方必须在函数调用的约定上一致,因此在后面加载 DLL 并获取此方法时也要求和你的声明一致。

      为了简单起见,加法方法的实现就放倒 DLL 入口点所在文件 , 代码如下:

//  NumDLL.cpp : 定义DLL 应用程序的入口点。
//
#include  " stdafx.h "
#include 
" MyNum.h "

#ifdef _MANAGED
#pragma  managed(push, off)
#endif

int  __stdcall AddNum( int  Num1, int  Num2)
{
    
return  Num1 + Num2;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    
return  TRUE;
}


#ifdef _MANAGED
#pragma  managed(pop)
#endif

      为了能在其他程序中显示链接此 DLL ,我们为它加入一个 .def 文件,命名为 NumDLL.def ,列出此 DLL 导出的方法名称:

LIBRARY     " NumDLL "
EXPORTS
    AddNum

至此我们的数学运算函数库 DLL 就完成了。

ATL 开发 ActiveX 控件

      开发 ActiveX 控件有两种方式,一是 MFC, 二是 ATL, 而后者是专门用于 COM 组件开发,因此更适合于 ActiveX 。因此这里选择后者,前者的开发示例参考我这篇文章( 用VC++开发ActiveX 控件完全教程(一) )。

      新建一个 ATL 项目,命名为 ”FuckATL” ,接受默认设置。右键项目名,添加一个 ”ATL 简单对象 ,命名为 CaluNumCtrl 点击下一步进入组件选项设置界面。

      修改类的头文件 CaluNumCtrl.h 如下:

class  ATL_NO_VTABLE CCaluNumCtrl :
    
public  CComObjectRootEx < CComSingleThreadModel > ,
    
public  CComCoClass < CCaluNumCtrl,  & CLSID_CaluNumCtrl > ,
    
public  ISupportErrorInfo,
    
public  IConnectionPointContainerImpl < CCaluNumCtrl > ,
    
public  CProxy_ICaluNumCtrlEvents < CCaluNumCtrl > ,
    
public  IObjectWithSiteImpl < CCaluNumCtrl > ,
    
public  IDispatchImpl < ICaluNumCtrl,  & IID_ICaluNumCtrl,  & LIBID_FuckATLLib,  /* wMajor = */   1 /* wMinor = */   0 >
{
public :
    typedef 
int  (__stdcall * PtrAddNum)( int , int );
    PtrAddNum MyAddNum;
    CCaluNumCtrl()
    
{
        
// 加载数学运算库DLL
        handle  =  ::LoadLibrary(_T( " D:\\dyk\\work\\NumDLL\\debug\\NumDLL.dll " ));
        
if  (handle  ==  NULL) 
        
{
            DWORD e 
=  GetLastError();
            
return ;
        }

        
// 获取加法运算函数指针
        MyAddNum  =  (PtrAddNum)GetProcAddress(handle, " AddNum " );
    }

DECLARE_REGISTRY_RESOURCEID(IDR_CALUNUMCTRL)
BEGIN_COM_MAP(CCaluNumCtrl)
    COM_INTERFACE_ENTRY(ICaluNumCtrl)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(ISupportErrorInfo)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(IObjectWithSite)
END_COM_MAP()

BEGIN_CONNECTION_POINT_MAP(CCaluNumCtrl)
    CONNECTION_POINT_ENTRY(__uuidof(_ICaluNumCtrlEvents))
END_CONNECTION_POINT_MAP()
//  ISupportsErrorInfo
    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    HRESULT FinalConstruct()
    
{
        
return  S_OK;
    }

    
void  FinalRelease()
    
{
    }

// 组件对外放出的加法方法
public :
    STDMETHOD(AddNumbers)(LONG Num1, LONG Num2, LONG
*  ReturnVal);     // 下面是一个NUM属性,也是用于测试,包含了读写设置方法
public :
    STDMETHOD(get_NUM)(SHORT
*  pVal);
public :
    STDMETHOD(put_NUM)(SHORT newVal);
public :
    HMODULE handle;
// 数学函数库模块句柄
}
;

在控件实现文件 CaluNumCtrl.cpp 中,代码如下:

STDMETHODIMP CCaluNumCtrl::AddNumbers(LONG Num1, LONG Num2, LONG *  ReturnVal)
{
    
int  sum  =   this -> MyAddNum(static_cast < int > (Num1),static_cast < int > (Num2));
    
* ReturnVal  =  static_cast < LONG > (sum);
    
return  S_OK;
}


STDMETHODIMP CCaluNumCtrl::get_NUM(SHORT
*  pVal)
{
    
* pVal  =   10 ;
    
return  S_OK;
}


STDMETHODIMP CCaluNumCtrl::put_NUM(SHORT newVal)
{
    
//  TODO: 在此添加实现代码
    
    
return  S_OK;
}

     好了, ActiveX 控件仅仅是简单地调用底层的数学运算库 DLL 来完成运算,下面我们写一个 exe 程序对这个 COM 组件进行测试。

一个控制台测试程序

      建立一个最简单的控制台程序来进行测试,代码如下:

#include  " ..\..\FuckATL\FuckATL\FuckATL.h "
#include 
" ..\..\FuckATL\FuckATL\FuckATL_i.c "
#include 
< iostream >
using    namespace  std;

void  main( void )
{
    
//  Declare and HRESULT and a pointer to the Simple_ATL interface
    HRESULT            hr;
    ICaluNumCtrl 
* IFirstATL  =  NULL;
    
//  Now we will intilize COM
    hr  =  CoInitialize( 0 );
    
//  Use the SUCCEDED macro and see if we can get a pointer to
    
//  the interface
     if (SUCCEEDED(hr))
    
{
        hr 
=  CoCreateInstance( CLSID_CaluNumCtrl, NULL, CLSCTX_INPROC_SERVER,
            IID_ICaluNumCtrl, (
void ** & IFirstATL);
        
//  If we succeeded then call the AddNumbers method, if it failed
        
//  then display an appropriate message to the user.
         if (SUCCEEDED(hr))
        
{
            
long  ReturnValue;
            hr 
=  IFirstATL -> AddNumbers( 5 7 & ReturnValue);
            cout 
<<   " The answer for 5 + 7 is:  "   <<  ReturnValue  <<  endl;
            
short  num; 
            IFirstATL
-> get_NUM( & num);
            cout
<< " num is:  " << num << endl;
            hr 
=  IFirstATL -> Release();
        }

        
else
        
{
            cout 
<<   " CoCreateInstance Failed. "   <<  endl;
        }

    }

    
//  Uninitialize COM
    CoUninitialize();
    system(
" pause " );
}

来到 IE 的世界

      最后我们将此 ActiveX 组件嵌入到 html 页面中,对其进行测试 . 新建一个 html 页面,代码如下:

< HTML >
< HEAD >
< TITLE > New Page </ TITLE >
< script  language ="javascript" >
    
function  doTest()
    
{
        
var  sum  =  FuckATL1.AddNumbers( 3 , 4 );
        alert(sum);
    }

</ script >
</ HEAD >
< BODY >
< OBJECT  ID ="FuckATL1"  CLASSID ="CLSID:7BF3B65F-A800-4604-AE6B-91844EFD5F05" >
</ OBJECT >
< input  type ="button"  value ="测试加法"  id ="btnOK"  onclick ="doTest();" ></ input >
</ BODY >
</ HTML >

     由于暂时先不考虑控件的安全性需要,因此会出现下面的警告信息,不过不要紧,这个问题以后再解决。

测试结果如下:


 前面 篇文章分 MFC ActiveX 用程序和使用 ATL 开发 ActiveX 简单实 例,但 两个问题 需要解

1 标记 ActiveX 控件 安全的控件 2) 控件 名。本文 将结 这两 简单 的介

Building a Safe ActiveX Control

      如何不想办法将控件标记为安全的,就会在 Web 页面与控件进行交互时出现如下图的警告信息:

     下面将分别介绍在 MFC ActiveX ATL 中如何标记一个控件为安全的控件。

     要标记一个 MFC ActiveX 控件为安全,可以仿照下面代码修改而得:

//  CardScan.cpp : CCardScanApp 和DLL 注册的实现。
#include  " stdafx.h "
#include 
" CardScan.h "
#include 
" comcat.h "
#include 
" strsafe.h "
#include 
" objsafe.h "

CCardScanApp theApp;
const  GUID CDECL BASED_CODE _tlid  =
        { 
0x29959268 0x9729 0x458E , {  0xA8 0x39 0xBB 0x39 0x2E 0xCB 0x7E 0x37  } };
const  WORD _wVerMajor  =   1 ;
const  WORD _wVerMinor  =   0 ;
const  CATID CLSID_SafeItem  =
{
0xB548F3C7 , 0x2135 , 0x4242 ,{ 0x92 , 0x0B , 0xA7 , 0xBD , 0xEE , 0x6D , 0x2B , 0xA3 }};

// { 0x36299202, 0x9ef, 0x4abf,{ 0xad, 0xb9, 0x47, 0xc5, 0x99, 0xdb, 0xe7, 0x78}};
//  CCardScanApp::InitInstance - DLL 初始化
BOOL CCardScanApp::InitInstance()
{
    BOOL bInit 
=  COleControlModule::InitInstance();
    
if  (bInit)
    {
    }
    
return  bInit;
}
//  CCardScanApp::ExitInstance - DLL 终止
int  CCardScanApp::ExitInstance()
{
    
return  COleControlModule::ExitInstance();
}
HRESULT CreateComponentCategory(CATID catid, CHAR 
* catDescription)
{
    ICatRegister 
* pcr  =  NULL ;
    HRESULT hr 
=  S_OK ;
    hr 
=  CoCreateInstance(CLSID_StdComponentCategoriesMgr, 
        NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (
void ** ) & pcr);
    
if  (FAILED(hr))
        
return  hr;
    
//  Make sure the HKCR\Component Categories\{..catid}
    
//  key is registered.
    CATEGORYINFO catinfo;
    catinfo.catid 
=  catid;
    catinfo.lcid 
=   0x0409  ;  //  english
    size_t len;
    
//  Make sure the provided description is not too long.
    
//  Only copy the first 127 characters if it is.
    
//  The second parameter of StringCchLength is the maximum
    
//  number of characters that may be read into catDescription.
    
//  There must be room for a NULL-terminator. The third parameter
    
//  contains the number of characters excluding the NULL-terminator.
    hr  =  StringCchLength(catDescription, STRSAFE_MAX_CCH,  & len);
    
if  (SUCCEEDED(hr))
    {
        
if  (len > 127 )
        {
            len 
=   127 ;
        }
    }   
    
else
    {
        
//  TODO: Write an error handler;
    }
    
//  The second parameter of StringCchCopy is 128 because you need 
    
//  room for a NULL-terminator.
    hr  =  StringCchCopy(COLE2T(catinfo.szDescription), len  +   1 , catDescription);
    
//  Make sure the description is null terminated.
    catinfo.szDescription[len  +   1 =   ' \0 ' ;
    hr 
=  pcr -> RegisterCategories( 1 & catinfo);
    pcr
-> Release();
    
return  hr;
}
//  HRESULT RegisterCLSIDInCategory -
//       Register your component categories information
HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
    
//  Register your component categories information.
    ICatRegister  * pcr  =  NULL ;
    HRESULT hr 
=  S_OK ;
    hr 
=  CoCreateInstance(CLSID_StdComponentCategoriesMgr, 
        NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (
void ** ) & pcr);
    
if  (SUCCEEDED(hr))
    {
        
//  Register this category as being "implemented" by the class.
        CATID rgcatid[ 1 ] ;
        rgcatid[
0 =  catid;
        hr 
=  pcr -> RegisterClassImplCategories(clsid,  1 , rgcatid);
    }
    
if  (pcr  !=  NULL)
        pcr
-> Release();
    
return  hr;
}

//  HRESULT UnRegisterCLSIDInCategory - Remove entries from the registry
HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
    ICatRegister 
* pcr  =  NULL ;
    HRESULT hr 
=  S_OK ;
    hr 
=  CoCreateInstance(CLSID_StdComponentCategoriesMgr, 
        NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (
void ** ) & pcr);
    
if  (SUCCEEDED(hr))
    {
        
//  Unregister this category as being "implemented" by the class.
        CATID rgcatid[ 1 ] ;
        rgcatid[
0 =  catid;
        hr 
=  pcr -> UnRegisterClassImplCategories(clsid,  1 , rgcatid);
    }
    
if  (pcr  !=  NULL)
        pcr
-> Release();
    
return  hr;
}
//  DllRegisterServer - 将项添加到系统注册表

STDAPI DllRegisterServer(
void )
{
    HRESULT hr;
    AFX_MANAGE_STATE(_afxModuleAddrThis);
    
if  ( ! AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
        
return  ResultFromScode(SELFREG_E_TYPELIB);
    
if  ( ! COleObjectFactoryEx::UpdateRegistryAll(TRUE))
        
return  ResultFromScode(SELFREG_E_CLASS);
    
//  Mark the control as safe for initializing.
    hr  =  CreateComponentCategory(CATID_SafeForInitializing, 
        _T(
" Controls safely initializable from persistent data! " ));
    
if  (FAILED(hr))
        
return  hr;
    hr 
=  RegisterCLSIDInCategory(CLSID_SafeItem, 
        CATID_SafeForInitializing);
    
if  (FAILED(hr))
        
return  hr;
    
//  Mark the control as safe for scripting.
    hr  =  CreateComponentCategory(CATID_SafeForScripting, 
        _T(
" Controls safely  scriptable! " ));
    
if  (FAILED(hr))
        
return  hr;
    hr 
=  RegisterCLSIDInCategory(CLSID_SafeItem, 
        CATID_SafeForScripting);
    
if  (FAILED(hr))
        
return  hr;
    
return  NOERROR;
}

//  DllUnregisterServer - 将项从系统注册表中移除

STDAPI DllUnregisterServer(
void )
{
    HRESULT hr;
    AFX_MANAGE_STATE(_afxModuleAddrThis);
    
//  Remove entries from the registry.
    hr = UnRegisterCLSIDInCategory(CLSID_SafeItem, 
        CATID_SafeForInitializing);
    
if  (FAILED(hr))
        
return  hr;
    hr
= UnRegisterCLSIDInCategory(CLSID_SafeItem, 
        CATID_SafeForScripting);
    
if  (FAILED(hr))
        
return  hr;
    
if  ( ! AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))
        
return  ResultFromScode(SELFREG_E_TYPELIB);
    
if  ( ! COleObjectFactoryEx::UpdateRegistryAll(FALSE))
        
return  ResultFromScode(SELFREG_E_CLASS);
    
return  NOERROR;
}

     这里值得注意的一个地方是 DllUnregisterServer 函数 ,在这段代码中,我是将

hr = UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing);

hr
= UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting);

这两句代码放在

if  ( ! AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))

           
return  ResultFromScode(SELFREG_E_TYPELIB);

      
if  ( ! COleObjectFactoryEx::UpdateRegistryAll(FALSE))

           
return  ResultFromScode(SELFREG_E_CLASS);

这两句代码的前面,如果你查阅 MSDN ,将会发现它上面的顺序和我是相反的,这应该是微软的一个错误代码,如果按照 MSDN 的代码来写,则你使用 regsvr32 -u CardScan.ocx 反注册时会报下面的错误:

调整为我所说的顺序就没问题了。

2 )要标记使用 ATL 写的 ActiveX 控件为安全的控件,这比 MFC 要简单的多,只需要在控件头文件中增加几行代码就可以了:

class  ATL_NO_VTABLE CTestCtrl :
    …
    
public  IObjectSafetyImpl < CTestCtrl, INTERFACESAFE_FOR_UNTRUSTED_CALLER |  INTERFACESAFE_FOR_UNTRUSTED_DATA > ,

然后在 COM 映射表中增加一项:

BEGIN_COM_MAP(CTestCtrl)
    …
    COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP()

Building a Signed ActiveX Control

      ActiveX 控件是个危险的东西,如果不对其合法性进行数字签名和验证, IE 是会拒绝其安装的。

      工具包准备: CABARC.exe, cert2spc.exe, makecab.exe, makecert.exe, signcode.exe( 或新版本中的 signtool) ,以上小工具都可以在 VS 的安装路径下 "Common7"Tools"Bin 找到,或去微软官方网站上下载。

ActiveX 控件的安装过程中,一部分工作就是自注册,这需要控件在 VERSIONINFO 结构中定义 OLESelfRegister 值,你可以对资源文件进行编辑如下

BEGIN
    BLOCK 
" StringFileInfo "
    BEGIN
        BLOCK 
" 080403a8 "
        BEGIN
            VALUE 
" CompanyName " ,   " TODO: <公司名> "
            VALUE 
" FileDescription " ,   " TODO: <文件说明> "
            VALUE 
" FileVersion " ,   " 1.0.0.1 "
            VALUE 
" InternalName " ,   " CardScan.ocx "
            VALUE 
" LegalCopyright " ,   " TODO: (C) <公司名>。保留所有权利。 "
            VALUE 
" OLESelfRegister " ,   " \0 "
            VALUE 
" OriginalFilename " ,   " CardScan.ocx "
            VALUE 
" ProductName " ,   " TODO: <产品名> "
            VALUE 
" ProductVersion " ,   " 1.0.0.1 "
        
END
    
END
    BLOCK 
" VarFileInfo "
    BEGIN
        VALUE 
" Translation " ,   0x804 ,   936
    
END
END

打包为 CAB 文件

因为 ActiveX 控件要放在网站上供客户下载到本地,因此压缩是必需的。一段典型的 html 代码如下:

< OBJECT  ID ="FuckATL1"   
CODEBASE 
="http://localhost:8080/CardScan.cab"
CLASSID
="CLSID:B548F3C7-2135-4242-920B-A7BDEE6D2BA3"  WIDTH =300  HEIGHT =200
/>

CODEBASE 就指明了要下载的压缩包,其中包含了 oxc dll 控件等所需要的文件。

通常 CAB 文件包含了一个 INF 文件,它用来描述 CAB 文件的所有细节信息,下面举个简单例子,代码如下:

;  Sample INF file  for  SCRIPTABLEACTIVEX . DLL
[version] 
;  version signature  ( same  for  both NT and Win95 )   do   not  remove
signature
= " $CHICAGO$ "
AdvancedINF
= 2.0   

[Add
. Code]
CardScan
. ocx = CardScan . ocx
CardScan
. inf = CardScan . inf

[CardScan
. ocx]
file-win32-x86
= thiscab
clsid
= {B548F3C7- 2135 - 4242 -920B-A7BDEE6D2BA3} 
FileVersion
= 1 , 0 , 0 , 1  
RegisterServer
= yes

[CardScan
. inf]
file
= thiscab
;   end  of INF file

至于打包就不赘述了,详尽的图解过程请看《 如何给ActiveX数字签名(Step by Step, Delphi)


源代码下载 – 74kb

,C++ 客户重用 C++ 对象

      假设已经有一个可以重用的类,我们就可以在自己的程序中去重用它,只需要将其定义和实现文件加入到我们自己的工程中,并且在使用它的文件中包含此类的定义文件就可以了,这也是我们最常用的 C++ 标准重用方法。就拿我自己来说,在 CodeProject 上遇到比较好的控件代码,都是这样直接用到自己的项目中来的。

      下面就给出我这个系列的第一个代码示例,在接下来的几篇文章中,将基于此代码不断进行改进,一步步从 C++ 走向 COM.

      简单介绍下我们要重用的 C++ 对象,它是一个简单的类似数据库的对象,用来管理内存中的数据,它包含一个指向 数据库 中所有表的指针数组,表实际是一个字符串数组,每个数组元素表示表格的一行。另外这个类还包含有一个数据表表名的数组。

DBSRV.h 文件:

typedef  long  HRESULT; // 模拟COM中的HRESULT

// 内存数据库类
class  CDB 
{
  
//  Interfaces
   public :
          
//  Interface for data access
        HRESULT Read( short  nTable,  short  nRow, LPTSTR lpszData); // 读数据,nTable指定数据表,nRow指定数据行
        HRESULT Write( short  nTable,  short  nRow, LPCTSTR lpszData); // 写数据,nTable指定数据表,nRow指定数据行
        
//  Interface for database management
        HRESULT Create( short   & nTable, LPCTSTR lpszName); // 创建数据表,表名为lpszName
        HRESULT Delete( short  nTable); // 删除数据表
        
//  Interface for database information
        HRESULT GetNumTables( short   & nNumTables); // 获取数据表个数
        HRESULT GetTableName( short  nTable, LPTSTR lpszName); // 获取指定数据表表名,nTable为数据表索引号
        HRESULT GetNumRows( short  nTable,  short   & nRows); // 获取指定数据表的数据行数,nTable为数据表索引号,nRows保存返回的行数
  
//  Implementation
   private :
      CPtrArray      m_arrTables;      
// 指向“数据库”中所有表的指针数组
    CStringArray m_arrNames;  // 数据表名称数组
     public :
        
~ CDB();
};

DBSRV.cpp 文件:

#include  " stdafx.h "
#include 
" ..\Interface\DBsrv.h "
//  Database object
HRESULT CDB::Read( short  nTable,  short  nRow, LPTSTR lpszData) 
{
  CStringArray 
* pTable;
  pTable
= (CStringArray * ) m_arrTables[nTable];
  lstrcpy (lpszData, (
* pTable)[nRow]);
  
return  NO_ERROR;
}

HRESULT CDB::Write(
short  nTable,  short  nRow, LPCTSTR lpszData) 
{
  CStringArray 
* pTable;
  pTable
= (CStringArray * ) m_arrTables[nTable];
  pTable
-> SetAtGrow(nRow, lpszData);
  
return  NO_ERROR;
}
HRESULT CDB::Create(
short   & nTable, LPCTSTR lpszName) 
{
  CStringArray 
* pTable = new  CStringArray;
  nTable
= m_arrTables.Add(pTable);
  m_arrNames.SetAtGrow(nTable, lpszName);
  
return  NO_ERROR;
}
HRESULT CDB::Delete(
short  nTable) 
{
  CStringArray 
* pTable;
  pTable
= (CStringArray * ) m_arrTables[nTable];
  delete pTable;
  m_arrTables[nTable]
= NULL;
  m_arrNames[nTable]
= "" ;
  
if  (nTable == m_arrTables.GetSize() - 1
  {
        m_arrTables.RemoveAt(nTable);
        m_arrNames.RemoveAt(nTable);
    }
  
return  NO_ERROR;
}
HRESULT CDB::GetNumTables(
short   & nNumTables) 
{
  nNumTables
= m_arrTables.GetSize();
    
return  NOERROR;
}

HRESULT CDB::GetTableName(
short  nTable, LPTSTR lpszName) 
{
  lstrcpy(lpszName, m_arrNames[nTable]);
  
return  NO_ERROR;
}
HRESULT CDB::GetNumRows(
short  nTable,  short   & nRows)
{
  CStringArray 
* pTable;
  pTable
= (CStringArray * ) m_arrTables[nTable];
  
return  pTable -> GetSize();
}
CDB::
~ CDB() 
{
  
short  nNumTables;
  
for  (GetNumTables(nNumTables);nNumTables > 0 ; GetNumTables(nNumTables)) 
  {
        Delete(nNumTables
- 1 );
  }
}

     客户程序是一个简单的 MFC 单文档程序,为程序添加三个菜单项 建表 写表 读表 ,对应的处理函数在 CDBDoc 中实现。
public :
    CDB 
* m_pDB;  //  pointer to database object
    CString m_csData;  //  last data read from database
     int  m_nCount;             //  number of writes to database
     short  m_nTable;         //  number of last table created
CDBDoc::CDBDoc()
{
    m_pDB
= NULL;
}
CDBDoc::
~ CDBDoc()
{
    
if  (m_pDB) 
    {
        delete m_pDB; 
// 释放对象
        m_pDB = NULL;
    }
}
BOOL CDBDoc::OnNewDocument()
{
    
if  ( ! CDocument::OnNewDocument())
        
return  FALSE;
    
// 新建数据库对象
    m_pDB = new  CDB;
    
//  初始化数据成员变量
    m_csData = " No data yet! "
    m_nCount
= 0 ;        
    m_nTable
=- 1 ;    
    
return  TRUE;
}
//  菜单项处理函数区
void  CDBDoc::OnCreateTable() 
{
//  建表
    m_pDB -> Create(m_nTable, _T( " Testing " ));    
    m_nCount
= 0 //  set number of writes to 0 
}
void  CDBDoc::OnReadTable() 
{
//  读表
    m_pDB -> Read(m_nTable,  0 , m_csData.GetBuffer( 80 ));
    m_csData.ReleaseBuffer();
    UpdateAllViews(NULL);
}
void  CDBDoc::OnWriteTable() 
{
//  写表
    m_nCount ++ ;
    CString csText;
    csText.Format(_T(
" Test data #%d in table %d, row 0! " ), m_nCount, ( int ) m_nTable);
    m_pDB
-> Write(m_nTable,  0 , csText);
}

最后在 CDBView OnDraw 函数中添加如下语句来显示读表读取到的内容:

pDC -> TextOut( 10 , 10 , pDoc -> m_csData); 

二,将 C++ 对象打包到 DLL

      第一节中的标准重用方法有一个大毛病:类的实现代码被泄露了,而这想必不是我们想要的结果。要解决这个问题,我们可以使用 DLL 将类的代码打包成一个 DLL ,并提供一个用于说明函数和结构的头文件,这样实现代码就封装起来了。基于上一节的代码,我们修改如下:

一)先修改接口文件: 1 )为每个成员函数添加 _declspec (dllexport) 声明; 2) CDB 类添加成员函数 Release(), 用于在对象不再被使用时删除自己; 3 )声明类工厂 CDBSrvFactory;4) 声明返回类工厂对象的引出函数 DllGetClassFactoryObject ,用于创建对应的类工厂

typedef  long  HRESULT;

#define  DEF_EXPORT _declspec(dllexport)

class  CDB
 {
    
//  Interfaces
public :
    
//  Interface for data access
    HRESULT DEF_EXPORT Read( short  nTable,  short  nRow, LPWSTR lpszData);
    HRESULT DEF_EXPORT Write(
short  nTable,  short  nRow, LPCWSTR lpszData);
    
//  Interface for database management
    HRESULT DEF_EXPORT Create( short   & nTable, LPCWSTR lpszName);
    HRESULT DEF_EXPORT Delete(
short  nTable);
    
//  Interfase para obtenber informacion sobre la base de datos
    HRESULT DEF_EXPORT GetNumTables( short   & nNumTables);
    HRESULT DEF_EXPORT GetTableName(
short  nTable, LPWSTR lpszName);
    HRESULT DEF_EXPORT GetNumRows(
short  nTable,  short   & nRows);
    ULONG DEF_EXPORT Release(); 
// CPPTOCOM: need to free an object in the DLL, since it was allocated here
    
//  Implementation
private :
    CPtrArray m_arrTables;      
//  Array of pointers to CStringArray (the "database")
    CStringArray m_arrNames;  //  Array of table names
public :
    
~ CDB();
};

class  CDBSrvFactory 
{
    
//  Interface
public :
    HRESULT DEF_EXPORT CreateDB(CDB
**  ppObject);
    ULONG   DEF_EXPORT Release();
};

HRESULT DEF_EXPORT DllGetClassFactoryObject(CDBSrvFactory 
**  ppObject);

     二)修改对象程序。在上一节中,重用的对象是以 DBSRV.h DBSRV.cpp 这两个文件形式存在的。这一次我们要将其封装为一个 DLL 供客户程序调用。

新建一个 Win32 DLL 项目,在其中加入两个 cpp 文件,一个用于实现 CDB 类,代码如下

#include  " ..\interface\DBsrv.h "    // 注意:接口头文件是DLL项目和客户程序共享的

//  Database object
HRESULT CDB::Read( short  nTable,  short  nRow, LPWSTR lpszData) 
{
    CStringArray 
* pTable;
    pTable
= (CStringArray * ) m_arrTables[nTable];
#ifndef UNICODE
    MultiByteToWideChar(CP_ACP, 
0 , ( * pTable)[nRow],  - 1 , lpszData,  80 );
#else
    lstrcpy (lpszData, (
* pTable)[nRow]);
#endif
    
return  NO_ERROR;
}

HRESULT CDB::Write(
short  nTable,  short  nRow, LPCWSTR lpszData)
{
    CStringArray 
* pTable;
    pTable
= (CStringArray * ) m_arrTables[nTable];
#ifdef UNICODE
    pTable
-> SetAtGrow(nRow, lpszData);
#else
    
char  szData[ 80 ];
    WideCharToMultiByte(CP_ACP, 
0 , lpszData,  - 1 , szData,  80 , NULL, NULL);
    pTable
-> SetAtGrow(nRow, szData);
#endif
    
return  NO_ERROR;
}
HRESULT CDB::Create(
short   & nTable, LPCWSTR lpszName)
{
    CStringArray 
* pTable = new  CStringArray;
    nTable
= m_arrTables.Add(pTable);
#ifdef UNICODE
     m_arrNames.SetAtGrow(nTable, lpszName);
#else
    
char  szName[ 80 ];
    WideCharToMultiByte(CP_ACP, 
0 , lpszName,  - 1 , szName,  80 , NULL, NULL);
    m_arrNames.SetAtGrow(nTable, szName);
#endif
    
return  NO_ERROR;
}

HRESULT CDB::Delete(
short  nTable) 
{
    CStringArray 
* pTable;
    pTable
= (CStringArray * ) m_arrTables[nTable];
    delete pTable;
    m_arrTables[nTable]
= NULL;
    m_arrNames[nTable]
= "" ;
    
if  (nTable == m_arrTables.GetSize() - 1
    {
        m_arrTables.RemoveAt(nTable);
        m_arrNames.RemoveAt(nTable);
    }
    
return  NO_ERROR;
}

HRESULT CDB::GetNumTables(
short   & nNumTables) 
{
    nNumTables
= m_arrTables.GetSize();
    
return  NOERROR;
}

HRESULT CDB::GetTableName(
short  nTable, LPWSTR lpszName) 
{
#ifndef UNICODE
    MultiByteToWideChar(CP_ACP, 
0 , m_arrNames[nTable],  - 1 , lpszName,  80 );
#else
    lstrcpy(lpszName, m_arrNames[nTable]);
#endif
    
return  NO_ERROR;
}

HRESULT CDB::GetNumRows(
short  nTable,  short   & nRows) 
{
    CStringArray 
* pTable;
    pTable
= (CStringArray * ) m_arrTables[nTable];
    
return  pTable -> GetSize();
}

ULONG CDB::Release() 
{
    delete 
this ;
    
return   0 ;
}

CDB::
~ CDB() 
{
  
short  nNumTables;
  
for  (GetNumTables(nNumTables);nNumTables > 0 ; GetNumTables(nNumTables)) 
  {
      Delete(nNumTables
- 1 );
  }
}

在另一个 DBSrvFact .cpp 文件中实现类工厂:

#include  " ..\interface\dbsrv.h "   // 注意:接口头文件是DLL项目和客户程序共享的

//  Create a new database object and return a pointer to it
HRESULT CDBSrvFactory::CreateDB(CDB **  ppvDBObject) 
{
  
* ppvDBObject = new  CDB;
  
return  NO_ERROR;
}

ULONG CDBSrvFactory::Release() 
{
    delete 
this ;
    
return   0 ;
}

HRESULT DEF_EXPORT DllGetClassFactoryObject(CDBSrvFactory 
**  ppObject) 
{
    
* ppObject = new  CDBSrvFactory;
    
return  NO_ERROR;
}

编译后生成引入库文件 (.LIB) 和动态链接库文件 (.DLL)

     三)修改客户程序

      1 )由于前面我们已经为 CDB 类添加了删除自己的函数 Release(), 因此在 CDBDoc 的析构函数中修改我们使用的 CDB 对象的删除方式如下:

CDBDoc:: ~ CDBDoc()
{
    
if  (m_pDB) 
    {
        m_pDB
-> Release(); // 不再是delete m_pDB
        m_pDB = NULL;
    }
}

 2)创建CDB类对象的方式改变了,我们通过对应的类工厂对象来创建CDB对象,而不再是直接地new一个CDB对象出来了。
BOOL CDBDoc::OnNewDocument()
{
    
if (!CDocument::OnNewDocument())
        
return FALSE;
    
//新建数据库对象
    
//m_pDB=new CDB;
    CDBSrvFactory *pDBFactory=NULL; //对应的类工厂对象
    DllGetClassFactoryObject(&pDBFactory); //获取对应的类工厂
    pDBFactory->CreateDB(&m_pDB); //由类工厂负责创建所请求的对象
    pDBFactory->Release(); // do not need the factory anymore
    
// 初始化数据成员变量
    …
}

3)将传入/传出DLL中的参数标准化为Unicode编码。若不是以Unicode方式编译(##ifndefUNICODE,则使用MultiByteToWideChar将输出参数由ASCII转换为Unicode,用WideCharToMultiByte将输入参数由Unicode转换为ASCII

 

void CDBDoc::OnReadTable() 
{
#ifdef UNICODE
    m_pDB
->Read(m_nTable, 0, m_csData.GetBuffer(80));
#else
    WCHAR szuData[
80];
    m_pDB
->Read(m_nTable, 0, szuData);
    WideCharToMultiByte(CP_ACP, 
0, szuData, -1, m_csData.GetBuffer(80), 80, NULL, NULL);
#endif
    m_csData.ReleaseBuffer();
    UpdateAllViews(NULL);
}

void CDBDoc::OnWriteTable() 
{
    m_nCount
++;
    CString csText;
    csText.Format(_T(
"Test data #%d in table %d, row 0!"), m_nCount, (int) m_nTable);
#ifdef UNICODE
    m_pDB
->Write(m_nTable, 0, csText);
#else
    WCHAR szuText[
80]; // special treatment for ASCII client
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, csText, -1, szuText, sizeof(szuText));
    m_pDB
->Write(m_nTable, 0, szuText);
#endif
}

4)连接DLL,创建客户程序。现在我们使用DLL,因此不再需要被重用对象的源代码,那么先将DBsrv.cppDBsrv.h两个文件从工程中删除。与DLL连接的方式采用隐式链接:在链接器à输入à附加依赖项中输入:.."object"Debug"db.lib。最后将DB.dll拷贝到客户程序目录下,运行客户程序。

     Ok,万里长征迈出了第一步

一,使用抽象基类重用C++对象

在上一篇文章《COM组件开发实践(四)---From C++ to COM :Part 1》中,我们已经将要复用的C++对象封装到DLL中了,对象的声明和实现已经实现了剥离,但还有问题:对象的私有成员(如我们示例中CDB类的数组变量m_arrTables)还是被客户看得一清二楚,即使客户没办法去访问它们;若对象改变了它的数据成员的大小,则所有的客户程序必须重新编译。

而实际上,客户需要的仅仅是对象的成员函数的地址,因此使用抽象基类可以很好地满足以上需求,客户就不会包含对象的私有数据成员,就算对象改变了数据成员的大小,客户程序也不用重新编译。

1.      修改接口文件

首先将接口都改成抽象基类,这是客户程序唯一所需要的代码。具体包括下面几步:1)将CDBCDBSrvFactory的函数都改成纯虚函数。2)删除数据成员。3)删除所有成员函数的引出标志。4)将CDB改成IDB(表示DB的接口)CDBSrvFactory改成IDBSrvFactory(表示DB类工厂的接口)

typedef long HRESULT;
#define DEF_EXPORT __declspec(dllexport)
class IDB 
{
    
// Interfaces
public:
    
// Interface for data access
    virtual HRESULT Read(short nTable, short nRow, LPWSTR lpszData) =0;
    
virtual HRESULT Write(short nTable, short nRow, LPCWSTR lpszData) =0;
    
// Interface for database management
    virtual HRESULT Create(short &nTable, LPCWSTR lpszName) =0;
    
virtual HRESULT Delete(short nTable) =0;
    
// Interfase para obtenber informacion sobre la base de datos
    virtual HRESULT GetNumTables(short &nNumTables) =0;
    
virtual HRESULT GetTableName(short nTable, LPWSTR lpszName) =0;
    
virtual HRESULT GetNumRows(short nTable, short &nRows) =0;
    
virtual ULONG Release() =0;
};
class IDBSrvFactory 
{
    
// Interface
public:
    
virtual HRESULT CreateDB(IDB** ppObject) =0;
    
virtual ULONG   Release() =0;
};

HRESULT DEF_EXPORT DllGetClassFactoryObject(IDBSrvFactory 
** ppObject);

2.修改对象程序

DLL项目中,我们实现为这个抽象接口声明并实现具体的子类,让CDBIDB派生,CDBSrvFactoryIDBSrvFactory派生,并且把类工厂CDBSrvFactoryCreateDB方法的参数由CDB**改成IDB**

#include "..\interface\dbsrv.h"
typedef 
long HRESULT;
class CDB : public IDB
{
    
// Interfaces
public:
    
// Interface for data access
    HRESULT Read(short nTable, short nRow, LPWSTR lpszData);
    HRESULT Write(
short nTable, short nRow, LPCWSTR lpszData);
    
// Interface for database management
    HRESULT Create(short &nTable, LPCWSTR lpszName);
    HRESULT Delete(
short nTable);
    
// Interfase para obtenber informacion sobre la base de datos
    HRESULT GetNumTables(short &nNumTables);
    HRESULT GetTableName(
short nTable, LPWSTR lpszName);
    HRESULT GetNumRows(
short nTable, short &nRows);
    ULONG Release(); 
//CPPTOCOM: need to free an object in the DLL, since it was allocated here
    
// Implementation
private:
    CPtrArray m_arrTables;      
// Array of pointers to CStringArray (the "database")
    CStringArray m_arrNames; // Array of table names
public:
    
~CDB();
};
class CDBSrvFactory : public IDBSrvFactory 
{
    
// Interface
public:
    HRESULT CreateDB(IDB
** ppObject);
    ULONG   Release();
};

     这两个具体子类的实现代码在此就省略不表了,参考上篇文章。

3.修改客户程序

      最后根据上面的修改,对客户程序也做相应修改:1)将CDBDoc类的数据成员类型由CDB*改成IDB*

IDB *m_pDB; // pointer to database object

  2CDBDoc::OnNewDocument函数中,将CDBSrvFactory*改成IDBSrvFactory*

BOOL CDBDoc::OnNewDocument()
{

    
//新建数据库对象
    
//m_pDB=new CDB;
    IDBSrvFactory *pDBFactory=NULL;
    DllGetClassFactoryObject(
&pDBFactory); 
    pDBFactory
->CreateDB(&m_pDB);
    pDBFactory
->Release(); // do not need the factory anymore
    
// 初始化数据成员变量

    
return TRUE;
}

     OK,最后重新编译DLL即可。可以看出,通过使用虚函数和抽象基类,这才算真正实现了面向接口编程。

 

     二,初步逼近COM--使用COM库加载C++对象

      上面的代码中声明了DLL的一个入口点DllGetClassFactoryObject,而客户程序就是通过调用这个函数,从而获取到相应的类工厂对象,再使用类工厂对象创建真正的对象的。我们这一步要尝试让客户程序不去直接调用对象的入口函数,而是让COM库为我们服务,并且要让对象使用标准的入口函数。

1,修改接口定义文件

      首先在接口定义文件中增加类ID和接口ID的声明,这两个ID在对象程序和客户程序中都会有定义,在这里只是对这两个外部变量进行说明。然后将引出函数DllGetClassFactoryObject删除,因为接下来我们将会使用标准入口函数DllGetObject,因此不需要再自己定义入口函数了。

typedef long HRESULT;
#define DEF_EXPORT __declspec(dllexport)
// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
extern const GUID CLSID_DBSAMPLE;
//{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// {30DF3431-0266-11cf-BAA6-00AA003E0EED}
extern const GUID IID_IDBSrvFactory;
//{ 0x30df3431, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
class IDB 
{
  
// Interfaces
  public:
          
// Interface for data access
        virtual HRESULT Read(short nTable, short nRow, LPWSTR lpszData) =0;
        
virtual HRESULT Write(short nTable, short nRow, LPCWSTR lpszData) =0;
        
// Interface for database management
        virtual HRESULT Create(short &nTable, LPCWSTR lpszName) =0;
        
virtual HRESULT Delete(short nTable) =0;
        
// Interfase para obtenber informacion sobre la base de datos
        virtual HRESULT GetNumTables(short &nNumTables) =0;
        
virtual HRESULT GetTableName(short nTable, LPWSTR lpszName) =0;
        
virtual HRESULT GetNumRows(short nTable, short &nRows) =0;
        
virtual ULONG Release() =0;
};
class IDBSrvFactory 
{
    
// Interface
    public:
        
virtual HRESULT CreateDB(IDB** ppObject) =0;
        
virtual ULONG   Release() =0;
};
//HRESULT DEF_EXPORT DllGetClassFactoryObject(IDBSrvFactory ** ppObject);

2,修改对象程序

      修改如下:1)为类ID和接口ID定义GUID2)将DllGetClassFactoryObject改成标准入口函数DllGetClassObject。具体代码如下:

#include "stdafx.h"
#include 
"DBsrvImp.h"
// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
static const GUID CLSID_DBSAMPLE = 
0x30df34300x2660x11cf, { 0xba0xa60x00xaa0x00x3e0xe0xed } };
// {30DF3431-0266-11cf-BAA6-00AA003E0EED}
static const GUID IID_IDBSrvFactory = 
0x30df34310x2660x11cf, { 0xba0xa60x00xaa0x00x3e0xe0xed } };
// Create a new database object and return a pointer to it
HRESULT CDBSrvFactory::CreateDB(IDB** ppvDBObject) 
{
     
*ppvDBObject=(IDB*new CDB;
     
return NO_ERROR;
}
ULONG CDBSrvFactory::Release() 
{
    delete 
this;
    
return 0;
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, 
void** ppObject) 
{
    
if (rclsid!=CLSID_DBSAMPLE) 
    {
        
return CLASS_E_CLASSNOTAVAILABLE;
    }
    
if (riid!=IID_IDBSrvFactory) 
    {
        
return E_INVALIDARG;
    }
    
*ppObject=(IDBSrvFactory*new CDBSrvFactory;
    
return NO_ERROR;
}

注:1)由于标准入口函数DllGetClassObject objbase.h文件中已经声明了,因此在我们的代码中不必再声明它。2)在stdafx.h中加入了#include<ole2.h>,并且在所有的include前加入了:#define_AFX_NO_BSTR_SUPPORT,这是因为MFC头文件中的一些定义和ole2.h中不一样。

      然后,要让客户程序访问到入口函数,我们要为创建一个模块定义DEF文件,在其中引出DllGetClassObject函数,DB.def代码如下:

EXPORTS
          
;WEP @1 RESIDENTNAME
                    DllGetClassObject

注:不能用__declspec(dllexport)来引出DllGetClassObject函数,因为在objbase.h中它的定义处已经使用了其他修饰词。

      最后,这个对象要想通过COM库创建,就必须在注册表中进行注册,但目前还没有加入自我注册部分,因此我们先对其进行手动注册吧。其实要做的事情很简单,就是把我们的DLL的路径告诉给COM库就行了。步骤如下:

1)HKEY_CLASSES_ROOT"CLSID下添加一个子键,名字就是上面定义的类ID:{30DF3430-0266-11cf-BAA6-00AA003E0EED}

2)为这个子键再添加一个子键InprocServer32,为它添加一个未命名的字符串值:类型为REG_SZ,数据为<path>"db.dll,也就是保存你的DLL的路径。

3,修改客户程序

1)在DBDoc.cpp中也加入类ID和接口ID的定义

// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
static const GUID CLSID_DBSAMPLE =
0x30df34300x2660x11cf, { 0xba0xa60x00xaa0x00x3e0xe0xed } };
// {30DF3431-0266-11cf-BAA6-00AA003E0EED}
static const GUID IID_IDBSrvFactory =
0x30df34310x2660x11cf, { 0xba0xa60x00xaa0x00x3e0xe0xed } };

2)初始化COM库。在我们使用COM库之前先对其进行初始化。

    if (FAILED(CoInitialize(NULL))) 
    {
        AfxMessageBox(_T(
"Could not initialize COM Libraries!"));
        
return FALSE;
    }

 并且在程序退出时调用CoUninitialize()

int CDBApp::ExitInstance() 
{
    CoUninitialize();
    
return CWinApp::ExitInstance();
}

     2)改为使用COM库函数来创建对象。在CDBDoc::OnNewDocument()函数中,使用COM库函数CoGetClassObject代替原来直接装载DLL的方式。

    //新建数据库对象
    
//m_pDB=new CDB;
    IDBSrvFactory *pDBFactory=NULL;
    HRESULT hRes;
    hRes
=CoGetClassObject(CLSID_DBSAMPLE, CLSCTX_SERVER, NULL, IID_IDBSrvFactory, (void**&pDBFactory);
    
if (FAILED(hRes)) 
    {
        CString csError;
        csError.Format(_T(
"Error %x creating DB Object!"), hRes);
        AfxMessageBox(csError);
        
return FALSE;
    }
    pDBFactory
->CreateDB(&m_pDB);
    pDBFactory
->Release(); // do not need the factory anymore

注:以前在客户程序中需要对应的DLL在路径下,但现在并不需要了,因为COM库会根据注册表中的信息找到DLL所在路径的。

 在上一篇文章《COM组件开发实践(五)---From C++ to COM :Part 2》中,我们进展到使用COM库加载C++对象了,这一篇中我们将真正将C++对象变成 COM对象,而在下一篇中我们将为它添加多接口支持

C++对象变成COM对象

要将一个C++对象变成一个真正的COM对象,只需要如下操作:

1)实现接口的引用计数。因此每个COM对象都需要有两个函数用于管理引用计数:

ULONG AddRef();

ULONG Release();

这两个函数不返回HRESULT,因为它们不可能失败,除非对象已经不存在,而且它们也不需要返回值,因为它们只是简单地加减引用计数。

2)对象允许实现多个接口。假如对象要为不同的客户返回不同的接口,则需要客户告诉对象它需要哪个接口。实际上前面已经运用了一种方法:DllGetClassObject函数的IID参数。如果对象有多个接口,则类工厂对象的CreateDB函数(也就是真正创建对象的函数)应该增加一个参数:接口ID,IID

      假如我们的DB对象有多个接口,一个接口用于建表,一个用于读写,一个用于获得数据库信息。我们可以让DB对象提供一个接口查询函数,

HRESULT QueryInterface(RIID riid,void** ppObj);

3)类工厂对象使用标准的IClassFactory接口。4)使用_stdcall调用约定。

5)实现DLL动态卸载。DLL的卸载由COM负责,COM查询DLL看是否还在使用,它会调用DLL的引出函数DllCanUnloadNow来判断是否可以卸载DLL.如果客户程序想让COM卸载所有未使用的库,它会调用CoFreeUnusedLibraries函数。

6)实现对象自动注册。只需要DLL实现两个引出函数DllRegisterServerDllUnregisterServer

1,修改接口文件

      1)将IDBIUnknown派生,删除Release成员函数声明,为IDB所有成员函数添加_stdcall(因为COM对象在win32下采用标准调用约定)。2)删除类工厂IDBSrvFactory声明,因为我们现在要使用标准类工厂接口IClassFactory,同时也删除IID_IDBSrvFactory的声明。3)声明外部变量IID_IDB,

typedef long HRESULT;
#define DEF_EXPORT __declspec(dllexport)
// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
extern const GUID CLSID_DBSAMPLE;
//{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// {30DF3432-0266-11cf-BAA6-00AA003E0EED}
extern const GUID IID_IDB;
//{ 0x30df3432, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
class IDB : public IUnknown 
{
  
// Interfaces
  public:
          
// Interface for data access
        virtual HRESULT _stdcall Read(short nTable, short nRow, LPWSTR lpszData) =0;
        
virtual HRESULT _stdcall Write(short nTable, short nRow, LPCWSTR lpszData) =0;
        
// Interface for database management
        virtual HRESULT _stdcall Create(short &nTable, LPCWSTR lpszName) =0;
        
virtual HRESULT _stdcall Delete(short nTable) =0;
        
// Interfase para obtenber informacion sobre la base de datos
        virtual HRESULT _stdcall GetNumTables(short &nNumTables) =0;
        
virtual HRESULT _stdcall GetTableName(short nTable, LPWSTR lpszName) =0;
        
virtual HRESULT _stdcall GetNumRows(short nTable, short &nRows) =0;
        
//virtual ULONG Release() =0;
};

2,修改对象程序

1)CDBSrvFactoryIDBSrvFactory派生改为由IClassFactory派生。将CDBSrvFactory::CreateDB改为CDBSrvFactory:: CreateInstance,并添加一个成员函数CDBSrvFactory:: LockServer。为CDBCDBSrvFactory都添加一个引用计数变量:ULONGm_dwRefCount;声明一个外部变量:externULONGg_dwRefCount;;为CDBCDBSrvFactory加上QueryInterfaceAddRefRelease三个成员函数

typedef long HRESULT;
class CDB : public IDB
{
  
// Interfaces
  public:
          
// Interface for data access
        HRESULT _stdcall Read(short nTable, short nRow, LPWSTR lpszData);
        HRESULT _stdcall Write(
short nTable, short nRow, LPCWSTR lpszData);
        
// Interface for database management
        HRESULT _stdcall Create(short &nTable, LPCWSTR lpszName);
        HRESULT _stdcall Delete(
short nTable);
        
// Interfase para obtenber informacion sobre la base de datos
        HRESULT _stdcall GetNumTables(short &nNumTables);
        HRESULT _stdcall GetTableName(
short nTable, LPWSTR lpszName);
        HRESULT _stdcall GetNumRows(
short nTable, short &nRows);
        HRESULT _stdcall QueryInterface(REFIID riid, 
void** ppObject);
        ULONG   _stdcall AddRef();
        ULONG   _stdcall Release();
  
// Implementation
  private:
        CPtrArray m_arrTables;      
// Array of pointers to CStringArray (the "database")
        CStringArray m_arrNames; // Array of table names
        ULONG m_dwRefCount;
    
public:
        CDB();
        
~CDB();
};
extern ULONG g_dwRefCount;
class CDBSrvFactory : public IClassFactory 
{
    
// Interface
    public:
        HRESULT _stdcall QueryInterface(REFIID riid, 
void** ppObject);
        ULONG   _stdcall AddRef();
        ULONG   _stdcall Release();
        HRESULT _stdcall CreateInstance(IUnknown 
*pUnkOuter, REFIID riid, void** ppObject);
        HRESULT    _stdcall LockServer(BOOL fLock);
    
// Implementation
    private:
        ULONG m_dwRefCount;
    
public:
        CDBSrvFactory();
};

2)为IID_IDB定义GUID,CDB构造函数中将m_dwRefCount初始化为0,实现CDBQueryInterfaceAddRefRelease三个成员函数。

CDB实现文件

注:头文件中成员函数的声明顺序不影响vtable中的顺序,因为vtable是按照IDB的声明顺序来定义的。

3)删除IID_IDBSrvFactory定义;定义全局变量g_dwRefCount;在构造函数中将m_dwRefCount初始化为0;实现CDBSrvFactoryQueryInterfaceAddRefRelease三个成员函数;将CDBSrvFactoryCreateDB函数修改为CreateInstance;实现CDBSrvFactory:: LockServer;添加 DllCanUnloadNow, DllUnregisterServer, DllRegisterServer三个成员函数。

CDBSrvFactory实现

4)修改DEF文件,现在我们需要在db.def中引出DllCanUnloadNowDllRegisterServerDllUnregisterServer三个函数,如下所示:

EXPORTS
          
;WEP @1 RESIDENTNAME
                    DllGetClassObject
                    DllCanUnloadNow
                    DllRegisterServer
                    DllUnregisterServer

5)创建并注册DLL,编译生成DB.dll,在命令行中运行regsvr32dll进行注册。

3,修改客户程序

1)DBDoc.cpp中定义IID_IDB,并且删除以前定义的IID_IDBSrvFactory:

// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
static const GUID CLSID_DBSAMPLE =
0x30df34300x2660x11cf, { 0xba0xa60x00xaa0x00x3e0xe0xed } };
// {30DF3432-0266-11cf-BAA6-00AA003E0EED}
static const GUID IID_IDB =
0x30df34320x2660x11cf, { 0xba0xa60x00xaa0x00x3e0xe0xed } };

     2)以前创建对象的过程使用的是IDBFactory::CreateDB来创建CDB对象的,现在改为IClassFactory:: CreateInstance:

    //新建数据库对象
    
//m_pDB=new CDB;
    
// create a database object through the exported function & class factory object
    IClassFactory *pDBFactory=NULL;
    HRESULT hRes;
    hRes
=CoGetClassObject(CLSID_DBSAMPLE, CLSCTX_SERVER, NULL, IID_IClassFactory, (void**&pDBFactory);
    
if (FAILED(hRes)) 
    {
        CString csError;
        csError.Format(_T(
"Error %x obtaining class factory for DB Object!"), hRes);
        AfxMessageBox(csError);
        
return FALSE;
    }
    hRes
=pDBFactory->CreateInstance(NULL, IID_IDB, (void**&m_pDB);
    
if (FAILED(hRes)) 
    {
        CString csError;
        csError.Format(_T(
"Error %x creating DB Object!"), hRes);
        AfxMessageBox(csError);
        
return FALSE;
    }
pDBFactory
->Release(); // do not need the factory anymore

     3)卸载不再使用的对象,通过调用CoFreeUnusedLibraries来确保任何不再使用的COM DLL对象及时卸载。

BOOL CDBApp::OnIdle(LONG lCount) 
{
    
if (CWinApp::OnIdle(lCount)) 
    {
        
return TRUE;
    }
    CoFreeUnusedLibraries();
    
return FALSE;
}

     Ok,到此为止,我们已经将一个简单的C++对象真正转变为一个COM对象了。

  最近遇到两个需求: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, 0sizeof dispparams);
    dispparams.cArgs 
= arraySize;
    dispparams.rgvarg 
= new VARIANT[dispparams.cArgs];
    
forint 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, 0sizeof 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控件类继承自这个虚基类,实现这些回调函数接口

classCMyActiveXCtrl : publicCOleControl,publicICallBack

线程基类

       为了处理线程方便,本文使用了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页面无法收到响应,就这个问题折腾了好几天,思路上看好像没什么错呀,怎么就回调不了呢?。。。

那么正确的做法应该是怎样的呢?限于本文篇幅,将在下一篇中给出解答,并放出完整源代码。

 

  在上一篇文章《COM组件开发实践(七)---多线程ActiveX控件和自动调整ActiveX控件大小()》中介绍了ActiveX控件中使用多线程的基本需求,并提出了一个简单的线程模型,但却出现了意想不到的问题,本文将尝试给出问题的一个可行的解法,并同时解决上文中提出的第二个问题

      其实解决的思路也很简单,一开始我也早就想到了的,就是使用让子线程PostMessage来发出自定义的消息来通知主线程,特定的事件已经发生了,需求主线程去响应。这不是什么了不起的想法,但我对子线程PostMessage非常恐惧,因为以前的一个项目中就是这个问题导致了内存泄露,所以这个方案一开始就被我否定了。

      遍寻解决之道不可得时,只得在csdn的论坛上发贴求教高手了,具体的讨论请参考这个帖子:

http://topic.csdn.net/u/20081226/17/9bf0ae08-c54d-4934-b1b2-91baa27ff76e.html

看到jameshooo(胡柏华)的回帖后,还是决定回到起点,尝试用PostMessage这个方案。

首先自定义两个事件,分别表示操作成功和操作失败

#define WM_OPTSUCCESS WM_APP+101 //操作成功
#define WM_OPTFAILED WM_APP+102    //操作失败

     然后回调函数中就变得非常简单,只需要post对应的事件即可。

/////////////////////
//回调函数
/////////////////////////
void CMyActiveXCtrl::OnSuccesful()
{
//操作成功
    this->PostMessage(WM_OPTSUCCESS,(WPARAM)NULL,(LPARAM)NULL);
}

void CMyActiveXCtrl::OnFailed()
{
//操作失败
    this->PostMessage(WM_OPTFAILED,(WPARAM)NULL,(LPARAM)NULL);
}

     再重载消息处理函数WindowProc,在其中调用外部的JavaScript函数或者Fire出外部页面可以响应的事件。

LRESULT CMyActiveXCtrl::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam)
{
    
switch (msg)
    {
    
case WM_OPTSUCCESS:
        {
//操作成功,通知外部页面
            CString strOnLoaded("OnLoaded");
            
this->CallJScript(strOnLoaded);
            
return 0;
        }
    
case WM_OPTFAILED:
        {
//操作失败,通知外部页面
            
//这里不写了,同上面
        }
    }
    
return   COleControl::WindowProc(msg,wParam,lParam);   
}

     在OnCreate函数中加入启动工作线程代码:


int CMyActiveXCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    
if (COleControl::OnCreate(lpCreateStruct) == -1)
        
return -1;
    m_MainDialog.Create(IDD_MAINDIALOG, 
this);
    pThread.SetICallBack(
this);//设置主线程回调函数
    pThread.Start();//启动工作线程
    return 0;
}

     重载掉OnClose函数,在其中加入关闭工作线程的代码:

void CMyActiveXCtrl::OnClose(DWORD dwSaveOption)
{
    pThread.Stop(
true);//强行关闭工作线程
    COMRELEASE(pWebBrowser);
    COMRELEASE(pHTMLDocument);
    COleControl::OnClose(dwSaveOption);
}

     到此为止,一个多线程的ActiveX控件就诞生了。这里是不会发生以前我遇到的内存泄露的,因为情况不同了,只是在回调函数中简单的post一个message,并没有new一个内存区域并将这块内存作为参数post给主线程,后面这种情况是可能会内存泄露的。

Ok,下面来考虑第二个问题,先简单介绍下具体需求:就是一个AcitveX控件会用到不同的页面中,每个页面对这个控件的需求也不同,也就要求在两个不同的页面中,控件显示的大小也不同。

      jameshooo(胡柏华)回帖说:改变控件大小要通知容器,由容器再反过来通知控件改变大小,不然没有任何效果。调用IOleInPlaceSite::OnPosRectChange即可。因此就根据这个来尝试提出一个解决方案来。

假设有两种模式的控件,一种是普通模式,如下图所示:

 

     一种是特殊模式,

 

     为了区别开两者,就考虑在web页面中通过设置参数的方式来通知ActiveX控件,对于不同的模式填充不同的对话框就可以了。我们在web页面中控件部分加入如下参数:

<PARAM NAME="IsSpecial" VALUE="TRUE">

        相应的在CMyActiveXCtrl类中加入一个变量,这里为简单起见,选择了类型为CString型,主要是为了传参数方便。

CString m_bIsSpecial;//是否是"特殊"页面

     Web页面传入的参数值在下面这个函数中读取:


void CMyActiveXCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_String(pPX,   _T(
"IsSpecial"),   m_bIsSpecial); //读取外部设置的参数
}

     为了供控件选择,这里提供了两种模式的对话框:

public:
    CMyDlgTwo m_dlgSpecial;
//特殊模式
    CMyDlgThree m_dlgCommon;//普通模式

     然后在创建和绘制对话框时,通过检测参数是否为空就知道待创建的对话框类型到底是普通还是特殊了。

int CMyActiveXCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    
if (COleControl::OnCreate(lpCreateStruct) == -1)
        
return -1;

    CRect newRc; 
    
if(m_bIsSpecial.Compare(_T(""))==0)
    {
//没设置参数,"普通“模式
        this->m_dlgCommon.Create(IDD_DIALOG3,this);
        
//设置控件的大小
        
        newRc.left 
= 0
        newRc.top 
= 0
        newRc.right 
= 200
        newRc.bottom 
= 200
    }
    
else
    {
//设置了参数,”特殊“模式
        this->m_dlgSpecial.Create(IDD_DIALOG2,this);
        
//设置控件的大小
        newRc.left = 0
        newRc.top 
= 0
        newRc.right 
= 200
        newRc.bottom 
= 200
    }
    
this->m_pInPlaceSite->OnPosRectChange(&newRc);
    
return 0;
}

void CMyActiveXCtrl::OnDraw(
            CDC
* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    
if (!pdc)
        
return;
    
if(m_bIsSpecial.Compare(_T(""))==0)
    {
//没设置参数,"普通“模式
        this->m_dlgCommon.MoveWindow(rcBounds,TRUE);
    }
    
else
    {
//设置了参数,”特殊“模式
        this->m_dlgSpecial.MoveWindow(rcBounds,TRUE);
    }
}

        这种方法对于我目前的需求刚好是满足的,但也许还有其他更好的方法,也希望有知道的能贡献出来,一起学习下。




参考资源

1 A Complete ActiveX Web Control Tutorial By David Marcionek

2. Add GIF-animation to your MFC and ATL projects with the help of CPictureEx and CPictureExWnd by Oleg Bykov, CodeProject.

 

作者:洞庭散人

出处:http://phinecos.cnblogs.com/    

posted on 2010-08-18 10:45 我风 阅读(5779) 评论(1)  编辑 收藏 引用

FeedBack:
# re: COM组件开发实践(转载)
2011-03-31 15:46 | 已逝的记忆
学习学习 (*^__^*)   回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理


<2010年8月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

常用链接

留言簿(12)

随笔分类

随笔档案

文章档案

相册

收藏夹

C++

MyFavorite

搜索

  •  

积分与排名

  • 积分 - 323970
  • 排名 - 75

最新评论

阅读排行榜

评论排行榜