Part IV Dialogs and Controls
Refresher on ATL Dialogs
ATL中有两种对话框实现:CDialogImple和CAxDialogImpl,后者在窗口作为ActiveX控件宿主的时候使用。通过三步创建新的对话框类:
1. 创建对话框资源;
2. 从CDialogImpl继承新对话框子类;
3. 在新类中添加公有变量IDD,并将对话框资源ID赋值给它。
然后就可以和Frame一样添加消息处理了。
Control Wrapper Classes
WTL中类名、方法名基本和MFC中的相似,所以可以参考MFC的文档来使用WTL,也可以通过F12参看源码来了解,如果喜欢的话。
一下列出内置的控件:
1. 用户控件:CStatic, CButton, CListBox, CComboBox, CEdit, CSrollBar, CDragListBox
2. 普通控件:CImageList, CListViewCtrl(CListCtrl in MFC), CTreeViewCtrl(CTreeCtrl in MFC), CHeaderCtrl, CToolBarCtrl, CStatusBarCtrl, CTabCtrl, CToolTipCtrl, CTrackBarCtrl(CSliderCtrl in MFC), CUpDownCtrl(CSpinButtonCtrl in MFC), CProgressBarCtrl, CHotKeyCtrl, CMonthCalenderCrl, CIPAddressCtrl
3. MFC中不存在的普通控件:CPagerCtrl, CFlatScrollBar, CLinkCtrl(可点击的超链接,XP及以后版本可用)
还有一些WTL特有的类:CBitmapButton, CCheckListViewCtrl(带复选框的List view), CTreeViewCtrlEx和CTreeItem(一起使用,CTreeItem是对HTREEITEM的封装), CHyperLink(可点击超链接,可在所有OS使用)
大部分的封装类都是对窗口句柄的封装,就像CWindow一样,他们封装了HWND和窗口消息(比如:CListBox::GetCurSel()是对LB_GETCURSEL的封装)。所以很容易封装一个窗体,只需要把他附加在已存在的控件中。同CWindow一样,当控件封装类销毁时控件并没有被真正销毁,除了CBitmapButton, CCheckListViewCtrl, 和CHyperLink。
Create a Dialog-Based App with the AppWizard
int WINAPI _tWinMain (
HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/,
LPTSTR lpstrCmdLine, int nCmdShow )
{
HRESULT hRes = ::CoInitialize(NULL); // 初始化COM,如果是非ActiveX宿主,可以注释掉
AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES); // 封装的InitCommonControlsEx
hRes = _Module.Init(NULL, hInstance);
int nRet = 0;
// BLOCK: Run application
{
CMainDlg dlgMain;
nRet = dlgMain.DoModal();
} // 重要的代码块,不能省略{},如果这样的话dlgMain的析构函数在_Module.Term()后执行,会导致异常,而且很难调试
_Module.Term();
::CoUninitialize(); //如果是非ActiveX宿主,可以注释掉
return nRet;
}
在对话框中增加一个CListViewCtrl, CEditor,由于使用了list view control,需要改变AtlInitCommonControls的调用:AtlInitCommonControls(ICC_WIN95_CLASSES);这种方法注册了比实际需要的更多的类,免去了当需要增加一种类型控件时需要增加ICC_开头的常量的困扰。
Using the Control Wrapper Classer
有很多种方法使成员变量与控件(感觉是控件资源)关联。有些直接用窗口对象类CWindows(或者其他窗体接口类,如CListViewCtrl),有些用CWindowImpl派生类。如果仅需要窗体对象的临时变量的话用CWindow,但用CWindowImpl的话可以子类化控件并处理控件的消息。
ATL Way 1- Attaching a CWindow
声明一个CWindow或其他窗体接口类,然后调用Attach的方法、或者用带参构造函数的方法、或者用赋值运算符来将变量与控件句柄HWND关联。
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的析构函数不会销毁控件句柄,所以不需要在离开作用域的时候detach。当然如果需要可以将变量作为成员变量,然后在OnInitDialog的时候绑定attach。
ATL Way 2 – CcontainedWindow
采用CContainedWindow是介于CWindow和CWindowImpl之间的一种方法。这种方法通过子类化控件并且将控件消息处理放在父窗体来实现。这样就将所有消息处理放在对话框中,而不需要为控件编写独立的CWindowImpl派生类。注意:不能用CContainedWindow方法处理控件的WM_COMMAND,WM_NOTIFY消息,因为这些消息总是发送到控件的父窗体的。
实际上使用的是CContainedWindowT模板类,CContainedWindow的定义如下
Typedef CContainedWindowT<CWindow> CContainedWindow
如果需要使用其他窗体接口类只需要修改模板参数,如:CContainedWindowT<CListViewCtrl>
如何和CContainedWindow挂钩呢?步骤如下:
1. 在对话框中创建CContainedWindowT成员变量;
2. 将控件的消息处理放在对话框消息映射的ATL_MSG_MAP区中;
3. 在对话框构造的时候,调用CContainedWindowT构造函数,设置需要路由的ALT_MSG_MAP区
4. 在OnInitDialog中调用CContainedWindowT::SubclassWindwo()将变量与控件关联。
class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
};
class CMainDlg : public CDialogImpl<CMainDlg>
{
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);
};
CMainDlg::CMainDlg() : m_wndOKBtn(this, // CMessageMap* 消息路由处
1), // 哪个ALT_MSG_MAP区
m_wndExitBtn(this, 2)
{
}
【提示】如果使用WTL7.0/7.1,会出现断言错误,在CWindowImpl或CDialogImpl派生类满足如下情况时:
1. 消息映射采用BEGIN_MSG_MAP 而不是BEGIN_MSG_MAP_EX
2. 消息映射中有ALT_MSG_MAP区
3. CContainedWindowT变量将消息路由到ALT_MSG_MAP区
4. ALT_MSG_MAP区中使用新WTL消息处理宏(如:MSG_开头)
解决办法是:使用BEGIN_MSG_MAP_EX代替BEGIN_MSG_MAP。
最后一部,绑定:
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
return TRUE;
}
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;
}
}
如果想使用CButton的特性,可以将变量声明改为:
CContainedWindowT<CButton> m_wndOKBtn;
ATL Way 3 – Subclassing
创建CWindowImpl派生类并子类化控件。方法同Way2,但是消息处理都在派生类中实现,而不是在对话框中完成。
class CButtonImpl : public CWindowImpl<CButtonImpl, CButton>
{
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;
}
}
};
class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn;
};
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 Way 1 – DDX_CONTROL
WTL对DDX(对话框数据交换)的支持和MFC类似,可以很方便的将变量和控件关联。首先,需要CWindowImpl派生类,本例中会子类化edit control,实现了新的CEditImpl。同时要使用DDX,需要在stdafx.h中#include atlddx.h。
让对话框拥有DDX功能,需要继承自CWinDataExchange
Class CMainDlg : public CDialogImpl<CMainDlg>
Public CWinDataExchange<CMainDlg>
{ … }
下一步创建DDX映射。对于不同类型的数据存在响应的DDX_*宏实现数据映射,这里我们用DDX_CONTROL来将变量和控件关联。
Class CEditImpl : public CWindowImpl<CEditImpl, CEdit>
{
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 CWindowImpl<CMainDlg>,
Public CWinDataExchange<CMainDlg>
{
// …
BEGIN_DDX_MAP(CMinDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
END_DDX_MAP()
Protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn;
CEditImpl m_wndEdit;
};
最后,在OnInitDialog中调用从CWinDataExchange中继承的DoDataExchange方法。当DoDataExchange第一次被调用的时候,会实现必要的控件子类化(由DDX映射决定)。
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;
}
WTL Way 2 – DDX_CONTROL_HANDLE
DDX_CONTROL_HANDLE宏是WTL7.1的新增特征。在WTL7.0中DDX_CONTROL只能绑定CWindowImpl派生类,其他扁平窗体接口类如CWindow,CListViewCtrl不能使用该宏进行绑定。DDX_CONTROL_HANDLE没有这样的限制。
如果还在使用WTL7.0,可以通过以下宏来实现DDX_CONTROL对非CWindowImpl派生类的通用(本质上是对扁平窗体接口类封装成CWindowImpl子类):
#define DDX_CONTROL_IMPL(x) /
class x##_ddx : public CWindowImpl<x##_ddx, x> /
{ public: DECLARE_EMPTY_MSG_MAP() };
对需要绑定的类行使用该宏:
DDX_CONTROL_IMPL(CListViewCtrl)
这样就有了一个CListViewCtrl_ddx类来代替CListViewCtrl功能,但是是可以被DDX_CONTROL接受。
More on DDX
当然,DDX实际上完成的是数据交换。WTL支持在编辑框和字符串变量件进行字符串数据交换。同样也可以通过解析字符串为数值,然后进行整型、浮点型数值交换。还支持对复选框或单选按钮组状态值交换。
DDX macros
每个DDX宏都解释为CWinDataExchange方法来完成数据交换。这些宏具有相同的格式:DDX_FOO(controlID, variable)。每个宏支持不同数据类型变量。
DDX_TEXT CString(需要define_WTL_USE_CSTRING),LPTSTR, BSTR, CComBSTR, or statically-acclocated character array。不支持堆字符串
DDX_INT int
DDX_UINT unsigned int
DDX_FLOAT float or double
DDX_CHECK 复选框状态 int or bool (int 0,1,and2 对应 BST_UNCHECKED, BST_CHECKED, BST_INDETERMINATE) (bool 从WTL7.1开始添加,表示没有中间状态的复选框,原中间状态返回false)
DDX_RADIO 单选按钮组 int
DDX_FLOAT_P(controlID, variable, precision) WTL7.1版本增加,指定浮点精度
【注意】出于优化考虑,对浮点数交换默认是不启用的。需要使用DDX_FLOAT和DDX_FLOAT_P的话需要在stdafx.h中:
#define _ATL_USE_DDX_FLOAT
More about DoDataExchange()
DoDataExchange方法和MFC中UpdateData()功能一样,原型:
BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE, // true表示保存数据同DDX_SAVE,从控件到数据变量;false表示刷新数据同DDX_LOAD,从变量到控件。
UINT nCtlID = (UINT)-1 ); // 指定需要交换的控件,-1表示所有控件
数据交换成功返回true,否则返回false。对于出错情况,可以重写两个错误处理方法:一个是OnDataExchangeError(),出现任何数据交换错误时调用,默认实现发出警告音并将焦点定位在出错控件。另一个是OnDataValidateError()这将在介绍DDV时介绍。
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_bShowMsg)
END_DDX_MAP()
protected:
// DDX variables
CString m_sEditContents;
int m_nEditNumber;
bool m_bShowMsg;
};
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 );
if(m_bShowMsg)
MessageBox(_T(“DDX complete!”), _T(“ConstrolManial”), MB_ICONINFOMATION);
}
如果输入非整型文本,就会出错,错误处理:
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) );
}
增加一个复选框,来测试复选框状态交换。
Handling Notificaitons from Controls
控件将通知以WM_COMMAND或WM_NOTIFY消息的形式发送给父窗体,由父窗体负责处理这些消息。父窗体可以自己处理这些消息或者将消息返回给控件
Handling Notifications in the parent
以WM_NOTIFY和WM_COMMAND发送的通知包含各种信息。WM_COMMAN的参数包含:发送消息的控件ID、控件句柄HWND、通知码。WM_NOTIFY消息参数包含同样内容,以NMHDR结构表示。这里介绍WTL中的通知消息映射宏,要使用这些宏必须在stdafx.h中引入atlcrack.h。
COMMAND_HANDLER_EX(id, code, func) 指定控件、指定通知码
COMMAND_CODE_HANDLER_EX(id, func) 指定控件、任何通知码
COMMAND_ID_HANDLER_EX(code, func) 任何控件、指定通知码
COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func) 指定多个控件、任何通知码
COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func) 指定多个控件、指定通知码
示例:
COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange): 处理 从 IDC_USERNAME.发出的EN_CHANGE通知
COMMAND_ID_HANDLER_EX(IDOK, OnOK): 处理从IDOK发出的所有通知
COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked): 处理ID为IDC_MONDAY 到 IDC_FRIDAY控件发出的 BN_CLICKED 通知
对于WM_NOTIFY的消息映射宏只需把COMMAND_换成NOTIFY_即可。
WM_COMMAND处理函数原型:
Void func(UINT uCode, int nCtrlID, HWND hwndCtrl);
WM_NOTIFY处理函数原型:
LRESULT func(NMHDR* phdr);
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
}
Reflecting Notifications
如果是用CWindowImpl派生类实现的控件如CEditImpl,可以在这个类中处理通知消息,而不需要在父窗体中实现。这叫做通知反馈,同MFC中的消息反馈一样。不同的是在反馈过程中父窗体和控件都参与了,而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()
};
REFLECT_NOTIFICATIONS增加了一些处理所有未经处理的通知消息的代码。根据消息对应的HWND将消息发送到该窗体。消息的值不再是WM_开头的了,而是改成具有相同消息反射系统的OLE控件消息,以OCM_开头,处理方式同非反射消息一致。
总共有18种反射消息:
控件通知:WM_COMMAND, WM_NOTIFY, WM_PARENTNOTIFY
自绘消息:WM_DRAWITEM, WM_MEASUREITEM, WM_COMPAREITEM, WM_DELETEITEM
列表框键盘消息:WM_VKEYTOITEM, WM_CHARTOITEM
其他:WM_HSCROLL, WM_VSCROLL, WM_CTLCOLOR
在控件类中,添加需要处理的反射消息映射,并在最后加上DEFAULT_REFLECTION_HANDLER()宏。这个宏把未处理的反射消息交由DefWindowProc()处理。下面看一个按钮自绘的例子:
Class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>
{
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 macros for handling reflected messages
上例中一个WTL反射消息MSG_OCM_DRAWITEM,还有17个以MSG_OCM_开头的可以被反馈的消息宏。因为WM_NOTIFY和WM_COMMAND需要将参数解析,WTL为他们提供了特殊的宏,这些宏很像COMMAND_HANDLER_EX和NOTIFY_HANDLER_EX,但是都加上了REFLECTED_前缀。如:
Class CMyTreeCtrl, : public CWindowImpl<CMyTreeCtrl, CTreeViewCtrl>
{
Public:
BEGIN_MSG_MAP_EX(CMyTreeCtrl)
REFLECTED_NOTIFY_CODE_HANDLER_EX(TVN_ITEMEXPANDING, OnItemExpanding)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
LRESULT OnItemExpanding(NMHDR* phdr);
};
LRESULT CBuffyTreeCtrl::OnItemExpanding(NMHDR* phdr)
{
NMTREEVIEW* pnmtv = (NMTREEVIEW*)phdr;
If(pnmtv->action & TVE_COLLAPSE)
Return TRUE; // don’t allow it
Else
Return FLASE; // allow it
}
Dialog Fonts
对话框中默认的字体是Sans Serif而不是Tahoma,在VC6中如果需要修改的话只能修改资源文件,需要修改三个地方:
1. 对话框类型:DIALOG -> DIALOGEX
2. 窗体类型:增加DS_SHELLFONT
3. 对话框字体:将MS Sans Serif改为MS Shell Dlg
但是不幸的是每次修改资源并保存的时候前两项都会被还原。
【改前】
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
在VC7中可以方便的通过对话框属性修改字体,当把Use System Font改为True的时候编辑器将字体改变。
_ATL_MIN_CRT
ATL存在一个优化属性,可以创建一个不需要连接C运行时库的应用,只需要在预处理设置中增加_ATL_MIN_CRT选项就行。向导生成的工程中设置了该属性。当时如果在使用CString或DDX中使用了浮点特性就必须去掉该选项。