ATL窗口

ATL窗口,第一部分

源程序

--------------------------------------------------------------------------------
这篇文章由Andrew Whitechapel所写,由孙凯翻译。

绪论

ATL窗口类并不难学,但其与MFC非常不同,很多开发者便说我喜欢丰富的MFC支持,为什么要花心思学ATL窗口呢?唔,MFC有着很大的尺寸和高的性能消费,强迫你用那套应用程序框架体系工作 -- 这些天你又怀疑文档是否继续有效 - 基于文档模板的连载机制。使用ATL窗口的一个大大的优点,当然是非常容易整合COM支持。如果你想更快的、轻量级的窗口和自由选择你的应用程序体系和持续协议的灵活性,你不能不选择ATL。

在这篇文章中,我会提供ATL窗口类介绍和一个简单的基于ATL框架/视图应用程序的食谱指南 -- 你会看到居然可以十分容易的实现象MFC一样的前端功能。ATL窗口的学习曲线比起MFC来更短更平滑,因为ATL更小。

虽然ATL最初是为支持COM而设计的,它也包含了一系列用于构建窗口的类。你可以用这些类构建有窗口的COM对象,如ActiveX控制,创建Windows应用程序时却可以不必包含COM。最重要的ATL窗口类列于下表中:

CWindow 一个小的包装了用于操作窗口的Win32 API的类,包含一个窗口句柄和一个将CWindow对象转换为HWND的HWND操作符。因而你可以将一个CWindow对象传递给任何一个需要窗口句柄参数的函数。
CWindowImpl 你可以用CWindowImpl创建基于一个新的窗口类的、已存在的类的超类或子类的一个窗口。
CContainedWindow 一个实现了发送消息到其它类的消息映射的窗口,允许你把消息处理集中到一个类中的类。
CAxWindow 允许你实现一个可包含ActiveX控制的窗口,它有一些创建控制或绑定一个存在的控制的函数。
CDialogImpl 用于实现一个模态或非模态的对话框的基类。CDialogImpl提供一个包含发送消息到你的派生类的默认消息映射的对话框过程。但它不支持ActiveX控制。
CSimpleDialog 依靠对话框资源ID实现模态对话框。CSimpleDialog有一个预先确定的能够处理IDOK和IDCANCEL命令的消息映射。
CAxDialogImpl 和CDialogImpl一样,也用于实现一个模态或非模态的对话框的基类。同样提供一个包含发送消息到你的派生类的默认消息映射的对话框过程。且它支持ActiveX控制。ATL Object Wizard支持向你的工程添加一个CAxDialogImpl的派生类,并产生相关的对话框资源。
CWndClassInfo 管理一个新的窗口类(Windows的WNDCLASS)的信息 -- 其实就是封装了WNDCLASSEX。
CWinTraits和CWinTraintsOR 封装了一个ATL窗口对象的窗口特征(WS_ 窗口风格)。

消息映射

人们不愿花时间学习ATL窗口类的一个主要原因是觉得ATL的消息映射机制有些奇怪。是的,它与MFC的不同,但是在你第一次看到MFC消息映射宏时,你 是否理解它呢?事实上,ATL消息映射机制是非常容易掌握的。允许你在CWindowImpl的派生类中处理窗口消息,因为ATL类继承自一个抽象的基类 CMessageMap。CMessageMap声明了一个纯虚函数ProcessWindowMessage,它是通过宏BEGIN_MSG_MAP和 END_MSG_MAP在你的CWindowImpl派生类中实现。
另外,与MFC的消息处理很相似,ATL的消息处理函数只是多一个BOOL&的变量(译者说明:在ATL源代码中, ProcessWindowMessage函数定义了变量BOOL bHandled = TRUE,这里即指它)。这是变量指出一个消息是否被处理,它默认为TRUE。一个消息处理函数可以将这个变量设置为FALSE,以指示消息未被处理。在 这种情况下,ATL会进入消息映射中更深一层的消息处理函数。通过将这个变量设置为FALSE,你可以首先执行一些操作以响应消息,然后允许默认处理或另 外的处理函数完成消息响应。

下表中列出了三组消息映射宏:

.所有消息的消息处理者
.WM_COMMAND消息的命令处理者
.WM_NOTIFY消息的通知处理者
MESSAGE_HANDLER 将一个窗口消息映射到一个处理函数。
COMMAND_HANDLER 将一个WM_COMMAND消息映射到一个基于通知代码和菜单项、控制或快捷键的ID的处理函数
COMMAND_ID_HANDLER 将一个WM_COMMAND消息映射到一个基于菜单项、控制或快捷键的ID的处理函数
COMMAND_CODE_HANDLER 将一个WM_COMMAND消息映射到一个基于通知代码的处理函数
NOTIFY_HANDLER 将一个WM_NOTIFY消息映射到一个基于通知代码和控制标识的处理函数
NOTIFY_ID_HANDLER 将一个WM_NOTIFY消息映射到一个基于控制标识的处理函数
NOTIFY_CODE_HANDLER 将一个WM_NOTIFY消息映射到一个基于通知代码的处理函数

例如,如果你有一个带有子控制的窗体的ATL对话框类,你会有一个像下面这样的消息映射:

BEGIN_MSG_MAP(CMyDialog)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_HANDLER(IDC_EDIT1, EN_CHANGE, OnChangeEdit1)
COMMAND_ID_HANDLER(IDOK, OnOK)
COMMAND_CODE_HANDLER(EN_ERRSPACE, OnErrEdits)
NOTIFY_HANDLER(IDC_LIST1, NM_CLICK, OnClickList1)
NOTIFY_ID_HANDLER(IDC_LIST2, OnSomethingList2)
NOTIFY_CODE_HANDLER(NM_DBLCLK, OnDblClkLists)
END_MSG_MAP()

MFC体系允许使用两种不同的消息传送方案:向基类纵向传送Windows消息,和在文档/视图类中横向传送命令消息。第一种方案不太适合ATL,因为 ATL有着比较松散的用类模板实现的层次结构。第二种方案也不适合,因为ATL没有像MFC文档/视图体系相当的严格的运用(体系)。

为了在一个消息映射中处理不同窗口发送的消息,ATL提供两种方法:预备的消息映射和链接的消息映射。一个父窗口也可以处理一个子控制发送给它的消息,一个回送给它的作为反射消息的消息。

预备的消息映射

预备的消息映射被主要设计成为ALT类CContainedWindow所使用的。这个类发送所有它的消息到其它类的消息映射中。
CContainedWindow的构造函数需要得到一个包含被使用的消息映射的类的地址,和消息映射内部的预备的消息映射的ID(或默认的消息映射值0)。

例如,当你创建一个基于Windows控制的ATL控制,ATL Object Wizard会为控制产生一个类,它包含一个代表子控制的CContainedWindow类型的成员。实际上,这个包含窗口超类,特殊的Windows控制,你已经决定让它基于你的ActiveX控制:

class ATL_NO_VTABLE CMyButton :
public CComObjectRootEx,
public CComCoClass,
public CComControl,
//...
{
public:
CContainedWindow m_ctlButton;
CMyButton() : m_ctlButton(_T("Button"), this, 1) { }

BEGIN_MSG_MAP(CMyButton)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
CHAIN_MSG_MAP(CComControl) file://链接消息映射
ALT_MSG_MAP(1) file://预备消息映射
END_MSG_MAP()
//...

注意“Button”是WNDCLASS风格,不是标题。包容类的this指针被作为第2个参数,值1被传递给ContainedWindow的构造函数用以标识预备消息映射。

如果你想处理控制的WM_LBUTTONDOWN消息,你需要更新消息映射如下。这样,这个消息会被发送到父窗口听消息映射中,然后再发送给消息映射的预备部分。

BEGIN_MSG_MAP(CMyButton)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
CHAIN_MSG_MAP(CComControl)
ALT_MSG_MAP(1)
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown) file://看这里
END_MSG_MAP()

因而,预备的消息映射是一种简单的策略,让你合并消息处理,在单个BEGIN_MSG_MAP/END_MSG_MAP宏块中。


链接的消息映射

链接的消息映射通过其它类或对象的消息映射发送消息。ATL提供几个映射链接宏:
CHAIN_MSG_MAP(theBaseClass) 传送消息到一个基类的默认消息映射。
CHAIN_MSG_MAP_ALT(theBaseClass, mapID) 传送消息到一个基类的预备的消息映射。
CHAIN_MSG_MAP_MEMBER(theMember) 传送消息到指定的数据成员的默认消息映射(派生于CMessageMap)。
CHAIN_MSG_MAP_ALT_MEMBER(theMember, mapID) 传送消息到指定数据成员的预备消息映射。

例如,当你创建一个基于Windows控制的ATL控制,ATL Object Wizard产生代码如下:

BEGIN_MSG_MAP(CMyButton)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
CHAIN_MSG_MAP(CComControl) file://看这里
ALT_MSG_MAP(1)
END_MSG_MAP()

这个说明WM_CREATE和WM_SETFOCUS消息被这个类处理,但其它消息被传送到基类CComControl<>中。同样,如果 WM_CREATE和WM_SETFOCUS的处理者将变量bHandled设置为false,这些消息仍会被传送到基类作更进一步的处理。

传送消息到一个数据成员,你需要更新映射如下:

BEGIN_MSG_MAP(CMyButton)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
CHAIN_MSG_MAP(CComControl)
ALT_MSG_MAP(1)
CHAIN_MSG_MAP_MEMBER(m_ctlButton) file://看这里
END_MSG_MAP()

这里假定m_ctlButton是包容窗口的一个成员,而且是一个CContainedWindow的派生类的实例,它可以为你感兴趣的消息获得一个消息映射的入口:

class CMyButtonControl : public CContainedWindow
{
//...
BEGIN_MSG_MAP(CMyButtonControl)
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
END_MSG_MAP()

因此,链接消息映射允许你形成消息传送链从一个映射到另外的映射--MFC也采用了类似的消息传送方案。

反射消息

一个父窗口可以处理发送给它的窗口消息,而子控制则依靠一个回送的称为反射消息的消息--这是在原始消息上加上一个标志。当子控制得到这些消息,它能识别 出它们是反射自包容窗口并适当地处理它们。例如,一个子控制想处理WM_DRAWITEM消息。为了做这个工作, REFLECT_NOTIFICATIONS必须在父窗口的消息映射中被给出:

BEGIN_MSG_MAP(CMyDialog)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(IDOK, OnOk)
NOTIFY_HANDLER(IDC_EDIT1, EN_CHANGE, OnChangeEdit1)
REFLECT_NOTIFICATIONS() //看这里
END_MSG_MAP()

REFLECT_NOTIFICATIONS宏展开来便是一个对CWindowImpl::ReflectNotifications的调用,该函数具有这个信号:

template
LRESULT CWindowImplRoot< TBase >::ReflectNotifications(UINT uMsg,
WPARAM wParam, LPARAM lParam, BOOL& bHandled);

这个函数取出子控制的窗口句柄,发送的消息的wParam或lParam(决定于消息类型),然后用如下的方式发送消息:

::SendMessage(hWndChild, OCM_ _BASE + uMsg, wParam, lParam);

子窗口使用标准的MESSAGE_HANDLER宏处理反射消息,预定义的反射消息的ID定义在olectrl.h中:

BEGIN_MSG_MAP(CMyContainedControl)
MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()

OCM_DRAWITEM, 在这个例子中看到的符号,其定义如下:

#define OCM_ _BASE (WM_USER+0x1c00)
#define OCM_COMMAND (OCM_ _BASE + WM_COMMAND)
//...
#define OCM_DRAWITEM (OCM_ _BASE + WM_DRAWITEM)

DEFAULT_REFLECTION_HANDLER宏将反射消息还原在原始消息,并传递给DefWindowProc。


第1步:ATL Window App

这是一个非常简单的练习,设计来示范使用ATL窗口类创建一个简单的应用程序是多么的容易。这个我很早就做好了:

ATL COM AppWizard是提供给COM对象服务的。如果你想创建一个非COM应用程序,ATL COM AppWizard产生的代码有许多你可以不要。因此,创建一个ATL应用程序,你有两种选择:

创建一个ATL COM AppWizard的EXE服务器,并接受常用选择(可能有些多余)。
创建一个Win32 Application,并手工加入ATL支持。
只有这样,你才可以看到什么是必需的,我们会故意的消除一些向导产生的代码,并随着第2条路线完成我们的应用程序的最小的轻量级的框架。

创建一个新的Win32 Application,选择简单的选项,这样我们可以得到stdafx.h和stdafx.cpp('afx',当然是MFC的残留,但名字是不相关的-什么是最重要的,是PCH)。

ATL支持

修改stdafx.h加入ATL需要的头文件和一个对CComModule类型的全局对象的外部引用:

#include
extern CComModule _Module;
#include
#include

加入一个ATL对象映射在主要的CPP文件中 - 它是空的,但为了CComModule对象我们需要它。声明全局的CComModule对象:

CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()

加入一个与这个工程同名的IDL文件。

library SomethingOrOther
{
};


窗口

因为没有向导支持创建一个派生于ATL窗口类的新类,所以我们使用新建类对话框加入一个普通类,然后手工修改。在ClassView的根节点上单击鼠标右 键,行动新建类。选择类类型:普通类,名字CMyWindow,基类是CWindowImpl<CMyWindow>。你会不满意ATL窗口 类,因为向导为你的新类产生一个新的头文件但没有#include stdafx.h(其中包含atlwin.h)且不知道这些类。自己补上#include stdafx.h。

在你的新窗口类中,声明一个消息映射并保存文件:

BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()

在ClassView中,在你的新的窗口类上单击鼠标右键获得WM_DESTROY的句柄。加入邮递一个退出消息的代码:

LRESULT OnDestroy(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
PostQuitMessage(0);
return 0;
}

同样的,我们可以获得WM_PAINT的句柄,并编写打印一个字符串的代码。你可以直接看到 - ATL常用的方法 - 我们直接使用API编码。ATL没有包装HDC的类,WTL则实现了它:

LRESULT OnPaint(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
PAINTSTRUCT ps;
HDC hDC = GetDC();
BeginPaint(&ps);
TextOut(hDC, 0, 0, _T("Hello world"), 11);
EndPaint(&ps);

return 0;
}


WinMain

在WinMain函数的开始和结束处,书写常用CComModule::Init和Term调用:

_Module.Init(NULL, hInstance);
// ...
_Module.Term();

在Init和Term之间,声明一个你的窗口类的实例,并调用Create函数初始化它(不要忘了用#include包含它的头文件)。然后设置一个消息循环:

CMyWindow wnd;
wnd.Create(NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE);

MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

哈,是不是很容易?


第2步:ATL框架/视图应用程序

在这个工程中我们会创建一个像MFC SDI框架/视图结构的范例,但我们使用的是ATL窗口类。这个程序的第一个版本看起来跟上一个例子SimpleWin差不多,但我们会加入视图、菜单和对话框。

创建一个新的“Simple”Win32 Application。在编码之间,先修改stdafx.h以加入ATL所需的头文件和全局的CComModule对象的外部引用。在主CPP文件中加入ATL对象映射并声明一个全局的CComModule对象。还要加入一个具有library块框架的IDL文件。


主框架窗口

在ClassView的根节点上单击鼠标右键,选择New Class。使用类类型(Class Type):普通类(Generic Class),命名为CMainFrame,设置基类为CWindowImpl<CMainFrame, CWindow, CFrameWinTraits>。注意,CFrameWinTraits是用typedef定义的(在atlwin.h中)为主框架窗口类型优化的CWinTraits。在这个新的CMainFrame类中,声明WNDCLASS结构名和一个消息映射:

DECLARE_WND_CLASS(_T("MyFrame"))

BEGIN_MSG_MAP(CMainFrame)
END_MSG_MAP()

我们能过继承得到一个函数OnFinalMessage - ATL中少数几个虚函数之一 - 当收到WM_NCDESTROY消息时它会被调用。我们需要重载这个函数以发送一个退出消息:

void OnFinalMessage(HWND /*hWnd*/)
{
::PostQuitMessage(0);
}

现在加入一些代码到WinMain中。在其开始和结束处加入常用的CComModule的初始化/终止函数:

_Module.Init(NULL, hInstance, NULL);

_Module.Term();

用#include包含主框架窗口(mainframe)的头文件,在Init和Term之间声明一个它的实例,也可声明框架的另外的实例,调用Create初始化它。然后是一个消息循环:

CMainFrame mf;
mf.Create(GetDesktopWindow(), CWindow::rcDefault,
_T("My App"), 0, 0, 0);
mf.ShowWindow(SW_SHOWNORMAL);

MSG msg;
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

编译并运行。


视图窗口

现在我们将加入一个视图类。在ClassView中单击鼠标右键创建另一个新的类。同样,也是生成一个普通类。命名为CViewWin,派生于CWindowImpl<CViewWin, CWindow, CWinTraits>。记住,没有用typedef预先的为视图优化的CWinTraits。

用#include包含stdafx.h并声明WNDCLASS和消息映射。然后加入一个CViewWin的实例作为CMainFrame的成员,在CMainFrame类中处理WM_CREATE来创建视图:

LRESULT OnCreate(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
m_wndView.Create(m_hWnd, CWindow::rcDefault,
_T("MyView"), 0, 0, 0);
return 0;
}

我们还要在框架中处理WM_SIZE以确定视图的大小。再次编译运行:

LRESULT OnSize(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
RECT r;
GetClientRect(&r);

m_wndView.SetWindowPos(NULL, &r,
SWP_NOZORDER | SWP_NOACTIVATE );

return 0;
}

用户界面

现在,我们将处理WM_LBUTTONDOWN、WM_MOUSEMOVE和WM_LBUTTONUP消息以提供非常简单的可让用户用鼠标画线的乱画程序版本。是的,我知道,但它提供了非常易于掌握的手段来学习UI响应和消息处理:
首先,添加两个POINT类型的数据成员到视图类中,在构造函数中用(-1,-1)来初始化它们。我们需要保留这些画线的开始和结束点的轨迹 -- (-1,-1)当然是鼠标消息中所没有的坐标值:

m_startPoint.x = m_startPoint.y = -1;
m_endPoint.x = m_endPoint.y = -1;

接下来,通过单击鼠标右键取得三个鼠标消息的句柄。与MFC中CWnd类的鼠标消息处理函数不同,ATL并不声明lParam为一个CPoint对象(也不是POINT),所以你必须自己从中分解鼠标坐标。首先,OnLButtonDown将鼠标坐标记录成画线的起始点:

LRESULT OnLButtonDown(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
m_startPoint.x = LOWORD(lParam);
m_startPoint.y = HIWORD(lParam);

return 0;
}

在OnLButtonUp中,将起始点复位为-1:

LRESULT OnLButtonUP(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
m_startPoint.x = m_startPoint.y = -1;
return 0;
}

OnMouseMove函数稍稍多一些工作。首先,设置画线的结束点为所取得的鼠标坐标值。然后获得一个DC,并调用MoveToEx和LineTo来画线。最后,更新起始点为当前的结束点,这样下一条线就可以从当前结束点开始:

LRESULT OnMouseMove(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
m_endPoint.x = LOWORD(lParam);
m_endPoint.y = HIWORD(lParam);

HDC hdc = GetDC();
if (m_startPoint.x != -1 )
{
MoveToEx(hdc, m_startPoint.x, m_startPoint.y, NULL);
LineTo(hdc, m_endPoint.x, m_endPoint.y);
m_startPoint.x = m_endPoint.x;
m_startPoint.y = m_endPoint.y;
}

return 0;
}

编译并运行。你也许会问为什么在ATL中没有消息解密?唔,当然,这是WTL要作的事...

第3步:ATL菜单

继续框架/视图工程。我们会加入一个简单的菜单,给用户选择画笔颜色的机会:

继续这个工程。首先,在视类中加入一个COLORREF类型的公有成员变量,命名为m_color。在视类的构造函数中将它初始化为黑色的。然后,在 OnMouseMove函数中用它创建画笔并选入DC中,操作如下。和往常一样,在使用完后,要将原来的画笔选回DC中,以便释放GDI资源:

HPEN hp = CreatePen(PS_SOLID, 2, m_color);
HPEN op = (HPEN)SelectObject(hdc, hp);

插入一个菜单资源。加入顶级标题:Color和三个菜单项:red、green和blue。

在WinMain中,用#include包含资源头文件。并在创建主框架窗口之间,装入菜单资源,然后将菜单资源的句柄作为参数来调用主框架窗口的Create函数:

HMENU hMenu = LoadMenu(_Module.GetResourceInstance(),
MAKEINTRESOURCE(IDR_MENU1));
mf.Create(GetDesktopWindow(), CWindow::rcDefault,
_T("My App"), 0, 0, (UINT)hMenu);

为了在主框架窗口中处理菜单项我们会加入命令处理函数,所以需要用#include将资源头文件包含进来,然后手工修改消息映射以加入三个新的条目:

BEGIN_MSG_MAP(CMainFrame)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SIZE, OnSize)
COMMAND_ID_HANDLER(ID_COLOR_RED, OnColorRed)
COMMAND_ID_HANDLER(ID_COLOR_GREEN, OnColorGreen)
COMMAND_ID_HANDLER(ID_COLOR_BLUE, OnColorBlue)
END_MSG_MAP()

设置三个处理函数作如下的工作,再次运行它:

LRESULT OnColorRed(WORD wNotifyCode, WORD wID,
HWND hWndCtl, BOOL& bHandled)
{
m_wndView.m_color = RGB(255,0,0);
return 0;
}


第4步:ATL对话框

我们现在将加入一个简单的对话框资源。一个包含丰富子控制(CEdit、CComboBox等等)的MFC用户界面,是ATL所没有的 -- 虽然WTL有。使用ATL创建对话框有多难?唔,我们的对话框会包含一个组合框,我们会故意在资源编辑器中不放入字符串到组合框中 - 这样可以看到在对话框程序机制中怎么样与控制一起工作。
继续我们的工程。加入一个顶级菜单标题:View和一个菜单项:Dialog。我们在主框架窗口的消息映射中加入一个菜单项的命令处理函数:

COMMAND_ID_HANDLER(ID_VIEW_DIALOG, OnViewDialog)

现在开始我们的对话框:第一版中,我们直接使用CSimpleDialog。首先,插入一个新的对话框资源,并在上面放置一个简单的静态框。然后改变菜单项命令处理函数来使用这个对话框。编译并运行:

LRESULT OnViewDialog(WORD wNotifyCode, WORD wID,
HWND hWndCtl, BOOL& bHandled)
{
CSimpleDialog dlg;
dlg.DoModal();

return 0;
}

如果我们想让我们的对话框拥有更多的复杂的行为,我们会从CSimpleDialog类派生。所以,首先回到资源编辑器向对话框加入一个下拉的组合框。

创建一个名叫CListDialog的新类,派生于CSimpleDialog<IDD_DIALOG1>。不要忘了包含stdafx.h。加入消息映射到这个新类,加入一个条目到映射中以链接到基类的消息映射:

BEGIN_MSG_MAP(CListDialog)
CHAIN_MSG_MAP(CSimpleDialog)
END_MSG_MAP()

下一步,我们响应WM_INITDIALOG对话框以加入一些字符串到组合框中。首先,去掉组合框的Sort风格。然后在(ClassView中)CListDialog类上单击鼠标右键并选择Add Windows Message Handler。改变对话框的消息过滤方式。加入并编辑WM_INITDIALOG处理函数。编码如下 - 你会看到,组合框对象的关键部分与MFC中的实现是一样的:

LRESULT OnInitDialog(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
CWindow combo(GetDlgItem(IDC_COMBO1));
combo.SendMessage(CB_ADDSTRING, 0, (LPARAM)"Red");
combo.SendMessage(CB_ADDSTRING, 0, (LPARAM)"Green");
combo.SendMessage(CB_ADDSTRING, 0, (LPARAM)"Blue");

return CSimpleDialog::OnInitDialog(
uMsg, wParam, lParam, bHandled);
}

注意:确保CHAIN_MSG_MAP宏是消息映射中的最后一条。改变主框架中的菜单处理函数以使用这个新的CListDialog类。编译并运行。

对了,DDX/DDV怎么办?我听到你所说的了。好,让我们为IDOK按钮编写代码,返回在组合框中选中的字符串。首先在消息映射中加入适当的宏:

COMMAND_ID_HANDLER(IDOK, OnOK)

对OnOK处理函数编码如下。我们把文本保存在对话框类的成员变量m_text中,一个CComBSTR类的对象:

LRESULT OnOK(WORD, WORD wID, HWND, BOOL&)
{
CComBSTR text;
GetDlgItemText(IDC_COMBO1, m_text.m_str);
::EndDialog(m_hWnd, wID);

return 0;
}

最后,修改主框架中的菜单项处理函数,使用来自对话框的文本,然后编译并运行:

LRESULT OnViewDialog(WORD wNotifyCode, WORD wID,
HWND hWndCtl, BOOL& bHandled)
{
// CSimpleDialog dlg;
CListDialog dlg;
if (IDOK == dlg.DoModal())
{
if (dlg.m_text == CComBSTR("Red"))
m_wndView.m_color = RGB(255,0,0);
else if (dlg.m_text == CComBSTR("Green"))
m_wndView.m_color = RGB(0,255,0);
else if (dlg.m_text == CComBSTR("Blue"))
m_wndView.m_color = RGB(0,0,255);
}

return 0;
}

如果你扩展这个应用程序以支持工具条和状态条,你可以使用ATL的CStatusBarCtrl和CToolBarCtrl类 - 这些类定义在atlcontrols.h中,虽然Microsoft没有正式支持它们。下一篇文章中,我会讲WTL -- 同样是Microsoft没有正式支持的库。你能够聪明的找出ATL和WTL之间前端支持的差异,和全面比较ATL/WTL和MFC。

WTL窗口

ATL窗口 第2部分

绪论


在我的第一篇文章中,我讲解了ATL的窗口类。文章带来了一个问题,关于从MFC移往ATL时缺少前台GUI支持的问题。这个争论的结果,产生了Microsoft Windows Template Library(WTL)。我会带你通过十分简单的步骤创建一个基于WTL的框架-视图应用程序,让你自己写出从MFC转向ATL的感受。

WTL是ATL的扩展,也是由ATL小组开发,包含在Microsoft于2000年1月发布的开发平台SDK包中(也可以从Microsoft网站上下 载),虽然Microsoft没有正式支持。WTL通过提供一个用于编写Win32应用程序和控制的轻量级的框架,一些特殊的视图,GDI对象和实用的 类,来扩展了ATL窗口类。

WTL包由750KB的窗口类库头文件,三个例子和一个Visual Studio WTL AppWizard组成。当然,它还要依靠有1MB大小ATL文件。

要安装WTL,你要做以下的工作:

将WTL目录中的内容复制到你指定的位置。

将WTL/include目录加入到VC++的头文件目录(include directories)列表中。

复制文件appwiz/atlapp60.awx到VC++的定制应用程序向导(Custom App Wizard)目录中,%VCDIR%/Common/MSDev98/Template,%VCDIR%即是安装VC++时指定的目录。

WTL设计特性--附带地,相对于MFC的优势--包括:

模板化,因此有较小的代码量。例如,一个简单的“hello world”SDI应用程序,基于WTL的程序只有24KB,而MFC静态连接结果是440KB,MFC动态连接的结果是24KB+1MB。

无太多相关性,并且可以自由地和SDK代码直接混合。

不会强迫使用特定的应用程序模型,尤其相对于MFC的应用程序框架。

WTL类包括:

标准控制(编辑框,列表框,按钮等等)

公共控制(包括列表视图,树形视图,进度条,微调按钮)

IE控制(rebar,平面滚动条,日历等等)

命令条,菜单,和更新UI类

公共对话框

属性单和页类

框架窗口,MDI框架和子框架,分隔条,可滚动的窗口

设备环境(DC)和GDI对象类(笔、刷子、位图等)

打印机及其信息和设备模式类

实用工具类:包括CPoint, CRect, CSize, 和CString类

WTL AppWizard允许你生成SDI、MDI、多线程SDI和基于对话框的应用程序。多线程SDI应用程序就象IE或Windows Explorer(我的电脑),看起来象是启动了多个实例,实质上它们是同一进程的多个视图。这些视图可以是普通的基于CWindowImpl的窗口,或 基于窗体、列表框、编辑框、列表视图、树形视图、丰富文本编辑框或HTML控制。你可以让你的应用程序拥有rebar、命令条(如同Windows CE)、工具条或状态条。你的应用程序可以包含ActiveX控制,甚至可以是一个COM服务器。


Hello WTL


在这个练习中,我们将创建一个基于WTL的简单的"Hello World"应用程序。

创建一个新的WTL AppWizard应用程序。取名HelloWorld。在WTL AppWizard的第一步对话框中,接受所有默认选项,单击下一步按钮。第二步对话框中,也是保留所有默认选项(包含工具条、rebar、命令条、状态 条和视图窗口),单击完成按钮。现在就编译运行该程序。你会看到一个非常普通的Win32应用程序,它有标准的框架窗口和视图、菜单、工具条、状态条,以 及关于对话框。File|Exit, View|Toolbar, View|Statusbar, 和Help|About菜单项/工具条按钮可以工作,尽管其它的不能。而且,菜单中的某些菜单项具有相应的工具条按钮图标:

现在来分析代码。首先,你应该注意到,在_tWinMain函数中有一个标准的ATL CComModule全局变量,它执行了初始化和终止函数。分析_tWinMain函数,你会发现它所做的其它工作仅仅是初始化公共控件(通过调用 InitCommonControlsEx函数)和调用全局函数Run(处理消息循环)。Run函数创建主框架窗口和CMessageLoop对象,调用 主框架窗口的ShowWindow函数,然后调用CMessageLoop::Run函数。CMessageLoop::Run函数实质上是轮流调用 GetMessage和DispatchMessage函数。

然后,让我们看一看由AppWizard产生的CMainFrame类。它的所有的父类定义在WTL/ATLFrame.h或WTL/ATLApp.h中。其中主要的功能来自于CFrameWindowImpl类。

CUpdateUI通过UPDATE_UI_MAP宏被连接起来,并且最终到达了我们的派生类CMainFrame中的OnViewToolBar和OnViewStatusBar函数。它们做的操作是我们所期望的ShowWindow和SetCheck。

派生于CMessageFilter和CIdleHandler类意味着CMainFrame类必须实现一个消息过滤器。这个消息过滤器用来在分派消息之前清除某些不需处理的消息(例如,改变用户击键被处理的方式)。当消息队列中没有任何消息时,空闲处理函数被调用。

派生的视图类和CComandBarCtrl对象作为成员变量被嵌入到框架窗口类中。




class CMainFrame : public CFrameWindowImpl<CMainFrame>,

public CUpdateUI<CMainFrame>,

public CMessageFilter, //消息过滤

public CIdleHandler //空闲处理

{

public:

DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) //定义框架窗口类信息


CHelloWorldView m_view;

CCommandBarCtrl m_CmdBar;


BEGIN_MSG_MAP(CMainFrame) //消息映射

MESSAGE_HANDLER(WM_CREATE, OnCreate)

COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)

COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)

COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar)

COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar)

COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)

CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)

CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)

END_MSG_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()

};


在框架窗口类中唯一最重要的函数是OnCreate。它初始化CComandBarCtrl对象,并与菜单关联、装入命令条图像列表(菜单上的图标)。实 际上,CComandBarCtrl类把资源文件中菜单描述转换为工具条元素,它可以很容易地为菜单项和工具条按钮关联同一个命令ID和图标。然后,框架 窗口继续创建工具条、伸缩条(rebar)和状态条。接着是初始化视图。最后一步是加入框架窗口的消息过滤器(message filter)和空闲处理器(idle handler)到CComModule类的应用程序对象中。消息过滤是这样一种技术,在你的程序中,在GetMessage函数将消息成功放入你的消息 队列之后Translate/DispatchMessage函数处理它之前,在各个窗口之间路由消息。


LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,

LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

HWND hWndCmdBar = m_CmdBar.Create(m_hWnd,

rcDefault,

NULL,

ATL_SIMPLE_CMDBAR_PANE_STYLE);


m_CmdBar.AttachMenu(GetMenu());

m_CmdBar.LoadImages(IDR_MAINFRAME);

SetMenu(NULL); //删除老式的菜单


HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd,

IDR_MAINFRAME,

FALSE,

ATL_SIMPLE_TOOLBAR_PANE_STYLE);


CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);

AddSimpleReBarBand(hWndCmdBar);

AddSimpleReBarBand(hWndToolBar, NULL, TRUE);

CreateSimpleStatusBar();


m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL,

WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |

WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);


UIAddToolBar(hWndToolBar);

UISetCheck(ID_VIEW_TOOLBAR, 1);

UISetCheck(ID_VIEW_STATUS_BAR, 1);


CMessageLoop* pLoop = _Module.GetMessageLoop();

pLoop->AddMessageFilter(this);

pLoop->AddIdleHandler(this);


return 0;

}


视图类派生于CWindowImpl类,应用程序向导为它产生一个消息处理器 - 为WM_PAINT - 带着TODO注释:


class CHelloWorldView: public CWindowImpl<CHelloWorldView>

{

public:

DECLARE_WND_CLASS(NULL)


BOOL PreTranslateMessage(MSG* pMsg)

{

pMsg;

return FALSE;

}


BEGIN_MSG_MAP(CHelloWorldView)

MESSAGE_HANDLER(WM_PAINT, OnPaint)

END_MSG_MAP()


LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/,

LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

CPaintDC dc(m_hWnd);


//TODO: Add your drawing code here


return 0;

}

};


在这里,作一个小的改动。加入TextOut函数到OnPaint函数中,以输入“Hello World”字符串,编译运行它。

将这个WTL版的程序与前一章(ATL窗口第一部分)的程序比较。我们的轻松来自于向导为我们做了些讨厌的工作。但不要忘了所有这些帮助我们是免费获得的 - 框架+视图、漂亮的菜单、关于对话框、工具条和状态条、包括显示/隐藏功能和更新用户界面处理。同样,与MFC的等价物相比较 - MFC应用程序向导会给你菜单、工具条、状态条和关于对话框,但在MFC中把工具条按钮图标加入到菜单中容易吗?而且,再比较可执行文件的大小,特别是发行版。


在我文章的最后,我想效仿上一章中的ATL Scribble应用程序。

好,如果你在类视图(ClassView)中点击鼠标右键,选择“Add Windows Message Handler”,然后再选择WM_LBUTTONDOWN处理函数,会产生以下代码:


LRESULT OnLButtonDown(UINT uMsg,

WPARAM wParam,

LPARAM lParam,

BOOL& bHandled)

{

return 0;

}


消息映射条目如下:


MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)


自从我们使用ATL之后,向导不再提供MFC所用的分类描述消息映射(cracked messages)的宏。“请暂停一下”我听见你说,“你的文章不是说WTL为我们提供了消息分类描述宏吗?”是的,WTL提供了类似于MFC的宏,在 ATLCRACK.H文件中。如果查看源代码,你会看见与Windows消息相对应的宏。你所要做的就是使用适当的宏,并实现相同格式的消息处理函数。还 有,你要使用BEGIN_MSG_MAP_EX来代替BEGIN_MSG_MAP宏。它为分类描述消息处理函数(cracked handlers)提供了重新得到当前消息和指定消息是否被处理。这是因为分类描述消息处理函数没有ATL处理函数该有的最后一个布尔(bool)参数。 因此,BEGIN_MSG_MAP_EX宏定义了一个额外的函数SetMessageHandled来完成此工作。例如,响应 WM_LBUTTONDOWN消息的宏如下:


#define MSG_WM_LBUTTONDOWN(func) /

if (uMsg == WM_LBUTTONDOWN) /

{ /

SetMsgHandled(TRUE); /

func((UINT)wParam, CPoint(GET_X_LPARAM(lParam), /

GET_Y_LPARAM(lParam))); /

lResult = 0; /

if(IsMsgHandled()) /

return TRUE; /

}


注意,这里使用了CPoint,因此要包含ATLMISC.H。

因此,在你的HelloWorldView.h文件最开始处加入#include "atlmisc.h"和"atlcrack.h"。动手将你的视图的消息映射宏改为EX版本,并加下如下的分类消息宏和处理函数。记住,如果你想使用像CPen一样的GDI对象,请将#include "atlgdi.h"加入到文件中。定义两个CPoint对象m_startPoint、m_endPoint到视图中,并在构造函数中初始化为(-1,-1)。


BEGIN_MSG_MAP_EX(CHelloWorldView)

MSG_WM_LBUTTONDOWN(OnLButtonDown)

MSG_WM_LBUTTONUP(OnLButtonUp)

MSG_WM_MOUSEMOVE(OnMouseMove)

END_MSG_MAP()


LRESULT OnLButtonDown (UINT flags, CPoint point)

{

m_startPoint = point;

return 0;

}


LRESULT OnLButtonUp (UINT flags, CPoint point)

{

m_startPoint.x = m_startPoint.y = -1;

return 0;

}


LRESULT OnMouseMove (UINT flags, CPoint point)

{

m_endPoint = point;


CClientDC dc(this->m_hWnd);

CPen np;

np.CreatePen(PS_SOLID, 2, RGB(255,0,0));

HPEN op = dc.SelectPen(np.m_hPen);


if (m_startPoint.x != -1 )

{

dc.MoveTo(m_startPoint.x, m_startPoint.y, NULL);

dc.LineTo(m_endPoint.x, m_endPoint.y);

m_startPoint.x = m_endPoint.x;

m_startPoint.y = m_endPoint.y;

}


dc.SelectPen(op);

return 0;

}

为了改变颜色和画笔,象上一篇文章(ATL窗口第1部分)中一样,我们加入简单的菜单/工具条支持。加入一个新的菜单,“Color”下有三个菜单项 “Red”、“Green”和“Blue”。同时,也加入相应的工具条按钮,并确信它们的ID是一致的。编写改变视图中COLORREF类型的成员变量的 值的命令处理函数。加入这些命令处理函数的消息映射条目到BEGIN_MSG_MAP_EX/END_MSG_MAP中。


COMMAND_ID_HANDLER_EX(ID_COLOR_RED, OnColorRed)

COMMAND_ID_HANDLER_EX(ID_COLOR_GREEN, OnColorGreen)

COMMAND_ID_HANDLER_EX(ID_COLOR_BLUE, OnColorBlue)


LRESULT OnColorRed(UINT, int, HWND)

{

m_color = RGB(255,0,0);

return 0;

}


LRESULT OnColorGreen(UINT, int, HWND)

{

m_color = RGB(0,255,0);

return 0;

}


LRESULT OnColorBlue(UINT, int, HWND)

{

m_color = RGB(0,0,255);

return 0;

}


如果你这个时候运行程序,你会发现菜单和工具条上这些新的条目可以使用,但却没有任何结果,原来菜单产生的消息并没有传递到视图类中。为什么?哦,命令消 息源自框架类,所以菜单(工具条)产生的消息只会传递给框架类。ATL/WTL使用了一种不同于MFC的消息路由策略使消息从一个类传递到另一个类,你必 须加入以下的宏到框架类的消息映射中(细节请看ATL窗口第1部分)。


CHAIN_MSG_MAP_MEMBER(m_view)


有些东西是与MFC不同的,尽管你已看到这些扩展来自于ATL。重要的美景是什么?认真的开发员不必太拘泥于很小的向导支持。另一方面,漂亮的菜单(cool menu)是很漂亮,但它真的值得从MFC改变过来?是的,WTL可以说是ATL++,ATL是重要的基于COM的开发工具。这是否就是使用它的充分理由?毕竟,WTL不被微软正式支持。

对于WTL,你需要些什么支持?微软使用WTL的早期版本已经几年了,因为用它开发的软件很小很高效,而且微软ATL/WTL小组和广大的ATL/WTL社团都承诺继续支持WTL。

ATL/WTL并不会很快取代MFC,但许多项目要求更快的生产效率,更快的运行效率,更少的消耗,加上对COM的轻松支持,ATL/WTL代替MFC是 可能的。我使用MFC已经10年了,但ATL/WTL组合是多么的诱人。如果保留我们对旧的技术的投资是一条标准,我们可能还在用COBOL。我们做的东 西已被大家接受,你还想到哪里去呢?
 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值