WTL 详细介绍

转自:WTL for MFC Programmers, Part I - ATL GUI Classes


在你开始使用WTL或着在本文章的讨论区张贴消息之前,我想请你先阅读下面的材料。

你需要开发平台SDK(Platform SDK)。你要使用WTL不能没有它,你可以使用 在线升级安装开发平台SDK,也可以 下载全部文件后在本地安装。在使用之前要将SDK的包含文件(.h头文件)和库文件(.Lib文件)路径添加到VC的搜索目录,SDK有现成的工具完成这个工作,这个工具位于开发平台SDK程序组的“Visual Studio Registration”文件夹里。

你需要安装 WTL。你可以从微软的网站上 下载WTL的7.0版,在安装之前可以先查看“ Introduction to WTL - Part 1”和“ Easy installation of WTL”这两篇文章,了解一下所要安装的文件的信息,虽然现在这些文章有些过时,但还是可以提供很多有用的信息。有一件我认为不该在本篇文章中提到的事是告诉VC如何搜索WTL的包含文件路径,如果你用的VC6,用鼠标点击 Tools\Options,转到Directories标签页,在显示路径的列表框中选择Include Files,然后将WTL的包含文件的存放路径添加到包含文件搜索路径列表中。

你需要了解MFC。很好地了解MFC将有助于你理解后面提到的有关消息映射的宏并能够编辑那些标有“不要编辑(DO NOT EDIT)”的代码而不会出现问题。

你需要清楚地知道如何使用Win32 API编程。如果你是直接从MFC开始学习Windows编程,没有学过API级别的消息处理方式,那很不幸你会在使用WTL时遇到麻烦。如果不了解Windows消息中WPARAM参数和LPARAM参数的意义,应该明白需要读一些这方面的文章(在CodeProject有大量的此类文章)。

你需要知道 C++ 模板的语法,你可以到 VC Forum FAQ相关的连接寻求答案。

我只是讨论了一些涵盖VC 6的特点,不过据我了解所有的程序都可以在VC 7上使用。由于我不使用VC 7,我无法对那些在VC 7中出现的问题提供帮助,不过你还是可以放心的在此张贴你的问题,因为其他的人可能会帮助你。

对本系列文章的总体介绍

WTL 具有两面性,确实是这样的。它没有MFC的界面(GUI)类库那样功能强大,但是能够生成很小的可执行文件。如果你象我一样使用MFC进行界面编程,你会觉得MFC提供的界面控件封装使用起来非常舒服,更不用说MFC内置的消息处理机制。当然,如果你也象我一样不希望自己的程序仅仅因为使用了MFC的框架就增加几百K的大小的话,WTL就是你的选择。当然,我们还要克服一些障碍:

ATL样式的模板类初看起来有点怪异
没有类向导的支持,所以要手工处理所有的消息映射。
MSDN没有正式的文档支持,你需要到处去收集有关的文档,甚至是查看WTL的源代码。
买不到参考书籍
没有微软的官方支持
ATL/WTL的窗口与MFC的窗口有很大的不同,你所了解的有关MFC的知识并不全部适用与WTL。
从另一方面讲,WTL也有它自身的优势:

不需要学习或掌握复杂的文档/视图框架。
具有MFC的基本的界面特色,比如DDX/DDV和命令状态的自动更新功能(译者加:比如菜单的Check标记和Enable标记)。
增强了一些MFC的特性(比如更加易用的分隔窗口)。
可生成比静态链接的MFC程序更小的可执行文件(译者加:WTL的所有源代码都是静态链接到你的程序中的)。
你可以修正自己使用的WTL中的错误(BUG)而不会影响其他的应用程序(相比之下,如果你修正了有BUG的MFC/CRT动态库就可能会引起其它应用程序的崩溃。
如果你仍然需要使用MFC,MFC的窗口和ATL/WTL的窗口可以“和平共处”。(例如我工作中的一个原型就使用了了MFC的CFrameWnd,并在其内包含了WTL的CSplitterWindow,在CSplitterWindow中又使用了MFC的CDialogs -- 我并不是为了炫耀什么,只是修改了MFC的代码使之能够使用WTL的分割窗口,它比MFC的分割窗口好的多)。
在这一系列文章中,我将首先介绍ATL的窗口类,毕竟WTL是构建与ATL之上的一系列附加类,所以需要很好的了解ATL的窗口类。介绍完ATL之后我将介绍WTL的特性以并展示它是如何使界面编程变得轻而易举。

对第一章的简单介绍

WTL是个很酷的工具,在理解这一点之前需要首先介绍ATL。WTL是构建与ATL之上的一系列附加类,如果你是个严格使用MFC的程序员那么你可能没有机会接触到ATL的界面类,所以请容忍我在开始WTL之前先罗索一些别的东西,绕道来介绍一下ATL是很有必要地。

在本文的第一部分,我将给出一点ATL的背景知识,包括一些编写ATL代码必须知道的基本知识,快速的解释一些令人不知所措的ATL模板类和基本的ATL窗口类。

ATL 背景知识

ATL 和 WTL 的发展历史


“活动模板库”(Active Template Library)是一个很古怪的名字,不是吗?那些年纪大的人可能还记得它最初被称为“网络组件模板库”,这可能是它更准确的称呼,因为ATL的目的就是使编写组件对象和ActiveX控件更容易一些(ATL是在微软开发新产品ActiveX-某某的过程中开发的,那些ActiveX-某某现在被称为某某.NET)。由于ATL是为了便于编写组件对象而存在的,所以只提供了简单的界面类,相当于MFC的窗口类(CWnd)和对话框类(CDialog)。幸运的是这些类非常的灵活,能够在其基础上构建象WTL这样的附加类。

WTL现在已经是第二次修正了,最初的版本是3.1,现在的版本是7(WTL的版本号之所以这样选择是为了与ATL的版本匹配,所以不存在1和2这样的版本号)。WTL 3.1可以与VC 6和VC 7一起使用,但是在VC 7下需要定义几个预处理标号。WTL 7向下兼容WTL 3.1,并且不作任何修改就可以与VC 7一起使用,现在看来没有任何理由还使用3.1来进行新的开发工作。

ATL-style 模板

即使你能够毫不费力地阅读C++的模板类代码,仍然有两件事可能会使你有些头晕,以下面这个类的定义为例:

[cpp] view plain copy print ?
  1. class  CMyWnd : public CWindowImpl  
  2. {  
  3.     ...  
  4. };    
class  CMyWnd : public CWindowImpl
{
    ...
};  

这样作是合法的,因为C++的语法解释说即使CMyWnd类只是被部分定义,类名CMyWnd已经被列入递归继承列表,是可以使用的。将类名作为模板类的参数是因为ATL要做另一件诡秘的事情,那就是编译期间的虚函数调用机制。

如果你想要了解它是如何工作地,请看下面的例子:

[cpp] view plain copy print ?
  1. template <class T>  
  2. class B1  
  3. {  
  4. public:   
  5.     void SayHi()   
  6.     {  
  7.         T* pT = static_cast(this);   // HUH?? 我将在下面解释  
  8.    
  9.         pT->PrintClassName();  
  10.     }  
  11. protected:  
  12.     void PrintClassName() { cout << "This is B1"; }  
  13. };  
  14.    
  15. class D1 : public B1  
  16. {  
  17.     // No overridden functions at all  
  18. };  
  19.    
  20. class D2 : public B1  
  21. {  
  22. protected:  
  23.     void PrintClassName() { cout << "This is D2"; }  
  24. };  
  25.    
  26. main()  
  27. {  
  28.     D1 d1;  
  29.     D2 d2;  
  30.    
  31.     d1.SayHi();    // prints "This is B1"  
  32.     d2.SayHi();    // prints "This is D2"   
  33. }  
template 
class B1
{
public: 
    void SayHi() 
    {
        T* pT = static_cast(this);   // HUH?? 我将在下面解释
 
        pT->PrintClassName();
    }
protected:
    void PrintClassName() { cout << "This is B1"; }
};
 
class D1 : public B1
{
    // No overridden functions at all
};
 
class D2 : public B1
{
protected:
    void PrintClassName() { cout << "This is D2"; }
};
 
main()
{
    D1 d1;
    D2 d2;
 
    d1.SayHi();    // prints "This is B1"
    d2.SayHi();    // prints "This is D2"
}

这句代码static_cast(this) 就是窍门所在。它根据函数调用时的特殊处理将指向B1类型的指针this指派为D1或D2类型的指针,因为模板代码是在编译其间生成的,所以只要编译器生成正确的继承列表,这样指派就是安全的。(如果你写成:

[cpp] view plain copy print ?
  1. class D3 : public B1  
class D3 : public B1

就会有麻烦) 之所以安全是因为this对象只可能是指向D1或D2(在某些情况下)类型的对象,不会是其他的东西。注意这很像C++的多态性(polymorphism),只是SayHi()方法不是虚函数。

要解释这是如何工作的,首先看对每个SayHi()函数的调用,在第一个函数调用,对象B1被指派为D1,所以代码被解释成:

[cpp] view plain copy print ?
  1. void B1::SayHi()  
  2. {  
  3.     D1* pT = static_cast(this);  
  4.    
  5.     pT->PrintClassName();  
  6. }  
void B1::SayHi()
{
    D1* pT = static_cast(this);
 
    pT->PrintClassName();
}

由于D1没有重载PrintClassName(),所以查看基类B1,B1有PrintClassName(),所以B1的PrintClassName()被调用。

现在看第二个函数调用SayHi(),这一次对象被指派为D2类型,SayHi()被解释成:

[cpp] view plain copy print ?
  1. void B1::SayHi()  
  2. {  
  3.     D2* pT = static_cast(this);  
  4.    
  5.     pT->PrintClassName();  
  6. }  
void B1::SayHi()
{
    D2* pT = static_cast(this);
 
    pT->PrintClassName();
}

这一次,D2含有PrintClassName()方法,所以D2的PrintClassName()方法被调用。

这种技术的有利之处在于:

不需要使用指向对象的指针。
节省内存,因为不需要虚函数表。
因为没有虚函数表所以不会发生在运行时调用空指针指向的虚函数。
所有的函数调用在编译时确定(译者加:区别于C++的虚函数机制使用的动态编连),有利于编译程序对代码的优化。
节省虚函数表在这个例子中看起来无足轻重(每个虚函数只有4个字节),但是设想一下如果有15个基类,每个类含有20个方法,加起来就相当可观了。

ATL 窗口类

好了,关于ATL的背景知识已经讲的构多了,到了该正式讲ATL的时候了。ATL在设计时接口定义和实现是严格区分开的,这在窗口类的设计中是最明显的,这一点类似于COM,COM的接口定义和实现是完全分开的(或者可能有多个实现)。

ATL有一个专门为窗口设计的接口,可以做全部的窗口操作,这就是CWindow。它实际上就是对HWND操作的包装类,对几乎所有以HWND句柄为第一个参数的窗口API的进行了封装,例如:SetWindowText() 和 DestroyWindow()。CWindow类有一个公有成员m_hWnd,使你可以直接对窗口的句柄操作,CWindow还有一个操作符HWND,你可以讲CWindow对象传递给以HWND为参数的函数,但这与CWnd::GetSafeHwnd()(译者加:MFC的方法)没有任何等同之处。

CWindow 与 MFC 的CWnd类有很大的不同,创建一个CWindow对象占用很少的资源,因为只有一个数据成员,没有MFC窗口中的对象链,MFC内部维持这一个对象链,此对象链将HWND映射到CWnd对象。还有一点与MFC的CWnd类不同的是当一个CWindow对象超出了作用域,它关联的窗口并不被销毁掉,这意味着你并不需要随时记得分离你所创建的临时CWindow对象。

在ATL类中对窗口过程的实现是CWindowImpl。CWindowImpl 含有所有窗口实现代码,例如:窗口类的注册,窗口的子类化,消息映射以及基本的WindowProc()函数,可以看出这与MFC的设计有很大的不同,MFC将所有的代码都放在一个CWnd类中。

还有两个独立的类包含对话框的实现,它们分别是CDialogImpl 和 CAxDialogImpl,CDialogImpl 用于实现普通的对话框而CAxDialogImpl实现含有ActiveX控件的对话框。

定义一个窗口的实现

任何非对话框窗口都是从CWindowImpl 派生的,你的新类需要包含三件事情:

一个窗口类的定义
一个消息映射链
窗口使用的默认窗口类型,称为window traits
窗口类的定义通过DECLARE_WND_CLASS宏或DECLARE_WND_CLASS_EX宏来实现。这辆个宏定义了一个CWndClassInfo结构,这个结构封装了WNDCLASSEX结构。DECLARE_WND_CLASS宏让你指定窗口类的类名,其他参数使用默认设置,而DECLARE_WND_CLASS_EX宏还允许你指定窗口类的类型和窗口的背景颜色,你也可以用NULL作为类名,ATL会自动为你生成一个类名。

让我们开始定义一个新类,在后面的章节我会逐步的完成这个类的定义。

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl  
  2. {  
  3. public:  
  4.     DECLARE_WND_CLASS(_T("My Window Class"))  
  5. };  
class CMyWindow : public CWindowImpl
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
};

接下来是消息映射链,ATL的消息映射链比MFC的简单的多,ATL的消息映射链被展开为switch语句,switch语句正确的消息处理者并调用相应的函数。使用消息映射链的宏是BEGIN_MSG_MAP 和 END_MSG_MAP,让我们为我们的窗口添加一个空的消息映射链。

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl  
  2. {  
  3. public:  
  4.     DECLARE_WND_CLASS(_T("My Window Class"))  
  5.    
  6.     BEGIN_MSG_MAP(CMyWindow)  
  7.     END_MSG_MAP()  
  8. };  
class CMyWindow : public CWindowImpl
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
    END_MSG_MAP()
};

我将在下一节展开讲如何如何添加消息处理到消息映射链。最后,我们需要为我们的窗口类定义窗口的特征,窗口的特征就是窗口类型和扩展窗口类型的联合体,用于创建窗口时指定窗口的类型。窗口类型被指定为参数模板,所以窗口的调用者不需要为指定窗口的正确类型而烦心,下面是是同ATL类CWinTraits定义窗口类型的例子:

[cpp] view plain copy print ?
  1. typedef CWinTraits CMyWindowTraits;  
  2.    
  3. class CMyWindow : public CWindowImpl  
  4. {  
  5. public:  
  6.     DECLARE_WND_CLASS(_T("My Window Class"))  
  7.    
  8.     BEGIN_MSG_MAP(CMyWindow)  
  9.     END_MSG_MAP()  
  10. };  
typedef CWinTraits CMyWindowTraits;
 
class CMyWindow : public CWindowImpl
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
    END_MSG_MAP()
};

调用者可以重载CMyWindowTraits的类型定义,但是一般情况下这是没有必要的,ATL提供了几个预先定义的特殊的类型,其中之一就是CFrameWinTraits,一个非常棒的框架窗口:

[cpp] view plain copy print ?
  1. typedef CWinTraits
  2.                                          WS_EX_APPWINDOW | WS_EX_WINDOWEDGE>  CFrameWinTraits;  
typedef CWinTraits  CFrameWinTraits;

填写消息映射链

ATL的消息映射链是对开发者不太友好的部分,也是WTL对其改进最大的部分。类向导至少可以让你添加消息响应,然而ATL没有消息相关的宏和象MFC那样的参数自动展开功能,在ATL中只有三种类型的消息处理,一个是WM_NOTIFY,一个是WM_COMMAND,第三类是其他窗口消息,让我们开始为我们的窗口添加WM_CLOSE 和 WM_DESTROY的消息相应函数。

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl  
  2. {  
  3. public:  
  4.     DECLARE_WND_CLASS(_T("My Window Class"))  
  5.    
  6.     BEGIN_MSG_MAP(CMyWindow)  
  7.         MESSAGE_HANDLER(WM_CLOSE, OnClose)  
  8.         MESSAGE_HANDLER(WM_DESTROY, OnDestroy)  
  9.     END_MSG_MAP()  
  10.    
  11.     LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  12.     {  
  13.         DestroyWindow();  
  14.         return 0;  
  15.     }  
  16.    
  17.     LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  18.     {  
  19.         PostQuitMessage(0);  
  20.         return 0;  
  21.     }  
  22. };  
class CMyWindow : public CWindowImpl
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
    END_MSG_MAP()
 
    LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        DestroyWindow();
        return 0;
    }
 
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        PostQuitMessage(0);
        return 0;
    }
};

你可能注意到消息响应函数的到的是原始的WPARAM 和 LPARAM值,你需要自己将其展开为相应的消息所需要的参数。还有第四个参数bHandled,这个参数在消息相应函数调用被ATL设置为TRUE,如果在你的消息响应处理完之后需要ATL调用默认的WindowProc()处理该消息,你可以讲bHandled设置为FALSE。这与MFC不同,MFC是显示的调用基类的响应函数来实现的默认的消息处理的。

让我们也添加一个对WM_COMMAND消息的处理,假设我们的窗口有一个ID为IDC_ABOUT的About菜单:

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl  
  2. {  
  3. public:  
  4.     DECLARE_WND_CLASS(_T("My Window Class"))  
  5.    
  6.     BEGIN_MSG_MAP(CMyWindow)  
  7.         MESSAGE_HANDLER(WM_CLOSE, OnClose)  
  8.         MESSAGE_HANDLER(WM_DESTROY, OnDestroy)  
  9.         COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)  
  10.     END_MSG_MAP()  
  11.    
  12.     LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  13.     {  
  14.         DestroyWindow();  
  15.         return 0;  
  16.     }  
  17.    
  18.     LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  19.     {  
  20.         PostQuitMessage(0);  
  21.         return 0;  
  22.     }  
  23.    
  24.     LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)  
  25.     {  
  26.         MessageBox ( _T("Sample ATL window"), _T("About MyWindow") );  
  27.         return 0;  
  28.     }  
  29. };  
class CMyWindow : public CWindowImpl
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
        COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
    END_MSG_MAP()
 
    LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        DestroyWindow();
        return 0;
    }
 
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        PostQuitMessage(0);
        return 0;
    }
 
    LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
        MessageBox ( _T("Sample ATL window"), _T("About MyWindow") );
        return 0;
    }
};

需要注意得是COMMAND_HANDLER宏已经将消息的参数展开了,同样,NOTIFY_HANDLER宏也将WM_NOTIFY消息的参数展开了。

高级消息映射链和嵌入类

ATL的另一个显著不同之处就是任何一个C++类都可以响应消息,而MFC只是将消息响应任务分给了CWnd类和CCmdTarget类,外加几个有PreTranslateMessage()方法的类。ATL的这种特性允许我们编写所谓的“嵌入类”,为我们的窗口添加特性只需将该类添加到继承列表中就行了,就这么简单!

一个基本的带有消息映射链的类通常是模板类,将派生类的类名作为模板的参数,这样它就可以访问派生类中的成员,比如m_hWnd(CWindow类中的HWND成员)。让我们来看一个嵌入类的例子,这个嵌入类通过响应WM_ERASEBKGND消息来画窗口的背景。

[cpp] view plain copy print ?
  1. template <class T, COLORREF t_crBrushColor>  
  2. class CPaintBkgnd : public CMessageMap  
  3. {  
  4. public:  
  5.     CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); }  
  6.     ~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); }  
  7.    
  8.     BEGIN_MSG_MAP(CPaintBkgnd)  
  9.         MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)  
  10.     END_MSG_MAP()  
  11.    
  12.     LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  13.     {  
  14.         T*   pT = static_cast(this);  
  15.         HDC  dc = (HDC) wParam;  
  16.         RECT rcClient;  
  17.    
  18.         pT->GetClientRect ( &rcClient );  
  19.         FillRect ( dc, &rcClient, m_hbrBkgnd );  
  20.         return 1;    // we painted the background  
  21.     }  
  22.    
  23. protected:  
  24.     HBRUSH m_hbrBkgnd;  
  25. };  
template 
class CPaintBkgnd : public CMessageMap
{
public:
    CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); }
    ~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); }
 
    BEGIN_MSG_MAP(CPaintBkgnd)
        MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
    END_MSG_MAP()
 
    LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        T*   pT = static_cast(this);
        HDC  dc = (HDC) wParam;
        RECT rcClient;
 
        pT->GetClientRect ( &rcClient );
        FillRect ( dc, &rcClient, m_hbrBkgnd );
        return 1;    // we painted the background
    }
 
protected:
    HBRUSH m_hbrBkgnd;
};

让我们来研究一下这个新类。首先,CPaintBkgnd有两个模板参数:使用CPaintBkgnd的派生类的名字和用来画窗口背景的颜色。(t_ 前缀通常用来作为模板类的模板参数的前缀)CPaintBkgnd也是从CMessageMap派生的,这并不是必须的,因为所有需要响应消息的类只需使用BEGIN_MSG_MAP宏就足够了,所以你可能看到其他的一些嵌入类的例子代码,它们并不是从该基类派生的。

构造函数和析构函数都相当简单,只是创建和销毁Windows画刷,这个画刷由参数t_crBrushColor决定颜色。接着是消息映射链,它响应WM_ERASEBKGND消息,最后由响应函数OnEraseBkgnd()用构造函数创建的画刷填充窗口的背景。在OnEraseBkgnd()中有两件事需要注意,一个是它使用了一个派生的窗口类的方法(即GetClientRect()),我们如何知道派生类中有GetClientRect()方法呢?如果派生类中没有这个方法我们的代码也不会有任何抱怨,由编译器确认派生类T是从CWindow派生的。另一个是OnEraseBkgnd()没有将消息参数wParam展开为设备上下文(DC)。(WTL最终会解决这个问题,我们很快就可以看到,我保证)
要在我们的窗口中使用这个嵌入类需要做两件事:首先,将它加入到继承列表:

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl,  
  2.                   public CPaintBkgnd  
class CMyWindow : public CWindowImpl,
                  public CPaintBkgnd

其次,需要CMyWindow将消息传递给CPaintBkgnd,就是将其链入到消息映射链,在CMyWindow的消息映射链中加入CHAIN_MSG_MAP宏:

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl,  
  2.                   public CPaintBkgnd   
  3. {  
  4. ...  
  5. typedef CPaintBkgnd CPaintBkgndBase;  
  6.    
  7.     BEGIN_MSG_MAP(CMyWindow)  
  8.         MESSAGE_HANDLER(WM_CLOSE, OnClose)  
  9.         MESSAGE_HANDLER(WM_DESTROY, OnDestroy)  
  10.         COMMAND_HANDLER(IDC_ABOUT, OnAbout)  
  11.         CHAIN_MSG_MAP(CPaintBkgndBase)  
  12.     END_MSG_MAP()  
  13. ...  
  14. };  
class CMyWindow : public CWindowImpl,
                  public CPaintBkgnd 
{
...
typedef CPaintBkgnd CPaintBkgndBase;
 
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
        COMMAND_HANDLER(IDC_ABOUT, OnAbout)
        CHAIN_MSG_MAP(CPaintBkgndBase)
    END_MSG_MAP()
...
};

任何CMyWindow没有处理的消息都被传递给CPaintBkgnd。应该注意的是WM_CLOSE,WM_DESTROY和IDC_ABOUT消息将不会传递,因为这些消息一旦被处理消息映射链的查找就会中止。使用typedef是必要地,因为宏是预处理宏,只能有一个参数,如果我们将CPaintBkgnd作为参数传递,那个“,”会使预处理器认为我们使用了多个参数。

你可以在继承列表中使用多个嵌入类,每一个嵌入类使用一个CHAIN_MSG_MAP宏,这样消息映射链就会将消息传递给它。这与MFC不同,MFC地CWnd派生类只能有一个基类,MFC自动将消息传递给基类。

ATL程序的结构

到目前为止我们已经有了一个完整地主窗口类(即使不完全有用),让我们看看如何在程序中使用它。一个ATL程序包含一个CComModule类型的全局变量_Module,这和MFC的程序都有一个CWinApp类型的全局变量theApp有些类似,唯一不同的是在ATL中这个变量必须命名为_Module。

下面是stdafx.h文件的开始部分:

[cpp] view plain copy print ?
  1. // stdafx.h:   
  2. #define STRICT   
  3. #define VC_EXTRALEAN   
  4.    
  5. #include         // 基本的ATL类  
  6. extern CComModule _Module;  // 全局_Module  
  7. #include          // ATL窗口类  
// stdafx.h:
#define STRICT
#define VC_EXTRALEAN
 
#include         // 基本的ATL类
extern CComModule _Module;  // 全局_Module
#include          // ATL窗口类

atlbase.h已经包含最基本的Window编程的头文件,所以我们不需要在包含windows.h,tchar.h之类的头文件。在CPP文件中声明了_Module变量:

[cpp] view plain copy print ?
  1. // main.cpp:   
  2. CComModule _Module;  
// main.cpp:
CComModule _Module;

CComModule含有程序的初始化和关闭函数,需要在WinMain()中显示的调用,让我们从这里开始:

[cpp] view plain copy print ?
  1. // main.cpp:   
  2. CComModule _Module;  
  3.    
  4. int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,  
  5.                    LPSTR szCmdLine, int nCmdShow)  
  6. {  
  7.     _Module.Init(NULL, hInst);  
  8.     _Module.Term();  
  9. }  
// main.cpp:
CComModule _Module;
 
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
                   LPSTR szCmdLine, int nCmdShow)
{
    _Module.Init(NULL, hInst);
    _Module.Term();
}

Init()的第一个参数只有COM的服务程序才有用,由于我们的EXE不含有COM对象,我们只需将NULL传递给Init()就行了。ATL不提供自己的WinMain()和类似MFC的消息泵,所以我们需要创建CMyWindow对象并添加消息泵才能使我们的程序运行。

[cpp] view plain copy print ?
  1. // main.cpp:   
  2. #include "MyWindow.h"   
  3. CComModule _Module;  
  4.    
  5. int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,  
  6.                    LPSTR szCmdLine, int nCmdShow)  
  7. {  
  8.     _Module.Init(NULL, hInst);  
  9.    
  10.     CMyWindow wndMain;  
  11.     MSG msg;  
  12.    
  13.     // Create & show our main window  
  14.     if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault,   
  15.                                  _T("My First ATL Window") ))  
  16.     {  
  17.         // Bad news, window creation failed  
  18.         return 1;  
  19.      }  
  20.    
  21.     wndMain.ShowWindow(nCmdShow);  
  22.     wndMain.UpdateWindow();  
  23.    
  24.     // Run the message loop   
  25.     while ( GetMessage(&msg, NULL, 0, 0) > 0 )  
  26.     {  
  27.         TranslateMessage(&msg);  
  28.         DispatchMessage(&msg);  
  29.     }  
  30.    
  31.     _Module.Term();  
  32.     return msg.wParam;  
  33. }  
// main.cpp:
#include "MyWindow.h"
CComModule _Module;
 
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
                   LPSTR szCmdLine, int nCmdShow)
{
    _Module.Init(NULL, hInst);
 
    CMyWindow wndMain;
    MSG msg;
 
    // Create & show our main window
    if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault, 
                                 _T("My First ATL Window") ))
    {
        // Bad news, window creation failed
        return 1;
     }
 
    wndMain.ShowWindow(nCmdShow);
    wndMain.UpdateWindow();
 
    // Run the message loop
    while ( GetMessage(&msg, NULL, 0, 0) > 0 )
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
 
    _Module.Term();
    return msg.wParam;
}

上面的代码唯一需要说明的是CWindow::rcDefault,这是CWindow中的成员(静态数据成员),数据类型是RECT。和调用CreateWindow() API时使用CW_USEDEFAULT指定窗口的宽度和高度一样,ATL使用rcDefault作为窗口的最初大小。

在ATL代码内部,ATL使用了一些类似汇编语言的魔法将主窗口的句柄与相应的CMyWindow对象联系起来,在外部看来就是可以毫无问题的在线程之间传递CWindow对象,而MFC的CWnd却不能这样作。

这就是我们的窗口:

WTL 详细介绍_第1张图片

我得承认这确实没有什么激动人心的地方。我们将添加一个About菜单并显示一个对话框,主要是为它增加一些情趣。

ATL中的对话框

我们前面提到过,ATL有两个对话框类,我们的About对话框使用CDialogImpl。生成一个新对话框和生成一个主窗口几乎一样,只有两点不同:
窗口的基类是CDialogImpl而不是CWindowImpl。
你需要定义名称为IDD的公有成员用来保存对话框资源的ID。
现在开始为About对话框定义一个新类:

[cpp] view plain copy print ?
  1. class CAboutDlg : public CDialogImpl  
  2. {  
  3. public:  
  4.     enum { IDD = IDD_ABOUT };  
  5.    
  6.     BEGIN_MSG_MAP(CAboutDlg)  
  7.     END_MSG_MAP()  
  8. };  
class CAboutDlg : public CDialogImpl
{
public:
    enum { IDD = IDD_ABOUT };
 
    BEGIN_MSG_MAP(CAboutDlg)
    END_MSG_MAP()
};

ATL没有在内部实现对“OK”和“Cancel”两个按钮的响应处理,所以我们需要自己添加这些代码,如果用户用鼠标点击标题栏的关闭按钮,WM_CLOSE的响应函数就会被调用。我们还需要处理WM_INITDIALOG消息,这样我们就能够在对话框出现时正确的设置键盘焦点,下面是完整的类定义和消息响应函数。

[cpp] view plain copy print ?
  1. class CAboutDlg : public CDialogImpl  
  2. {  
  3. public:  
  4.     enum { IDD = IDD_ABOUT };  
  5.    
  6.     BEGIN_MSG_MAP(CAboutDlg)  
  7.         MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)  
  8.         MESSAGE_HANDLER(WM_CLOSE, OnClose)  
  9.         COMMAND_ID_HANDLER(IDOK, OnOKCancel)  
  10.         COMMAND_ID_HANDLER(IDCANCEL, OnOKCancel)  
  11.     END_MSG_MAP()  
  12.    
  13.     LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  14.     {  
  15.         CenterWindow();  
  16.         return TRUE;    // let the system set the focus  
  17.     }  
  18.    
  19.     LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  20.     {  
  21.         EndDialog(IDCANCEL);  
  22.         return 0;  
  23.     }  
  24.    
  25.     LRESULT OnOKCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)  
  26.     {  
  27.         EndDialog(wID);  
  28.         return 0;  
  29.     }  
  30. };  
class CAboutDlg : public CDialogImpl
{
public:
    enum { IDD = IDD_ABOUT };
 
    BEGIN_MSG_MAP(CAboutDlg)
        MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        COMMAND_ID_HANDLER(IDOK, OnOKCancel)
        COMMAND_ID_HANDLER(IDCANCEL, OnOKCancel)
    END_MSG_MAP()
 
    LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        CenterWindow();
        return TRUE;    // let the system set the focus
    }
 
    LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        EndDialog(IDCANCEL);
        return 0;
    }
 
    LRESULT OnOKCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
        EndDialog(wID);
        return 0;
    }
};

我使用一个消息响应函数同时处理“OK”和“Cancel”两个按钮的WM_COMMAND消息,因为命令响应函数的wID参数就已经指明了消息是来自“OK”按钮还是来自“Cancel”按钮。

显示对话框的方法与MFC相似,创建一个新对话框类的实例,然后调用DoModal()方法。现在我们返回主窗口,添加一个带有About菜单项的菜单用来显示我们的对话框,这需要再添加两个消息响应函数,一个是响应WM_CREATE,另一个是响应菜单的IDC_ABOUT命令。

[cpp] view plain copy print ?
  1. class CMyWindow : public CWindowImpl,  
  2.                   public CPaintBkgnd  
  3. {  
  4. public:  
  5.     BEGIN_MSG_MAP(CMyWindow)  
  6.         MESSAGE_HANDLER(WM_CREATE, OnCreate)  
  7.         COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)  
  8.         // ...   
  9.         CHAIN_MSG_MAP(CPaintBkgndBase)  
  10.     END_MSG_MAP()  
  11.    
  12.     LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)  
  13.     {  
  14.     HMENU hmenu = LoadMenu ( _Module.GetResourceInstance(),  
  15.                              MAKEINTRESOURCE(IDR_MENU1) );  
  16.    
  17.         SetMenu ( hmenu );  
  18.         return 0;  
  19.     }  
  20.    
  21.     LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)  
  22.     {  
  23.     CAboutDlg dlg;  
  24.    
  25.         dlg.DoModal();  
  26.         return 0;  
  27.     }  
  28.     // ...   
  29. };  
class CMyWindow : public CWindowImpl,
                  public CPaintBkgnd
{
public:
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CREATE, OnCreate)
        COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
        // ...
        CHAIN_MSG_MAP(CPaintBkgndBase)
    END_MSG_MAP()
 
    LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
    HMENU hmenu = LoadMenu ( _Module.GetResourceInstance(),
                             MAKEINTRESOURCE(IDR_MENU1) );
 
        SetMenu ( hmenu );
        return 0;
    }
 
    LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
    CAboutDlg dlg;
 
        dlg.DoModal();
        return 0;
    }
    // ...
};

在指定对话框的父窗口的方式上有些不同,MFC是通过构造函数将父窗口的指针传递给对话框而在ATL中是将父窗口的指针作为DoModal()方法的第一个参数传递给对话框的,如果象上面的代码一样没有指定父窗口,ATL会使用GetActiveWindow()得到的窗口(也就是我们的主框架窗口)作为对话框的父窗口。

对LoadMenu()方法的调用展示了CComModule的另一个方法-GetResourceInstance(),它返回你的EXE的HINSTANCE实例,和MFC的AfxGetResourceHandle()方法相似。(当然还有CComModule::GetModuleInstance(),它相当于MFC的AfxGetInstanceHandle()。)

这就是主窗口和对话框的显示效果:

WTL 详细介绍_第2张图片

我会继续讲WTL,我保证!

我会继续讲WTL的,只是会在第二部分。我觉得既然这些文章是写给使用MFC的开发者的,所以有必要在投入WTL之前先介绍一些ATL。如果你是第一次接触到ATL,那现在你就可以尝试写一些小程序,处理消息和使用嵌入类,你也可以尝试用类向导支持消息映射链,使它能够自动添加消息响应。现在就开始,右键单击CMyWindow项,在弹出的上下文菜单中单击“Add Windows Message Handler”菜单项。

在第二部分,我将全面介绍基本的WTL窗口类和一个更好的消息映射宏。

 

对第二部分的介绍

好了,现在正式开始介绍WTL!在这一部分我讲的内容包括生成一个基本的主窗口和WTL提供的一些友好的改进,比如UI界面的更新(如菜单上的选择标记)和更好的消息映射机制。为了更好地掌握本章的内容,你应该安装WTL并将WTL库的头文件目录添加到VC的搜索目录中,还要将WTL的应用程序生成向导复制到正确的位置。WTL的发布版本中有文档具体介绍如何做这些设置,如果遇到困难可以查看这些文档。

WTL 总体印象

WTL的类大致可以分为几种类型:

主框架窗口的实现- CFrameWindowImpl, CMDIFrameWindowImpl
控件的封装- CButton, CListViewCtrl
GDI 对象的封装- CDC, CMenu
一些特殊的界面特性 - CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw
实用的工具类和宏- CString, CRect, BEGIN_MSG_MAP_EX
本篇文章将深入地介绍框架窗口类,还将简要地讲一下有关的界面特性类和工具类,这些界面特性类和工具类中绝大多数都是独立的类,尽管有一些是嵌入类,例如:CDialogResize。

开始写WTL程序

如果你没有用WTL的应用程序生成向导也没关系(我将在后面介绍这个向导的用法), WTL的程序的代码结构很像ATL的程序,本章使用的例子代码有别于第一章的例子,主要是为了显示WTL的特性,没有什么实用价值。

这一节我们将在WTL生成的代码基础上添加代码,生成一个新的程序,程序主窗口的客户区显示当前的时间。stdafx.h的代码如下:

[cpp] view plain copy print ?
  1. #define STRICT   
  2. #define WIN32_LEAN_AND_MEAN   
  3. #define _WTL_USE_CSTRING   
  4.    
  5. #include        // 基本的ATL类  
  6. #include         // 基本的WTL类  
  7. extern CAppModule _Module; // WTL 派生的CComModule版本  
  8. #include         // ATL 窗口类  
  9. #include       // WTL 主框架窗口类  
  10. #include        // WTL 实用工具类,例如:CString  
  11. #include       // WTL 增强的消息宏  
#define STRICT
#define WIN32_LEAN_AND_MEAN
#define _WTL_USE_CSTRING
 
#include        // 基本的ATL类
#include         // 基本的WTL类
extern CAppModule _Module; // WTL 派生的CComModule版本
#include         // ATL 窗口类
#include       // WTL 主框架窗口类
#include        // WTL 实用工具类,例如:CString
#include       // WTL 增强的消息宏


atlapp.h 是你的工程中第一个包含的头文件,这个文件内定义了有关消息处理的类和CAppModule,CAppModule是从CComModule派生的类。如果你打算使用CString类,你需要手工定义_WTL_USE_CSTRING标号,因为CString类是在atlmisc.h中定义的,而许多包含在atlmisc.h之前的头文件都会用到CString,定义_WTL_USE_CSTRING之后,atlapp.h就会向前声明CString类,其他的头文件就知道CString类的存在,从而避免编译器为此大惊小怪。
接下来定义框架窗口。我们的SDI窗口是从CFrameWindowImpl派生的,在定义窗口类时使用DECLARE_FRAME_WND_CLASS代替前面使用的DECLARE_WND_CLASS。下面时MyWindow.h中窗口定义的开始部分:

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     DECLARE_FRAME_WND_CLASS(_T("First WTL window"), IDR_MAINFRAME);  
  5.   
  6.     BEGIN_MSG_MAP(CMyWindow)  
  7.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  8.     END_MSG_MAP()  
  9. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    DECLARE_FRAME_WND_CLASS(_T("First WTL window"), IDR_MAINFRAME);

    BEGIN_MSG_MAP(CMyWindow)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
};


DECLARE_FRAME_WND_CLASS有两个参数,窗口类名(类名可以是NULL,ATL会替你生成一个类名)和资源ID,创建窗口时WTL用这个ID装载图标,菜单和加速键表。我们还要象CFrameWindowImpl中的消息处理(例如WM_SIZE和WM_DESTROY消息)那样将消息链入窗口的消息中。

现在来看看WinMain()函数,它和第一部分中的例子代码中的WinMain()函数几乎一样,只是创建窗口部分的代码略微不同。

[cpp] view plain copy print ?
  1. // main.cpp:   
  2. #include "stdafx.h"   
  3. #include "MyWindow.h"   
  4.    
  5. CAppModule _Module;  
  6.    
  7. int APIENTRY WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance,  
  8.                        LPSTR lpCmdLine, int nCmdShow )  
  9. {  
  10.     _Module.Init ( NULL, hInstance );  
  11.    
  12. CMyWindow wndMain;  
  13. MSG msg;  
  14.    
  15.     // Create the main window   
  16.     if ( NULL == wndMain.CreateEx() )  
  17.         return 1;       // Window creation failed  
  18.    
  19.     // Show the window   
  20.     wndMain.ShowWindow ( nCmdShow );  
  21.     wndMain.UpdateWindow();  
  22.    
  23.     // Standard Win32 message loop  
  24.     while ( GetMessage ( &msg, NULL, 0, 0 ) > 0 )  
  25.         {  
  26.         TranslateMessage ( &msg );  
  27.         DispatchMessage ( &msg );  
  28.         }  
  29.    
  30.     _Module.Term();  
  31.     return msg.wParam;  
  32. }  
// main.cpp:
#include "stdafx.h"
#include "MyWindow.h"
 
CAppModule _Module;
 
int APIENTRY WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPSTR lpCmdLine, int nCmdShow )
{
    _Module.Init ( NULL, hInstance );
 
CMyWindow wndMain;
MSG msg;
 
    // Create the main window
    if ( NULL == wndMain.CreateEx() )
        return 1;       // Window creation failed
 
    // Show the window
    wndMain.ShowWindow ( nCmdShow );
    wndMain.UpdateWindow();
 
    // Standard Win32 message loop
    while ( GetMessage ( &msg, NULL, 0, 0 ) > 0 )
        {
        TranslateMessage ( &msg );
        DispatchMessage ( &msg );
        }
 
    _Module.Term();
    return msg.wParam;
}


CFrameWindowImpl中的CreateEx()函数的参数使用了常用的默认值,所以我们不需要特别指定任何参数。正如前面介绍的,CFrameWindowImpl会处理资源的装载,你只需要使用IDR_MAINFRAME作为ID定义你的资源就行了(译者注:主要是图标,菜单和加速键表),你也可以直接使用本章的例子代码。

如果你现在就运行程序,你会看到主框架窗口,事实上它没有做任何事情。我们需要手工添加一些消息处理,所以现在是介绍WTL的消息映射宏的最佳时间。

WTL 对消息映射的增强

将Win32 API通过消息传递过来的WPARAM和LPARAM数据还原出来是一件麻烦的事情并且很容易出错,不幸得是ATL并没有为我们提供更多的帮助,我们仍然需要从消息中还原这些数据,当然WM_COMMAND和WM_NOTIFY消息除外。但是WTL的出现拯救了这一切!

WTL的增强消息映射宏定义在atlcrack.h中。(这个名字来源于“消息解密者”,是一个与windowsx.h的宏所使用的相同术语)首先将BEGIN_MSG_MAP改为BEGIN_MSG_MAP_EX,带_EX的版本产生“解密”消息的代码。

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMyWindow)  
  5.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  6.     END_MSG_MAP()  
  7. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
};


对于我们的时钟程序,我们需要处理WM_CREATE消息来设置定时器,WTL的消息处理使用MSG_作为前缀,后面是消息名称,例如MSG_WM_CREATE。这些宏只是代表消息响应处理的名称,现在我们来添加对WM_CREATE消息的响应:

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMyWindow)  
  5.         MSG_WM_CREATE(OnCreate)  
  6.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  7.     END_MSG_MAP()  
  8.    
  9.     // OnCreate(...) ?   
  10. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        MSG_WM_CREATE(OnCreate)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
 
    // OnCreate(...) ?
};


WTL的消息响应处理看起来有点象MFC,每一个处理函数根据消息传递的参数不同也有不同的原型。由于我们没有向导自动添加消息响应,所以我们需要自己查找正确的消息处理函数。幸运的是VC可以帮我们的忙,将鼠标光标移到“MSG_WM_CREATE”宏的文字上按F12键就可以来到这个宏的定义代码处。如果是第一次使用这个功能,VC会要求从新编译全部文件以建立浏览信息数据库(browse info database),建立了这个数据库之后,VC会打开atlcrack.h并将代码定位到MSG_WM_CREATE的定义位置:

[cpp] view plain copy print ?
  1. #define MSG_WM_CREATE(func) \   
  2.     if (uMsg == WM_CREATE) \  
  3.     { \  
  4.         SetMsgHandled(TRUE); \  
  5.         lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \  
  6.         if(IsMsgHandled()) \  
  7.             return TRUE; \  
  8.     }  
#define MSG_WM_CREATE(func) \
    if (uMsg == WM_CREATE) \
    { \
        SetMsgHandled(TRUE); \
        lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \
        if(IsMsgHandled()) \
            return TRUE; \
    }


标记为红色的那一行非常重要,就是在这里调用实际的消息响应函数,他告诉我们消息响应函数有一个LPCREATESTRUCT类型的参数,返回值的类型是LRESULT。请注意这里没有ATL的宏所用的 bHandled 参数,SetMsgHandled()函数代替了这个参数,我会对此作些简要的介绍。

现在为我们的窗口类添加OnCreate()响应函数:

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMyWindow)  
  5.         MSG_WM_CREATE(OnCreate)  
  6.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  7.     END_MSG_MAP()  
  8.    
  9.     LRESULT OnCreate(LPCREATESTRUCT lpcs)  
  10.     {  
  11.         SetTimer ( 1, 1000 );  
  12.         SetMsgHandled(false);  
  13.         return 0;  
  14.     }  
  15. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        MSG_WM_CREATE(OnCreate)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
 
    LRESULT OnCreate(LPCREATESTRUCT lpcs)
    {
        SetTimer ( 1, 1000 );
        SetMsgHandled(false);
        return 0;
    }
};


CFrameWindowImpl 是直接从CWindow类派生的, 所以它继承了所有CWindow类的方法,如SetTimer()。这使得对窗口API的调用有点象MFC的代码,只是MFC使用CWnd类包装这些API。

我们使用SetTimer()函数创建一个定时器,它每隔一秒钟(1000毫秒)触发一次。由于我们需要让CFrameWindowImpl也处理WM_CREATE消息,所以我们调用SetMsgHandled(false),让消息通过CHAIN_MSG_MAP宏链入基类,这个调用代替了ATL宏使用的bHandled参数。(即使CFrameWindowImpl类不需要处理WM_CREATE消息,调用SetMsgHandled(false)让消息流入基类是个好的习惯,因为这样我们就不必总是记着哪个消息需要基类处理那些消息不需要基类处理,这和VC的类向导产生的代码相似,多数的派生类的消息处理函数的开始或结尾都会调用基类的消息处理函数)

为了能够停止定时器我们还需要响应WM_DESTROY消息,添加消息响应的过程和前面一样,MSG_WM_DESTROY宏的定义是这样的:

[cpp] view plain copy print ?
  1. #define MSG_WM_DESTROY(func) \   
  2.     if (uMsg == WM_DESTROY) \  
  3.     { \  
  4.         SetMsgHandled(TRUE); \  
  5.         func(); \  
  6.         lResult = 0; \  
  7.         if(IsMsgHandled()) \  
  8.             return TRUE; \  
  9.     }  
#define MSG_WM_DESTROY(func) \
    if (uMsg == WM_DESTROY) \
    { \
        SetMsgHandled(TRUE); \
        func(); \
        lResult = 0; \
        if(IsMsgHandled()) \
            return TRUE; \
    }


OnDestroy()函数没有参数也没有返回值,CFrameWindowImpl也要处理WM_DESTROY消息,所以还要调用SetMsgHandled(false):

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMyWindow)  
  5.         MSG_WM_CREATE(OnCreate)  
  6.         MSG_WM_DESTROY(OnDestroy)  
  7.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  8.     END_MSG_MAP()  
  9.   
  10.   
  11.   
  12.     void OnDestroy()  
  13.     {  
  14.         KillTimer(1);  
  15.         SetMsgHandled(false);  
  16.     }  
  17. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        MSG_WM_CREATE(OnCreate)
        MSG_WM_DESTROY(OnDestroy)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()



    void OnDestroy()
    {
        KillTimer(1);
        SetMsgHandled(false);
    }
};


接下来是响应WM_TIMER消息的处理函数,它每秒钟被调用一次。你应该知道怎样使用F12键的窍门了,所以我直接给出响应函数的代码:

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMyWindow)  
  5.         MSG_WM_CREATE(OnCreate)  
  6.         MSG_WM_DESTROY(OnDestroy)  
  7.         MSG_WM_TIMER(OnTimer)  
  8.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  9.     END_MSG_MAP()  
  10.    
  11.     void OnTimer ( UINT uTimerID, TIMERPROC pTimerProc )  
  12.     {  
  13.         if ( 1 != uTimerID )  
  14.             SetMsgHandled(false);  
  15.         else  
  16.             RedrawWindow();  
  17.     }  
  18. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        MSG_WM_CREATE(OnCreate)
        MSG_WM_DESTROY(OnDestroy)
        MSG_WM_TIMER(OnTimer)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
 
    void OnTimer ( UINT uTimerID, TIMERPROC pTimerProc )
    {
        if ( 1 != uTimerID )
            SetMsgHandled(false);
        else
            RedrawWindow();
    }
};


这个响应函数只是在每次定时器触发时重画窗口的客户区。最后我们要响应WM_ERASEBKGND消息,在窗口客户区的左上角显示当前的时间。

[cpp] view plain copy print ?
  1. class CMyWindow : public CFrameWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMyWindow)  
  5.         MSG_WM_CREATE(OnCreate)  
  6.         MSG_WM_DESTROY(OnDestroy)  
  7.         MSG_WM_TIMER(OnTimer)  
  8.         MSG_WM_ERASEBKGND(OnEraseBkgnd)  
  9.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  10.     END_MSG_MAP()  
  11.    
  12.     LRESULT OnEraseBkgnd ( HDC hdc )  
  13.     {  
  14.     CDCHandle  dc(hdc);  
  15.     CRect      rc;  
  16.     SYSTEMTIME st;  
  17.     CString    sTime;  
  18.    
  19.         // Get our window's client area.  
  20.         GetClientRect ( rc );  
  21.    
  22.         // Build the string to show in the window.  
  23.         GetLocalTime ( &st );  
  24.         sTime.Format ( _T("The time is %d:%02d:%02d"),   
  25.                        st.wHour, st.wMinute, st.wSecond );  
  26.    
  27.         // Set up the DC and draw the text.  
  28.         dc.SaveDC();  
  29.    
  30.         dc.SetBkColor ( RGB(255,153,0) );  
  31.         dc.SetTextColor ( RGB(0,0,0) );  
  32.         dc.ExtTextOut ( 0, 0, ETO_OPAQUE, rc, sTime,   
  33.                         sTime.GetLength(), NULL );  
  34.    
  35.         // Restore the DC.   
  36.         dc.RestoreDC(-1);  
  37.         return 1;    // We erased the background (ExtTextOut did it)  
  38.     }  
  39. };  
class CMyWindow : public CFrameWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        MSG_WM_CREATE(OnCreate)
        MSG_WM_DESTROY(OnDestroy)
        MSG_WM_TIMER(OnTimer)
        MSG_WM_ERASEBKGND(OnEraseBkgnd)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
 
    LRESULT OnEraseBkgnd ( HDC hdc )
    {
    CDCHandle  dc(hdc);
    CRect      rc;
    SYSTEMTIME st;
    CString    sTime;
 
        // Get our window's client area.
        GetClientRect ( rc );
 
        // Build the string to show in the window.
        GetLocalTime ( &st );
        sTime.Format ( _T("The time is %d:%02d:%02d"), 
                       st.wHour, st.wMinute, st.wSecond );
 
        // Set up the DC and draw the text.
        dc.SaveDC();
 
        dc.SetBkColor ( RGB(255,153,0) );
        dc.SetTextColor ( RGB(0,0,0) );
        dc.ExtTextOut ( 0, 0, ETO_OPAQUE, rc, sTime, 
                        sTime.GetLength(), NULL );
 
        // Restore the DC.
        dc.RestoreDC(-1);
        return 1;    // We erased the background (ExtTextOut did it)
    }
};


这个消息处理函数不仅使用了CRect和CString类,还使用了一个GDI包装类CDCHandle。对于CString类我想说的是它等同与MFC的CString类,我在后面的文章中还会介绍这些包装类,现在你只需要知道CDCHandle是对HDC的简单封装就行了,使用方法与MFC的CDC类相似,只是CDCHandle的实例在超出作用域后不会销毁它所操作的设备上下文。

所有的工作完成了,现在看看我们的窗口是什么样子:

WTL 详细介绍_第3张图片

例子代码中还使用了WM_COMMAND响应菜单消息,在这里我不作介绍,但是你可以查看例子代码,看看WTL的COMMAND_ID_HANDLER_EX宏是如何工作的。

从WTL的应用程序生成向导能得到什么

WTL的发布版本附带一个很棒的应用程序生成向导,让我们以一个SDI 应用为例看看它有什么特性。

使用向导的整个过程

在VC的IDE环境下单击File|New菜单,从列表中选择ATL/WTL AppWizard,我们要重写时钟程序,所以用WTLClock作为项目的名字:

WTL 详细介绍_第4张图片

在下一页你可以选择项目的类型,SDI,MDI或者是基于对话框的应用,当然还有其它选项,如下图所示设置这些选项,然后点击“下一步”:

WTL 详细介绍_第5张图片

在最后一页你可以选择是否使用toolbar,rebar和status bar,为了简单起见,取消这些选项并单击“结束”。

WTL 详细介绍_第6张图片

查看生成的代码

向导完成后,在生成的代码中有三个类:CMainFrame, CAboutDlg, 和CWTLClockView,从名字上就可以猜出这些类的作用。虽然也有一个是视图类,但它仅仅是从CWindowImpl派生出来的一个简单的窗口类,没有象MFC那样的文档/视图结构。

还有一个_tWinMain()函数,它先初始化COM环境,公用控件和_Module,然后调用全局函数Run()。Run()函数创建主窗口并开始消息循环,Run()调用CMessageLoop::Run(),消息泵实际上是位于CMessageLoop::Run()内,我将在下一个章节介绍CMessageLoop的更多细节。
CAboutDlg是CDialogImpl的派生类,它对应于ID IDD_ABOUTBOX资源,我在第一部分已经介绍过对话框,所以你应该能看懂CAboutDlg的代码。
CWTLClockView是我们的程序的视图类,它的作用和MFC的视图类一样,没有标题栏,覆盖整个主窗口的客户区。CWTLClockView类有一个PreTranslateMessage()函数,也和MFC中的同名函数作用相同,还有一个WM_PAINT的消息响应函数。这两个函数都没有什么特别之处,只是我们会填写OnPaint()函数来显示时间。

最后是我们的CMainFrame类,它有许多有趣的新东西,这是这个类的定义缩略版本:

[cpp] view plain copy print ?
  1. class CMainFrame : public CFrameWindowImpl,  
  2.                    public CUpdateUI,  
  3.                    public CMessageFilter,  
  4.                    public CIdleHandler  
  5. {  
  6. public:  
  7.     DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)  
  8.   
  9.     CWTLClockView m_view;  
  10.   
  11.     virtual BOOL PreTranslateMessage(MSG* pMsg);  
  12.     virtual BOOL OnIdle();  
  13.   
  14.     BEGIN_UPDATE_UI_MAP(CMainFrame)  
  15.     END_UPDATE_UI_MAP()  
  16.   
  17.     BEGIN_MSG_MAP(CMainFrame)  
  18.         // ...   
  19.         CHAIN_MSG_MAP(CUpdateUI)  
  20.         CHAIN_MSG_MAP(CFrameWindowImpl)  
  21.     END_MSG_MAP()  
  22. };  
class CMainFrame : public CFrameWindowImpl,
                   public CUpdateUI,
                   public CMessageFilter,
                   public CIdleHandler
{
public:
    DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

    CWTLClockView m_view;

    virtual BOOL PreTranslateMessage(MSG* pMsg);
    virtual BOOL OnIdle();

    BEGIN_UPDATE_UI_MAP(CMainFrame)
    END_UPDATE_UI_MAP()

    BEGIN_MSG_MAP(CMainFrame)
        // ...
        CHAIN_MSG_MAP(CUpdateUI)
        CHAIN_MSG_MAP(CFrameWindowImpl)
    END_MSG_MAP()
};


CMessageFilter是一个嵌入类,它提供PreTranslateMessage()函数,CIdleHandler也是一个嵌入类,它提供了OnIdle()函数。CMessageLoop, CIdleHandler 和 CUpdateUI三个类互相协同完成界面元素的状态更新(UI update),就像MFC中的ON_UPDATE_COMMAND_UI宏一样。

CMainFrame::OnCreate()中创建了视图窗口并保存这个窗口的句柄,当主窗口改变大小时视图窗口的大小也会随之改变。OnCreate()函数还将CMainFrame对象添加到由CAppModule维持的消息过滤器队列和空闲处理队列,我将在稍后介绍这些。

[cpp] view plain copy print ?
  1. LRESULT CMainFrame::OnCreate(UINT /*uMsg*/WPARAM /*wParam*/,   
  2.                              LPARAM /*lParam*/BOOL/*bHandled*/)  
  3. {  
  4.     m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, |  
  5.                                  WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |  
  6.                                    WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);  
  7.    
  8.     // register object for message filtering and idle updates  
  9.     CMessageLoop* pLoop = _Module.GetMessageLoop();  
  10.     pLoop->AddMessageFilter(this);  
  11.     pLoop->AddIdleHandler(this);  
  12.    
  13.     return 0;  
  14. }  
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, 
                             LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, |
                                 WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
                                   WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
 
    // register object for message filtering and idle updates
    CMessageLoop* pLoop = _Module.GetMessageLoop();
    pLoop->AddMessageFilter(this);
    pLoop->AddIdleHandler(this);
 
    return 0;
}


m_hWndClient是CFrameWindowImpl对象的一个成员变量,当主窗口大小改变时此窗口的大小也将改变。

在生成的CMainFrame中还添加了对File|New, File|Exit, 和 Help|About菜单消息的处理。我们的时钟程序不需要这些默认的菜单项,但是现在将它们留在代码中也没有害处。现在可以编译并运行向导生成的代码,不过这个程序确实没有什么用处。如果你感兴趣的话可以深入CMainFrame::CreateEx()函数的内部看看主窗口和它的资源是如何被加载和创建得。

我们的下一步WTL之旅是CMessageLoop,它掌管消息泵和空闲处理。

CMessageLoop 的内部实现

CMessageLoop为我们的应用程序提供一个消息泵,除了一个标准的DispatchMessage/TranslateMessage循环外,它还通过调用PreTranslateMessage()函数实现了消息过滤机制,通过调用OnIdle()实现了空闲处理功能。下面是Run()函数的伪代码:

[cpp] view plain copy print ?
  1. int Run()  
  2. {  
  3. MSG msg;  
  4.    
  5.     for(;;)  
  6.         {  
  7.         while ( !PeekMessage(&msg) )  
  8.             DoIdleProcessing();  
  9.    
  10.         if ( 0 == GetMessage(&msg) )  
  11.             break;    // WM_QUIT retrieved from the queue  
  12.    
  13.         if ( !PreTranslateMessage(&msg) )  
  14.             {  
  15.             TranslateMessage(&msg);  
  16.             DispatchMessage(&msg);  
  17.             }  
  18.         }  
  19.    
  20.     return msg.wParam;  
  21. }  
int Run()
{
MSG msg;
 
    for(;;)
        {
        while ( !PeekMessage(&msg) )
            DoIdleProcessing();
 
        if ( 0 == GetMessage(&msg) )
            break;    // WM_QUIT retrieved from the queue
 
        if ( !PreTranslateMessage(&msg) )
            {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            }
        }
 
    return msg.wParam;
}


那些需要过滤消息的类只需要象CMainFrame::OnCreate()函数那样调用CMessageLoop::AddMessageFilter()函数就行了,CMessageLoop就会知道该调用那个PreTranslateMessage()函数,同样,如果需要空闲处理就调用CMessageLoop::AddIdleHandler()函数。

需要注意得是在这个消息循环中没有调用TranslateAccelerator() 或 IsDialogMessage() 函数,因为CFrameWindowImpl在这之前已经做了处理,但是如果你在程序中使用了非模式对话框,那你就需要在CMainFrame::PreTranslateMessage()函数中添加对IsDialogMessage()函数的调用。

CFrameWindowImpl 的内部实现

CFrameWindowImpl 和它的基类 CFrameWindowImplBase提供了对toolbars,rebars, status bars,工具条按钮的工具提示和菜单项的掠过式帮助,这些也是MFC的CFrameWnd类的基本特征。我会逐步介绍这些特征,完整的讨论CFrameWindowImpl类需要再写两篇文章,但是现在看看CFrameWindowImpl是如何处理WM_SIZE和它的客户区就足够了。需要记住一点前面提到的东西,m_hWndClient是CFrameWindowImplBase类的成员变量,它存储主窗口内的“视图”窗口的句柄。

CFrameWindowImpl类处理了WM_SIZE消息:

[cpp] view plain copy print ?
  1. LRESULT OnSize(UINT /*uMsg*/WPARAM wParam, LPARAM /*lParam*/BOOL& bHandled)  
  2. {  
  3.     if(wParam != SIZE_MINIMIZED)  
  4.     {  
  5.         T* pT = static_cast(this);  
  6.         pT->UpdateLayout();  
  7.     }  
  8.    
  9.     bHandled = FALSE;  
  10.     return 1;  
  11. }  
LRESULT OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
{
    if(wParam != SIZE_MINIMIZED)
    {
        T* pT = static_cast(this);
        pT->UpdateLayout();
    }
 
    bHandled = FALSE;
    return 1;
}


它首先检查窗口是否最小化,如果不是就调用UpdateLayout(),下面是UpdateLayout():

[cpp] view plain copy print ?
  1. void UpdateLayout(BOOL bResizeBars = TRUE)  
  2. {  
  3. RECT rect;  
  4.    
  5.     GetClientRect(&rect);  
  6.    
  7.     // position bars and offset their dimensions  
  8.     UpdateBarsPosition(rect, bResizeBars);  
  9.    
  10.     // resize client window   
  11.     if(m_hWndClient != NULL)  
  12.         ::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top,  
  13.             rect.right - rect.left, rect.bottom - rect.top,  
  14.             SWP_NOZORDER | SWP_NOACTIVATE);  
  15. }  
void UpdateLayout(BOOL bResizeBars = TRUE)
{
RECT rect;
 
    GetClientRect(&rect);
 
    // position bars and offset their dimensions
    UpdateBarsPosition(rect, bResizeBars);
 
    // resize client window
    if(m_hWndClient != NULL)
        ::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top,
            rect.right - rect.left, rect.bottom - rect.top,
            SWP_NOZORDER | SWP_NOACTIVATE);
}


注意这些代码是如何使用m_hWndClient得,既然m_hWndClient是一般窗口的句柄,它就可能是任何窗口,对这个窗口的类型没有限制。这一点不像MFC,MFC在很多情况下需要CView的派生类(例如分隔窗口类)。如果你回过头看看CMainFrame::OnCreate()就会看到它创建了一个视图窗口并赋值给m_hWndClient,由m_hWndClient确保视图窗口被设置为正确的大小。

回到前面的时钟程序

现在我们已经看到了主窗口类的一些细节,现在回到我们的时钟程序。视图窗口用来响应定时器消息并负责显示时钟,就像前面的CMyWindow类。下面是这个类的部分定义:

[cpp] view plain copy print ?
  1. class CWTLClockView : public CWindowImpl  
  2. {  
  3. public:  
  4.     DECLARE_WND_CLASS(NULL)  
  5.    
  6.     BOOL PreTranslateMessage(MSG* pMsg);  
  7.    
  8.     BEGIN_MSG_MAP_EX(CWTLClockView)  
  9.         MESSAGE_HANDLER(WM_PAINT, OnPaint)  
  10.         MSG_WM_CREATE(OnCreate)  
  11.         MSG_WM_DESTROY(OnDestroy)  
  12.         MSG_WM_TIMER(OnTimer)  
  13.         MSG_WM_ERASEBKGND(OnEraseBkgnd)  
  14.     END_MSG_MAP()  
  15. };  
class CWTLClockView : public CWindowImpl
{
public:
    DECLARE_WND_CLASS(NULL)
 
    BOOL PreTranslateMessage(MSG* pMsg);
 
    BEGIN_MSG_MAP_EX(CWTLClockView)
        MESSAGE_HANDLER(WM_PAINT, OnPaint)
        MSG_WM_CREATE(OnCreate)
        MSG_WM_DESTROY(OnDestroy)
        MSG_WM_TIMER(OnTimer)
        MSG_WM_ERASEBKGND(OnEraseBkgnd)
    END_MSG_MAP()
};


使用BEGIN_MSG_MAP_EX代替BEGIN_MSG_MAP后,ATL的消息映射宏可以和WTL的宏混合使用,前面的例子在OnEraseBkgnd()中显示(画)时钟,现在被被搬到了OnPaint()中。新窗口看起来是这个样子的:

WTL 详细介绍_第7张图片

最后为我们的程序添加UI updating功能,为了演示这些用法,我们为窗口添加Start菜单和Stop菜单用于开始和停止时钟,Start菜单和Stop菜单将被适当的设置为可用和不可用。

界面元素的自动更新(UI Updating)

空闲时间的界面更新是几件事情协同工作的结果: CMessageLoop对象,嵌入类CIdleHandler 和 CUpdateUI,CMainFrame类继承了这两个嵌入类,当然还有CMainFrame类中的UPDATE_UI_MAP宏。CUpdateUI能够操作5种不同的界面元素:顶级菜单项(就是菜单条本身),弹出式菜单的菜单项,工具条按钮,状态条的格子和子窗口(如对话框中的控件)。每一种界面元素都对应CUpdateUIBase类的一个常量:

  • 菜单条项: UPDUI_MENUBAR
  • 弹出式菜单项: UPDUI_MENUPOPUP
  • 工具条按钮: UPDUI_TOOLBAR
  • 状态条格子: UPDUI_STATUSBAR
  • 子窗口: UPDUI_CHILDWINDOW

CUpdateUI可以设置enabled状态,checked状态和文本(当然不是所有的界面元素都支持所有状态,如果一个子窗口是编辑框它就不能被check)。菜单项可以被设置为默认状态,这样它的文字会被加重显示。

要使用UI updating需要做四件事:

  1. 主窗口需要继承CUpdateUI 和 CIdleHandler
  2. 将 CMainFrame 的消息链入 CUpdateUI
  3. 将主窗口添加到模块的空闲处理队列
  4. 在主窗口中添加 UPDATE_UI_MAP 宏

向导生成的代码已经为我们做了三件事,现在我们只需要决定那个菜单项需要更新和他们是么时候可用什么时候不可用。

添加控制时钟的新菜单项

在菜单条添加一个Clock菜单,它有两个菜单项:IDC_START and IDC_STOP:

WTL 详细介绍_第8张图片

然后在UPDATE_UI_MAP宏中为每个菜单项添加一个入口:

[cpp] view plain copy print ?
  1. class CMainFrame : public ...  
  2. {  
  3. public:  
  4.     // ...   
  5.     BEGIN_UPDATE_UI_MAP(CMainFrame)  
  6.         UPDATE_ELEMENT(IDC_START, UPDUI_MENUPOPUP)  
  7.         UPDATE_ELEMENT(IDC_STOP, UPDUI_MENUPOPUP)  
  8.     END_UPDATE_UI_MAP()  
  9.     // ...   
  10. };  
class CMainFrame : public ...
{
public:
    // ...
    BEGIN_UPDATE_UI_MAP(CMainFrame)
        UPDATE_ELEMENT(IDC_START, UPDUI_MENUPOPUP)
        UPDATE_ELEMENT(IDC_STOP, UPDUI_MENUPOPUP)
    END_UPDATE_UI_MAP()
    // ...
};


我们只需要调用CUpdateUI::UIEnable()就可以改变这两个菜单项的任意一个的使能状态时。UIEnable()有两个参数,一个是界面元素的ID,另一个是标志界面元素是否可用的bool型变量(true表示可用,false表示不可用)。


这套体系比MFC的ON_UPDATE_COMMAND_UI体系笨拙一些,在MFC中我们只需编写处理函数,由MFC选择界面元素的显示状态,在WTL中我们需要告诉WTL界面元素的状态在何时改变。当然,这两个库都是在菜单将要显示的时候才应用菜单状态的改变。

调用 UIEnable()

现在返回到OnCreate()函数看看是如何设置Clock菜单的初始状态。

[cpp] view plain copy print ?
  1. LRESULT CMainFrame::OnCreate(UINT /*uMsg*/WPARAM /*wParam*/,   
  2.                              LPARAM /*lParam*/BOOL/*bHandled*/)  
  3. {  
  4.     m_hWndClient = m_view.Create(...);  
  5.     
  6.     // register object for message filtering and idle updates  
  7.     // [omitted for clarity]   
  8.    
  9.     // Set the initial state of the Clock menu items:  
  10.     UIEnable ( IDC_START, false );  
  11.     UIEnable ( IDC_STOP, true );  
  12.    
  13.     return 0;  
  14. }  
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, 
                             LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    m_hWndClient = m_view.Create(...);
  
    // register object for message filtering and idle updates
    // [omitted for clarity]
 
    // Set the initial state of the Clock menu items:
    UIEnable ( IDC_START, false );
    UIEnable ( IDC_STOP, true );
 
    return 0;
}


我们的程序开始时Clock菜单是这样的:

WTL 详细介绍_第9张图片

CMainFrame现在需要处理两个新菜单项,在视图类调用它们开始和停止时钟时处理函数需要翻转这两个菜单项的状态。这是MFC的内建消息处理无法想象的地方之一。在MFC的程序中,所有的界面更新和命令消息处理必须完整的放在视图类中,但是在WTL中,主窗口类和视图类通过某种方式沟通;菜单由主窗口拥有,主窗口获得这些菜单消息并做相应的处理,要么响应这些消息,要么发送给视图类。

这种沟通是通过PreTranslateMessage()完成的,当然CMainFrame仍然要调用UIEnable()。CMainFrame可以将this指针传递给视图类,这样视图类也可以通过这个指针调用UIEnable()。在这个例子中我选择的这种解决方案导致主窗口和视图成为紧密耦合体,但是我发现这很容易理解(和解释!)。

[cpp] view plain copy print ?
  1. class CMainFrame : public ...  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMainFrame)  
  5.         // ...   
  6.         COMMAND_ID_HANDLER_EX(IDC_START, OnStart)  
  7.         COMMAND_ID_HANDLER_EX(IDC_STOP, OnStop)  
  8.     END_MSG_MAP()  
  9.    
  10.     // ...   
  11.     void OnStart(UINT uCode, int nID, HWND hwndCtrl);  
  12.     void OnStop(UINT uCode, int nID, HWND hwndCtrl);  
  13. };  
  14.    
  15. void CMainFrame::OnStart(UINT uCode, int nID, HWND hwndCtrl)  
  16. {  
  17.     // Enable Stop and disable Start  
  18.     UIEnable ( IDC_START, false );  
  19.     UIEnable ( IDC_STOP, true );  
  20.    
  21.     // Tell the view to start its clock.  
  22.     m_view.StartClock();  
  23. }  
  24.    
  25. void CMainFrame::OnStop(UINT uCode, int nID, HWND hwndCtrl)  
  26. {  
  27.     // Enable Start and disable Stop  
  28.     UIEnable ( IDC_START, true );  
  29.     UIEnable ( IDC_STOP, false );  
  30.    
  31.     // Tell the view to stop its clock.  
  32.     m_view.StopClock();  
  33. }  
class CMainFrame : public ...
{
public:
    BEGIN_MSG_MAP_EX(CMainFrame)
        // ...
        COMMAND_ID_HANDLER_EX(IDC_START, OnStart)
        COMMAND_ID_HANDLER_EX(IDC_STOP, OnStop)
    END_MSG_MAP()
 
    // ...
    void OnStart(UINT uCode, int nID, HWND hwndCtrl);
    void OnStop(UINT uCode, int nID, HWND hwndCtrl);
};
 
void CMainFrame::OnStart(UINT uCode, int nID, HWND hwndCtrl)
{
    // Enable Stop and disable Start
    UIEnable ( IDC_START, false );
    UIEnable ( IDC_STOP, true );
 
    // Tell the view to start its clock.
    m_view.StartClock();
}
 
void CMainFrame::OnStop(UINT uCode, int nID, HWND hwndCtrl)
{
    // Enable Start and disable Stop
    UIEnable ( IDC_START, true );
    UIEnable ( IDC_STOP, false );
 
    // Tell the view to stop its clock.
    m_view.StopClock();
}


每个处理函数都更新Clock菜单,然后在视图类中调用一个方法,选择在视图类中使用是因为时钟是由视图类控制得。StartClock() 和 StopClock()得代码没有列出,但可以在这个工程得例子代码中找到它们。

消息映射链中最后需要注意的地方

如果你使用VC 6,你会注意到将BEGIN_MSG_MAP改为BEGIN_MSG_MAP_EX后ClassView显得有些杂乱无章:

WTL 详细介绍_第10张图片

出现这种情况是因为ClassView不能解释BEGIN_MSG_MAP_EX宏,它以为所有得WTL消息映射宏是函数定义。你可以将宏改回为BEGIN_MSG_MAP并在stdafx.h文件得结尾处添加这两行代码来解决这个问题:

[cpp] view plain copy print ?
  1. #undef BEGIN_MSG_MAP   
  2. #define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x)  
#undef BEGIN_MSG_MAP
#define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x)


下一站, 1995

我们现在只是掀起了WTL的一角,在下一篇文章我会为我们的时钟程序添加一些Windows 95的界面标准,比如工具条和状态条,同时体验一下CUpdateUI的新东西。例如试着用UISetCheck()代替UIEnable(),看看菜单项会有什么变化。

对第三部分的介绍

自从作为Windows 95的通用控件出现以来,工具条和状态条就变成了很普遍的事物。由于MFC支持浮动的工具条从而使它们更受欢迎。随着通用控件的更新,Rebars(最初被称为Coollbar)使得工具条有了另一种展示方式。在第三部分,我将介绍WTL对这些控制条的支持和如何在你的程序中使用它们。

主窗口的工具条和状态条

CFrameWindowImpl有三个HWND类型的成员变量在窗口创建时被初始化,我们已经见过m_hWndClient,它是填充主窗口客户区的“视图”窗口的句柄,现在我们遇到了另外两个:

  • m_hWndToolBar: 工具条或Rebar的窗口句柄
  • m_hWndStatusBar: 状态条的窗口句柄

CFrameWindowImpl只支持一个工具条,也没有像MFC那样的可多点停靠的工具条,如果你想使用多个工具条又不想修改CFrameWindowImpl的内部代码,你就需要使用Rebar。我将介绍它们二者并演示如何使用应用程序向导添加工具条和Rebar。
CFrameWindowImpl::OnSize()消息响应函数调用了UpdateLayout(),UpdateLayout()做两件事:从新定位所有控制条和改变视图窗口的大小使之填充整个客户区。实际工作是由UpdateBarsPosition()完成的,UpdateLayout()只是调用了该函数。实现的代码相当简单,向工具条和状态条发送WM_SIZE消息,由这些控制条的默认窗口处理过程将它们定位到主窗口的顶部或底部。
当你告诉应用程序向导给你的窗口添加工具条和状态条时,向导就在CMainFrame::OnCreate()中添加了创建它们的代码。现在我们来看看这些代码,当然是为了再写一个时钟程序。

向导为工具条和状态条生成得代码

我们将开始一个新的工程,让向导为主窗口创建工具条和状态条。首先创建一个名为WTLClock2的新工程,在向导的第一页,选SDI并使“生成CPP文件”检查框被选中:

WTL 详细介绍_第11张图片

在第二页,取消Rebar使向导仅仅创建一个普通的工具条:

WTL 详细介绍_第12张图片

从第二部分的程序中复制相应的代码,新程序看起来是这样的:

WTL 详细介绍_第13张图片

CMainFraCMainFrame 如何创建工具条和状态条


在这个例子中,向导向CMainFrame::OnCreate()函数添加了更多的代码,这些代码的作用就是创建控制条并通知CUpdateUI更新工具条上的按钮。

[cpp] view plain copy print ?
  1. LRESULT CMainFrame::OnCreate(UINT /*uMsg*/WPARAM /*wParam*/,   
  2.                              LPARAM /*lParam*/BOOL/*bHandled*/)  
  3. {  
  4.     CreateSimpleToolBar();  
  5.     CreateSimpleStatusBar();  
  6.   
  7.     m_hWndClient = m_view.Create(...);  
  8.   
  9. // ...   
  10.   
  11.     // register object for message filtering and idle updates  
  12.     CMessageLoop* pLoop = _Module.GetMessageLoop();  
  13.     ATLASSERT(pLoop != NULL);  
  14.     pLoop->AddMessageFilter(this);  
  15.     pLoop->AddIdleHandler(this);  
  16.   
  17.     return 0;  
  18. }  
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, 
                             LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    CreateSimpleToolBar();
    CreateSimpleStatusBar();

    m_hWndClient = m_view.Create(...);

// ...

    // register object for message filtering and idle updates
    CMessageLoop* pLoop = _Module.GetMessageLoop();
    ATLASSERT(pLoop != NULL);
    pLoop->AddMessageFilter(this);
    pLoop->AddIdleHandler(this);

    return 0;
}


这是新添加的代码的开始部分,CFrameWindowImpl::CreateSimpleToolBar()函数使用资源IDR_MAINFRAME创建工具条并将其句柄赋值给m_hWndToolBar,下面是CreateSimpleToolBar()函数的代码:

[cpp] view plain copy print ?
  1. BOOL CFrameWindowImpl::CreateSimpleToolBar(  
  2.     UINT nResourceID = 0,   
  3.     DWORD dwStyle = ATL_SIMPLE_TOOLBAR_STYLE,   
  4.     UINT nID = ATL_IDW_TOOLBAR)  
  5. {  
  6.     ATLASSERT(!::IsWindow(m_hWndToolBar));  
  7.    
  8.     if(nResourceID == 0)  
  9.         nResourceID = T::GetWndClassInfo().m_uCommonResourceID;  
  10.    
  11.     m_hWndToolBar = T::CreateSimpleToolBarCtrl(m_hWnd, nResourceID, TRUE,   
  12.                                                dwStyle, nID);  
  13.     return (m_hWndToolBar != NULL);  
  14. }  
BOOL CFrameWindowImpl::CreateSimpleToolBar(
    UINT nResourceID = 0, 
    DWORD dwStyle = ATL_SIMPLE_TOOLBAR_STYLE, 
    UINT nID = ATL_IDW_TOOLBAR)
{
    ATLASSERT(!::IsWindow(m_hWndToolBar));
 
    if(nResourceID == 0)
        nResourceID = T::GetWndClassInfo().m_uCommonResourceID;
 
    m_hWndToolBar = T::CreateSimpleToolBarCtrl(m_hWnd, nResourceID, TRUE, 
                                               dwStyle, nID);
    return (m_hWndToolBar != NULL);
}


参数:

nResourceID
工具条资源得ID。如果使用默认值0作为参数,程序将使用DECLARE_FRAME_WND_CLASS宏指定得资源,这里使用的IDR_MAINFRAME是向导生成的代码。
dwStyle
工具条的类型或样式。默认值ATL_SIMPLE_TOOLBAR_STYLE被定义为TBSTYLE_TOOLTIPS,子窗口和可见三种风格的结合,这使得鼠标移到按钮上时工具条会弹出工具提示。
nID
工具条的窗口ID,通常都会使用默认值。


CreateSimpleToolBar()首先检查是否已经创建了一个工具条,然后调用CreateSimpleToolBarCtrl()函数创建工具条控制,CreateSimpleToolBarCtrl()返回的工具条控制句柄保存在m_hWndToolBar中。CreateSimpleToolBarCtrl()负责读出资源并创建相应的工具条按钮,然后返回工具条窗口的句柄。这部分的代码相当长,我不在这里做具体介绍,如果你对此感兴趣得话何以在atlframe.h中找到这些代码。

OnCreate()函数接下来会调用CFrameWindowImpl::CreateSimpleStatusBar()函数,此函数创建状态条并将句柄存在m_hWndStatusBar,下面是该函数的代码:

[cpp] view plain copy print ?
  1. BOOL CFrameWindowImpl::CreateSimpleStatusBar(  
  2.     UINT nTextID = ATL_IDS_IDLEMESSAGE,   
  3.     DWORD dwStyle = ... SBARS_SIZEGRIP,   
  4.     UINT nID = ATL_IDW_STATUS_BAR)  
  5. {  
  6.     TCHAR szText[128];    // max text lentgth is 127 for status bars  
  7.     szText[0] = 0;  
  8.     ::LoadString(_Module.GetResourceInstance(), nTextID, szText, 128);  
  9.     return CreateSimpleStatusBar(szText, dwStyle, nID);  
  10. }  
BOOL CFrameWindowImpl::CreateSimpleStatusBar(
    UINT nTextID = ATL_IDS_IDLEMESSAGE, 
    DWORD dwStyle = ... SBARS_SIZEGRIP, 
    UINT nID = ATL_IDW_STATUS_BAR)
{
    TCHAR szText[128];    // max text lentgth is 127 for status bars
    szText[0] = 0;
    ::LoadString(_Module.GetResourceInstance(), nTextID, szText, 128);
    return CreateSimpleStatusBar(szText, dwStyle, nID);
}


显示在状态条的文字是从字符串资源中装载的,这个函数的参数是:

nTextID
用于在状态条上显示的字符串的资源ID,向导生成的ATL_IDS_IDLEMESSAGE对应的字符串是“Ready”。
dwStyle
状态条的样式。默认值包含了SBARS_SIZEGRIP风格,这使得状态条的右下角会显示一个改变窗口大小的标志。
nID
状态条的窗口ID,通常都会使用默认值。


CreateSimpleStatusBar()调用另外一个重载函数创建状态条:

[cpp] view plain copy print ?
  1. BOOL CFrameWindowImpl::CreateSimpleStatusBar(  
  2.     LPCTSTR lpstrText,  
  3.     DWORD dwStyle = ... SBARS_SIZEGRIP,  
  4.     UINT nID = ATL_IDW_STATUS_BAR)  
  5. {  
  6.     ATLASSERT(!::IsWindow(m_hWndStatusBar));  
  7.     m_hWndStatusBar = ::CreateStatusWindow(dwStyle, lpstrText, m_hWnd, nID);  
  8.     return (m_hWndStatusBar != NULL);  
  9. }  
BOOL CFrameWindowImpl::CreateSimpleStatusBar(
    LPCTSTR lpstrText,
    DWORD dwStyle = ... SBARS_SIZEGRIP,
    UINT nID = ATL_IDW_STATUS_BAR)
{
    ATLASSERT(!::IsWindow(m_hWndStatusBar));
    m_hWndStatusBar = ::CreateStatusWindow(dwStyle, lpstrText, m_hWnd, nID);
    return (m_hWndStatusBar != NULL);
}


这个重载的版本首先检查是否已经创建了状态条,然后调用CreateStatusWindow()创建状态条,状态条的句柄存放在m_hWndStatusBar中。

显示和隐藏工具条和状态条

CMainFrame类也有一个视图菜单,它有两个命令:显示/隐藏工具条和状态条,它们的ID是ID_VIEW_TOOLBAR和ID_VIEW_STATUS_BAR。CMainFrame类有这两个命令的响应函数,分别显示和隐藏相应的控制条,下面是OnViewToolBar()函数的代码:

[cpp] view plain copy print ?
  1. LRESULT CMainFrame::OnViewToolBar(WORD /*wNotifyCode*/WORD /*wID*/,   
  2.                                   HWND /*hWndCtl*/BOOL/*bHandled*/)  
  3. {  
  4.     BOOL bVisible = !::IsWindowVisible(m_hWndToolBar);  
  5.     ::ShowWindow(m_hWndToolBar, bVisible ? SW_SHOWNOACTIVATE : SW_HIDE);  
  6.     UISetCheck(ID_VIEW_TOOLBAR, bVisible);  
  7.     UpdateLayout();  
  8.     return 0;  
  9. }  
LRESULT CMainFrame::OnViewToolBar(WORD /*wNotifyCode*/, WORD /*wID*/, 
                                  HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
    BOOL bVisible = !::IsWindowVisible(m_hWndToolBar);
    ::ShowWindow(m_hWndToolBar, bVisible ? SW_SHOWNOACTIVATE : SW_HIDE);
    UISetCheck(ID_VIEW_TOOLBAR, bVisible);
    UpdateLayout();
    return 0;
}


这些代码翻转控制条的显示状态,相应的翻转View|Toolbar菜单上的检查标记,然后调用UpdateLayout()重新定位控制条并改变视图窗口的大小。

工具条和状态条的内在特征

MFC的框架提供了很多好的特性,例如工具条按钮的工具提示和菜单项的掠过式帮助。WTL中相对应的功能实现在CFrameWindowImpl类中。下面的屏幕截图显示了工具提示和掠过式帮助。

WTL 详细介绍_第14张图片 WTL 详细介绍_第15张图片

CFrameWindowImplBase类有两个消息相应函数用来实现这些功能,OnMenuSelect()处理WM_MENUSELECT消息,它像MFC那样查找掠过式帮助的字符串:首先装载与菜单资源ID相同的字符串资源,在字符串中查找 \n 字符,使用\n之前的内容作为掠过帮助的内容。OnToolTipTextA() 和 OnToolTipTextW() 函数分别响应 TTN_GETDISPINFOA消息和TTN_GETDISPINFOW消息,提供工具条按钮的工具提示。这两个处理函数和OnMenuSelect()函数一样装载相应的字符串,只是使用\n后面的字符串。(边注:OnMenuSelect()和OnToolTipTextA()函数对于DBCS字符是不安全的,因为它在查找\n字符时没有检查DBCS字符串的头部和尾部)下面是工具条及其关联的帮助字符串的例子:

WTL 详细介绍_第16张图片

创建不同样式的工具条

如果你不喜欢在工具条上显示3D按钮(尽管从可用性观点来看平面的界面元素是件糟糕的事情),你可以通过改变CreateSimpleToolBar()函数的参数来改变工具条的样式。例如,你可以在CMainFrame::OnCreate()使用如下代码创建一个IE风格的工具条:

[cpp] view plain copy print ?
  1. CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE |   
  2.                                TBSTYLE_FLAT | TBSTYLE_LIST );  
CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE | 
                               TBSTYLE_FLAT | TBSTYLE_LIST );


如果你使用向导为你的程序添加了manifest文件,它就会在Windows XP系统上使用6.0版的通用控件,你不能选择按钮的类型,工具条会自动使用平面按钮即使你创建工具条时没有添加TBSTYLE_FLAT风格。

工具条编辑器

正如我们前面所见,向导为我们的程序创建了几个默认的按钮,当然只有About按钮有事件处理。你可以像在MFC的工程中一样使用工具条编辑器修改工具条资源,CreateSimpleToolBarCtrl()用这个工具条资源创建工具条。下面是向导生成的工具条在编辑器中的样子:

WTL 详细介绍_第17张图片

对于我们的时钟程序,我们添加四个按钮,两个按钮用来改变视图窗口的颜色,另外两个用来显示/隐藏工具条和状态条。下面是我们的新工具条:

WTL 详细介绍_第18张图片

这些按钮是:

  • IDC_CP_COLORS: 将视图窗口颜色改为CodeProject网站的颜色
  • IDC_BW_COLORS: 将视图窗口颜色改为黑白颜色
  • ID_VIEW_STATUS_BAR: 显示或隐藏状态条
  • ID_VIEW_TOOLBAR: 显示或隐藏工具条

前两个按钮都有相应的菜单项,它们都调用视图类的一个新函数SetColor(),向这个函数传递前景颜色和背景颜色,视图窗口用这两个参数改变窗口的显示。响应这两个按钮的处理函数与响应相应的菜单项的处理函数在使用COMMAND_ID_HANDLER_EX宏上没有区别,你可以查看例子工程的代码了解这些消息处理的细节。在下一节我将介绍状态条和工具条按钮的UI状态更新,使它们能够反映工具条或状态条当前的状态。

工具条按钮的UI状态更新

向导生成的代码已经为CMainFrame添加了对View|Toolbar和View|Status Bar两个菜单项的Check和Uncheck的UI更新处理。这和第二章的程序一样:对CMainFrame类的两个命令使用UI更新的宏:

[cpp] view plain copy print ?
  1. BEGIN_UPDATE_UI_MAP(CMainFrame)  
  2.     UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)  
  3.     UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)  
  4. END_UPDATE_UI_MAP()  
BEGIN_UPDATE_UI_MAP(CMainFrame)
    UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)
    UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()


我们的时钟程序的工具条按钮与对应的菜单项有相同的ID,所以第一步就是为每个宏添加UPDUI_TOOLBAR标志:

[cpp] view plain copy print ?
  1. BEGIN_UPDATE_UI_MAP(CMainFrame)  
  2.     UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)  
  3.     UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)  
  4. END_UPDATE_UI_MAP()  
BEGIN_UPDATE_UI_MAP(CMainFrame)
    UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
    UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
END_UPDATE_UI_MAP()


还需要添加两个函数响应工具条按钮的更新,但幸运的是向导已经为我们做了,所以如果此时编译这个程序,菜单项和工具条按钮都会更新。

使一个工具条支持UI状态更新

如果查看CMainFrame::OnCreate()的代码你就会发现一段新的代码,这段代码设置了两个菜单项的初始状态:

[cpp] view plain copy print ?
  1. LRESULT CMainFrame::OnCreate( ... )  
  2. {  
  3. // ...   
  4.     m_hWndClient = m_view.Create(...);  
  5.    
  6.     UIAddToolBar(m_hWndToolBar);  
  7.     UISetCheck(ID_VIEW_TOOLBAR, 1);  
  8.     UISetCheck(ID_VIEW_STATUS_BAR, 1);  
  9. // ...   
  10. }  
LRESULT CMainFrame::OnCreate( ... )
{
// ...
    m_hWndClient = m_view.Create(...);
 
    UIAddToolBar(m_hWndToolBar);
    UISetCheck(ID_VIEW_TOOLBAR, 1);
    UISetCheck(ID_VIEW_STATUS_BAR, 1);
// ...
}


UIAddToolBar()将工具条的窗口句柄传给CUpdateUI,所以当需要更新按钮的状态时CUpdateUI会向这个窗口发消息。另一个重要的调用位于OnIdle()中:

[cpp] view plain copy print ?
  1. BOOL CMainFrame::OnIdle()  
  2. {  
  3.     UIUpdateToolBar();  
  4.     return FALSE;  
  5. }  
BOOL CMainFrame::OnIdle()
{
    UIUpdateToolBar();
    return FALSE;
}


当消息队列中没有消息等待时CMessageLoop::Run()就会调用OnIdle(),UIUpdateToolBar()遍历UI更新表,寻找那些带有UPDUI_TOOLBAR标志又被UISetCheck()之类的函数改变了状态的的界面元素(当然是工具条),相应的改变按钮的状态。需要注意得是如果更新弹出式菜单的状态就不需要做以上两步,因为CUpdateUI响应WM_INITMENUPOPUP消息,只有接到此消息时才更新菜单状态。

如果查看例子代码就会发现它也演示了如何更新框架窗口的菜单条上的顶级菜单项的状态。有一个菜单项是执行Start和Stop命令,起到开始和停止时钟的作用,当然这需要做一些不平常的事情:菜单条上的菜单项总是处于弹出状态。为了完整的介绍CUpdateUI我将它们也加进例子代码中,要了解它们可以查找对UIAddMenuBar()和UIUpdateMenuBar()两个函数的调用。

使用Rebar代替简单的工具条

CFrameWindowImpl也支持使用Rebar控件,使你的程序看起来像IE,使用Rebar也是在程序中使用多个工具条的一个方法(译者加:前面讲过,另一个方法就是修改WTL的源代码)。要使用Rebar需要在向导的第二页选上支持Rebar的检查框,如下所示:

WTL 详细介绍_第19张图片

第二个例子工程WTLClock3就使用了Rebar控件,如果你正在跟着例子代码学习,那现在就打开WTLClock3。

你首先会注意到创建工具条的代码有些不同,出现这种感觉是因为我们在程序中使用了rebar。以下是相关的代码:

[cpp] view plain copy print ?
  1. LRESULT CMainFrame::OnCreate(...)  
  2. {  
  3.     HWND hWndToolBar = CreateSimpleToolBarCtrl ( m_hWnd,   
  4.                            IDR_MAINFRAME, FALSE,   
  5.                            ATL_SIMPLE_TOOLBAR_PANE_STYLE );  
  6.    
  7.     CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);  
  8.     AddSimpleReBarBand(hWndToolBar);  
  9. // ...   
  10. }  
LRESULT CMainFrame::OnCreate(...)
{
    HWND hWndToolBar = CreateSimpleToolBarCtrl ( m_hWnd, 
                           IDR_MAINFRAME, FALSE, 
                           ATL_SIMPLE_TOOLBAR_PANE_STYLE );
 
    CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
    AddSimpleReBarBand(hWndToolBar);
// ...
}


代码从创建工具条开始,只是使用了不同的风格,也就是ATL_SIMPLE_TOOLBAR_PANE_STYLE,它定义在atlframe.h文件中,与ATL_SIMPLE_TOOLBAR_STYLE风格相似,只是附加了一些诸如CCS_NOPARENTALIGN之类的风格,这是使工具条作为Rebar的子窗口能够正常工作所必需的风格。

下一行代码是调用CreateSimpleReBar()函数,该函数创建Rebar控件并将句柄存到m_hWndToolBar中。接下来调用AddSimpleReBarBand()函数为Rebar创建一个条位并告诉Rebar这个条位上是一个工具条。

WTL 详细介绍_第20张图片

CMainFrame::OnViewToolBar()函数也有些不同,它只隐藏Rebar上工具条所在的条位而不是隐藏m_hWndToolBar(如果隐藏m_hWndToolBar将隐藏整个Rebar而不仅仅是工具条)。

如果你使用多个工具条,只需像向导为我们生成的关于第一个工具条的代码那在OnCreate()创建它们并调用AddSimpleReBarBand()添加到Rebar就行了。CFrameWindowImpl使用标准的Rebar控件,不像MFC那样支持可停靠的工具条,你所能作得就是排列这些工具条在Rebar中的位置。

多窗格的状态条

WTL另有一个状态条类实现多窗格的状态条,与MFC的默认的状态条一样有CAPS,LOCK和NUM LOCK指示器,这个类就是CMultiPaneStatusBarCtrl,在WTLClock3例子工程中演示了如何使用这个类。这个类支持有限的UI更新,当弹出式菜单被显示时有“Default”属性的窗格会延伸到整个状态条的宽度用于显示菜单的掠过式帮助。

第一步就是在CMainFrame中声明一个CMultiPaneStatusBarCtrl类型的成员变量:

[cpp] view plain copy print ?
  1. class CMainFrame : public ...  
  2. {  
  3. //...   
  4. protected:  
  5.     CMultiPaneStatusBarCtrl m_wndStatusBar;  
  6. };  
class CMainFrame : public ...
{
//...
protected:
    CMultiPaneStatusBarCtrl m_wndStatusBar;
};


接着在OnCreate()中创建状态条并这只UI更新:

[cpp] view plain copy print ?
  1. m_hWndStatusBar = m_wndStatusBar.Create ( *this );  
  2. UIAddStatusBar ( m_hWndStatusBar );  
    m_hWndStatusBar = m_wndStatusBar.Create ( *this );
    UIAddStatusBar ( m_hWndStatusBar );


就像CreateSimpleStatusBar()函数做得那样,我们也将状态条的句柄存放在m_hWndStatusBar中。

下一步就是调用CMultiPaneStatusBarCtrl::SetPanes()函数建立窗格:

[cpp] view plain copy print ?
  1. BOOL SetPanes(int* pPanes, int nPanes, bool bSetText = true);  
BOOL SetPanes(int* pPanes, int nPanes, bool bSetText = true);


参数:

pPanes
存放窗格ID的数组
nPanes
窗格ID数组中元素的个数(译者加:就是窗格数)
bSetText
如果是true,所有的窗格被立即设置文字,这一点将在下面解释。


窗格ID可以是ID_DEFAULT_PANE,此ID用于创建支持掠过式帮助的窗格,窗格ID也可以是字符串资源ID。对于非默认的窗格WTL装载这个ID对应的字符串并计算宽度,并将窗格设置为相应的宽度,这和MFC使用的逻辑是一样的。

bSetText控制着窗格是否立即显示相关的字符串,如果是true,SetPanes()显示每个窗格的字符串,否则窗格就被置空。

下面是我们对SetPanes()的调用:

[cpp] view plain copy print ?
  1. // Create the status bar panes.   
  2. anPanes[] = { ID_DEFAULT_PANE, IDPANE_STATUS,   
  3.               IDPANE_CAPS_INDICATOR };  
  4.   
  5. m_wndStatusBar.SetPanes ( anPanes, 3, false );  
    // Create the status bar panes.
int anPanes[] = { ID_DEFAULT_PANE, IDPANE_STATUS, 
                  IDPANE_CAPS_INDICATOR };
 
    m_wndStatusBar.SetPanes ( anPanes, 3, false );


IDPANE_STATUS对应的字符串是“@@@@”,这样应该有足够的宽度(希望是)显示两个时钟状态字符串“Running”和“Stopped”。和MFC一样,你需要自己估算窗格的宽度,IDPANE_CAPS_INDICATOR对应的字符串是“CAPS”。

窗格的UI状态更新

为了更新窗格上的文本,我们需要将相应的窗格添加到UI更新表:

[cpp] view plain copy print ?
  1. BEGIN_UPDATE_UI_MAP(CMainFrame)  
  2.     //...   
  3.     UPDATE_ELEMENT(1, UPDUI_STATUSBAR)  // clock status  
  4.     UPDATE_ELEMENT(2, UPDUI_STATUSBAR)  // CAPS indicator  
  5. END_UPDATE_UI_MAP()  
    BEGIN_UPDATE_UI_MAP(CMainFrame)
        //...
        UPDATE_ELEMENT(1, UPDUI_STATUSBAR)  // clock status
        UPDATE_ELEMENT(2, UPDUI_STATUSBAR)  // CAPS indicator
    END_UPDATE_UI_MAP()


这个宏的第一个参数是窗格的索引而不是ID,这很不幸,因为如果你重新排列了窗格,你要记得更新UI更新表。

由于我们在调用SetPanes()是第三个参数是false,所以窗格初始是空的。我们下一步要做得就是将时钟状态窗格的初始文本设为“Running”

[cpp] view plain copy print ?
  1. // Set the initial text for the clock status pane.  
  2. UISetText ( 1, _T("Running") );  
    // Set the initial text for the clock status pane.
    UISetText ( 1, _T("Running") );


和前面一样,第一个参数是窗格的索引。UISetText()是状态条唯一支持的UI更新函数。

最后,在CMainFrame::OnIdle()中添加对UIUpdateStatusBar()函数的调用,使状态条的窗格能够在空闲时间被更新:

[cpp] view plain copy print ?
  1. BOOL CMainFrame::OnIdle()  
  2. {  
  3.     UIUpdateToolBar();  
  4.     UIUpdateStatusBar();  
  5.     return FALSE;  
  6. }  
BOOL CMainFrame::OnIdle()
{
    UIUpdateToolBar();
    UIUpdateStatusBar();
    return FALSE;
}


当你使用UIUpdateStatusBar()时CUpdateUI的一个问题就暴露出来了--菜单项的文本在调用UISetText()后没有改变!如果你在看WTLClock3工程的代码,时钟的开始/停止菜单项被移到了Clock菜单,在菜单项命令的响应处理函数中设置菜单项的文本。无论如何,如果当前调用的是UIUpdateStatusBar(),那么对UISetText()的调用就不会起作用。我没有研究这个问题是否可以被修复,所以如果你打算改变菜单的文本,你需要留意这个地方。

最后,我们需要检查CAPS LOCK键的状态,更新相应的两个窗格。这些代码是通过OnIdle()被调用的,所以程序会在每次空闲时间检查它们的状态。

[cpp] view plain copy print ?
  1. BOOL CMainFrame::OnIdle()  
  2. {  
  3.     // Check the current Caps Lock state, and if it is on, show the  
  4.     // CAPS indicator in pane 2 of the status bar.  
  5.     if ( GetKeyState(VK_CAPITAL) & 1 )  
  6.         UISetText ( 2, CString(LPCTSTR(IDPANE_CAPS_INDICATOR)) );  
  7.     else  
  8.         UISetText ( 2, _T("") );  
  9.    
  10.     UIUpdateToolBar();  
  11.     UIUpdateStatusBar();  
  12.     return FALSE;  
  13. }  
BOOL CMainFrame::OnIdle()
{
    // Check the current Caps Lock state, and if it is on, show the
    // CAPS indicator in pane 2 of the status bar.
    if ( GetKeyState(VK_CAPITAL) & 1 )
        UISetText ( 2, CString(LPCTSTR(IDPANE_CAPS_INDICATOR)) );
    else
        UISetText ( 2, _T("") );
 
    UIUpdateToolBar();
    UIUpdateStatusBar();
    return FALSE;
}


第一次调用UISetText()时将从字符串资源中装载“CAPS”字符串,但是在CString的构造函数中使用了一个机灵的窍门(有充分的文档说明)。

在完成所有的代码之后,状态条看起来是这个样子:

WTL 详细介绍_第21张图片

承上启下:有关对话框的话题

在第四章我将介绍对话框的用法(包括ATL的类和WTL的增强功能),控件的包装类和WTL有关对话框消息处理的改进。

 

对第四章的介绍

MFC 的对话框和控件的封装真得可以节省你很多时间和功夫。没有MFC对控件的封装,你要操作控件就得耐着性子填写各种结构并写很多的SendMessage调用。MFC还提供了对话框数据交换(DDX),它可以在控件和变量之间传输数据。WTL 当然也提供了这些功能,并对控件的封装做了很多改进。本文将着眼于一个基于对话框的程序演示你以前用MFC实现的功能,除此之外还有WTL消息处理的增强功能。第五章将介绍高级界面特性和WTL对新控件的封装。

回顾一下ATL的对话框

现在回顾一下第一章提到的两个对话框类,CDialogImpl 和 CAxDialogImpl。CAxDialogImpl用于包含ActiveX控件的对话框。本文不准备介绍ActiveX控件,所以只使用CDialogImpl。

创建一个对话框需要做三件事:

  1. 创建一个对话框资源
  2. 从CDialogImpl类派生一个新类
  3. 添加一个公有成员变量IDD,将它设置为对话框资源的ID.

然后就像主框架窗口那样添加消息处理函数,WTL没有改变这些,不过确实添加了一些其他能够在对话框中使用得特性。

通用控件的封装类

WTL有许多控件的封装类对你应该比较熟悉,因为它们使用与MFC相同(或几乎相同)的名字。控件的方法的命名也和MFC一样,所以你可以参照MFC的文档使用这些WTL的封装类。不足之处是F12键不能方便地跳到类的定义代码处。

下面是Windows内建控件的封装类:

  • 用户控件: CStatic, CButton, CListBox, CComboBox, CEdit, CScrollBar, CDragListBox
  • 通用控件: CImageList, CListViewCtrl (CListCtrl in MFC), CTreeViewCtrl (CTreeCtrl in MFC), CHeaderCtrl, CToolBarCtrl, CStatusBarCtrl, CTabCtrl, CToolTipCtrl, CTrackBarCtrl (CSliderCtrl in MFC), CUpDownCtrl (CSpinButtonCtrl in MFC), CProgressBarCtrl, CHotKeyCtrl, CAnimateCtrl, CRichEditCtrl, CReBarCtrl, CComboBoxEx, CDateTimePickerCtrl, CMonthCalendarCtrl, CIPAddressCtrl
  • MFC中没有的封装类: CPagerCtrl, CFlatScrollBar, CLinkCtrl (clickable hyperlink, available on XP only)

还有一些是WTL特有的类:CBitmapButton, CCheckListViewCtrl (带检查选择框的list控件), CTreeViewCtrlEx 和 CTreeItem (通常一起使用, CTreeItem 封装了HTREEITEM), CHyperLink (类似于网页上的超链接对象,支持所有操作系统)
需要注意得一点是大多数封装类都是基于CWindow接口的,和CWindow一样,它们封装了HWND并对控件的消息进行了封装(例如,CListBox::GetCurSel()封装了LB_GETCURSEL消息)。所以和CWindow一样,创建一个控件的封装对象并将它与已经存在的控件关联起来只占用很少的资源,当然也和CWindow一样,控件封装对象销毁时不销毁控件本身。也有一些例外,如CBitmapButton, CCheckListViewCtrl和CHyperLink。

由于这些文章定位于有经验的MFC程序员,我就不浪费时间介绍这些封装类,它们和MFC相应的控件封装相似。当然我会介绍WTL的新类:CBitmapButtonCBitmapButton类与MFC的同名类有很大的不同,CHyperLink则完全是新事物。

用应用程序向导生成基于对话框的程序

运行VC并启动WTL应用向导,相信你在做时钟程序时已经用过它了,为我们的新程序命名为ControlMania1。在向导的第一页选择基于对话框的应用,还要选择是使用模式对话框还是使用非模式对话框。它们有很大的区别,我将在第五章介绍它们的不同,现在我们选择简单的一种:模式对话框。如下所示选择模式对话框和生成CPP文件选项:

WTL 详细介绍_第22张图片

第二页上所有的选项只对主窗口是框架窗口时有意义,现在它们是不可用状态,单击"Finish",再单击"OK"完成向导。

正如你想的那样,向导生成的基于对话框程序的代码非常简单。_tWinMain()函数在ControlMania1.cpp中,下面是重要的部分:

[cpp] view plain copy print ?
  1. int WINAPI _tWinMain (   
  2.     HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/,   
  3.     LPTSTR lpstrCmdLine, int nCmdShow )  
  4. {  
  5.     HRESULT hRes = ::CoInitialize(NULL);  
  6.    
  7.     AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES);  
  8.    
  9.     hRes = _Module.Init(NULL, hInstance);  
  10.    
  11.     int nRet = 0;  
  12.     // BLOCK: Run application   
  13.     {  
  14.         CMainDlg dlgMain;  
  15.         nRet = dlgMain.DoModal();  
  16.     }  
  17.    
  18.     _Module.Term();  
  19.     ::CoUninitialize();  
  20.     return nRet;  
  21. }  
int WINAPI _tWinMain ( 
    HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, 
    LPTSTR lpstrCmdLine, int nCmdShow )
{
    HRESULT hRes = ::CoInitialize(NULL);
 
    AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES);
 
    hRes = _Module.Init(NULL, hInstance);
 
    int nRet = 0;
    // BLOCK: Run application
    {
        CMainDlg dlgMain;
        nRet = dlgMain.DoModal();
    }
 
    _Module.Term();
    ::CoUninitialize();
    return nRet;
}


代码首先初始化COM并创建一个单线程公寓,这对于使用ActiveX控件的对话框是有必要得,接着调用WTL的功能函数AtlInitCommonControls(),这个函数是对InitCommonControlsEx()的封装。全局对象_Module被初始化,主对话框显示出来。(注意所有使用DoModal()创建的ATL对话框实际上是模式的,这不像MFC,MFC的所有对话框是非模式的,MFC通过代码禁用对话框的父窗口来模拟模式对话框的行为)最后,_Module和COM被释放,DoModal()的返回值被用来作为程序的结束码。

将CMainDlg变量放在一个区块中是很重要的,因为CMainDlg可能有成员使用了ATL和WTL的特性,这些成员在析构时也会用到ATL/WTL的特性,如果不使用区块,CMainDlg将在_Module.Term()(这个函数完成ATL/WTL的清理工作)调用之后调用析构函数销毁自己(和成员),并试图使用ATL/WTL的特性,这将导致程序出现诊断错误崩溃。(WTL 3的向导生成的代码没有使用区块,使得我的一些程序在结束时崩溃)

你现在可以编译并运行这个程序,尽管它只是一个简陋的对话框:

WTL 详细介绍_第23张图片

CMainDlg 的代码处理了WM_INITDIALOG, WM_CLOSE和三个按钮的消息,如果你喜欢可以浏览一下这些代码,你应该能够看懂CMainDlg的声明,它的消息映射和它的消息处理函数。

这个简单的工程还演示了如何将控件和变量联系起来,这个程序使用了几个控件。在接下来的讨论中你可以随时回来查看这些图表。

WTL 详细介绍_第24张图片

由于程序使用了list view控件,所以对AtlInitCommonControls()的调用需要作些修改,将其改为:

[cpp] view plain copy print ?
  1. AtlInitCommonControls ( ICC_WIN95_CLASSES );  
 AtlInitCommonControls ( ICC_WIN95_CLASSES );


虽然这样注册的控件类比我们用到的多,但是当我们向对话框添加不同类型的控件时就不用随时记得添加名为ICC_*的常量(译者加:以ICC_开头的一系列常量)。

使用控件的封装类

有几种方法将一个变量和控件建立关联,可以使用CWindows(或其它Window接口类,如CListViewCtrl),也可以使用CWindowImpl的派生类。如果只是需要一个临时变量就用CWindow,如果需要子类化一个控件并处理发送给该控件的消息就需要使用CWindowImpl。

ATL 方式 1 - 连接一个CWindow对象

最简单的方法是声明一个CWindow或其它window接口类,然后调用Attach()方法,还可以使用CWindow的构造函数直接将变量与控件的HWND关联起来。
下面的代码三种方法将变量和一个list控件联系起来:

[cpp] view plain copy print ?
  1. HWND hwndList = GetDlgItem(IDC_LIST);  
  2. CListViewCtrl wndList1 (hwndList);  // use constructor  
  3. CListViewCtrl wndList2, wndList3;  
  4.    
  5.   wndList2.Attach ( hwndList );     // use Attach method  
  6.   wndList3 = hwndList;              // use assignment operator  
HWND hwndList = GetDlgItem(IDC_LIST);
CListViewCtrl wndList1 (hwndList);  // use constructor
CListViewCtrl wndList2, wndList3;
 
  wndList2.Attach ( hwndList );     // use Attach method
  wndList3 = hwndList;              // use assignment operator


记住CWindow的析构函数并不销毁控件窗口,所以在变量超出作用域时不需要将其脱离控件,如果你愿意的话还可以将其作为成员变量使用:你可以在OnInitDialog()处理函数中建立变量与控件的联系。

ATL 方式 2 - 包容器窗口(CContainedWindow)

CContainedWindow是介于CWindow和CWindowImpl之间的类,它可以子类化控件,在控件的父窗口中处理控件的消息,这使得所有的消息处理都放在对话框类中,不需要为为每个控件生成一个单独的CWindowImpl派生类对象。需要注意的是不能用CContainedWindow 处理WM_COMMAND, WM_NOTIFY和其他通知消息,因为这些消息是发给控件的父窗口的。

CContainedWindow只是CContainedWindowT定义的一个数据类型,CContainedWindowT才是真正的类,它是一个模板类,使用window接口类的类名作为模板参数。这个特殊的CContainedWindowT和CWindow功能一样,
CContainedWindow只是它定义的一个简写名称,要使用不同的window接口类只需将该类的类名作为模板参数就行了,例如CContainedWindowT

钩住一个CContainedWindow对象需要做四件事:

  1. 在对话框中创建一个CContainedWindowT 成员变量。
  2. 将消息处理添加到对话框消息映射的ALT_MSG_MAP小节。
  3. 在对话框的构造函数中调用CContainedWindowT 构造函数并告诉它哪个ALT_MSG_MAP小节的消息需要处理。
  4. 在OnInitDialog()中调用CContainedWindowT::SubclassWindow() 方法与控件建立关联。

在ControlMania1中,我对三个按钮分别使用了一个CContainedWindow,对话框处理发送到每一个按钮的WM_SETCURSOR消息,并改变鼠标指针形状。

现在仔细看看这一步,首先,我们在CMainDlg中添加了CContainedWindow成员。

[cpp] view plain copy print ?
  1. class CMainDlg : public CDialogImpl  
  2. {  
  3. // ...   
  4. protected:  
  5.     CContainedWindow m_wndOKBtn, m_wndExitBtn;  
  6. };  
class CMainDlg : public CDialogImpl
{
// ...
protected:
    CContainedWindow m_wndOKBtn, m_wndExitBtn;
};


其次,我们添加了ALT_MSG_MAP小节,OK按钮使用1小节,Exit按钮使用2小节。这意味着所有发送给OK按钮的消息将由ALT_MSG_MAP(1)小节处理,所有发给Exit按钮的消息将由ALT_MSG_MAP(2)小节处理。

[cpp] view plain copy print ?
  1. class CMainDlg : public CDialogImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMainDlg)  
  5.         MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)  
  6.         COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)  
  7.         COMMAND_ID_HANDLER(IDOK, OnOK)  
  8.         COMMAND_ID_HANDLER(IDCANCEL, OnCancel)  
  9.     ALT_MSG_MAP(1)  
  10.         MSG_WM_SETCURSOR(OnSetCursor_OK)  
  11.     ALT_MSG_MAP(2)  
  12.         MSG_WM_SETCURSOR(OnSetCursor_Exit)  
  13.     END_MSG_MAP()  
  14.    
  15.     LRESULT OnSetCursor_OK(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);  
  16.     LRESULT OnSetCursor_Exit(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);  
  17. };  
class CMainDlg : public CDialogImpl
{
public:
    BEGIN_MSG_MAP_EX(CMainDlg)
        MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
        COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
        COMMAND_ID_HANDLER(IDOK, OnOK)
        COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
    ALT_MSG_MAP(1)
        MSG_WM_SETCURSOR(OnSetCursor_OK)
    ALT_MSG_MAP(2)
        MSG_WM_SETCURSOR(OnSetCursor_Exit)
    END_MSG_MAP()
 
    LRESULT OnSetCursor_OK(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);
    LRESULT OnSetCursor_Exit(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);
};


接着,我们调用每个CContainedWindow的构造函数,告诉它使用ALT_MSG_MAP的哪个小节。

[cpp] view plain copy print ?
  1. CMainDlg::CMainDlg() : m_wndOKBtn(this, 1),   
  2.                        m_wndExitBtn(this, 2)  
  3. {  
  4. }  
CMainDlg::CMainDlg() : m_wndOKBtn(this, 1), 
                       m_wndExitBtn(this, 2)
{
}


构造函数的参数是消息映射链的地址和ALT_MSG_MAP的小节号码,第一个参数通常使用this,就是使用对话框自己的消息映射链,第二个参数告诉对象将消息发给ALT_MSG_MAP的哪个小节。

最后,我们将每个CContainedWindow对象与控件关联起来。

[cpp] view plain copy print ?
  1. LRESULT CMainDlg::OnInitDialog(...)  
  2. {  
  3. // ...   
  4.     // Attach CContainedWindows to OK and Exit buttons  
  5.     m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );  
  6.     m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );  
  7.    
  8.     return TRUE;  
  9. }  
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
    // Attach CContainedWindows to OK and Exit buttons
    m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
    m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
 
    return TRUE;
}


下面是新的WM_SETCURSOR消息处理函数:

[cpp] view plain copy print ?
  1. LRESULT CMainDlg::OnSetCursor_OK (HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )  
  2. {  
  3. static HCURSOR hcur = LoadCursor ( NULL, IDC_HAND );  
  4.    
  5.     if ( NULL != hcur )  
  6.         {  
  7.         SetCursor ( hcur );  
  8.         return TRUE;  
  9.         }  
  10.     else  
  11.         {  
  12.         SetMsgHandled(false);  
  13.         return FALSE;  
  14.         }  
  15. }  
  16.    
  17. LRESULT CMainDlg::OnSetCursor_Exit ( HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )  
  18. {  
  19. static HCURSOR hcur = LoadCursor ( NULL, IDC_NO );  
  20.    
  21.     if ( NULL != hcur )  
  22.         {  
  23.         SetCursor ( hcur );  
  24.         return TRUE;  
  25.         }  
  26.     else  
  27.         {  
  28.         SetMsgHandled(false);  
  29.         return FALSE;  
  30.         }  
  31. }  
LRESULT CMainDlg::OnSetCursor_OK (HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_HAND );
 
    if ( NULL != hcur )
        {
        SetCursor ( hcur );
        return TRUE;
        }
    else
        {
        SetMsgHandled(false);
        return FALSE;
        }
}
 
LRESULT CMainDlg::OnSetCursor_Exit ( HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_NO );
 
    if ( NULL != hcur )
        {
        SetCursor ( hcur );
        return TRUE;
        }
    else
        {
        SetMsgHandled(false);
        return FALSE;
        }
}


如果你还想使用按钮类的特性,你需要这样声明变量:

[cpp] view plain copy print ?
  1. CContainedWindowT m_wndOKBtn;  
CContainedWindowT m_wndOKBtn;


这样就可以使用CButton类的方法。

当你把鼠标光标移到这些按钮上就可以看到WM_SETCURSOR消息处理函数的作用结果:

WTL 详细介绍_第25张图片 WTL 详细介绍_第26张图片

ATL 方式 3 - 子类化(Subclassing)

第三种方法创建一个CWindowImpl派生类并用它子类化一个控件。这和第二种方法有些相似,只是消息处理放在CWindowImpl类内部而不是对话框类中。

ControlMania1使用这种方法子类化主对话框的About按钮。下面是CButtonImpl类,他从CWindowImpl类派生,处理WM_SETCURSOR消息:

[cpp] view plain copy print ?
  1. class CButtonImpl : public CWindowImpl  
  2. {  
  3.     BEGIN_MSG_MAP_EX(CButtonImpl)  
  4.         MSG_WM_SETCURSOR(OnSetCursor)  
  5.     END_MSG_MAP()  
  6.    
  7.     LRESULT OnSetCursor(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg)  
  8.     {  
  9.     static HCURSOR hcur = LoadCursor ( NULL, IDC_SIZEALL );  
  10.    
  11.         if ( NULL != hcur )  
  12.             {  
  13.             SetCursor ( hcur );  
  14.             return TRUE;  
  15.             }  
  16.         else  
  17.             {  
  18.             SetMsgHandled(false);  
  19.             return FALSE;  
  20.             }  
  21.     }  
  22. };  
class CButtonImpl : public CWindowImpl
{
    BEGIN_MSG_MAP_EX(CButtonImpl)
        MSG_WM_SETCURSOR(OnSetCursor)
    END_MSG_MAP()
 
    LRESULT OnSetCursor(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg)
    {
    static HCURSOR hcur = LoadCursor ( NULL, IDC_SIZEALL );
 
        if ( NULL != hcur )
            {
            SetCursor ( hcur );
            return TRUE;
            }
        else
            {
            SetMsgHandled(false);
            return FALSE;
            }
    }
};


接着在主对话框声明一个CButtonImpl成员变量:

[cpp] view plain copy print ?
  1. class CMainDlg : public CDialogImpl  
  2. {  
  3. // ...   
  4. protected:  
  5.     CContainedWindow m_wndOKBtn, m_wndExitBtn;  
  6.     CButtonImpl m_wndAboutBtn;  
  7. };  
class CMainDlg : public CDialogImpl
{
// ...
protected:
    CContainedWindow m_wndOKBtn, m_wndExitBtn;
    CButtonImpl m_wndAboutBtn;
};


最后,在OnInitDialog()种子类化About按钮。

[cpp] view plain copy print ?
  1. LRESULT CMainDlg::OnInitDialog(...)  
  2. {  
  3. // ...   
  4.     // Attach CContainedWindows to OK and Exit buttons  
  5.     m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );  
  6.     m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );  
  7.    
  8.     // CButtonImpl: subclass the About button  
  9.     m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );  
  10.    
  11.     return TRUE;  
  12. }  
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
    // Attach CContainedWindows to OK and Exit buttons
    m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
    m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
 
    // CButtonImpl: subclass the About button
    m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
 
    return TRUE;
}


WTL 方式 - 对话框数据交换(DDX)

WTL的DDX(对话框数据交换)很像MFC,可以使用很简单的方法将变量和控件关联起来。首先,和前面的例子一样你需要从CWindowImpl派生一个新类,这次我们使用一个新类CEditImpl,因为这次我们使用得是Edit控件。你还需要将#include atlddx.h 添加到stdafx.h中,这样就可以使用DDX代码。

要使主对话框支持DDX,需要将CWinDataExchange添加到继承列表中:

[cpp] view plain copy print ?
  1. class CMainDlg : public CDialogImpl,   
  2.                  public CWinDataExchange  
  3. {  
  4. //...   
  5. };  
class CMainDlg : public CDialogImpl, 
                 public CWinDataExchange
{
//...
};


接着在对话框类中添加DDX链,这和MFC的类向导使用的DoDataExchange()函数功能相似。对于不同类型的数据可以使用不同的DDX宏,我们使用DDX_CONTROL用来连接变量和控件,这次我们使用CEditImpl处理WM_CONTEXTMENU消息,使它能够在你右键单控件时做一些事情。

[cpp] view plain copy print ?
  1. class CEditImpl : public CWindowImpl  
  2. {  
  3.     BEGIN_MSG_MAP_EX(CEditImpl)  
  4.         MSG_WM_CONTEXTMENU(OnContextMenu)  
  5.     END_MSG_MAP()  
  6.    
  7.     void OnContextMenu ( HWND hwndCtrl, CPoint ptClick )  
  8.     {  
  9.         MessageBox("Edit control handled WM_CONTEXTMENU");  
  10.     }  
  11. };  
  12.    
  13. class CMainDlg : public CDialogImpl,   
  14.                  public CWinDataExchange  
  15. {  
  16. //...   
  17.    
  18.     BEGIN_DDX_MAP(CMainDlg)  
  19.         DDX_CONTROL(IDC_EDIT, m_wndEdit)  
  20.     END_DDX_MAP()  
  21.    
  22. protected:  
  23.     CContainedWindow m_wndOKBtn, m_wndExitBtn;  
  24.     CButtonImpl m_wndAboutBtn;  
  25.     CEditImpl   m_wndEdit;  
  26. };  
class CEditImpl : public CWindowImpl
{
    BEGIN_MSG_MAP_EX(CEditImpl)
        MSG_WM_CONTEXTMENU(OnContextMenu)
    END_MSG_MAP()
 
    void OnContextMenu ( HWND hwndCtrl, CPoint ptClick )
    {
        MessageBox("Edit control handled WM_CONTEXTMENU");
    }
};
 
class CMainDlg : public CDialogImpl, 
                 public CWinDataExchange
{
//...
 
    BEGIN_DDX_MAP(CMainDlg)
        DDX_CONTROL(IDC_EDIT, m_wndEdit)
    END_DDX_MAP()
 
protected:
    CContainedWindow m_wndOKBtn, m_wndExitBtn;
    CButtonImpl m_wndAboutBtn;
    CEditImpl   m_wndEdit;
};


最后,在OnInitDialog()中调用DoDataExchange()函数,这个函数是继承自CWinDataExchange。DoDataExchange()第一次被调用时完成相关控件的子类化工作,所以在这个例子中,DoDataExchange()子类化ID为IDC_EDIT的控件,将其与m_wndEdit建立关联。

[cpp] view plain copy print ?
  1. LRESULT CMainDlg::OnInitDialog(...)  
  2. {  
  3. // ...   
  4.     // Attach CContainedWindows to OK and Exit buttons  
  5.     m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );  
  6.     m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );  
  7.    
  8.     // CButtonImpl: subclass the About button  
  9.     m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );  
  10.    
  11.     // First DDX call, hooks up variables to controls.  
  12.     DoDataExchange(false);  
  13.    
  14.     return TRUE;  
  15. }  
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
    // Attach CContainedWindows to OK and Exit buttons
    m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
    m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
 
    // CButtonImpl: subclass the About button
    m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
 
    // First DDX call, hooks up variables to controls.
    DoDataExchange(false);
 
    return TRUE;
}


DoDataExchange()的参数与MFC的UpdateData()函数的参数意义相同,我会在下一节详细介绍。

现在运行ControlMania1程序,可以看到子类化的效果。鼠标右键单击编辑框将弹出消息框,当鼠标通过按钮上时鼠标形状会改变。

DDX的详细内容

当然,DDX是用来做数据交换的,WTL支持在Edit控件和字符串之间交换数据,也可以将字符串解析成数字,转换成整型或浮点型变量,还支持Check box和Radio button组的状态与int型变量之间的转换。

DDX 宏

DDX可以使用6种宏,每一种宏都对应一个CWinDataExchange类的方法支持其工作,每一种宏都用相同的形式:DDX_FOO(控件ID, 变量),每一种宏都可以支持多种类型的变量,例如DDX_TEXT的重载就支持多种类型的数据。

DDX_TEXT

在字符串和edit box控件之间传输数据,变量类型可以是CString, BSTR, CComBSTR或者静态分配的字符串数组,但是不能使用new动态分配的数组。

DDX_INT

在edit box控件和数字变量之间传输int型数据。

DDX_UINT

在edit box控件和数字变量之间传输无符号int型数据。

DDX_FLOAT

在edit box控件和数字变量之间传输浮点型(float)数据或双精度型数据(double)。

DDX_CHECK

在check box控件和int型变量之间转换check box控件的状态。

DDX_RADIO

在radio buttons控件组和int型变量之间转换radio buttons控件组的状态。

DDX_FLOAT宏有一些特殊,要使用DDX_FLOAT宏需要在stdafx.h文件的所有WTL头文件包含之前添加一行定义:

[cpp] view plain copy print ?
  1. #define _ATL_USE_DDX_FLOAT  
#define _ATL_USE_DDX_FLOAT


这个定义是必要的,因为默认状态为了优化程序的大小而不支持浮点数。

有关 DoDataExchange()的详细内容

调用DoDataExchange()方法和在MFC中使用UpdateData()一样,DoDataExchange()的函数原型是:

[cpp] view plain copy print ?
  1. BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1 );  
BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1 );


参数:

bSaveAndValidate

指示数据传输方向的标志。TRUE表示将数据从控件传输给变量,FALSE表示将数据从变量传输给控件。需要注意得是这个参数的默认值是FALSE,而MFC的UpdateData()函数的默认值是TRUE。为了方便记忆,你可以使用DDX_SAVE 和 DDX_LOAD标号(它们分别被定义为TRUE和FALSE)。

nCtlID

使用-1可以更新所有控件,如果只想DDX宏作用于一个控件就使用控件的ID。

如果控件更新成功DoDataExchange()会返回TRUE,如果失败就返回FALSE,对话框类有两个重载函数处理数据交换错误。一个是OnDataExchangeError(),无论什么原因的错误都会调用这个函数,这个函数的默认实现在CWinDataExchange中,它仅仅是驱动PC喇叭发出一声蜂鸣并将出错的控件设为当前焦点。另一个函数是OnDataValidateError(),但是要到本文的第五章介绍DDV时才用得到。

使用DDX

在CMainDlg中添加几个变量,演示DDX的使用方法。

[cpp] view plain copy print ?
  1. class CMainDlg : public ...  
  2. {  
  3. //...   
  4.     BEGIN_DDX_MAP(CMainDlg)  
  5.         DDX_CONTROL(IDC_EDIT, m_wndEdit)  
  6.         DDX_TEXT(IDC_EDIT, m_sEditContents)  
  7.         DDX_INT(IDC_EDIT, m_nEditNumber)  
  8.     END_DDX_MAP()  
  9.    
  10. protected:  
  11.     // DDX variables   
  12.     CString m_sEditContents;  
  13.     int     m_nEditNumber;  
  14. };  
class CMainDlg : public ...
{
//...
    BEGIN_DDX_MAP(CMainDlg)
        DDX_CONTROL(IDC_EDIT, m_wndEdit)
        DDX_TEXT(IDC_EDIT, m_sEditContents)
        DDX_INT(IDC_EDIT, m_nEditNumber)
    END_DDX_MAP()
 
protected:
    // DDX variables
    CString m_sEditContents;
    int     m_nEditNumber;
};


在OK按钮的处理函数中,我们首先调用DoDataExchange()将将edit控件的数据传送给我们刚刚添加的两个变量,然后将结果显示在列表控件中。

[cpp] view plain copy print ?
  1. LRESULT CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )  
  2. {  
  3. CString str;  
  4.    
  5.     // Transfer data from the controls to member variables.  
  6.     if ( !DoDataExchange(true) )  
  7.         return;  
  8.    
  9.     m_wndList.DeleteAllItems();  
  10.    
  11.     m_wndList.InsertItem ( 0, _T("DDX_TEXT") );  
  12.     m_wndList.SetItemText ( 0, 1, m_sEditContents );  
  13.    
  14.     str.Format ( _T("%d"), m_nEditNumber );  
  15.     m_wndList.InsertItem ( 1, _T("DDX_INT") );  
  16.     m_wndList.SetItemText ( 1, 1, str );  
  17. }  
LRESULT CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )
{
CString str;
 
    // Transfer data from the controls to member variables.
    if ( !DoDataExchange(true) )
        return;
 
    m_wndList.DeleteAllItems();
 
    m_wndList.InsertItem ( 0, _T("DDX_TEXT") );
    m_wndList.SetItemText ( 0, 1, m_sEditContents );
 
    str.Format ( _T("%d"), m_nEditNumber );
    m_wndList.InsertItem ( 1, _T("DDX_INT") );
    m_wndList.SetItemText ( 1, 1, str );
}


WTL 详细介绍_第27张图片

如果编辑控件输入的不是数字,DDX_INT将会失败并触发OnDataExchangeError()的调用,CMainDlg重载了OnDataExchangeError()函数显示一个消息框:

[cpp] view plain copy print ?
  1. void CMainDlg::OnDataExchangeError ( UINT nCtrlID, BOOL bSave )  
  2. {  
  3. CString str;  
  4.    
  5.     str.Format ( _T("DDX error during exchange with control: %u"), nCtrlID );  
  6.     MessageBox ( str, _T("ControlMania1"), MB_ICONWARNING );  
  7.        
  8.     ::SetFocus ( GetDlgItem(nCtrlID) );  
  9. }  
void CMainDlg::OnDataExchangeError ( UINT nCtrlID, BOOL bSave )
{
CString str;
 
    str.Format ( _T("DDX error during exchange with control: %u"), nCtrlID );
    MessageBox ( str, _T("ControlMania1"), MB_ICONWARNING );
     
    ::SetFocus ( GetDlgItem(nCtrlID) );
}


WTL 详细介绍_第28张图片

作为最后一个使用DDX的例子,我们添加一个check box演示DDX_CHECK的使用:

WTL 详细介绍_第29张图片

DDX_CHECK使用的变量类型是int型,它的可能值是0,1,2,分别对应check box的未选择状态,选择状态和不确定状态。你也可以使用常量BST_UNCHECKED,BST_CHECKED,和 BST_INDETERMINATE代替,对于check box来说只有选择和未选择两种状态,你可以将其视为布尔型变量。

以下是为使用check box的DDX而做的改动:

[cpp] view plain copy print ?
  1. class CMainDlg : public ...  
  2. {  
  3. //...   
  4.     BEGIN_DDX_MAP(CMainDlg)  
  5.         DDX_CONTROL(IDC_EDIT, m_wndEdit)  
  6.         DDX_TEXT(IDC_EDIT, m_sEditContents)  
  7.         DDX_INT(IDC_EDIT, m_nEditNumber)  
  8.         DDX_CHECK(IDC_SHOW_MSG, m_nShowMsg)  
  9.     END_DDX_MAP()  
  10.    
  11. protected:  
  12.     // DDX variables   
  13.     CString m_sEditContents;  
  14.     int     m_nEditNumber;  
  15.     int     m_nShowMsg;  
  16. };  
class CMainDlg : public ...
{
//...
    BEGIN_DDX_MAP(CMainDlg)
        DDX_CONTROL(IDC_EDIT, m_wndEdit)
        DDX_TEXT(IDC_EDIT, m_sEditContents)
        DDX_INT(IDC_EDIT, m_nEditNumber)
        DDX_CHECK(IDC_SHOW_MSG, m_nShowMsg)
    END_DDX_MAP()
 
protected:
    // DDX variables
    CString m_sEditContents;
    int     m_nEditNumber;
    int     m_nShowMsg;
};


在OnOK()的最后,检查m_nShowMsg的值看看check box是否被选中。

[cpp] view plain copy print ?
  1. void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )  
  2. {  
  3.     // Transfer data from the controls to member variables.  
  4.     if ( !DoDataExchange(true) )  
  5.         return;  
  6. //...   
  7.     if ( m_nShowMsg )  
  8.         MessageBox ( _T("DDX complete!"), _T("ControlMania1"),   
  9.                      MB_ICONINFORMATION );  
  10. }  
void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )
{
    // Transfer data from the controls to member variables.
    if ( !DoDataExchange(true) )
        return;
//...
    if ( m_nShowMsg )
        MessageBox ( _T("DDX complete!"), _T("ControlMania1"), 
                     MB_ICONINFORMATION );
}


使用其它DDX_*宏的例子代码包含在例子工程中。

处理控件发送的通知消息

在WTL中处理通知消息与使用API方式编程相似,控件以WM_COMMAND 或 WM_NOTIFY 消息的方式向父窗口发送通知事件,父窗口相应并做相应处理。少数其它的消息也可以看作是通知消息,例如:WM_DRAWITEM,当一个自画控件需要画自己时就会发送这个消息,父窗口可以自己处理这个消息,也可以再将它反射给控件,MFC采用得就是消息反射方式,使得控件能够自己处理通知消息,提高了代码的封装性和可重用性。

在父窗口中响应控件的通知消息

以WM_NOTIFY和WM_COMMAND消息形式发送的通知消息包含各种信息。WM_COMMAND消息的参数包含发送通知消息的控件ID,控件的窗口句柄和通知代码,WM_NOTIFY消息的参数还包含一个NMHDR数据结构的指针。ATL和WTL有各种消息映射宏用来处理这些通知消息,我在这里只介绍WTL宏,因为本文就是讲WTL得。使用这些宏需要在消息映射链中使用BEGIN_MSG_MAP_EX并包含atlcrack.h文件。

消息映射宏

要处理WM_COMMAND通知消息需要使用COMMAND_HANDLER_EX宏:

COMMAND_HANDLER_EX(id, code, func)

处理从某个控件发送得某个通知代码。

COMMAND_ID_HANDLER_EX(id, func)

处理从某个控件发送得所有通知代码。

COMMAND_CODE_HANDLER_EX(code, func)

处理某个通知代码得所有消息,不管是从那个控件发出的。

COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func)

处理ID在idFirst和idLast之间得控件发送的所有通知代码。

COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func)

处理ID在idFirst和idLast之间得控件发送的某个通知代码。

例子:

  • COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange): 处理从ID是IDC_USERNAME的edit box控件发出的EN_CHANGE通知消息。
  • COMMAND_ID_HANDLER_EX(IDOK, OnOK): 处理ID是IDOK的控件发送的所有通知消息。
  • COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked): 处理ID在IDC_MONDAY和IDC_FRIDAY之间控件发送的BN_CLICKED通知消息。

还有一些宏专门处理WM_NOTIFY消息,和上面的宏功能类似,只是它们的名字开头以“NOTIFY_”代替“COMMAND_”。

WM_COMMAND 消息处理函数的原型是:

[cpp] view plain copy print ?
  1. void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );  
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );


WM_COMMAND通知消息不需要返回值,所以处理函数也不需要返回值,WM_NOTIFY消息处理函数的原型是:

[cpp] view plain copy print ?
  1. LRESULT func ( NMHDR* phdr );  
LRESULT func ( NMHDR* phdr );


消息处理函数的返回值用作消息相应的返回值,这不同于MFC,MFC的消息响应通过消息处理函数的LRESULT*参数得到返回值。发送通知消息的控件的窗口句柄和通知代码包含在NMHDR结构中,分别是code和hendFrom成员。和MFC一样的是如果通知消息发送的不是普通的NMHDR结构,你的消息处理函数应该将phdr参数转换成正确的类型。

我们将为CMainDlg添加LVN_ITEMCHANGED通知的处理函数,处理从list控件发出的这个通知,在对话框中显示当前选择的项目,先从添加消息映射宏和消息处理函数开始:

[cpp] view plain copy print ?
  1. class CMainDlg : public ...  
  2. {  
  3.     BEGIN_MSG_MAP_EX(CMainDlg)  
  4.         NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)  
  5.     END_MSG_MAP()  
  6.    
  7.     LRESULT OnListItemchanged(NMHDR* phdr);  
  8. //...   
  9. };  
class CMainDlg : public ...
{
    BEGIN_MSG_MAP_EX(CMainDlg)
        NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
    END_MSG_MAP()
 
    LRESULT OnListItemchanged(NMHDR* phdr);
//...
};


下面是消息处理函数:

[cpp] view plain copy print ?
  1. LRESULT CMainDlg::OnListItemchanged ( NMHDR* phdr )  
  2. {  
  3. NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;  
  4. int nSelItem = m_wndList.GetSelectedIndex();  
  5. CString sMsg;  
  6.    
  7.     // If no item is selected, show "none". Otherwise, show its index.  
  8.     if ( -1 == nSelItem )  
  9.         sMsg = _T("(none)");  
  10.     else  
  11.         sMsg.Format ( _T("%d"), nSelItem );  
  12.    
  13.     SetDlgItemText ( IDC_SEL_ITEM, sMsg );  
  14.     return 0;   // retval ignored  
  15. }  
LRESULT CMainDlg::OnListItemchanged ( NMHDR* phdr )
{
NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;
int nSelItem = m_wndList.GetSelectedIndex();
CString sMsg;
 
    // If no item is selected, show "none". Otherwise, show its index.
    if ( -1 == nSelItem )
        sMsg = _T("(none)");
    else
        sMsg.Format ( _T("%d"), nSelItem );
 
    SetDlgItemText ( IDC_SEL_ITEM, sMsg );
    return 0;   // retval ignored
}


该处理函数并未用到phdr参数,我将他强制转换成NMLISTVIEW*只是为了演示用法。

反射通知消息

如果你是用CWindowImpl的派生类封装控件,比如前面使用的CEditImpl,你可以在类的内部处理通知消息而不是在对话框中,这就是通知消息的反射,它和MFC的消息反射相似。不同的是在WTL中父窗口和控件都可以处理通知消息,而在MFC中只有控件能处理通知消息(译者加:除非你重载 WindowProc函数,在MFC反射这些消息之前截获它们)。

如果需要将通知消息反射给控件封装类,只需在对话框的消息映射链中添加REFLECT_NOTIFICATIONS()宏:

[cpp] view plain copy print ?
  1. class CMainDlg : public ...  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CMainDlg)  
  5.         //...   
  6.         NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)  
  7.         REFLECT_NOTIFICATIONS()  
  8.     END_MSG_MAP()  
  9. };  
class CMainDlg : public ...
{
public:
    BEGIN_MSG_MAP_EX(CMainDlg)
        //...
        NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
        REFLECT_NOTIFICATIONS()
    END_MSG_MAP()
};


这个宏向消息映射链添加了一些代码处理那些未被前面的宏处理的通知消息,它检查消息传递的HWND窗口句柄是否有效并将消息转发给这个窗口,当然,消息代码的数值被改变成OLE控件所使用的值,OLE控件有与之相似的消息反射系统。新的消息代码值用OCM_xxx代替了WM_xxx,但是消息的处理方式和未反射前一样。

有18中被反射的消息:

  • 控件通知消息: WM_COMMAND, WM_NOTIFY, WM_PARENTNOTIFY
  • 自画消息: WM_DRAWITEM, WM_MEASUREITEM, WM_COMPAREITEM, WM_DELETEITEM
  • List box 键盘消息: WM_VKEYTOITEM, WM_CHARTOITEM
  • 其它: WM_HSCROLL, WM_VSCROLL, WM_CTLCOLOR*

在你想添加反射消息处理的控件类内不要忘了使用DEFAULT_REFLECTION_HANDLER()宏,DEFAULT_REFLECTION_HANDLER()宏确保将未被处理的消息交给DefWindowProc()正确处理。 下面的例子是一个自画按钮类,它相应了从父窗口反射的WM_DRAWITEM消息。

[cpp] view plain copy print ?
  1. class CODButtonImpl : public CWindowImpl  
  2. {  
  3. public:  
  4.     BEGIN_MSG_MAP_EX(CODButtonImpl)  
  5.         MSG_OCM_DRAWITEM(OnDrawItem)  
  6.         DEFAULT_REFLECTION_HANDLER()  
  7.     END_MSG_MAP()  
  8.    
  9.     void OnDrawItem ( UINT idCtrl, LPDRAWITEMSTRUCT lpdis )  
  10.     {  
  11.         // do drawing here...   
  12.     }  
  13. };  
class CODButtonImpl : public CWindowImpl
{
public:
    BEGIN_MSG_MAP_EX(CODButtonImpl)
        MSG_OCM_DRAWITEM(OnDrawItem)
        DEFAULT_REFLECTION_HANDLER()
    END_MSG_MAP()
 
    void OnDrawItem ( UINT idCtrl, LPDRAWITEMSTRUCT lpdis )
    {
        // do drawing here...
    }
};


用来处理反射消息的WTL宏

我们现在只看到了WTL的消息反射宏中的一个:MSG_OCM_DRAWITEM,还有17个这样的反射宏。由于WM_NOTIFY和WM_COMMAND消息带的参数需要展开,WTL提供了特殊的宏MSG_OCM_COMMAND和MSG_OCM_NOTIFY做这些事情。这些宏所作的工作与COMMAND_HANDLER_EX和NOTIFY_HANDLER_EX宏相同,只是前面加了“REFLECTED_”,例如,一个树控件类可能存在这样的消息映射链:

[cpp] view plain copy print ?
  1. class CMyTreeCtrl : public CWindowImpl  
  2. {  
  3. public:  
  4.   BEGIN_MSG_MAP_EX(CMyTreeCtrl)  
  5.     REFLECTED_NOTIFY_CODE_HANDLER_EX(TVN_ITEMEXPANDING, OnItemExpanding)  
  6.     DEFAULT_REFLECTION_HANDLER()  
  7.   END_MSG_MAP()  
  8.    
  9.   LRESULT OnItemExpanding ( NMHDR* phdr );  
  10. };  
class CMyTreeCtrl : public CWindowImpl
{
public:
  BEGIN_MSG_MAP_EX(CMyTreeCtrl)
    REFLECTED_NOTIFY_CODE_HANDLER_EX(TVN_ITEMEXPANDING, OnItemExpanding)
    DEFAULT_REFLECTION_HANDLER()
  END_MSG_MAP()
 
  LRESULT OnItemExpanding ( NMHDR* phdr );
};


在ControlMania1对话框中用了一个树控件,和上面的代码一样处理TVN_ITEMEXPANDING消息,CMainDlg类的成员m_wndTree使用DDX连接到控件上,CMainDlg反射通知消息,树控件的处理函数OnItemExpanding()是这样的:

[cpp] view plain copy print ?
  1. LRESULT CBuffyTreeCtrl::OnItemExpanding ( NMHDR* phdr )  
  2. {  
  3. NMTREEVIEW* pnmtv = (NMTREEVIEW*) phdr;  
  4.    
  5.     if ( pnmtv->action & TVE_COLLAPSE )  
  6.         return TRUE;    // don't allow it  
  7.     else  
  8.         return FALSE;   // allow it  
  9. }  
LRESULT CBuffyTreeCtrl::OnItemExpanding ( NMHDR* phdr )
{
NMTREEVIEW* pnmtv = (NMTREEVIEW*) phdr;
 
    if ( pnmtv->action & TVE_COLLAPSE )
        return TRUE;    // don't allow it
    else
        return FALSE;   // allow it
}


运行ControlMania1,用鼠标点击树控件上的+/-按钮,你就会看到消息处理函数的作用-节点展开后就不能再折叠起来。

容易出错和混淆的地方

对话框的字体


如果你像我一样对界面非常讲究并且正在只用windows 2000或XP,你就会奇怪为什么对话框使用MS Sans Serif字体而不是Tahoma字体,因为VC6太老了,它生成的资源文件在NT 4上工作的很好,但是对于新的版本就会有问题。你可以自己修改,需要手工编辑资源文件,据我所知VC 7不存在这个问题。

在资源文件中对话框的入口处需要修改3个地方:

  1. 对话框类型: 将DIALOG改为DIALOGEX
  2. 窗口类型: 添加DS_SHELLFONT
  3. 对话框字体: 将MS Sans Serif改为MS Shell Dlg

不幸的是前两个修改会在每次保存资源文件时丢失(被VC又改回原样),所以需要重复这些修改,下面是改动之前的代码:

[cpp] view plain copy print ?
  1. IDD_ABOUTBOX DIALOG DISCARDABLE  0, 0, 187, 102  
  2. STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU  
  3. CAPTION "About"  
  4. FONT 8, "MS Sans Serif"  
  5. BEGIN  
  6.   ...  
  7. END  
IDD_ABOUTBOX DIALOG DISCARDABLE  0, 0, 187, 102
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Sans Serif"
BEGIN
  ...
END


这是改动之后的代码:

[cpp] view plain copy print ?
  1. IDD_ABOUTBOX DIALOGEX DISCARDABLE  0, 0, 187, 102  
  2. STYLE DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU  
  3. CAPTION "About"  
  4. FONT 8, "MS Shell Dlg"  
  5. BEGIN  
  6.   ...  
  7. END  
IDD_ABOUTBOX DIALOGEX DISCARDABLE  0, 0, 187, 102
STYLE DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Shell Dlg"
BEGIN
  ...
END


这样改了之后,对话框将在新的操作系统上使用Tahoma字体,而在老的操作系统上仍旧使用MS Sans Serif字体。

_ATL_MIN_CRT

本文的论坛 FAQ已经做过解释, ATL包含的优化设置让你创建一个不使用C运行库(CRT)的程序,使用这个优化需要在预处理设置中添加_ATL_MIN_CRT标号,向导生成的代码在Release配置中默认使用了这个优化。由于我写程序总是会用到CRT函数,所以我总是去掉这个标号,如果你在CString类或DDX中用到了浮点运算特性,你也要去掉这个标号。

继续

在第五章,我将介绍对话框数据验证(DDV),WTL对新控件的封装和自画控件、自定外观控件等一些高级界面特性。

WTL for MFC Programmers, Part V - Advanced Dialog UI Classes

分类: wtl 22人阅读 评论(0) 收藏 举报
wtl c++
第五章介绍

在上一篇文章我们介绍了一些与对话框和控件有关的WTL的特性,它们和MFC的相应的类作用相同。本文将介绍一些新类实现高级界面特性新类:控件自画和自定外观控件,新的WTL控件,UI updating和对话框数据验证(DDV)。

特别的自画和外观定制类

由于自画和定制外观控件在图形用户界面中是很常用的手段,所以WTL提供了几个嵌入类来完成这些令人厌烦的工作。我接着就会介绍它们,事实上我们在上一个例子工程ControlMania2的结尾部分已经这么做了。如果你正随着我的讲解用应用程序生成向导创建新工程,请不要忘了使用无模式对话框,为了使正常工作必须使用无模式对话框,我会在对话框中控件的UI Updating部分详细解释为什么这样作。

COwnerDraw

控件的自画需要响应四个消息:WM_MEASUREITEM, WM_DRAWITEM, WM_COMPAREITEM, 和WM_DELETEITEM,在atlframe.h头文件中定义的COwnerDraw类可以简化这些工作,使用这个类就不需要处理这四个消息,你只需将消息链入COwnerDraw,它会调用你的类中的重载函数。

如何将消息链入COwnerDraw取决与你是否将消息反射给控件,两种方法有些不同。下面是COwnerDraw类的消息映射链,它使得两种方法的差别更加明显:

[cpp] view plain copy print ?
  1. template <class T> class COwnerDraw  
  2. {  
  3. public:  
  4.   BEGIN_MSG_MAP(COwnerDraw)  
  5.     MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)  
  6.     MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)  
  7.     MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)  
  8.     MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)  
  9.   ALT_MSG_MAP(1)  
  10.     MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)  
  11.     MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)  
  12.     MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)  
  13.     MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)  
  14.   END_MSG_MAP()  
  15. };  
template  class COwnerDraw
{
public:
  BEGIN_MSG_MAP(COwnerDraw)
    MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
    MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
    MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)
    MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)
  ALT_MSG_MAP(1)
    MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
    MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)
    MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)
    MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)
  END_MSG_MAP()
};

注意,消息映射链的主要部分处理WM_*消息,而ATL部分处理反射的消息,OCM_*。自画的通知消息就像WM_NOTIFY消息一样,你可以在父窗口处理它们,也可以将它们反射会控件,如果你使用前一种方法,消息被直接链入COwnerDraw:

[cpp] view plain copy print ?
  1. class CSomeDlg : public COwnerDraw, ...  
  2. {  
  3.   BEGIN_MSG_MAP(CSomeDlg)  
  4.     //...   
  5.     CHAIN_MSG_MAP(COwnerDraw)  
  6.   END_MSG_MAP()  
  7.    
  8.   void DrawItem ( LPDRAWITEMSTRUCT lpdis );  
  9. };  
class CSomeDlg : public COwnerDraw, ...
{
  BEGIN_MSG_MAP(CSomeDlg)
    //...
    CHAIN_MSG_MAP(COwnerDraw)
  END_MSG_MAP()
 
  void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

当然,如果你想要控件自己处理这些消息,你需要使用CHAIN_MSG_MAP_ALT宏将消息链入ALT_MSG_MAP(1)部分:

[cpp] view plain copy print ?
  1. class CSomeButtonImpl : public COwnerDraw, ...  
  2. {  
  3.   BEGIN_MSG_MAP(CSomeButtonImpl)  
  4.     //...   
  5.     CHAIN_MSG_MAP_ALT(COwnerDraw, 1)  
  6.     DEFAULT_REFLECTION_HANDLER()  
  7.   END_MSG_MAP()  
  8.    
  9.   void DrawItem ( LPDRAWITEMSTRUCT lpdis );  
  10. };  
class CSomeButtonImpl : public COwnerDraw, ...
{
  BEGIN_MSG_MAP(CSomeButtonImpl)
    //...
    CHAIN_MSG_MAP_ALT(COwnerDraw, 1)
    DEFAULT_REFLECTION_HANDLER()
  END_MSG_MAP()
 
  void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

COwnerDraw类将对消息传递的参数展开,然后调用你的类中的实现函数。上面的例子中,我们自己的类实现DrawItem()函数,当有WM_DRAWITEM或OCM_DRAWITEM消息被链入COwnerDraw时,这个函数就会被调用。你可以重载的方法有:

[cpp] view plain copy print ?
  1. void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);  
  2. void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);  
  3. int  CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);  
  4. void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct);  
void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
int  CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);
void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct);

如果你不想处理某个消息,你可以调用SetMsgHandled(false),消息会被传递给消息映射链中的其他响应者。SetMsgHandled()事实上是COwnerDraw类的成员函数,但是它的作用和在BEGIN_MSG_MAP_EX()中使用SetMsgHandled()一样。

对于ControlMania2,它从ControlMania1中的树控件开始,添加了自画按钮处理反射的WM_DRAWITEM消息,下面是资源编辑器中的新按钮:

WTL 详细介绍_第30张图片

现在我们需要一个新类实现自画按钮:

[cpp] view plain copy print ?
  1. class CODButtonImpl : public CWindowImpl,  
  2.                       public COwnerDraw  
  3. {  
  4. public:  
  5.     BEGIN_MSG_MAP_EX(CODButtonImpl)  
  6.         CHAIN_MSG_MAP_ALT(COwnerDraw, 1)  
  7.         DEFAULT_REFLECTION_HANDLER()  
  8.     END_MSG_MAP()  
  9.    
  10.     void DrawItem ( LPDRAWITEMSTRUCT lpdis );  
  11. };  
class CODButtonImpl : public CWindowImpl,
                      public COwnerDraw
{
public:
    BEGIN_MSG_MAP_EX(CODButtonImpl)
        CHAIN_MSG_MAP_ALT(COwnerDraw, 1)
        DEFAULT_REFLECTION_HANDLER()
    END_MSG_MAP()
 
    void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

DrawItem()使用了像BitBlt()这样的GDI函数向按钮的表面画位图,代码应该很容易理解,因为WTL使用的类名和函数名都和MFC类似。

[cpp] view plain copy print ?
  1. void CODButtonImpl::DrawItem ( LPDRAWITEMSTRUCT lpdis )  
  2. {  
  3. // NOTE: m_bmp is a CBitmap init'ed in the constructor.  
  4. CDCHandle dc = lpdis->hDC;  
  5. CDC dcMem;  
  6.    
  7.     dcMem.CreateCompatibleDC ( dc );  
  8.     dc.SaveDC();  
  9.     dcMem.SaveDC();  
  10.    
  11.     // Draw the button's background, red if it has the focus, blue if not.  
  12.     if ( lpdis->itemState & ODS_FOCUS )   
  13.         dc.FillSolidRect ( &lpdis->rcItem, RGB(255,0,0) );  
  14.     else  
  15.         dc.FillSolidRect ( &lpdis->rcItem, RGB(0,0,255) );  
  16.    
  17.     // Draw the bitmap in the top-left, or offset by 1 pixel if the button  
  18.     // is clicked.   
  19.     dcMem.SelectBitmap ( m_bmp );  
  20.    
  21.     if ( lpdis->itemState & ODS_SELECTED )   
  22.         dc.BitBlt ( 1, 1, 80, 80, dcMem, 0, 0, SRCCOPY );  
  23.     else  
  24.         dc.BitBlt ( 0, 0, 80, 80, dcMem, 0, 0, SRCCOPY );  
  25.    
  26.     dcMem.RestoreDC(-1);  
  27.     dc.RestoreDC(-1);  
  28. }  
void CODButtonImpl::DrawItem ( LPDRAWITEMSTRUCT lpdis )
{
// NOTE: m_bmp is a CBitmap init'ed in the constructor.
CDCHandle dc = lpdis->hDC;
CDC dcMem;
 
    dcMem.CreateCompatibleDC ( dc );
    dc.SaveDC();
    dcMem.SaveDC();
 
    // Draw the button's background, red if it has the focus, blue if not.
    if ( lpdis->itemState & ODS_FOCUS ) 
        dc.FillSolidRect ( &lpdis->rcItem, RGB(255,0,0) );
    else
        dc.FillSolidRect ( &lpdis->rcItem, RGB(0,0,255) );
 
    // Draw the bitmap in the top-left, or offset by 1 pixel if the button
    // is clicked.
    dcMem.SelectBitmap ( m_bmp );
 
    if ( lpdis->itemState & ODS_SELECTED ) 
        dc.BitBlt ( 1, 1, 80, 80, dcMem, 0, 0, SRCCOPY );
    else
        dc.BitBlt ( 0, 0, 80, 80, dcMem, 0, 0, SRCCOPY );
 
    dcMem.RestoreDC(-1);
    dc.RestoreDC(-1);
}

我们的按钮看起来是这个样子:

WTL 详细介绍_第31张图片

CCustomDraw

CCustomDraw类使用和COwnerDraw类相同的方法处理NM_CUSTOMDRAW消息,对于自定绘制的每个阶段都有相应的重载函数:

[cpp] view plain copy print ?
  1. DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  2. DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  3. DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  4. DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  5.    
  6. DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  7. DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  8. DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  9. DWORD OnItemPostEraset(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
  10.    
  11. DWORD OnSubItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);  
DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
 
DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPostEraset(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
 
DWORD OnSubItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);

这些函数默认都是返回CDRF_DODEFAULT,如果想自画控件或返回一个不同的值,就需要重载这些函数:

你可能注意到上面的屏幕截图将“道恩”(Dawn:女名)显示成绿色,这是因为CBuffyTreeCtrl将消息链入CCustomDraw并重载了OnPrePaint()和OnItemPrePaint()方法。向树控件中添加节点时,节点的item data字段被设置成1,OnItemPrePaint()检查这个值,然后改变文字的颜色。

[cpp] view plain copy print ?
  1. DWORD CBuffyTreeCtrl::OnPrePaint(int idCtrl,   
  2.                                  LPNMCUSTOMDRAW lpNMCD)  
  3. {  
  4.     return CDRF_NOTIFYITEMDRAW;  
  5. }  
  6.    
  7. DWORD CBuffyTreeCtrl::OnItemPrePaint(int idCtrl,   
  8.                                      LPNMCUSTOMDRAW lpNMCD)  
  9. {  
  10.     if ( 1 == lpNMCD->lItemlParam )  
  11.         pnmtv->clrText = RGB(0,128,0);  
  12.    
  13.     return CDRF_DODEFAULT;  
  14. }  
DWORD CBuffyTreeCtrl::OnPrePaint(int idCtrl, 
                                 LPNMCUSTOMDRAW lpNMCD)
{
    return CDRF_NOTIFYITEMDRAW;
}
 
DWORD CBuffyTreeCtrl::OnItemPrePaint(int idCtrl, 
                                     LPNMCUSTOMDRAW lpNMCD)
{
    if ( 1 == lpNMCD->lItemlParam )
        pnmtv->clrText = RGB(0,128,0);
 
    return CDRF_DODEFAULT;
}

CCustomDraw类也有SetMsgHandled()函数,你可以像在COwnerDraw类那样使用这个函数。

WTL的新控件

WTL有几个新控件,它们要么是其他封装类的扩展(像 CTreeViewCtrlEx),要么是提供windows标准控件没有的新功能(像 CHyperLink)。

CBitmapButton

WTL的CBitmapButton类声明在atlctrlx.h中,它比MFC的同名类使用起来要简单的多。WTL的CBitmapButton类使用image list而不是单个的位图资源,你可以将多个按钮的图像放到一个位图文件中,减少GDI资源的占用。这对于使用很多图片并需要在Windows 9X系统上运行的程序很有好处,因为使用太多的单个位图将会很快耗尽GDI资源并导致系统崩溃。

CBitmapButton是一个CWindowImpl派生类,它又很多特色:自动调整控件的大小,自动生成3D边框,支持hot-tracking,每个按钮可以使用多个图像分别表示按钮的不同状态。

在ControlMania2中,我们对前面的例子创建的自画按钮使用CBitmapButton类。现在CMainDlg对话框类中添加CBitmapButton类型的变量m_wndBmpBtn,调用SubclassWindow()函数或使用DDX将其和控件联系起来,将位图装载到image list并告诉按钮使用这个image list,还要告诉按钮每个图像分别对应按钮的什么状态。下面是OnInitDialog()函数中建立和使用这个按钮的代码段:

[cpp] view plain copy print ?
  1. // Set up the bitmap button   
  2. geList iml;  
  3.   
  4. iml.CreateFromImage ( IDB_ALYSON_IMGLIST, 81, 1, CLR_NONE,  
  5.                       IMAGE_BITMAP, LR_CREATEDIBSECTION );  
  6.   
  7. m_wndBmpBtn.SubclassWindow ( GetDlgItem(IDC_ALYSON_BMPBTN) );  
  8. m_wndBmpBtn.SetToolTipText ( _T("Alyson") );  
  9. m_wndBmpBtn.SetImageList ( iml );  
  10. m_wndBmpBtn.SetImages ( 0, 1, 2, 3 );  
    // Set up the bitmap button
CImageList iml;
 
    iml.CreateFromImage ( IDB_ALYSON_IMGLIST, 81, 1, CLR_NONE,
                          IMAGE_BITMAP, LR_CREATEDIBSECTION );
 
    m_wndBmpBtn.SubclassWindow ( GetDlgItem(IDC_ALYSON_BMPBTN) );
    m_wndBmpBtn.SetToolTipText ( _T("Alyson") );
    m_wndBmpBtn.SetImageList ( iml );
    m_wndBmpBtn.SetImages ( 0, 1, 2, 3 );

默认情况下,按钮只是引用image list,所以OnInitDialog()不能delete它所创建的image list。下面显示的是新按钮的一般状态,注意控件是如何根据图像的大小来调整自己的大小。

WTL 详细介绍_第32张图片

因为CBitmapButton是一个非常有用的类,我想介绍一下它的公有方法。

CBitmapButton methods

CBitmapButtonImpl类包含了实现一个按钮的所有代码,除非你想重载某个方法或消息处理,你可以对控件直接使用CBitmapButton类。

CBitmapButtonImpl constructor

[cpp] view plain copy print ?
  1. CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE,HIMAGELIST hImageList = NULL)  
CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE,HIMAGELIST hImageList = NULL)

构造函数可以指定按钮的扩展样式(这与窗口的样式不冲突)和图像列表,通常使用默认参数就足够了,因为可以使用其他的方法设定这些属性。

SubclassWindow()

[cpp] view plain copy print ?
  1. BOOL SubclassWindow(HWND hWnd)  
BOOL SubclassWindow(HWND hWnd)

SubclassWindow()是个重载函数,主要完成控件的子类化和初始化控件类保有的内部数据。

Bitmap button extended styles

[cpp] view plain copy print ?
  1. DWORD GetBitmapButtonExtendedStyle()  
  2. DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)  
DWORD GetBitmapButtonExtendedStyle()
DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)

CBitmapButton支持一些扩展样式,这些扩展样式会对按钮的外观和操作方式产生影响:

BMPBTN_HOVER
使用hot-tracking,当鼠标移到按钮上时按钮被画成焦点状态。
BMPBTN_AUTO3D_SINGLE, BMPBTN_AUTO3D_DOUBLE
在按钮图像周围自动产生一个三维边框,当按钮拥有焦点时会显示一个表示焦点的虚线矩形框。另外如果你没有指定按钮按下状态的图像,将会自动生成一个。BMPBTN_AUTO3D_DOUBLE样式生成的边框稍微粗一些,其他特征和BMPBTN_AUTO3D_SINGLE一样。
BMPBTN_AUTOSIZE
按钮调整自己的大小以适应图像大小,这是默认样式。
BMPBTN_SHAREIMAGELISTS
如果指定这个样式,按钮不负责销毁按钮使用的image list,如果不使用这个样式,CBitmapButton的析构函数会销毁按钮使用的image list。
BMPBTN_AUTOFIRE
如果设置这个样式,在按钮上按住鼠标左键不放将会产生连续的WM_COMMAND消息。
调用SetBitmapButtonExtendedStyle()时,dwMask参数控制着那个样式将被改变,默认值是0,意味着用新样式完全替换旧的样式。

Image list management

[cpp] view plain copy print ?
  1. HIMAGELIST GetImageList()  
  2. HIMAGELIST SetImageList(HIMAGELIST hImageList)  
HIMAGELIST GetImageList()
HIMAGELIST SetImageList(HIMAGELIST hImageList)

调用SetImageList()设置按钮使用的image list。

Tooltip management

[cpp] view plain copy print ?
  1. int  GetToolTipTextLength()  
  2. bool GetToolTipText(LPTSTR lpstrText, int nLength)  
  3. bool SetToolTipText(LPCTSTR lpstrText)  
int  GetToolTipTextLength()
bool GetToolTipText(LPTSTR lpstrText, int nLength)
bool SetToolTipText(LPCTSTR lpstrText)

CBitmapButton支持显示工具提示(tooltip),调用SetToolTipText()指定显示的文字。

Setting the images to use

[cpp] view plain copy print ?
  1. void SetImages(int nNormal, int nPushed = -1,int nFocusOrHover = -1, int nDisabled = -1)  
void SetImages(int nNormal, int nPushed = -1,int nFocusOrHover = -1, int nDisabled = -1)

调用SetImages()函数告诉按钮分别使用image list的拿一个图像表示那个状态。nNormal是必须的,其它是可选的,使用-1表示对应的状态没有图像。

CCheckListViewCtrl

CCheckListViewCtrl类在atlctrlx.h中定义,它是一个CWindowImpl派生类,实现了一个带检查框的list view控件。它和MFC的CCheckListBox不同,CCheckListBox只是一个list box,不是list view。CCheckListViewCtrl类非常简单,只添加了很少的函数,当然,它使用了一个新的辅助类CCheckListViewCtrlImplTraits,它和CWinTraits类的作用类似,只是第三个参数是list view控件的扩展样式属性,如果你没有定义自己的CCheckListViewCtrlImplTraits,它将使用没默认的样式:LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT。

下面是一个定义list view扩展样式属性的例子,加入了一个使用这个样式的新类。(注意,扩展属性必须包含LVS_EX_CHECKBOXES,否则会因起断言错误消息。)

[cpp] view plain copy print ?
  1. typedef CCheckListViewCtrlImplTraits<  
  2.     WS_CHILD | WS_VISIBLE | LVS_REPORT,   
  3.     WS_EX_CLIENTEDGE,  
  4.     LVS_EX_CHECKBOXES | LVS_EX_GRIDLINES | LVS_EX_UNDERLINEHOT |  
  5.       LVS_EX_ONECLICKACTIVATE> CMyCheckListTraits;  
  6.    
  7. class CMyCheckListCtrl :  
  8.     public CCheckListViewCtrlImpl
  9.                                   CMyCheckListTraits>  
  10. {  
  11. private:  
  12.     typedef CCheckListViewCtrlImpl
  13.                                    CMyCheckListTraits> baseClass;  
  14. public:  
  15.     BEGIN_MSG_MAP(CMyCheckListCtrl)  
  16.         CHAIN_MSG_MAP(baseClass)  
  17.     END_MSG_MAP()  
  18. };  
typedef CCheckListViewCtrlImplTraits<
    WS_CHILD | WS_VISIBLE | LVS_REPORT, 
    WS_EX_CLIENTEDGE,
    LVS_EX_CHECKBOXES | LVS_EX_GRIDLINES | LVS_EX_UNDERLINEHOT |
      LVS_EX_ONECLICKACTIVATE> CMyCheckListTraits;
 
class CMyCheckListCtrl :
    public CCheckListViewCtrlImpl
{
private:
    typedef CCheckListViewCtrlImpl baseClass;
public:
    BEGIN_MSG_MAP(CMyCheckListCtrl)
        CHAIN_MSG_MAP(baseClass)
    END_MSG_MAP()
};

CCheckListViewCtrl methods

SubclassWindow()

当子类化一个已经存在的list view控件时,SubclassWindow()查看CCheckListViewCtrlImplTraits的扩展样式属性并将之应用到控件上。未用到前两个参数(窗口样式和扩展窗口样式)。

SetCheckState() and GetCheckState()

这些方法实际上是在CListViewCtrl中,SetCheckState()使用行的索引和一个布尔类型参数,该布尔参数的值表示是否check这一行。GetCheckState()以行索引未参数,返回改行的checked状态。

CheckSelectedItems()

这个方法使用item的索引作为参数,它翻转这个item的check状态,这个item必须是被选定的,同时还将其他所有被选择的item设置成相应状态(译者加:多选状态下)。你大概不会用到这个方法,因为CCheckListViewCtrl会在check box被单击或用户按下了空格键时设置相应的item的状态。

下面是ControlMania2中的CCheckListViewCtrl的样子:

WTL 详细介绍_第33张图片

CTreeViewCtrlEx and CTreeItem

有两个类使得树控件的使用简化了很多:CTreeItem类封装了HTREEITEM,一个CTreeItem对象含有一个HTREEITEM和一个指向包含这个HTREEITEM的树控件的指针,使你不必每次调用都引用树控件;CTreeViewCtrlEx和CTreeViewCtrl一样,只是它的方法操作CTreeItem而不是HTREEITEM。例如,InsertItem()函数返回一个CTreeItem而不是HTREEITEM,你可以使用CTreeItem操作新添加的item。下面是一个例子:

[cpp] view plain copy print ?
  1. // Using plain HTREEITEMs:   
  2. HTREEITEM hti, hti2;  
  3.    
  4.     hti = m_wndTree.InsertItem ( "foo", TVI_ROOT, TVI_LAST );  
  5.     hti2 = m_wndTree.InsertItem ( "bar", hti, TVI_LAST );  
  6.     m_wndTree.SetItemData ( hti2, 100 );  
  7.    
  8. // Using CTreeItems:   
  9. CTreeItem ti, ti2;  
  10.    
  11.     ti = m_wndTreeEx.InsertItem ( "foo", TVI_ROOT, TVI_LAST );  
  12.     ti2 = ti.AddTail ( "bar", 0 );  
  13.     ti2.SetData ( 100 );  
// Using plain HTREEITEMs:
HTREEITEM hti, hti2;
 
    hti = m_wndTree.InsertItem ( "foo", TVI_ROOT, TVI_LAST );
    hti2 = m_wndTree.InsertItem ( "bar", hti, TVI_LAST );
    m_wndTree.SetItemData ( hti2, 100 );
 
// Using CTreeItems:
CTreeItem ti, ti2;
 
    ti = m_wndTreeEx.InsertItem ( "foo", TVI_ROOT, TVI_LAST );
    ti2 = ti.AddTail ( "bar", 0 );
    ti2.SetData ( 100 );

CTreeViewCtrl对HTREEITEM的每一个操作,CTreeItem都有与之对应的方法,正像每一个关于HWND的API都有一个CWindow方法与之对应一样。查看ControlMania2的代码可以看到更多的CTreeViewCtrlEx和CTreeItem类的方法的演示。

CHyperLink

CHyperLink是一个CWindowImpl派生类,它子类化一个static text控件,使之变成可点击的超链接。CHyperLink根据用户的IE使用的颜色画链接对象,还支持键盘导航。CHyperLink类的构造函数没有参数,下面是其它的公有方法。

CHyperLink methods

CHyperLinkImpl类内含实现一个超链接的全部代码,如果不需要重载它的方法或处理消息的话,你可以直接使用CHyperLink类。

SubclassWindow()

[cpp] view plain copy print ?
  1. BOOL SubclassWindow(HWND hWnd)  
BOOL SubclassWindow(HWND hWnd)

重载函数SubclassWindow()完成控件子类化,然后初始化该类保有的内部数据。

Text label management

[cpp] view plain copy print ?
  1. bool GetLabel(LPTSTR lpstrBuffer, int nLength)  
  2. bool SetLabel(LPCTSTR lpstrLabel)  
bool GetLabel(LPTSTR lpstrBuffer, int nLength)
bool SetLabel(LPCTSTR lpstrLabel)

获得或设置控件显示的文字,如果不指定显示文字,控件会显示资源编辑器指定给控件的静态字符串。

Hyperlink management

[cpp] view plain copy print ?
  1. bool GetHyperLink(LPTSTR lpstrBuffer, int nLength)  
  2. bool SetHyperLink(LPCTSTR lpstrLink)  
bool GetHyperLink(LPTSTR lpstrBuffer, int nLength)
bool SetHyperLink(LPCTSTR lpstrLink)

获得或设置控件关联超链接的URL,如果不指定超链接URL,控件会使用显示的文字字符串作为URL。

Navigation

[cpp] view plain copy print ?
  1. bool Navigate()  
bool Navigate()

导航到当前超链接的URL,该URL或者是由SetHyperLink()函数指定的URL,或者就是控件的窗口文字。

Tooltip management

没有公开的方法设置工具提示,所以需要直接使用CToolTipCtrl成员m_tip。

下图显示的就是ControlMania2对话框中的超链接控件:

WTL 详细介绍_第34张图片

在OnInitDialog()函数中设置URL:

[cpp] view plain copy print ?
  1. m_wndLink.SetHyperLink ( _T("http://www.codeproject.com/") );  
 m_wndLink.SetHyperLink ( _T("http://www.codeproject.com/") );

对话框中控件的UI Updating

对话框中的的UI updating控制比MFC中简单得多,在MFC中,你需要响应未公开的WM_KICKIDLE消息,处理这个消息并触发控件的updating,在WTL中,没有这个诡计,不过向导存在一个BUG,需要手工添加一行代码解决这个问题。

首先需要记住的是对话框必须是无模式的,因为CUpdateUI需要在程序的消息循环控制下工作。如果对话框是模式的,系统处理消息循环,我们程序的空闲处理函数就不会被调用,由于CUpdateUI是在空闲时间工作的,所以没有空闲处理就没有UI updating。

ControlMania2的对话框是非模式的,类定义的开始部分很像是一个框架窗口类:

[cpp] view plain copy print ?
  1. class CMainDlg : public CDialogImplpublic CUpdateUI,  
  2.                  public CMessageFilter, public CIdleHandler  
  3. {  
  4. public:  
  5.     enum { IDD = IDD_MAINDLG };  
  6.    
  7.     virtual BOOL PreTranslateMessage(MSG* pMsg);  
  8.     virtual BOOL OnIdle();  
  9.    
  10.     BEGIN_MSG_MAP_EX(CMainDlg)  
  11.         MSG_WM_INITDIALOG(OnInitDialog)  
  12.         COMMAND_ID_HANDLER_EX(IDOK, OnOK)  
  13.         COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)  
  14.         COMMAND_ID_HANDLER_EX(IDC_ALYSON_BTN, OnAlysonODBtn)  
  15.     END_MSG_MAP()  
  16.    
  17.     BEGIN_UPDATE_UI_MAP(CMainDlg)  
  18.     END_UPDATE_UI_MAP()  
  19. //...   
  20. };  
class CMainDlg : public CDialogImpl, public CUpdateUI,
                 public CMessageFilter, public CIdleHandler
{
public:
    enum { IDD = IDD_MAINDLG };
 
    virtual BOOL PreTranslateMessage(MSG* pMsg);
    virtual BOOL OnIdle();
 
    BEGIN_MSG_MAP_EX(CMainDlg)
        MSG_WM_INITDIALOG(OnInitDialog)
        COMMAND_ID_HANDLER_EX(IDOK, OnOK)
        COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
        COMMAND_ID_HANDLER_EX(IDC_ALYSON_BTN, OnAlysonODBtn)
    END_MSG_MAP()
 
    BEGIN_UPDATE_UI_MAP(CMainDlg)
    END_UPDATE_UI_MAP()
//...
};

注意CMainDlg类从CUpdateUI派生并含有一个update UI链。OnInitDialog()做了这些工作,这和前面介绍的框架窗口中的代码很相似:

[cpp] view plain copy print ?
  1. // register object for message filtering and idle updates  
  2. CMessageLoop* pLoop = _Module.GetMessageLoop();  
  3. ATLASSERT(pLoop != NULL);  
  4. pLoop->AddMessageFilter(this);  
  5. pLoop->AddIdleHandler(this);  
  6.   
  7. UIAddChildWindowContainer(m_hWnd);  
    // register object for message filtering and idle updates
    CMessageLoop* pLoop = _Module.GetMessageLoop();
    ATLASSERT(pLoop != NULL);
    pLoop->AddMessageFilter(this);
    pLoop->AddIdleHandler(this);
 
    UIAddChildWindowContainer(m_hWnd);

只是这次我们不是调用UIAddToolbar()或UIAddStatusBar(),而是调用UIAddChildWindowContainer(),它告诉CUpdateUI我们的对话框含有需要updating的字窗口,只要看看OnIdle(),你会怀疑少了写什么:

[cpp] view plain copy print ?
  1. BOOL CMainDlg::OnIdle()  
  2. {  
  3.     return FALSE;  
  4. }  
BOOL CMainDlg::OnIdle()
{
    return FALSE;
}

你可能猜想这里应该调用另一个CUpdateUI的方法做一些实在的updating工作,你是对的,应该是这样的,向导在OnIdle()中漏掉了一行代码,现在加上:

[cpp] view plain copy print ?
  1. BOOL CMainDlg::OnIdle()  
  2. {  
  3.     UIUpdateChildWindows();  
  4.     return FALSE;  
  5. }  
BOOL CMainDlg::OnIdle()
{
    UIUpdateChildWindows();
    return FALSE;
}

为了演示UI updating,我们设定鼠标点击左边的位图按钮,使得右边的按钮变得可用或禁用。先在update UI链中添加一个消息入口,使用UPDUI_CHILDWINDOW标志表示此入口是子窗口类型:

[cpp] view plain copy print ?
  1. BEGIN_UPDATE_UI_MAP(CMainDlg)  
  2.     UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)  
  3. END_UPDATE_UI_MAP()  
    BEGIN_UPDATE_UI_MAP(CMainDlg)
        UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)
    END_UPDATE_UI_MAP()

在左边的按钮的单击事件处理中,我们调用UIEnable()来翻转另一个按钮的使能状态:

[cpp] view plain copy print ?
  1. void CMainDlg::OnAlysonODBtn ( UINT uCode, int nID, HWND hwndCtrl )  
  2. {  
  3. static bool s_bBtnEnabled = true;  
  4.   
  5.     s_bBtnEnabled = !s_bBtnEnabled;  
  6.     UIEnable ( IDC_ALYSON_BMPBTN, s_bBtnEnabled );  
  7. }  
void CMainDlg::OnAlysonODBtn ( UINT uCode, int nID, HWND hwndCtrl )
{
static bool s_bBtnEnabled = true;

    s_bBtnEnabled = !s_bBtnEnabled;
    UIEnable ( IDC_ALYSON_BMPBTN, s_bBtnEnabled );
}

DDV

WTL的对话框数据验证(DDV)比MFC简单一些,在MFC中你需要分别使用DDX(对话框数据交换)宏和DDV(对话框数据验证)宏,在WTL中只需一个宏就可以了,WTL包含基本的数据验证支持,在DDV链中可以使用三个宏:

DDX_TEXT_LEN
和DDX_TEXT一样,只是还要验证字符串的长度(不包含结尾的空字符)小于或等于限制长度。
DDX_INT_RANGE and DDX_UINT_RANGE
和DDX_INT,DDX_UINT一样,还加了对数字的最大最小值的验证。
DDX_FLOAT_RANGE
除了像DDX_FLOAT一样完成数据交换之外,还验证数字的最大最小值。
ControlMania2有一个ID是IDC_FAV_SEASON的edit box,它和成员变量m_nSeason相关联。

WTL 详细介绍_第35张图片

由于有效的值是1到7,所以使用这样的数据验证宏:

[cpp] view plain copy print ?
  1. BEGIN_DDX_MAP(CMainDlg)  
  2. //...   
  3.     DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)  
  4. END_DDX_MAP()  
    BEGIN_DDX_MAP(CMainDlg)
    //...
        DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
    END_DDX_MAP()

OnOK()调用DoDataExchange()获得season的数值,并验证是在1到7之间。

处理DDV验证失败

如果控件的数据验证失败,CWinDataExchange会调用重载函数OnDataValidateError(),默认到处理是驱动PC喇叭发出声音,你可能想给出更友好的错误指示。OnDataValidateError()的函数原型是:

[cpp] view plain copy print ?
  1. void OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data );  
void OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data );

_XData是一个WTL的内部数据结构,CWinDataExchange根据输入的数据和允许的数据范围填充这个数据结构。下面是这个数据结构的定义:

[cpp] view plain copy print ?
  1. struct _XData  
  2. {  
  3.     _XDataType nDataType;  
  4.     union  
  5.     {  
  6.         _XTextData textData;  
  7.         _XIntData intData;  
  8.         _XFloatData floatData;  
  9.     };  
  10. };  
struct _XData
{
    _XDataType nDataType;
    union
    {
        _XTextData textData;
        _XIntData intData;
        _XFloatData floatData;
    };
};

nDataType指示联合中的三个成员那个是有意义的,nDataType 的取值可以是:

[cpp] view plain copy print ?
  1. enum _XDataType  
  2. {  
  3.     ddxDataNull = 0,  
  4.     ddxDataText = 1,  
  5.     ddxDataInt = 2,  
  6.     ddxDataFloat = 3,  
  7.     ddxDataDouble = 4  
  8. };  
enum _XDataType
{
    ddxDataNull = 0,
    ddxDataText = 1,
    ddxDataInt = 2,
    ddxDataFloat = 3,
    ddxDataDouble = 4
};

在我们的例子中,nDataType的值是ddxDataInt,这表示_XData中的_XIntData成员是有效的,_XIntData是个简单的数据结构:

[cpp] view plain copy print ?
  1. struct _XIntData  
  2. {  
  3.     long nVal;  
  4.     long nMin;  
  5.     long nMax;  
  6. }  
struct _XIntData
{
    long nVal;
    long nMin;
    long nMax;
}

我们重载OnDataValidateError()函数,显示错误信息并告诉用户允许的数值范围:

[cpp] view plain copy print ?
  1. void CMainDlg::OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data )  
  2. {  
  3. CString sMsg;  
  4.    
  5.     sMsg.Format ( _T("Enter a number between %d and %d"),  
  6.                   data.intData.nMin, data.intData.nMax );  
  7.    
  8.     MessageBox ( sMsg, _T("ControlMania2"), MB_ICONEXCLAMATION );  
  9.    
  10.     ::SetFocus ( GetDlgItem(nCtrlID) );  
  11. }  
void CMainDlg::OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data )
{
CString sMsg;
 
    sMsg.Format ( _T("Enter a number between %d and %d"),
                  data.intData.nMin, data.intData.nMax );
 
    MessageBox ( sMsg, _T("ControlMania2"), MB_ICONEXCLAMATION );
 
    ::SetFocus ( GetDlgItem(nCtrlID) );
}

_XData中的另外两个结构_XTextData和_XFloatData的定义在atlddx.h中,感兴趣的话可以打开这个文件查看一下。

改变对话框的大小

WTL引起我的注意的第一件事是对可调整大小对话框的内建的支持。在这之前我曾写过一篇 关于这个主题的文章,详情请参考这篇文章。简单的说就是将CDialogResize类添加到对话框的集成列表,在OnInitDialog()中调用DlgResize_Init(),然后将消息链入CDialogResize。

继续

下一章,我将介绍如何在对话框中使用ActiveX控件和如何处理控件触发的事件。

 

WTL for MFC Programmers, Part VI - Hosting ActiveX Controls

分类: wtl 18人阅读 评论(0) 收藏 举报
c++ wtl
介绍

在第六章,我将介绍ATL对在对话框中使用ActiveX控件的支持,由于ActiveX控件就是ATL的专业,所以WTL没有添加其他的辅助类。不过,在ATL中使用ActiveX控件与在MFC中有很大的不同,所以需要重点介绍。我将介绍如何包容一个控件并处理控件的事件,开发ATL应用程序相对于MFC的类向导来说有点不方便。在WTL程序中自然可以使用ATL对包容ActiveX控件的支持。

例子工程演示如何使用IE的浏览器控件,我选择浏览器控件有两个好处:

  1. 每台计算机都有这个控件,并且
  2. 它有很多方法和事件,是个用来做演示的好例子。
我当然无法与那些花了大量时间编写基于IE浏览器控件的定制浏览器的人相比,不过,当你读完本篇文章之后,你就知道如何开始编写自己定制的浏览器!

从使用向导开始

创建工程


WTL的向导可以创建一个支持包容ActiveX控件的程序,我将开始一个名为IEHoster的新工程。我们像上一章一样使用无模式对话框,只是这次要选上支持ActiveX控件包容(Enable ActiveX Control Hosting),如下图:

WTL 详细介绍_第36张图片

选上这个check box将使我们的对话框从CAxDialogImpl派生,这样就可以包容ActiveX控件。在向导的第二页还有一个名为包容ActiveX控件的check box,但是选择这个好像对最后的结果没有影响,所以在第一页就可以点击“Finish”结束向导。

向导生成的代码

在这一节我将介绍一些以前没有见过的新代码(由向导生成的),下一节介绍ActiveX包容类的细节。

首先要看的文件是stdafx.h,它包含了这些文件:

[cpp] view plain copy print ?
  1. #include    
  2. #include    
  3.    
  4. extern CAppModule _Module;  
  5.    
  6. #include    
  7. #include    
  8. #include    
  9. #include    
  10. // .. other WTL headers ...  
#include 
#include 
 
extern CAppModule _Module;
 
#include 
#include 
#include 
#include 
// .. other WTL headers ...
[cpp] view plain copy print ?
  1.   

atlcom.h和atlhost.h是很重要的两个,它们含有一些COM相关类的定义(比如智能指针CComPtr),还有可以包容控件的窗口类。

接下来看看maindlg.h中声明的CMainDlg类:
[cpp] view plain copy print ?
  1. class CMainDlg : public CAxDialogImpl,  
  2.                  public CUpdateUI,  
  3.                  public CMessageFilter, public CIdleHandler  
class CMainDlg : public CAxDialogImpl,
                 public CUpdateUI,
                 public CMessageFilter, public CIdleHandler

CMainDlg现在是从CAxDialogImpl类派生的,这是使对话框支持包容ActiveX控件的第一步。

最后,看看WinMain()中新加的一行代码:

[cpp] view plain copy print ?
  1. int WINAPI _tWinMain(...)  
  2. {  
  3. //...   
  4.     _Module.Init(NULL, hInstance);  
  5.    
  6.     AtlAxWinInit();  
  7.    
  8.     int nRet = Run(lpstrCmdLine, nCmdShow);  
  9.    
  10.     _Module.Term();  
  11.     return nRet;  
  12. }  
int WINAPI _tWinMain(...)
{
//...
    _Module.Init(NULL, hInstance);
 
    AtlAxWinInit();
 
    int nRet = Run(lpstrCmdLine, nCmdShow);
 
    _Module.Term();
    return nRet;
}

AtlAxWinInit()注册了一个类名未AtlAxWin的窗口类,ATL用它创建ActiveX控件的包容窗口。

使用资源编辑器添加控件

和MFC的程序一样,ATL也可以使用资源编辑器向对话框添加控件。首先,在对话框编辑器上点击鼠标右键,在弹出的菜单中选择“Insert ActiveX control”:

WTL 详细介绍_第37张图片

VC将系统安装的控件显示在一个列表中,滚动列表选择“Microsoft Web Browser”,单击Insert按钮将控件加入到对话框中。查看控件的属性,将ID设为IDC_IE。对话框中的控件显示应该是这个样子的:

WTL 详细介绍_第38张图片

如果现在编译运行程序,你会看到对话框中的浏览器控件,它将显示一个空白页,因为我们还没有告诉它到哪里去。

在下一节,我将介绍与创建和包容ActiveX控件有关的ATL类,同时我们也会明白这些类是如何与浏览器交换信息的。

ATL中使用控件的类

在对话框中使用ActiveX控件需要两个类协同工作:CAxDialogImpl和CAxWindow。它们处理所有控件容器必须实现的接口方法,提供通用的功能函数,例如查询控件的某个特殊的COM接口。

CAxDialogImpl

第一个类是CAxDialogImpl,你的对话框要能够包容控件就必须从CAxDialogImpl类派生而不是从CDialogImpl类派生。CAxDialogImpl类重载了Create()和DoModal()函数,这两个函数分别被全局函数AtlAxCreateDialog()和AtlAxDialogBox()调用。既然IEHoster对话框是由Create()创建的,我们看看AtlAxCreateDialog()到底做了什么工作。

AtlAxCreateDialog()使用辅助类_DialogSplitHelper装载对话框资源,这个辅助类遍历所以对话框的控件,查找由资源编辑器创建的特殊的入口,这些特殊的入口表示这是一个ActiveX控件。例如,下面是IEHoster.rc文件中浏览器控件的入口:

[cpp] view plain copy print ?
  1. CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",  
  2.         WS_TABSTOP,7,7,116,85  
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",
        WS_TABSTOP,7,7,116,85

第一个参数是窗口文字(空字符串),第二个是控件的ID,第三个是窗口的类名。_DialogSplitHelper::SplitDialogTemplate()函数找到以'{'开始的窗口类名时就知道这是一个ActiveX控件的入口。它在内存中创建了一个临时对话框模板,在这个新模板中这些特殊的控件入口被创建的AtlAxWin窗口代替,新的入口是在内存中的等价体:

[cpp] view plain copy print ?
  1. CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",IDC_IE,"AtlAxWin",  
  2.         WS_TABSTOP,7,7,116,85  
CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",IDC_IE,"AtlAxWin",
        WS_TABSTOP,7,7,116,85

结果就是创建了一个相同ID的AtlAxWin窗口,窗口的标题是ActiveX控件的GUID。所以你调用GetDlgItem(IDC_IE)返回的值是AtlAxWin窗口的句柄而不是ActiveX控件本身。

SplitDialogTemplate()函数完成工作后,AtlAxCreateDialog()接着调用CreateDialogIndirectParam()函数使用修改后的模板创建对话框。

AtlAxWin and CAxWindow

正如上面讲到的,AtlAxWin实际上是ActiveX控件的宿主窗口,AtlAxWin还会用到一个特殊的窗口接口类:CAxWindow,当AtlAxWin从模板创建一个对话框后,AtlAxWin的窗口处理过程,AtlAxWindowProc(),就会处理WM_CREATE消息并创建相应的ActiveX控件。ActiveX控件还可以在运行其间动态创建,不需要对话框模板,我会在后面介绍这种方法。

WM_CREATE的消息处理函数调用全局函数AtlAxCreateControl(),将AtlAxWin窗口的窗口标题作为参数传递给该函数,大家应该记得那实际就是浏览器控件的GUID。AtlAxCreateControl()有会调用一堆其他函数,不过最终会用到CreateNormalizedObject()函数,这个函数将窗口标题转换成GUID,并最终调用CoCreateInstance()创建ActiveX控件。

由于ActiveX控件是AtlAxWin的子窗口,所以对话框不能直接访问控件,当然CAxWindow提供了这些方法通控件通信,最常用的一个是QueryControl(),这个方法调用控件的QueryInterface()方法。例如,你可以使用QueryControl()从浏览器控件得到IWebBrowser2接口,然后使用这个接口将浏览器引导到指定的URL。

调用控件的方法

既然我们的对话框有一个浏览器控件,我们可以使用COM接口与之交互。我们做得第一件事情就是使用IWebBrowser2接口将其引导到一个新URL处。在OnInitDialog()函数中,我们将一个CAxWindow变量与包容控件的AtlAxWin联系起来。

[cpp] view plain copy print ?
  1. CAxWindow wndIE = GetDlgItem(IDC_IE);  
CAxWindow wndIE = GetDlgItem(IDC_IE);

然后声明一个IWebBrowser2的接口指针并查询浏览器控件的这个接口,使用CAxWindow::QueryControl():

[cpp] view plain copy print ?
  1. CComPtr pWB2;  
  2. HRESULT hr;  
  3. hr = wndIE.QueryControl ( &pWB2 );  
CComPtr pWB2;
HRESULT hr;
hr = wndIE.QueryControl ( &pWB2 );

QueryControl()调用浏览器控件的QueryInterface()方法,如果成功就会返回IWebBrowser2接口,我们可以调用Navigate():

[cpp] view plain copy print ?
  1. if ( pWB2 )  
  2.     {  
  3.     CComVariant v;  // empty variant  
  4.   
  5.     pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"),   
  6.                      &v, &v, &v, &v );  
  7.     }  
    if ( pWB2 )
        {
        CComVariant v;  // empty variant
 
        pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"), 
                         &v, &v, &v, &v );
        }

响应控件触发的事件

从浏览器控件得到接口非常简单,通过它可以单向的与控件通信。通常控件也会以事件的形式与外界通信,ATL有专用的类包装连接点和事件相应,所以我们可以从控件接收到这些事件。为使用对事件的支持需要做四件事:

  1. 将CMainDlg变成COM对象
  2. 添加IDispEventSimpleImpl到CMainDlg的继承列表
  3. 填写事件映射链,它指示哪些事件需要处理
  4. 编写事件响应函数
CMainDlg的修改

将CMainDlg转变成COM对象的原因是事件相应是基于IDispatch的,为了让CMainDlg暴露这个接口,它必须是个COM对象。IDispEventSimpleImpl提供了IDispatch接口的实现和建立连接点所需的处理函数,当事件发生时IDispEventSimpleImpl还调用我们想要接收的事件的处理函数。

以下的类需要添加到CMainDlg的集成列表中,同时COM_MAP列出了CMainDlg暴露的接口:

[cpp] view plain copy print ?
  1. #include     // browser control definitions  
  2. #include   // browser event dispatch IDs  
  3.    
  4. class CMainDlg : public CAxDialogImpl,  
  5.                  public CUpdateUI,  
  6.                  public CMessageFilter, public CIdleHandler,  
  7.                  public CComObjectRootEx,  
  8.                  public CComCoClass,  
  9.                  public IDispEventSimpleImpl<37, CMainDlg, &DIID_DWebBrowserEvents2>  
  10. {  
  11. ...  
  12.   BEGIN_COM_MAP(CMainDlg)  
  13.     COM_INTERFACE_ENTRY2(IDispatch, IDispEventSimpleImpl)  
  14.   END_COM_MAP()  
  15. };  
#include     // browser control definitions
#include   // browser event dispatch IDs
 
class CMainDlg : public CAxDialogImpl,
                 public CUpdateUI,
                 public CMessageFilter, public CIdleHandler,
                 public CComObjectRootEx,
                 public CComCoClass,
                 public IDispEventSimpleImpl<37, CMainDlg, &DIID_DWebBrowserEvents2>
{
...
  BEGIN_COM_MAP(CMainDlg)
    COM_INTERFACE_ENTRY2(IDispatch, IDispEventSimpleImpl)
  END_COM_MAP()
};

CComObjectRootEx类CComCoClass共同使CMainDlg成为一个COM对象,IDispEventSimpleImpl的模板参数是事件的ID,我们的类名和连接点接口的IID。事件ID可以是任意正数,连接点对象的IID是DIID_DWebBrowserEvents2,可以在浏览器控件的相关文档中找到这些参数,也可以查看exdisp.h。

填写事件映射链

下一步是给CMainDlg添加事件映射链,这个映射链将我们感兴趣的事件和我们的处理函数联系起来。我们要看的第一个事件是DownloadBegin,当浏览器开始下载一个页面时就会触发这个事件,我们响应这个事件显示“please wait”信息给用户,让用户知道浏览器正在忙。在MSDN中可以查到DWebBrowserEvents2::DownloadBegin事件的原型

[cpp] view plain copy print ?
  1. void DownloadBegin();  
  void DownloadBegin();

这个事件没有参数,也不需要返回值。为了将这个事件的原型转换成事件响应链,我们需要写一个_ATL_FUNC_INFO结构,它包含返回值,参数的个数和参数类型。由于事件是基于IDispatch的,所以所有的参数都用VARIANT表示,这个数据结构的描述相当长(支持很多个数据类型),以下是常用的几个:

VT_EMPTY: void
VT_BSTR: BSTR 格式的字符串
VT_I4: 4字节有符号整数,用于long类型的参数
VT_DISPATCH: IDispatch*
VT_VARIANT>: VARIANT
VT_BOOL: VARIANT_BOOL (允许的取值是VARIANT_TRUE和VARIANT_FALSE)

另外,标志VT_BYREF表示将一个参数转换成相应的指针。例如,VT_VARIANT|VT_BYREF表示VARIANT*类型。下面是_ATL_FUNC_INFO的定义:

[cpp] view plain copy print ?
  1. #define _ATL_MAX_VARTYPES 8   
  2.    
  3. struct _ATL_FUNC_INFO  
  4. {  
  5.     CALLCONV cc;  
  6.     VARTYPE  vtReturn;  
  7.     SHORT    nParams;  
  8.     VARTYPE  pVarTypes[_ATL_MAX_VARTYPES];  
  9. };  
#define _ATL_MAX_VARTYPES 8
 
struct _ATL_FUNC_INFO
{
    CALLCONV cc;
    VARTYPE  vtReturn;
    SHORT    nParams;
    VARTYPE  pVarTypes[_ATL_MAX_VARTYPES];
};

参数:

cc
我们的事件响应函数的调用方式约定,这个参数必须是CC_STDCALL,表示是__stdcall方式
vtReturn
事件响应函数的返回值类型
nParams
事件带的参数个数
pVarTypes
相应的参数类型,按从左到右的顺序
了解这些之后,我们就可以填写DownloadBegin事件处理的_ATL_FUNC_INFO结构:

[cpp] view plain copy print ?
  1. _ATL_FUNC_INFO DownloadInfo = { CC_STDCALL, VT_EMPTY, 0 };  
_ATL_FUNC_INFO DownloadInfo = { CC_STDCALL, VT_EMPTY, 0 };

现在,回到事件响应链,我们为每一个我们想要处理的事件添加一个SINK_ENTRY_INFO宏,下面是处理DownloadBegin事件的宏:

[cpp] view plain copy print ?
  1. class CMainDlg : public ...  
  2. {  
  3. ...  
  4.   BEGIN_SINK_MAP(CMainDlg)  
  5.     SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,  
  6.                     OnDownloadBegin, &DownloadInfo)  
  7.   END_SINK_MAP()  
  8. };  
class CMainDlg : public ...
{
...
  BEGIN_SINK_MAP(CMainDlg)
    SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,
                    OnDownloadBegin, &DownloadInfo)
  END_SINK_MAP()
};

这个宏的参数是事件的ID(37,与我们在IDispEventSimpleImpl的继承列表中使用的ID一样),事件接口的IID,事件的dispatch ID(可以在MSDN或exdispid.h头文件中查到),事件处理函数的名字和指向描述这个事件处理的_ATL_FUNC_INFO结构的指针。

编写事件处理函数

好了,等了这么长时间(吹个口哨!),我们可以写事件处理函数了:

[cpp] view plain copy print ?
  1. void __stdcall CMainDlg::OnDownloadBegin()  
  2. {  
  3.   // show "Please wait" here...  
  4. }  
void __stdcall CMainDlg::OnDownloadBegin()
{
  // show "Please wait" here...
}

现在来看一个复杂一点的事件,比如BeforeNavigate2,这个事件的原型是:

[cpp] view plain copy print ?
  1. void BeforeNavigate2 (   
  2.     IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,  
  3.     VARIANT* TargetFrameName, VARIANT* PostData,  
  4.     VARIANT* Headers, VARIANT_BOOL* Cancel );  
void BeforeNavigate2 ( 
    IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
    VARIANT* TargetFrameName, VARIANT* PostData,
    VARIANT* Headers, VARIANT_BOOL* Cancel );

此方法有7个参数,对于VARIANT类型参数可以从MSDN查到它到底传递的是什么类型的数据,我们感兴趣的是URL,是一个BSTR类型的字符串。

描述BeforeNavigate2事件的_ATL_FUNC_INFO结构是这样的:

[cpp] view plain copy print ?
  1. _ATL_FUNC_INFO BeforeNavigate2Info =  
  2.     { CC_STDCALL, VT_EMPTY, 7,  
  3.         { VT_DISPATCH, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF,  
  4.           VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF,  
  5.           VT_BOOL|VT_BYREF }  
  6. };  
_ATL_FUNC_INFO BeforeNavigate2Info =
    { CC_STDCALL, VT_EMPTY, 7,
        { VT_DISPATCH, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF,
          VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF,
          VT_BOOL|VT_BYREF }
};

和前面一样,返回值类型是VT_EMPTY表示没有返回值,nParams是7,表示有7个参数。接着是参数类型数组,这些类型前面介绍过了,例如VT_DISPATCH表示IDispatch*。

事件响应链的入口与前面的例子很相似:

[cpp] view plain copy print ?
  1. BEGIN_SINK_MAP(CMainDlg)  
  2.   SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,  
  3.                   OnDownloadBegin, &DownloadInfo)  
  4.   SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2,  
  5.                   OnBeforeNavigate2, &BeforeNavigate2Info)  
  6. END_SINK_MAP()  
  BEGIN_SINK_MAP(CMainDlg)
    SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,
                    OnDownloadBegin, &DownloadInfo)
    SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2,
                    OnBeforeNavigate2, &BeforeNavigate2Info)
  END_SINK_MAP()

事件处理函数是这个样子:

[cpp] view plain copy print ?
  1. void __stdcall CMainDlg::OnBeforeNavigate2 (  
  2.     IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,   
  3.     VARIANT* TargetFrameName, VARIANT* PostData,   
  4.     VARIANT* Headers, VARIANT_BOOL* Cancel )  
  5. {  
  6. CString sURL = URL->bstrVal;  
  7.    
  8.   // ... log the URL, or whatever you'd like ...  
  9. }  
void __stdcall CMainDlg::OnBeforeNavigate2 (
    IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, 
    VARIANT* TargetFrameName, VARIANT* PostData, 
    VARIANT* Headers, VARIANT_BOOL* Cancel )
{
CString sURL = URL->bstrVal;
 
  // ... log the URL, or whatever you'd like ...
}

我打赌你现在是越来越喜欢ClassWizard了,因为当你向MFC的对话框插入一个ActiveX控件时ClassWizard自动为你完成了所有工作。

将CMainDlg转换成对象需要注意几件事情,首先必须修改全局函数Run(),现在CMainDlg是个COM对象,我们必须使用CComObject创建CMainDlg:

[cpp] view plain copy print ?
  1. int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)  
  2. {  
  3.     CMessageLoop theLoop;  
  4.     _Module.AddMessageLoop(&theLoop);  
  5.    
  6. CComObject dlgMain;  
  7.    
  8.     dlgMain.AddRef();  
  9.    
  10.     if ( dlgMain.Create(NULL) == NULL )  
  11.         {  
  12.         ATLTRACE(_T("Main dialog creation failed!\n"));  
  13.         return 0;  
  14.         }  
  15.    
  16.     dlgMain.ShowWindow(nCmdShow);  
  17.    
  18.     int nRet = theLoop.Run();  
  19.    
  20.     _Module.RemoveMessageLoop();  
  21.     return nRet;  
  22. }  
int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
    CMessageLoop theLoop;
    _Module.AddMessageLoop(&theLoop);
 
CComObject dlgMain;
 
    dlgMain.AddRef();
 
    if ( dlgMain.Create(NULL) == NULL )
        {
        ATLTRACE(_T("Main dialog creation failed!\n"));
        return 0;
        }
 
    dlgMain.ShowWindow(nCmdShow);
 
    int nRet = theLoop.Run();
 
    _Module.RemoveMessageLoop();
    return nRet;
}

另一个可替代的方法是不使用CComObject,而使用CComObjectStack类,并删除dlgMain.AddRef()这一行代码,CComObjectStack对IUnknown的三个方法的实现有些微不足道(它们只是简单的从函数返回),因为它们不是必需的--这样的COM对象可以忽略对引用的计数,因为它们仅仅是创建在栈中的临时对象。

当然这并不是完美的解决方案,CComObjectStack用于短命的临时对象,不幸的是只要调用它的任何一个IUnknown方法都会引发断言错误。因为CMainDlg对象在开始监听事件时会调用AddRef,所以CComObjectStack不适用于这种情况。

解决这个问题要么坚持使用CComObject,要么从CComObjectStack派生一个CComObjectStack2类,允许对IUnknow方法调用。CComObject的那个不必要的引用计数并无大碍--人们不会注意到它的发生--但是如果你必须节省那个CPU时钟周期的话,你可以使用本章的例子工程代码中的CComObjectStack2类。

回顾例子工程

现在我们已经看到事件响应如何工作了,再来看看完整的IEHoster工程,它包容了一个浏览器控件并响应了6个事件,它还显示了一个事件列表,你会对浏览器如何使用它们提供带进度条的界面有个感性的认识,程序处理了以下几个事件:

  • BeforeNavigate2和NavigateComplete2:这些事件让程序可以控制URL的导航,如果你响应了BeforeNavigate2事件,你可以在事件的处理函数中取消导航。
  • DownloadBegin和DownloadComplete:程序使用这些事件控制“wait”消息,这表示浏览器正在工作。一个更优美的程序会像IE一样在此期间使用一段动画。
  • CommandStateChange:这个事件告诉程序向前和向后导航命令何时可用,应用程序将相应的按钮变为可用或不可用。
  • StatusTextChange:这个事件会在几种情况下触发,例如鼠标移到一个超链接上。这个事件发送一个字符串,应用程序响应这个事件,将这个字符串显示在浏览器窗口下的静态控件上。
程序有四个按钮控制浏览器工作:向后,向前,停止和刷新,它们分别调用IWebBrowser2相应的方法。

事件和伴随事件发送的数据都被记录在列表控件中,你可以看到事件的触发,你还可以关闭一些事件记录而仅仅观察其中的一辆个事件。为了演示事件处理的重要作用,我们在BeforeNavigate2事件处理函数中检查URL,如果发现“doubleclick.net”就取消导航。广告和弹出窗口过滤器等一些IE的插件使用的就是这个方法而不是HTTP代理,下面就是做这些检查的代码。

[cpp] view plain copy print ?
  1. void __stdcall CMainDlg::OnBeforeNavigate2 (  
  2.     IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,   
  3.     VARIANT* TargetFrameName, VARIANT* PostData,   
  4.     VARIANT* Headers, VARIANT_BOOL* Cancel )  
  5. {  
  6. USES_CONVERSION;  
  7. CString sURL;  
  8.    
  9.     sURL = URL->bstrVal;  
  10.    
  11.     // You can set *Cancel to VARIANT_TRUE to stop the   
  12.     // navigation from happening. For example, to stop   
  13.     // navigates to evil tracking companies like doubleclick.net:  
  14.     if ( sURL.Find ( _T("doubleclick.net") ) > 0 )  
  15.         *Cancel = VARIANT_TRUE;  
  16. }  
void __stdcall CMainDlg::OnBeforeNavigate2 (
    IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, 
    VARIANT* TargetFrameName, VARIANT* PostData, 
    VARIANT* Headers, VARIANT_BOOL* Cancel )
{
USES_CONVERSION;
CString sURL;
 
    sURL = URL->bstrVal;
 
    // You can set *Cancel to VARIANT_TRUE to stop the 
    // navigation from happening. For example, to stop 
    // navigates to evil tracking companies like doubleclick.net:
    if ( sURL.Find ( _T("doubleclick.net") ) > 0 )
        *Cancel = VARIANT_TRUE;
}

下面就是我们的程序工作起来的样子:

WTL 详细介绍_第39张图片

IEHoster还使用了前几章介绍过得类:CBitmapButton(用于浏览器控制按钮),CListViewCtrl(用于事件记录),DDX (跟踪checkbox的状态)和CDialogResize.

运行时创建ActiveX控件

出了使用资源编辑器,还可以在运行其间动态创建ActiveX控件。About对话框演示了这种技术。对话框编辑器预先放置了一个group box用于浏览器控件的定位:

WTL 详细介绍_第40张图片

在OnInitDialog()函数中我们使用 CAxWindow创建了一个新AtlAxWin,它定位于我们预先放置好的group box的位置上(这个group box随后被销毁):


[cpp] view plain copy print ?
  1. LRESULT CAboutDlg::OnInitDialog(...)  
  2. {  
  3. CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER );  
  4. CRect rc;  
  5. CAxWindow wndIE;  
  6.    
  7.     // Get the rect of the placeholder group box, then destroy   
  8.     // that window because we don't need it anymore.  
  9.     wndPlaceholder.GetWindowRect ( rc );  
  10.     ScreenToClient ( rc );  
  11.     wndPlaceholder.DestroyWindow();  
  12.    
  13.     // Create the AX host window.  
  14.     wndIE.Create ( *this, rc, _T(""),   
  15.                    WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN );  
LRESULT CAboutDlg::OnInitDialog(...)
{
CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER );
CRect rc;
CAxWindow wndIE;
 
    // Get the rect of the placeholder group box, then destroy 
    // that window because we don't need it anymore.
    wndPlaceholder.GetWindowRect ( rc );
    ScreenToClient ( rc );
    wndPlaceholder.DestroyWindow();
 
    // Create the AX host window.
    wndIE.Create ( *this, rc, _T(""), 
                   WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN );

接下来我们用CAxWindow方法创建一个ActiveX控件,有两个方法可以选择:CreateControl()和CreateControlEx()。CreateControlEx()用一个额外的参数返回接口指针,这样就不需要再调用QueryControl()函数。我们感兴趣的两个参数是第一个和第四个参数,第一个参数是字符串形式的浏览器控件的GUID,第四个参数是一个IUnknown*类型的指针,这个指针指向ActiveX控件的IUnknown接口。创建控件后就可以查询IWebBrowser2接口,然后就可以像前面一样控制它导航到某个URL。

[cpp] view plain copy print ?
  1. CComPtr punkCtrl;  
  2. CComQIPtr pWB2;  
  3. CComVariant v;  
  4.    
  5.     // Create the browser control using its GUID.  
  6.     wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}",   
  7.                             NULL, NULL, &punkCtrl );  
  8.    
  9.     // Get an IWebBrowser2 interface on the control and navigate to a page.  
  10.     pWB2 = punkCtrl;  
  11.     pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v );  
  12. }  
CComPtr punkCtrl;
CComQIPtr pWB2;
CComVariant v;
 
    // Create the browser control using its GUID.
    wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}", 
                            NULL, NULL, &punkCtrl );
 
    // Get an IWebBrowser2 interface on the control and navigate to a page.
    pWB2 = punkCtrl;
    pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v );
}

对于有ProgID的ActiveX控件可以传递ProgID给CreateControlEx(),代替GUID。例如,我们可以这样创建浏览器控件:

[cpp] view plain copy print ?
  1. // 使用控件的ProgID: 创建Shell.Explorer:   
  2. wndIE.CreateControlEx ( L"Shell.Explorer", NULL,  
  3.                         NULL, &punkCtrl );  
    // 使用控件的ProgID: 创建Shell.Explorer:
    wndIE.CreateControlEx ( L"Shell.Explorer", NULL,
                            NULL, &punkCtrl );

CreateControl()和CreateControlEx()还有一些重载函数用于一些使用浏览器的特殊情况,如果你的应用程序使用WEb页面作为HTML资源,你可以将资源ID作为第一个参数,ATL会创建浏览器控件并导航到这个资源。IEHoster包含一个ID为IDR_ABOUTPAGE的WEB页面资源,我们在About对话框中使用这些代码显示这个页面:

[cpp] view plain copy print ?
  1. wndIE.CreateControl ( IDR_ABOUTPAGE );  
    wndIE.CreateControl ( IDR_ABOUTPAGE );

这是显示结果:

WTL 详细介绍_第41张图片

例子代码对上面提到的三个方法都用到了,你可以查看CAboutDlg::OnInitDialog()中的注释和未注释的代码,看看它们分别是如何工作的。

键盘事件处理

最后一个但是非常重要的细节是键盘消息。ActiveX控件的键盘处理非常复杂,因为控件和它的宿主程序必须协同工作以确保控件能够看到它感兴趣的消息。例如,浏览器控件允许你使用TAB键在链接之间切换。MFC自己处理了所有工作,所以你永远不会意识到让键盘完美并正确的工作需要多么大的工作量。

不幸的是向导没有为基于对话框的程序生成键盘处理代码,当然,如果你使用Form View作为视图类的SDI程序,你会看到必要的代码已经被添加到PreTranslateMessage()中。当程序从消息队列中得到鼠标或键盘消息时,就使用ATL的WM_FORWARDMSG消息将此消息传递给当前拥有焦点的控件。它们通常不作什么事情,但是如果是ActiveX控件,WM_FORWARDMSG消息最终被送到包容这个控件的AtlAxWin,AtlAxWin识别WM_FORWARDMSG消息并采取必要的措施看看是否控件需要亲自处理这个消息。

如果拥有焦点的窗口没有识别WM_FORWARDMSG消息,PreTranslateMessage()就会接着调用IsDialogMessage()函数,使得像TAB这样的标准对话框的导航键能正常工作。

例子工程的PreTranslateMessage()函数中含有这些必需的代码,由于PreTranslateMessage()只在无模式对话框中有效,所以如果你想在基于对话框的应用程序中正确使用键盘就必须使用无模式对话框。

继续

在下一章,我们将回到框架窗口并介绍如何使用分隔窗口。

 



引用和参考

"How to use the WTL multipane status bar control" by Ed Gadziemski 更详细的介绍了CMultiPaneStatusBarCtrl类的用法。

原作 :Michael Dunn [英文原文]


 

你可能感兴趣的:(WTL类)