假设需求如下:底层是一个数学运算库
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.cpp和DBsrv.h两个文件从工程中删除。与DLL连接的方式采用隐式链接:在”链接器à输入à附加依赖项“中输入:.."object"Debug"db.lib。最后将DB.dll拷贝到客户程序目录下,运行客户程序。 Ok,万里长征迈出了第一步…
一,使用抽象基类重用C++对象
在上一篇文章《COM组件开发实践(四)---From C++ to COM :Part 1》中,我们已经将要复用的C++对象封装到DLL中了,对象的声明和实现已经实现了剥离,但还有问题:对象的私有成员(如我们示例中CDB类的数组变量m_arrTables)还是被客户看得一清二楚,即使客户没办法去访问它们;若对象改变了它的数据成员的大小,则所有的客户程序必须重新编译。
而实际上,客户需要的仅仅是对象的成员函数的地址,因此使用抽象基类可以很好地满足以上需求,客户就不会包含对象的私有数据成员,就算对象改变了数据成员的大小,客户程序也不用重新编译。
1. 修改接口文件
首先将接口都改成抽象基类,这是客户程序唯一所需要的代码。具体包括下面几步:1)将CDB和CDBSrvFactory的函数都改成纯虚函数。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项目中,我们实现为这个抽象接口声明并实现具体的子类,让CDB从IDB派生,CDBSrvFactory从IDBSrvFactory派生,并且把类工厂CDBSrvFactory的CreateDB方法的参数由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
2)CDBDoc::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定义GUID。2)将DllGetClassFactoryObject改成标准入口函数DllGetClassObject。具体代码如下:
#include "stdafx.h"
#include "DBsrvImp.h"
// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
static const GUID CLSID_DBSAMPLE =
{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// {30DF3431-0266-11cf-BAA6-00AA003E0EED}
static const GUID IID_IDBSrvFactory =
{ 0x30df3431, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// 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 =
{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// {30DF3431-0266-11cf-BAA6-00AA003E0EED}
static const GUID IID_IDBSrvFactory =
{ 0x30df3431, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
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实现两个引出函数DllRegisterServer和DllUnregisterServer
1,修改接口文件
1)将IDB由IUnknown派生,删除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)将CDBSrvFactory由IDBSrvFactory派生改为由IClassFactory派生。将CDBSrvFactory::CreateDB改为CDBSrvFactory:: CreateInstance,并添加一个成员函数CDBSrvFactory:: LockServer。为CDB和CDBSrvFactory都添加一个引用计数变量:ULONGm_dwRefCount;声明一个外部变量:externULONGg_dwRefCount;;为CDB和CDBSrvFactory加上QueryInterface,AddRef和Release三个成员函数
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,实现CDB的QueryInterface,AddRef,Release三个成员函数。
CDB实现文件
CDB::CDB()
{
m_dwRefCount=0;
}
HRESULT CDB::QueryInterface(REFIID riid, void** ppObject)
{
if (riid==IID_IUnknown || riid==IID_IDB)
{
*ppObject=(IDB*) this;
}
else
{
return E_NOINTERFACE;
}
AddRef();
return NO_ERROR;
}
ULONG CDB::AddRef()
{
g_dwRefCount++;
m_dwRefCount++;
return m_dwRefCount;
}
ULONG CDB::Release()
{
g_dwRefCount--;
m_dwRefCount--;
if (m_dwRefCount==0)
{
delete this;
return 0;
}
return m_dwRefCount;
}
注:头文件中成员函数的声明顺序不影响vtable中的顺序,因为vtable是按照IDB的声明顺序来定义的。
3)删除IID_IDBSrvFactory定义;定义全局变量g_dwRefCount;在构造函数中将m_dwRefCount初始化为0;实现CDBSrvFactory的QueryInterface,AddRef,Release三个成员函数;将CDBSrvFactory的CreateDB函数修改为CreateInstance;实现CDBSrvFactory:: LockServer;添加 DllCanUnloadNow, DllUnregisterServer, DllRegisterServer三个成员函数。
CDBSrvFactory实现
ULONG g_dwRefCount=0;
// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
static const GUID CLSID_DBSAMPLE =
{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// Create a new database object and return a pointer to it
HRESULT CDBSrvFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void** ppObject)
{
if (pUnkOuter!=NULL)
{
return CLASS_E_NOAGGREGATION;
}
CDB* pDB=new CDB;
if (FAILED(pDB->QueryInterface(riid, ppObject)))
{
delete pDB;
*ppObject=NULL;
return E_NOINTERFACE;
}
return NO_ERROR;
}
HRESULT CDBSrvFactory::LockServer(BOOL fLock)
{
if (fLock)
{
g_dwRefCount++;
}
else
{
g_dwRefCount--;
}
return NO_ERROR;
}
CDBSrvFactory::CDBSrvFactory()
{
m_dwRefCount=0;
}
HRESULT CDBSrvFactory::QueryInterface(REFIID riid, void** ppObject)
{
if (riid==IID_IUnknown || riid==IID_IClassFactory)
{
*ppObject=(IDB*) this;
}
else
{
return E_NOINTERFACE;
}
AddRef();
return NO_ERROR;
}
ULONG CDBSrvFactory::AddRef()
{
g_dwRefCount++;
m_dwRefCount++;
return m_dwRefCount;
}
ULONG CDBSrvFactory::Release()
{
g_dwRefCount--;
m_dwRefCount--;
if (m_dwRefCount==0)
{
delete this;
return 0;
}
return m_dwRefCount;
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppObject)
{
if (rclsid==CLSID_DBSAMPLE)
{
CDBSrvFactory *pFactory= new CDBSrvFactory;
if (FAILED(pFactory->QueryInterface(riid, ppObject)))
{
delete pFactory;
*ppObject=NULL;
return E_INVALIDARG;
}
}
else
{
// here you could check for additional CLSID's you DLL may provide
return CLASS_E_CLASSNOTAVAILABLE;
}
return NO_ERROR;
}
HRESULT _stdcall DllCanUnloadNow()
{
if (g_dwRefCount)
{
return S_FALSE;
}
else
{
return S_OK;
}
}
STDAPI DllRegisterServer(void)
{
HKEY hKeyCLSID, hKeyInproc32;
DWORD dwDisposition;
if (RegCreateKeyEx(HKEY_CLASSES_ROOT, _T("CLSID\\{30DF3430-0266-11cf-BAA6-00AA003E0EED}"), NULL, _T(""), REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKeyCLSID, &dwDisposition)!=ERROR_SUCCESS)
{
return E_UNEXPECTED;
}
if (RegSetValueEx(hKeyCLSID, _T(""), NULL, REG_SZ, (BYTE*) _T("DB Sample Server"), sizeof(_T("DB Sample Server")))!=ERROR_SUCCESS)
{
RegCloseKey(hKeyCLSID);
return E_UNEXPECTED;
}
if (RegCreateKeyEx(hKeyCLSID, _T("InprocServer32"), NULL, _T(""), REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKeyInproc32, &dwDisposition)!=ERROR_SUCCESS)
{
RegCloseKey(hKeyCLSID);
return E_UNEXPECTED;
}
HMODULE hModule=GetModuleHandle(_T("DB.DLL"));
if (!hModule)
{
RegCloseKey(hKeyInproc32);
RegCloseKey(hKeyCLSID);
return E_UNEXPECTED;
}
TCHAR szName[MAX_PATH+1];
if (GetModuleFileName(hModule, szName, sizeof(szName))==0)
{
RegCloseKey(hKeyInproc32);
RegCloseKey(hKeyCLSID);
return E_UNEXPECTED;
}
if (RegSetValueEx(hKeyInproc32, _T(""), NULL, REG_SZ, (BYTE*) szName, sizeof(TCHAR)*(lstrlen(szName)+1))!=ERROR_SUCCESS)
{
RegCloseKey(hKeyInproc32);
RegCloseKey(hKeyCLSID);
return E_UNEXPECTED;
}
RegCloseKey(hKeyInproc32);
RegCloseKey(hKeyCLSID);
return NOERROR;
}
STDAPI DllUnregisterServer(void)
{
if (RegDeleteKey(HKEY_CLASSES_ROOT, _T("CLSID\\{30DF3430-0266-11cf-BAA6-00AA003E0EED}\\InprocServer32"))!=ERROR_SUCCESS)
{
return E_UNEXPECTED;
}
if (RegDeleteKey(HKEY_CLASSES_ROOT, _T("CLSID\\{30DF3430-0266-11cf-BAA6-00AA003E0EED}"))!=ERROR_SUCCESS)
{
return E_UNEXPECTED;
}
return NOERROR;
}
4)修改DEF文件,现在我们需要在db.def中引出DllCanUnloadNow,DllRegisterServer,DllUnregisterServer三个函数,如下所示:
EXPORTS
;WEP @1 RESIDENTNAME
DllGetClassObject
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer
5)创建并注册DLL,编译生成DB.dll,在命令行中运行regsvr32对dll进行注册。
3,修改客户程序
1)在DBDoc.cpp中定义IID_IDB,并且删除以前定义的IID_IDBSrvFactory:
// {30DF3430-0266-11cf-BAA6-00AA003E0EED}
static const GUID CLSID_DBSAMPLE =
{ 0x30df3430, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
// {30DF3432-0266-11cf-BAA6-00AA003E0EED}
static const GUID IID_IDB =
{ 0x30df3432, 0x266, 0x11cf, { 0xba, 0xa6, 0x0, 0xaa, 0x0, 0x3e, 0xe, 0xed } };
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 ,选择“添加事件”,这种方式就不赘述了。
第二种方式是利用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 = 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控件类继承自这个虚基类,实现这些回调函数接口
classCMyActiveXCtrl : publicCOleControl,publicICallBack
线程基类
为了处理线程方便,本文使用了CodeProject上《TrafficWatcher》这篇文章中的一个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);
}
}
这种方法对于我目前的需求刚好是满足的,但也许还有其他更好的方法,也希望有知道的能贡献出来,一起学习下。
参考资源