1.11
容纳控件
1.11.1
容纳控件
利用
ATL
所支持的控件容纳,可实现容纳控件。比如,
CAxDialogImpl
中的
Ax
两字就表示
ActiveX
控件,表示对话框具有容纳控件的能力。在对话框中实现容纳控件,只需在对话框资源上点击右键,从弹出菜单选择
Insert ActiveX Control
,然后弹出一个对话框,列举了系统安装的所有控件,如图
1-17
所示。
图
1-17
插入
ActiveX
控件对话框
插入控件后,点击控件可以在控件的属性窗口设置控件的属性。如图
1-18
所示。
图
1-18
控件属性对话框
在属性对话框的工具栏上点击控件事件按钮,还可以选择处理控件的事件,如图
1-19
所示。
图
1-19
选择处理的控件事件
容纳对话框运行显示时,控件被创建,同时根据开发阶段设置的属性初始化控件。图
1-20
显示了容纳了一个控件的对话框。
图
1-20
容纳
COM
控件的对话框
ATL
不但提供了对话框的容纳控件功能,其他窗口也同样支持:声明为对话框资源的
UI
控件(称为复合控件);声明为
HTML
资源的
UI
控件(称为
HTML
控件)。关于控件容器的更多信息请参考第十二章“控件容器”。
1.11.2 C++ COM
客户端
至少在理论上,
COM
与
C++
是一致的。一个
COM
接口直接映射为一个
C++
的抽象类。使用
COM
对象,仅仅需要使用
MIDL
编译器运行
IDL
文件,就可以生成一个头文件,里面包含有所有需要的信息。
所有的这一切都运行正常,直到
VB
团队询问他们也是否可以使用
COM
技术。
VB
开发人员通常不知道,也不想知道
C++
语言。
IDL
也是一个与
C++
传统语言相似的语言,其中也支持许多
C/C++
的特性(比如数组和指针)。
VB
需要一种方法来存储这些
COM
对象的类型信息,以方便
VB
开发人员使用和理解它们。
因此类型库诞生了(也称为
typelib
)。类型库存储
COM
对象的信息:对象支持的接口
classid
;接口的方法;
IDL
文件中看到的所有信息,等等
(
除了一些不合宜的、大部分必须等同
C
数组处理内容
)
。
COM
系统包含一系列可以根据
typelib
内容编程访问的
COM
对象。最好的就是类型库可以直接嵌入到
DLL
或者
EXE
,因此不必担心类型库信息的丢失。
现在,当一些
COM
组件没有打包
IDL
文件时,类型库对
VB
开发人员具有非常的意义;类型库包含有使用组件需要的所有信息。现在只缺少一样:如何在
C++
语言中使用类型库?
C++
语言并不能理解类型库,它需要头文件。这就引发了一系列的问题。从
Visual Studio 6
开始,微软扩展了编译器,使你可以像使用头文件一样使用类型库。这种扩展使通过语句
#import
实现的。
#import
可以像
#include
一样使用,一般使用形式如下:
#import “pisvr.dll” <options>
#import
语句根据选项的不同,生成一个或者两个
C++
头文件。这些头文件的扩展名是
.tlh
(用于类型库头文件)和
.tli
(用于类型库内联)。都生成在工程的输出目录(
debug
版默认在
Debug
目录,
release
版默认在
Release
目录)。
#import
语句提供了很多的选项来控制生成的文件内容。可以在
Visual Studio
文档中查看所有的选项列表。此处只介绍一些比较常用的控制项。
选项
no_namespace
告诉编译器我们不希望生成的文件内容放入一个
C++
名字空间内。默认情况下,生成文件的内容被放入按类型库命名的
C++
名字空间内。
选项
name_guids
告诉编译器我们希望类型库中的
GUID
都有一个命名符号。默认情况下,因为名字
CLSID_PISvr
没有定义,下面的语句不能被编译:
::CoCreateInstance( CLSID_PISvr, … );
相反,应该使用下面的语句形式:
::CoCreateInstance( __uuidof ( PISvr), … );
我们同样需要使用
__uuidof()
来获取接口的
IID
。
选项
raw_interfaces_only
应该是最复杂的。默认情况下,当
#import
生成头文件时,它不仅仅是生成接口类定义。实际上,生成包装类使得
COM
接口尽可能便于使用。比如,考虑下面的接口定义:
interface ICalcPi : IDispatch {
[propget, id(1), helpstring("property Digits")]
HRESULT Digits([out, retval] LONG* pVal);
[propput, id(1), helpstring("property Digits")]
HRESULT Digits([in] LONG newVal);
[id(2), helpstring("method CalcPi")]
HRESULT CalcPi([out,retval] BSTR* pbstrPi);
};
通常情况下,可以如下使用这个接口:
HRESULT DoStuff( long nDigits, ICalcPi *pCalc ) {
HRESULT hr = pCalc->put_Digits( nDigits );
if( FAILED( hr ) ) return hr;
BSTR bstrResult;
hr = pCalc->CalcPi( &bstrResult );
if( FAILED( hr ) ) return hr;
std::cout << "PI to " << nDigits << " digits is "
<< CW2A( bstrResult );
::SysFreeString( bstrResult );
return S_OK;
}
另外一种方法是使用
#import
语句,可以如下使用接口:
void DoStuff( long nDigits, ICalcPiPtr spCalc ) {
spCalc->Digits = nDigits;
_bstr_t bstrResults = spCalc->CalcPi();
std::cout << "PI to " << spCalc->Digits << " digits is "
<< ( char * )bstrResults;
}
ICalcPiPtr
类型是一种智能指针,它由
_com_ptr_t
类用
typedef
定义得到。这个类本身并不属于
ATL
,而是直接属于
COM
编译器的扩展部分,定义在系统的
comdef.h
头文件中(封装类使用的一些其他类型也同属此文件)。智能指针自动管理引用计数,
_bstr_t
类型管理
BSTR
的内存(第二章的“字符串和文本”讨论)。
包装类中最值得注意的就是后面的
HRESULT
试验。作为替换,包装类把所有的
HRESULT
错误都翻译为
C++
异常(更精确的
_com_error
类)。这样就允许生成的代码用方法的
[retval]
变量作为实际的返回值,排除了很多的临时变量和输出参数。
包装类可以大大的简化编写
COM
客户端,当然他们也有缺点(
downside
)。最大的缺点是需要使用
C++
异常。在一些工程中我们不愿意为使用异常处理而带来的效率代价,抛出异常就意味着要求开发人员在安全处理异常时必须非常小心。
对
ATL
开发人员来说,包装类的另一个缺点是
ATL
对
COM
接口(参考第三章“
ATL
智能类型”)和
BSTR
(参考第二章)。
ATL
包装类比
comdef.h
文件所定义的功能更好已是无可争辩。比如,我们可以偶然地调用
ICalcPiPtr
的
Release
方法,但是如果使用
ATL
包装类,调用将产生编译错误。
默认情况下,使用
#import
可以生成这些包装类。如果决定不使用它们,或者因为某些原因不能编译(我们已经知道,在处理一些复杂、生疏的类型库,至少有一个谦虚的程序员偶然遇到这种编译问题,),我们可以关闭这些包装类,而使用
raw_interfaces_only
选项仅仅得到接口的直接定义。
1.12
ATL Server
的
Web
工程
毫无疑问,
ATL
库最近所添加的最激动人心的就是:一组称为术语
ATL Server
的类和工具集合。
ATL8.0
比
ATL3.0
大小增加近四倍,其中
ATL Server
占据了几乎所有的增长空间。这些扩展类库对建立
WEB
应用程序、
XML Web Servers
提供了非常全面的支持。虽然传统的
ASP
和
ASP.NET
平台提供的基于
WEB
开发的易用框架具有很强的吸引力,但是仍然有很多应用程序开发人员在编写应用程序需要利用原始的
ISAPI
编程,以获得最底层的控制和最大的效率。
ATL Server
设计用来提供和
ISAPI
相近的性能和控制力,以及和
ASP
一样的生产力。最后,
ATL Server
也采用之前的设计模式,它使得
ATL
开发更方便,如过去一样的高效:名字短小、快速、弹性的代码。
VS
提供出色的向导来支持建立
WEB
应用程序和服务。实际上,纵览
ATL Server
工程提供的大量可用选项,能帮助我们非常深刻的理解其结构,也是了解其支持提供功能的决好机会。
VS
提供了一个向导帮助我们用
ATL Server
建立
Web
应用程序。从新建工程对话框的
Visual C++
文件夹下选择
ATL Server
工程可以打开此向导。
图
1-21
所示的工程设置页显示了生成、部署我们的
WEB
应用程序所选项的选项。
图
1-21 ATL Server
工程的工程设置
默认情况下,
ATL Servre
在解决方案中生成两个工程:一个
Web
应用程序
DLL
和一个
ISAPI
扩展
DLL
。
ISAPI
扩展
DLL
被加载到
IIS
进程中(
inerinfo.exe
),逻辑结构上位于
IIS
和
Web
应用程序
DLL
之间。尽管
ISAPI
扩展可以自己处理
HTTP
请求,更普通的做法是让其提供通用的基础结构服务,比如线程池和缓冲,而让
Web
应用程序
DLL
提供真正的
HTTP
响应逻辑。
ATL Server
工程向导生成一个
ISAPI
扩展实现了
Web
应用程序调用处理函数中的特殊函数通信。图
1-22
描述了这种关系。
图
1-22
基本的
ISAPI
结构
在图
1-21
的工程设置对话框中,
Generate Combined DLL
选择框允许我们把所有的内容都合成到一个
DLL
当中。当
ISAPI
扩展不打算在其他的
Web
应用程序使用时,选择它比较合适。相反如果不选择它,开发人员就可以创建特殊的
ISAPI
扩展,利用自定义线程池、高速的缓存调整机制,提高
ATL Server
的扩展性特征。这些
ISAPI
扩展很可能会在多个
Web
应用程序之间交互运用。而且,让
ISAPI
扩展保存在单独的
DLL
当中,在我们向
Web
应用程序添加处理函数的时候有更大的弹性,不需要重新启动
Web
服务(稍后讨论处理类)。在我们的第一个
Web
应用程序中没有选中此项,让
VS
生成一个单独的
DLL
。
Deployment Support
选择框可以启用
VS
网页部署工具。选中此选项后,
Visual Studio
编译进程会自动执行一些步骤,以适当的部署我们的
WEB
应用程序使它利用
IIS
提供的服务。稍后就会看到这种集成部署的功能使多么的方便,
图
1-23
所示的
Server Options
选项中,可以选择各种
Web
应用程序效率相关的选项。支持多种缓存类型,包括支持任意的二进制数据(
Blob
缓存),文件缓存,数据库连接缓存(数据源缓存)。此外,高效性站点是依赖于健壮的
Session
状态管理。
ATL Server
提供了两种机制持续化
Session
状态。
OLE DB-backed session-state services
按钮支持把
Session
状态持续到数据库(或者其他的
OLE DB
数据源),此选项对于运行在
Web Farms
上的应用程序非常有用。
图
1-23 ATL Server
工程的
Server Options
页
图
1-24
显示在
Application Options
页可用的选择项。
Validation Support
项生成一些代码对客户的
HTTP
请求的项目进行验证,比如请求参数和表单变量。
Stencil Processing Support
生成框架代码以使用称为服务响应文件(
Server response files
,
SRF
)的
HTML
代码模板。这些文本文件(也称为模板)以
.srf
为扩展名,含有带特殊替代标签的静态
HTML
内容,这些内容经过我们的代码处理后可以在运行时生成动态内容。启用
Stencil Processin
后,向导允许我们选择响应的适当地点和代码页。
Create as Web Service
会在后续章节做进一步的讨论。因为我们现在开发的是
Web
应用程序,现在我们不选择此项。
图
1-24 ATL Server
的
Application Options
页
ATL Server
工程中可以设置的其他项都在
Developer Support Options
页,如图
1-25
所示。
Generating TODO comments
简单的提醒开发人员注意附加实现应该提供的区域。如果我们选中
Custom Assert and Trace Handling Support
,调试编译时会包含一个
CDebugReportHook
类的实例,它能大大的简化从远程机器上调试
Web
应用程序的过程。
图
1-25 ATL Server
的
Developer Support Options
页
点击图
1
-
25
的
Finish
按钮,向导会生成一个解决方案,其中包含两个工程:一个是
Web
应用程序
DLL
(名称与我们在
New Project
对话框中输入的工程名一样);一个是
ISAPI
扩展(名称是工程名加上
Isapi
)。我们先看看在
ISAPI
扩展工程中生成的代码。生成的
ISAPI
扩展
.cpp
文件内容如下:
class CPiSvrWebAppModule :
public CAtlDllModuleT<CPiSvrWebAppModule> {
public:
};
CPiSvrWebAppModule _AtlModule;
typedef CIsapiExtension<> ExtensionType;
// The ATL Server ISAPI extension
ExtensionType theExtension;
// Delegate ISAPI exports to theExtension
extern "C"
DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) {
return theExtension.HttpExtensionProc(lpECB);
}
extern "C"
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) {
return theExtension.GetExtensionVersion(pVer);
}
extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) {
return theExtension.TerminateExtension(dwFlags);
}
// DLL Entry Point
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason,
LPVOID lpReserved) {
hInstance;
return _AtlModule.DllMain(dwReason, lpReserved);
}
因为
ISAPI
扩展使用了
ATL
的对象创建功能,它也需要一个
ATL
模块对象。同样,在其生成的代码中也实现了三个非常有名的入口点:
HttpExtensionProc
,
GetExtensionVersion
和
TerminateExtension
。
IIS
正是用它们与
ISAPI
扩展进行通信,处理
HTTP
请求信息。这些实现都被简单的委派到
CIsapiExtension
的全局实例,其定义如下:
template <
class ThreadPoolClass=CThreadPool<CIsapiWorker>,
class CRequestStatClass=CNoRequestStats,
class HttpUserErrorTextProvider=CDefaultErrorProvider,
class WorkerThreadTraits=DefaultThreadTraits,
class CPageCacheStats=CNoStatClass,
class CStencilCacheStats=CNoStatClass
>
class CIsapiExtension :
public IServiceProvider,
public IIsapiExtension,
public IRequestStats
{... }>
此类提供了实现
ISAPI
扩展的样板函数。类中的模板参数提供了某些功能的插件实现,比如线程池管理、错误报告和静态缓冲。在类的
.CPP
文件中,替换为我们自己从
CIsapiExtension
派生的类作为模板参数,这样就可以高度自定义
ISAPI
扩展的行为。具体的实现技术在第十三章“你好,
ATL Server
”讲述。
ISAPI
扩展的默认实现对现在的演示目的已经比较适用。
大多数的编码都是在
Web
应用程序工程中进行的。向导为我们生成一个
SRF
文件框架并加入到工程中。集成到
VS
中的
HTML
编辑器使我们能非常方便的查看、操纵文件内容。
<html>
{{ handler PiSvrWebApp.dll/Default }}
<head>
</head>
<body>
This is a test: {{Hello}}<br>
</body>
</html>
在双大括号之间的命令项将被传递给模板处理器。
{{handler}}
命令指定了响应类的宿主
DLL
名称,而类用来处理出现在
SRF
文件标签替换。其中的
/Default
参数能确保在处理标签替换时使用默认的请求处理类。一般来说,应用程序
DLL
可以包含多个处理
SRF
命令的处理类,甚至这些类可以存在于多个
DLL
中。我们在单个的
DLL
中仅仅使用一个处理类,因此流向处理类的所有命令都将被路由到同一处理类。在早期向导生成的框架中,
{{Hello}}
标签将传递到一个处理类,被类实现方法中生成的
HTML
所替换。
在我们的应用程序
DLL
中,
ATL Server
通过几个宏把
SRF
文件的命名映射到处理类。向导生成的
<projectname>.h
定义中,说明了这些宏是怎样使用的:
class CPiSvrWebAppHandler
: public CRequestHandlerT<CPiSvrWebAppHandler>
{
public:
BEGIN_REPLACEMENT_METHOD_MAP(CPiSvrWebAppHandler)
REPLACEMENT_METHOD_ENTRY("Hello", OnHello)
END_REPLACEMENT_METHOD_MAP()
HTTP_CODE ValidateAndExchange() {
// Set the content-type
m_HttpResponse.SetContentType("text/html");
return HTTP_SUCCESS;
}
protected:
HTTP_CODE OnHello(void) {
m_HttpResponse << "Hello World!";
return HTTP_SUCCESS;
}
};
基类
CRequestHandlerT
提供了一个请求处理类的实现。用
REPLACEMENT_METHOD_MAP
把
SRF
文件中的字符串映射到中适当的处理函数。
在处理器
DLL
的
.CPP
文件,除了请求处理类本身,还有一些其他分全局宏:
BEGIN_HANDLER_MAP()
HANDLER_ENTRY("Default", CPiSvrWebAppHandler)
END_HANDLER_MAP()
HANDLER_MAP
宏被用来判断使用哪个类处理带特殊名称的替换。在这种情况下,
”Default”
字符串,同
SRF
文件中的处理标签一样,被映射到
CPiSvrWebAppHandler
类。当在
SRF
文件中遇到
{{Hello}}
标签时,
OnHello
方法被调用(通过
REPLACEMENT_METHOD_MAP
)。它用声明在
CRequestHandlerT
中的一个
CHttpReponse
成员变量实例去生成标签的替换代码。
让我们修改向导生成的代码,以根据
HTTP
请求字符串中指定的小数位数显示
PI
结果。首先,把
SRF
文件按照如下修改:
<html>
{{ handler PiSvrWebApp.dll/Default }}
<head>
</head>
<body>
PI = {{Pi}}<br>
</body>
</html>
然后,我们添加一个称为
OnPi
的替换方法到处理类,再用
[tag_name]
属性把此方法与
{{Pi}}
替换标签关联起来。在
OnPi
方法的实现中,我们从查询字符串取得请求的小数位数。存储在
m_HttpRequest
成员变量中的
CHttpRequest
类暴露一个
CHttpRequestParams
实例。此类提供一个简单的
Lookup
方法从查询字符串取得单独的查询参数,作为名称值对。因此处理类似下面的请求就非常简单:
http://localhost/PiSvrWebApp/PiSvrWebApp.srf?digits=6
当我们编译解决方案时,
VS
根据我们的行为执行一些方便的任务。因此它是
Web
应用程序,不能简单的把代码编译入
DLL
就算结束。应用程序必须被适当的部署到
Web
服务器上,并在
IIS
注册。我们需要创建一个虚拟目录,指定一个合适的隔离处理级别,把
.srf
后缀的文件映射到我们的
ISAPI
扩展
DLL
。回想一下,创建工程的时候,我们在
ATL Server
工程向导的
Project Settings
页选择了
deployment support
项,参考前图
1-21
。因此,
VS
会自动调用工具
VCDeploy.exe
为我们执行所有的部署步骤。简单的用普通方式编译解决方案,把我们的应用程序
DLL
、
ISAPI
扩展
DLL
、
SRF
文件都放到默认
Web
站点下的一个目录。通常是位于目录
<drive>:\inetpub\wwwroot\<projectName>
。
VS
使用我们的
Web
应用程序工程名称作为虚拟目录名称,因此浏览
http://localhost/PiSvrWebApp/PiSvrWebApp.srf?digits=50将产生图1-26
所示的结果:
图
1-26
显示
PI
后
50
小数的
Web
应用程序
关于用
ATL Server
建立
ISAPI
应用程序,包括
Web Services
的更多信息,请参考第十三章“你好
, ATL Server
”。
1.13
总结
在本章,我们入旋风般的浏览了
ATL
向导提高的一些功能,包括一些基本的接口实现。即使有丰富的向导功能,
ATL
也不是牢固的
COM
知识的替代品,这一点勿庸置疑。我们仍需要掌握如何设计、实现自定义接口。在本书的剩余部分你将看到,我们仍需要熟悉接口指针、引用计数、运行时类型发现、线程、持续化等等。
ATL
能帮助我们,当我们仍需要熟悉
COM
。
我们必须知道:向导并不能使我们亲密接触
ATL
或者
Web
应用程序开发。在本章看到的
ATL
信息的精选功能,都有不止
10
个突出的细节、扩展和缺点。尽管向导可以节省我们很多时间,它并不能完成所有的工作。它不能确保设计和实现目标满足我们的要求,那是我们的职责。