我只是讨论了一些涵盖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++的模板类代码,仍然有两件事可能会使你有些头晕,以下面这个类的定义为例:
class CMyWnd : public CWindowImpl
{
...
};
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"
}
class D3 : public B1
void B1::SayHi()
{
D1* pT = static_cast(this);
pT->PrintClassName();
}
void B1::SayHi()
{
D2* pT = static_cast(this);
pT->PrintClassName();
}
class CMyWindow : public CWindowImpl
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
};
class CMyWindow : public CWindowImpl
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()
};
typedef CWinTraits CMyWindowTraits;
class CMyWindow : public CWindowImpl
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()
};
typedef CWinTraits CFrameWinTraits;
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;
}
};
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;
}
};
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;
};
class CMyWindow : public CWindowImpl,
public CPaintBkgnd
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()
...
};
// stdafx.h:
#define STRICT
#define VC_EXTRALEAN
#include // 基本的ATL类
extern CComModule _Module; // 全局_Module
#include // ATL窗口类
// main.cpp:
CComModule _Module;
// main.cpp:
CComModule _Module;
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
LPSTR szCmdLine, int nCmdShow)
{
_Module.Init(NULL, hInst);
_Module.Term();
}
// 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;
}
我得承认这确实没有什么激动人心的地方。我们将添加一个About菜单并显示一个对话框,主要是为它增加一些情趣。
ATL中的对话框
我们前面提到过,ATL有两个对话框类,我们的About对话框使用CDialogImpl。生成一个新对话框和生成一个主窗口几乎一样,只有两点不同:
窗口的基类是CDialogImpl而不是CWindowImpl。
你需要定义名称为IDD的公有成员用来保存对话框资源的ID。
现在开始为About对话框定义一个新类:
class CAboutDlg : public CDialogImpl
{
public:
enum { IDD = IDD_ABOUT };
BEGIN_MSG_MAP(CAboutDlg)
END_MSG_MAP()
};
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;
}
};
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;
}
// ...
};
我会继续讲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的代码如下:
#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中窗口定义的开始部分:
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()函数几乎一样,只是创建窗口部分的代码略微不同。
// 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的版本产生“解密”消息的代码。
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消息的响应:
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的定义位置:
#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()响应函数:
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宏的定义是这样的:
#define MSG_WM_DESTROY(func) \
if (uMsg == WM_DESTROY) \
{ \
SetMsgHandled(TRUE); \
func(); \
lResult = 0; \
if(IsMsgHandled()) \
return TRUE; \
}
OnDestroy()函数没有参数也没有返回值,CFrameWindowImpl也要处理WM_DESTROY消息,所以还要调用SetMsgHandled(false):
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键的窍门了,所以我直接给出响应函数的代码:
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消息,在窗口客户区的左上角显示当前的时间。
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的实例在超出作用域后不会销毁它所操作的设备上下文。
所有的工作完成了,现在看看我们的窗口是什么样子:
例子代码中还使用了WM_COMMAND响应菜单消息,在这里我不作介绍,但是你可以查看例子代码,看看WTL的COMMAND_ID_HANDLER_EX宏是如何工作的。
从WTL的应用程序生成向导能得到什么
WTL的发布版本附带一个很棒的应用程序生成向导,让我们以一个SDI 应用为例看看它有什么特性。
使用向导的整个过程
在VC的IDE环境下单击File|New菜单,从列表中选择ATL/WTL AppWizard,我们要重写时钟程序,所以用WTLClock作为项目的名字:
在下一页你可以选择项目的类型,SDI,MDI或者是基于对话框的应用,当然还有其它选项,如下图所示设置这些选项,然后点击“下一步”:
在最后一页你可以选择是否使用toolbar,rebar和status bar,为了简单起见,取消这些选项并单击“结束”。
查看生成的代码
向导完成后,在生成的代码中有三个类: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类,它有许多有趣的新东西,这是这个类的定义缩略版本:
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维持的消息过滤器队列和空闲处理队列,我将在稍后介绍这些。
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()函数的伪代码:
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消息:
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():
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类。下面是这个类的部分定义:
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()中。新窗口看起来是这个样子的:
最后为我们的程序添加UI updating功能,为了演示这些用法,我们为窗口添加Start菜单和Stop菜单用于开始和停止时钟,Start菜单和Stop菜单将被适当的设置为可用和不可用。
界面元素的自动更新(UI Updating)
空闲时间的界面更新是几件事情协同工作的结果: CMessageLoop对象,嵌入类CIdleHandler 和 CUpdateUI,CMainFrame类继承了这两个嵌入类,当然还有CMainFrame类中的UPDATE_UI_MAP宏。CUpdateUI能够操作5种不同的界面元素:顶级菜单项(就是菜单条本身),弹出式菜单的菜单项,工具条按钮,状态条的格子和子窗口(如对话框中的控件)。每一种界面元素都对应CUpdateUIBase类的一个常量:
CUpdateUI可以设置enabled状态,checked状态和文本(当然不是所有的界面元素都支持所有状态,如果一个子窗口是编辑框它就不能被check)。菜单项可以被设置为默认状态,这样它的文字会被加重显示。
要使用UI updating需要做四件事:
向导生成的代码已经为我们做了三件事,现在我们只需要决定那个菜单项需要更新和他们是么时候可用什么时候不可用。
添加控制时钟的新菜单项
在菜单条添加一个Clock菜单,它有两个菜单项:IDC_START and IDC_STOP:
然后在UPDATE_UI_MAP宏中为每个菜单项添加一个入口:
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菜单的初始状态。
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菜单是这样的:
CMainFrame现在需要处理两个新菜单项,在视图类调用它们开始和停止时钟时处理函数需要翻转这两个菜单项的状态。这是MFC的内建消息处理无法想象的地方之一。在MFC的程序中,所有的界面更新和命令消息处理必须完整的放在视图类中,但是在WTL中,主窗口类和视图类通过某种方式沟通;菜单由主窗口拥有,主窗口获得这些菜单消息并做相应的处理,要么响应这些消息,要么发送给视图类。
这种沟通是通过PreTranslateMessage()完成的,当然CMainFrame仍然要调用UIEnable()。CMainFrame可以将this指针传递给视图类,这样视图类也可以通过这个指针调用UIEnable()。在这个例子中我选择的这种解决方案导致主窗口和视图成为紧密耦合体,但是我发现这很容易理解(和解释!)。
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显得有些杂乱无章:
出现这种情况是因为ClassView不能解释BEGIN_MSG_MAP_EX宏,它以为所有得WTL消息映射宏是函数定义。你可以将宏改回为BEGIN_MSG_MAP并在stdafx.h文件得结尾处添加这两行代码来解决这个问题:
#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,它是填充主窗口客户区的“视图”窗口的句柄,现在我们遇到了另外两个:
CFrameWindowImpl只支持一个工具条,也没有像MFC那样的可多点停靠的工具条,如果你想使用多个工具条又不想修改CFrameWindowImpl的内部代码,你就需要使用Rebar。我将介绍它们二者并演示如何使用应用程序向导添加工具条和Rebar。
CFrameWindowImpl::OnSize()消息响应函数调用了UpdateLayout(),UpdateLayout()做两件事:从新定位所有控制条和改变视图窗口的大小使之填充整个客户区。实际工作是由UpdateBarsPosition()完成的,UpdateLayout()只是调用了该函数。实现的代码相当简单,向工具条和状态条发送WM_SIZE消息,由这些控制条的默认窗口处理过程将它们定位到主窗口的顶部或底部。
当你告诉应用程序向导给你的窗口添加工具条和状态条时,向导就在CMainFrame::OnCreate()中添加了创建它们的代码。现在我们来看看这些代码,当然是为了再写一个时钟程序。
向导为工具条和状态条生成得代码
我们将开始一个新的工程,让向导为主窗口创建工具条和状态条。首先创建一个名为WTLClock2的新工程,在向导的第一页,选SDI并使“生成CPP文件”检查框被选中:
在第二页,取消Rebar使向导仅仅创建一个普通的工具条:
从第二部分的程序中复制相应的代码,新程序看起来是这样的:
CMainFraCMainFrame 如何创建工具条和状态条
在这个例子中,向导向CMainFrame::OnCreate()函数添加了更多的代码,这些代码的作用就是创建控制条并通知CUpdateUI更新工具条上的按钮。
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()函数的代码:
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,下面是该函数的代码:
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()调用另外一个重载函数创建状态条:
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()函数的代码:
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类中。下面的屏幕截图显示了工具提示和掠过式帮助。
CFrameWindowImplBase类有两个消息相应函数用来实现这些功能,OnMenuSelect()处理WM_MENUSELECT消息,它像MFC那样查找掠过式帮助的字符串:首先装载与菜单资源ID相同的字符串资源,在字符串中查找 \n 字符,使用\n之前的内容作为掠过帮助的内容。OnToolTipTextA() 和 OnToolTipTextW() 函数分别响应 TTN_GETDISPINFOA消息和TTN_GETDISPINFOW消息,提供工具条按钮的工具提示。这两个处理函数和OnMenuSelect()函数一样装载相应的字符串,只是使用\n后面的字符串。(边注:OnMenuSelect()和OnToolTipTextA()函数对于DBCS字符是不安全的,因为它在查找\n字符时没有检查DBCS字符串的头部和尾部)下面是工具条及其关联的帮助字符串的例子:
创建不同样式的工具条
如果你不喜欢在工具条上显示3D按钮(尽管从可用性观点来看平面的界面元素是件糟糕的事情),你可以通过改变CreateSimpleToolBar()函数的参数来改变工具条的样式。例如,你可以在CMainFrame::OnCreate()使用如下代码创建一个IE风格的工具条:
CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE |
TBSTYLE_FLAT | TBSTYLE_LIST );
如果你使用向导为你的程序添加了manifest文件,它就会在Windows XP系统上使用6.0版的通用控件,你不能选择按钮的类型,工具条会自动使用平面按钮即使你创建工具条时没有添加TBSTYLE_FLAT风格。
工具条编辑器
正如我们前面所见,向导为我们的程序创建了几个默认的按钮,当然只有About按钮有事件处理。你可以像在MFC的工程中一样使用工具条编辑器修改工具条资源,CreateSimpleToolBarCtrl()用这个工具条资源创建工具条。下面是向导生成的工具条在编辑器中的样子:
对于我们的时钟程序,我们添加四个按钮,两个按钮用来改变视图窗口的颜色,另外两个用来显示/隐藏工具条和状态条。下面是我们的新工具条:
这些按钮是:
前两个按钮都有相应的菜单项,它们都调用视图类的一个新函数SetColor(),向这个函数传递前景颜色和背景颜色,视图窗口用这两个参数改变窗口的显示。响应这两个按钮的处理函数与响应相应的菜单项的处理函数在使用COMMAND_ID_HANDLER_EX宏上没有区别,你可以查看例子工程的代码了解这些消息处理的细节。在下一节我将介绍状态条和工具条按钮的UI状态更新,使它们能够反映工具条或状态条当前的状态。
工具条按钮的UI状态更新
向导生成的代码已经为CMainFrame添加了对View|Toolbar和View|Status Bar两个菜单项的Check和Uncheck的UI更新处理。这和第二章的程序一样:对CMainFrame类的两个命令使用UI更新的宏:
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标志:
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()的代码你就会发现一段新的代码,这段代码设置了两个菜单项的初始状态:
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()中:
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的检查框,如下所示:
第二个例子工程WTLClock3就使用了Rebar控件,如果你正在跟着例子代码学习,那现在就打开WTLClock3。
你首先会注意到创建工具条的代码有些不同,出现这种感觉是因为我们在程序中使用了rebar。以下是相关的代码:
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这个条位上是一个工具条。
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类型的成员变量:
class CMainFrame : public ...
{
//...
protected:
CMultiPaneStatusBarCtrl m_wndStatusBar;
};
接着在OnCreate()中创建状态条并这只UI更新:
m_hWndStatusBar = m_wndStatusBar.Create ( *this );
UIAddStatusBar ( m_hWndStatusBar );
就像CreateSimpleStatusBar()函数做得那样,我们也将状态条的句柄存放在m_hWndStatusBar中。
下一步就是调用CMultiPaneStatusBarCtrl::SetPanes()函数建立窗格:
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()的调用:
// 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更新表:
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”
// Set the initial text for the clock status pane.
UISetText ( 1, _T("Running") );
和前面一样,第一个参数是窗格的索引。UISetText()是状态条唯一支持的UI更新函数。
最后,在CMainFrame::OnIdle()中添加对UIUpdateStatusBar()函数的调用,使状态条的窗格能够在空闲时间被更新:
BOOL CMainFrame::OnIdle()
{
UIUpdateToolBar();
UIUpdateStatusBar();
return FALSE;
}
当你使用UIUpdateStatusBar()时CUpdateUI的一个问题就暴露出来了--菜单项的文本在调用UISetText()后没有改变!如果你在看WTLClock3工程的代码,时钟的开始/停止菜单项被移到了Clock菜单,在菜单项命令的响应处理函数中设置菜单项的文本。无论如何,如果当前调用的是UIUpdateStatusBar(),那么对UISetText()的调用就不会起作用。我没有研究这个问题是否可以被修复,所以如果你打算改变菜单的文本,你需要留意这个地方。
最后,我们需要检查CAPS LOCK键的状态,更新相应的两个窗格。这些代码是通过OnIdle()被调用的,所以程序会在每次空闲时间检查它们的状态。
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的构造函数中使用了一个机灵的窍门(有充分的文档说明)。
在完成所有的代码之后,状态条看起来是这个样子:
承上启下:有关对话框的话题
在第四章我将介绍对话框的用法(包括ATL的类和WTL的增强功能),控件的包装类和WTL有关对话框消息处理的改进。
对第四章的介绍
MFC 的对话框和控件的封装真得可以节省你很多时间和功夫。没有MFC对控件的封装,你要操作控件就得耐着性子填写各种结构并写很多的SendMessage调用。MFC还提供了对话框数据交换(DDX),它可以在控件和变量之间传输数据。WTL 当然也提供了这些功能,并对控件的封装做了很多改进。本文将着眼于一个基于对话框的程序演示你以前用MFC实现的功能,除此之外还有WTL消息处理的增强功能。第五章将介绍高级界面特性和WTL对新控件的封装。
回顾一下ATL的对话框
现在回顾一下第一章提到的两个对话框类,CDialogImpl 和 CAxDialogImpl。CAxDialogImpl用于包含ActiveX控件的对话框。本文不准备介绍ActiveX控件,所以只使用CDialogImpl。
创建一个对话框需要做三件事:
然后就像主框架窗口那样添加消息处理函数,WTL没有改变这些,不过确实添加了一些其他能够在对话框中使用得特性。
通用控件的封装类
WTL有许多控件的封装类对你应该比较熟悉,因为它们使用与MFC相同(或几乎相同)的名字。控件的方法的命名也和MFC一样,所以你可以参照MFC的文档使用这些WTL的封装类。不足之处是F12键不能方便地跳到类的定义代码处。
下面是Windows内建控件的封装类:
还有一些是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文件选项:
第二页上所有的选项只对主窗口是框架窗口时有意义,现在它们是不可用状态,单击"Finish",再单击"OK"完成向导。
正如你想的那样,向导生成的基于对话框程序的代码非常简单。_tWinMain()函数在ControlMania1.cpp中,下面是重要的部分:
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的向导生成的代码没有使用区块,使得我的一些程序在结束时崩溃)
你现在可以编译并运行这个程序,尽管它只是一个简陋的对话框:
CMainDlg 的代码处理了WM_INITDIALOG, WM_CLOSE和三个按钮的消息,如果你喜欢可以浏览一下这些代码,你应该能够看懂CMainDlg的声明,它的消息映射和它的消息处理函数。
这个简单的工程还演示了如何将控件和变量联系起来,这个程序使用了几个控件。在接下来的讨论中你可以随时回来查看这些图表。
由于程序使用了list view控件,所以对AtlInitCommonControls()的调用需要作些修改,将其改为:
AtlInitCommonControls ( ICC_WIN95_CLASSES );
虽然这样注册的控件类比我们用到的多,但是当我们向对话框添加不同类型的控件时就不用随时记得添加名为ICC_*的常量(译者加:以ICC_开头的一系列常量)。
使用控件的封装类
有几种方法将一个变量和控件建立关联,可以使用CWindows(或其它Window接口类,如CListViewCtrl),也可以使用CWindowImpl的派生类。如果只是需要一个临时变量就用CWindow,如果需要子类化一个控件并处理发送给该控件的消息就需要使用CWindowImpl。
ATL 方式 1 - 连接一个CWindow对象
最简单的方法是声明一个CWindow或其它window接口类,然后调用Attach()方法,还可以使用CWindow的构造函数直接将变量与控件的HWND关联起来。
下面的代码三种方法将变量和一个list控件联系起来:
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
CContainedWindow只是它定义的一个简写名称,要使用不同的window接口类只需将该类的类名作为模板参数就行了,例如CContainedWindowT
钩住一个CContainedWindow对象需要做四件事:
在ControlMania1中,我对三个按钮分别使用了一个CContainedWindow,对话框处理发送到每一个按钮的WM_SETCURSOR消息,并改变鼠标指针形状。
现在仔细看看这一步,首先,我们在CMainDlg中添加了CContainedWindow成员。
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)小节处理。
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的哪个小节。
CMainDlg::CMainDlg() : m_wndOKBtn(this, 1),
m_wndExitBtn(this, 2)
{
}
构造函数的参数是消息映射链的地址和ALT_MSG_MAP的小节号码,第一个参数通常使用this,就是使用对话框自己的消息映射链,第二个参数告诉对象将消息发给ALT_MSG_MAP的哪个小节。
最后,我们将每个CContainedWindow对象与控件关联起来。
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
return TRUE;
}
下面是新的WM_SETCURSOR消息处理函数:
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;
}
}
如果你还想使用按钮类的特性,你需要这样声明变量:
CContainedWindowT m_wndOKBtn;
这样就可以使用CButton类的方法。
当你把鼠标光标移到这些按钮上就可以看到WM_SETCURSOR消息处理函数的作用结果:
ATL 方式 3 - 子类化(Subclassing)
第三种方法创建一个CWindowImpl派生类并用它子类化一个控件。这和第二种方法有些相似,只是消息处理放在CWindowImpl类内部而不是对话框类中。
ControlMania1使用这种方法子类化主对话框的About按钮。下面是CButtonImpl类,他从CWindowImpl类派生,处理WM_SETCURSOR消息:
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成员变量:
class CMainDlg : public CDialogImpl
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn;
};
最后,在OnInitDialog()种子类化About按钮。
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添加到继承列表中:
class CMainDlg : public CDialogImpl,
public CWinDataExchange
{
//...
};
接着在对话框类中添加DDX链,这和MFC的类向导使用的DoDataExchange()函数功能相似。对于不同类型的数据可以使用不同的DDX宏,我们使用DDX_CONTROL用来连接变量和控件,这次我们使用CEditImpl处理WM_CONTEXTMENU消息,使它能够在你右键单控件时做一些事情。
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建立关联。
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头文件包含之前添加一行定义:
#define _ATL_USE_DDX_FLOAT
这个定义是必要的,因为默认状态为了优化程序的大小而不支持浮点数。
有关 DoDataExchange()的详细内容
调用DoDataExchange()方法和在MFC中使用UpdateData()一样,DoDataExchange()的函数原型是:
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的使用方法。
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控件的数据传送给我们刚刚添加的两个变量,然后将结果显示在列表控件中。
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 );
}
如果编辑控件输入的不是数字,DDX_INT将会失败并触发OnDataExchangeError()的调用,CMainDlg重载了OnDataExchangeError()函数显示一个消息框:
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) );
}
作为最后一个使用DDX的例子,我们添加一个check box演示DDX_CHECK的使用:
DDX_CHECK使用的变量类型是int型,它的可能值是0,1,2,分别对应check box的未选择状态,选择状态和不确定状态。你也可以使用常量BST_UNCHECKED,BST_CHECKED,和 BST_INDETERMINATE代替,对于check box来说只有选择和未选择两种状态,你可以将其视为布尔型变量。
以下是为使用check box的DDX而做的改动:
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是否被选中。
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之间得控件发送的某个通知代码。
例子:
还有一些宏专门处理WM_NOTIFY消息,和上面的宏功能类似,只是它们的名字开头以“NOTIFY_”代替“COMMAND_”。
WM_COMMAND 消息处理函数的原型是:
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );
WM_COMMAND通知消息不需要返回值,所以处理函数也不需要返回值,WM_NOTIFY消息处理函数的原型是:
LRESULT func ( NMHDR* phdr );
消息处理函数的返回值用作消息相应的返回值,这不同于MFC,MFC的消息响应通过消息处理函数的LRESULT*参数得到返回值。发送通知消息的控件的窗口句柄和通知代码包含在NMHDR结构中,分别是code和hendFrom成员。和MFC一样的是如果通知消息发送的不是普通的NMHDR结构,你的消息处理函数应该将phdr参数转换成正确的类型。
我们将为CMainDlg添加LVN_ITEMCHANGED通知的处理函数,处理从list控件发出的这个通知,在对话框中显示当前选择的项目,先从添加消息映射宏和消息处理函数开始:
class CMainDlg : public ...
{
BEGIN_MSG_MAP_EX(CMainDlg)
NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
END_MSG_MAP()
LRESULT OnListItemchanged(NMHDR* phdr);
//...
};
下面是消息处理函数:
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()宏:
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中被反射的消息:
在你想添加反射消息处理的控件类内不要忘了使用DEFAULT_REFLECTION_HANDLER()宏,DEFAULT_REFLECTION_HANDLER()宏确保将未被处理的消息交给DefWindowProc()正确处理。 下面的例子是一个自画按钮类,它相应了从父窗口反射的WM_DRAWITEM消息。
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_”,例如,一个树控件类可能存在这样的消息映射链:
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()是这样的:
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个地方:
不幸的是前两个修改会在每次保存资源文件时丢失(被VC又改回原样),所以需要重复这些修改,下面是改动之前的代码:
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
这是改动之后的代码:
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对新控件的封装和自画控件、自定外观控件等一些高级界面特性。
如何将消息链入COwnerDraw取决与你是否将消息反射给控件,两种方法有些不同。下面是COwnerDraw类的消息映射链,它使得两种方法的差别更加明显:
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()
};
class CSomeDlg : public COwnerDraw, ...
{
BEGIN_MSG_MAP(CSomeDlg)
//...
CHAIN_MSG_MAP(COwnerDraw)
END_MSG_MAP()
void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};
class CSomeButtonImpl : public COwnerDraw, ...
{
BEGIN_MSG_MAP(CSomeButtonImpl)
//...
CHAIN_MSG_MAP_ALT(COwnerDraw, 1)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};
void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
int CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);
void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct);
现在我们需要一个新类实现自画按钮:
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 );
};
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);
}
CCustomDraw
CCustomDraw类使用和COwnerDraw类相同的方法处理NM_CUSTOMDRAW消息,对于自定绘制的每个阶段都有相应的重载函数:
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);
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;
}
// 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 );
因为CBitmapButton是一个非常有用的类,我想介绍一下它的公有方法。
CBitmapButton methods
CBitmapButtonImpl类包含了实现一个按钮的所有代码,除非你想重载某个方法或消息处理,你可以对控件直接使用CBitmapButton类。
CBitmapButtonImpl constructor
CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE,HIMAGELIST hImageList = NULL)
BOOL SubclassWindow(HWND hWnd)
DWORD GetBitmapButtonExtendedStyle()
DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
HIMAGELIST GetImageList()
HIMAGELIST SetImageList(HIMAGELIST hImageList)
int GetToolTipTextLength()
bool GetToolTipText(LPTSTR lpstrText, int nLength)
bool SetToolTipText(LPCTSTR lpstrText)
void SetImages(int nNormal, int nPushed = -1,int nFocusOrHover = -1, int nDisabled = -1)
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()
};
CTreeViewCtrlEx and CTreeItem
有两个类使得树控件的使用简化了很多:CTreeItem类封装了HTREEITEM,一个CTreeItem对象含有一个HTREEITEM和一个指向包含这个HTREEITEM的树控件的指针,使你不必每次调用都引用树控件;CTreeViewCtrlEx和CTreeViewCtrl一样,只是它的方法操作CTreeItem而不是HTREEITEM。例如,InsertItem()函数返回一个CTreeItem而不是HTREEITEM,你可以使用CTreeItem操作新添加的item。下面是一个例子:
// 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 );
BOOL SubclassWindow(HWND hWnd)
bool GetLabel(LPTSTR lpstrBuffer, int nLength)
bool SetLabel(LPCTSTR lpstrLabel)
bool GetHyperLink(LPTSTR lpstrBuffer, int nLength)
bool SetHyperLink(LPCTSTR lpstrLink)
bool Navigate()
在OnInitDialog()函数中设置URL:
m_wndLink.SetHyperLink ( _T("http://www.codeproject.com/") );
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()
//...
};
// register object for message filtering and idle updates
CMessageLoop* pLoop = _Module.GetMessageLoop();
ATLASSERT(pLoop != NULL);
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
UIAddChildWindowContainer(m_hWnd);
BOOL CMainDlg::OnIdle()
{
return FALSE;
}
BOOL CMainDlg::OnIdle()
{
UIUpdateChildWindows();
return FALSE;
}
BEGIN_UPDATE_UI_MAP(CMainDlg)
UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)
END_UPDATE_UI_MAP()
void CMainDlg::OnAlysonODBtn ( UINT uCode, int nID, HWND hwndCtrl )
{
static bool s_bBtnEnabled = true;
s_bBtnEnabled = !s_bBtnEnabled;
UIEnable ( IDC_ALYSON_BMPBTN, s_bBtnEnabled );
}
由于有效的值是1到7,所以使用这样的数据验证宏:
BEGIN_DDX_MAP(CMainDlg)
//...
DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
END_DDX_MAP()
处理DDV验证失败
如果控件的数据验证失败,CWinDataExchange会调用重载函数OnDataValidateError(),默认到处理是驱动PC喇叭发出声音,你可能想给出更友好的错误指示。OnDataValidateError()的函数原型是:
void OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data );
struct _XData
{
_XDataType nDataType;
union
{
_XTextData textData;
_XIntData intData;
_XFloatData floatData;
};
};
enum _XDataType
{
ddxDataNull = 0,
ddxDataText = 1,
ddxDataInt = 2,
ddxDataFloat = 3,
ddxDataDouble = 4
};
struct _XIntData
{
long nVal;
long nMin;
long nMax;
}
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) );
}
#include
#include
extern CAppModule _Module;
#include
#include
#include
#include
// .. other WTL headers ...
atlcom.h和atlhost.h是很重要的两个,它们含有一些COM相关类的定义(比如智能指针CComPtr),还有可以包容控件的窗口类。
接下来看看maindlg.h中声明的CMainDlg类:
- class CMainDlg : public CAxDialogImpl
,
- public CUpdateUI
,
- public CMessageFilter, public CIdleHandler
class CMainDlg : public CAxDialogImpl,
public CUpdateUI,
public CMessageFilter, public CIdleHandler
int WINAPI _tWinMain(...)
{
//...
_Module.Init(NULL, hInstance);
AtlAxWinInit();
int nRet = Run(lpstrCmdLine, nCmdShow);
_Module.Term();
return nRet;
}
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",
WS_TABSTOP,7,7,116,85
CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",IDC_IE,"AtlAxWin",
WS_TABSTOP,7,7,116,85
CAxWindow wndIE = GetDlgItem(IDC_IE);
CComPtr pWB2;
HRESULT hr;
hr = wndIE.QueryControl ( &pWB2 );
if ( pWB2 )
{
CComVariant v; // empty variant
pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"),
&v, &v, &v, &v );
}
#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()
};
void DownloadBegin();
VT_EMPTY: void
VT_BSTR: BSTR 格式的字符串
VT_I4: 4字节有符号整数,用于long类型的参数
VT_DISPATCH: IDispatch*
VT_VARIANT>: VARIANT
VT_BOOL: VARIANT_BOOL (允许的取值是VARIANT_TRUE和VARIANT_FALSE)
#define _ATL_MAX_VARTYPES 8
struct _ATL_FUNC_INFO
{
CALLCONV cc;
VARTYPE vtReturn;
SHORT nParams;
VARTYPE pVarTypes[_ATL_MAX_VARTYPES];
};
我们的事件响应函数的调用方式约定,这个参数必须是CC_STDCALL,表示是__stdcall方式vtReturn
事件响应函数的返回值类型nParams
事件带的参数个数pVarTypes
相应的参数类型,按从左到右的顺序了解这些之后,我们就可以填写DownloadBegin事件处理的_ATL_FUNC_INFO结构:
_ATL_FUNC_INFO DownloadInfo = { CC_STDCALL, VT_EMPTY, 0 };
class CMainDlg : public ...
{
...
BEGIN_SINK_MAP(CMainDlg)
SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,
OnDownloadBegin, &DownloadInfo)
END_SINK_MAP()
};
void __stdcall CMainDlg::OnDownloadBegin()
{
// show "Please wait" here...
}
void BeforeNavigate2 (
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel );
_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 }
};
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()
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 ...
}
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;
}
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;
}
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 );
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: 创建Shell.Explorer:
wndIE.CreateControlEx ( L"Shell.Explorer", NULL,
NULL, &punkCtrl );
wndIE.CreateControl ( IDR_ABOUTPAGE );
引用和参考
"How to use the WTL multipane status bar control" by Ed Gadziemski 更详细的介绍了CMultiPaneStatusBarCtrl类的用法。
原作 :Michael Dunn [英文原文]