1.11   容纳控件

 

1.11.1      容纳控件

利用 ATL 所支持的控件容纳,可实现容纳控件。比如, CAxDialogImpl 中的 Ax 两字就表示 ActiveX 控件,表示对话框具有容纳控件的能力。在对话框中实现容纳控件,只需在对话框资源上点击右键,从弹出菜单选择 Insert ActiveX Control ,然后弹出一个对话框,列举了系统安装的所有控件,如图 1-17 所示。

 

1-17.JPG
1-17   插入 ActiveX 控件对话框

 

插入控件后,点击控件可以在控件的属性窗口设置控件的属性。如图 1-18 所示。

 

1-18.JPG
1-18 控件属性对话框

 

在属性对话框的工具栏上点击控件事件按钮,还可以选择处理控件的事件,如图 1-19 所示。

 

1-19.JPG
1-19 选择处理的控件事件

 

容纳对话框运行显示时,控件被创建,同时根据开发阶段设置的属性初始化控件。图 1-20 显示了容纳了一个控件的对话框。

 

1-20.JPG
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.JPG
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.JPG
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.JPG
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.JPG
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.JPG
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.JPG
1-26 显示 PI 50 小数的 Web 应用程序

 

关于用 ATL Server 建立 ISAPI 应用程序,包括 Web Services 的更多信息,请参考第十三章“你好 , ATL Server ”。

 

1.13   总结

 

在本章,我们入旋风般的浏览了 ATL 向导提高的一些功能,包括一些基本的接口实现。即使有丰富的向导功能, ATL 也不是牢固的 COM 知识的替代品,这一点勿庸置疑。我们仍需要掌握如何设计、实现自定义接口。在本书的剩余部分你将看到,我们仍需要熟悉接口指针、引用计数、运行时类型发现、线程、持续化等等。 ATL 能帮助我们,当我们仍需要熟悉 COM

 

我们必须知道:向导并不能使我们亲密接触 ATL 或者 Web 应用程序开发。在本章看到的 ATL 信息的精选功能,都有不止 10 个突出的细节、扩展和缺点。尽管向导可以节省我们很多时间,它并不能完成所有的工作。它不能确保设计和实现目标满足我们的要求,那是我们的职责。