MFC 程序员的 WTL 教程 ( 4 ) — 对话框和控件
内容
第四部分简介对话框和控件是 MFC 确确实实节省你时间和精力的一个地方。如果没有 MFC 的控件类,你就会被淹没在填充结构以及写下成吨的
重温 ATL 对话框回忆一下第一部分,ATL 有两个对话框类, 创建一个新的对话框类,要做三件事:
然后你就可以像在框架窗口中那样添加消息处理器了。WTL 并没有改变此过程,但确实添加了可用于对话框的附加特性。 控件封装类WTL 有许多的控件封装类,对于它们,你应该感到熟悉,因为 WTL 类通常与其在 MFC 中的对应类使用相同的(或者极其相似的)名字。通常方法的名字也是一致的,因此当你使用 WTL 的封装类时你可以使用 MFC 的文档。不过当你需要跳转到某个类的定义时,F12 键就派不上用场了。 下面是内建控件的封装类:
还有一些 WTL 特有的类: 需要注意的是大多数的封装类都是窗口接口类,就像 因为本系列面向有经验的 MFC 程序员,我不会在与 MFC 中的对应类相似的封装类的细节上花费太多的时间。不过,我会介绍 WTL 中的新类。 使用 AppWizard 创建基于对话框的应用启动 VC 并打开 WTL AppWizard。我敢确信你和我一样,对于时钟程序已经厌倦了,所以我们不妨把下一个应用叫做 ControlMania1(译者注:意思是控件狂)。在 AppWizard 的第一页,点击 Dialog Based。我们还需要在制作模态还是非模态对话框之间作一个选择。其差异很重要,我将会在第五部分里介绍,不过现在我们可以拣一个简单一点的来,模态的吧。象下面一样选中 Modal Dialog 和 Generate .CPP Files: VC 6 的向导的第二页或者 VC 7 的向导的“User Interface Features”标签中,所有的选项仅当主窗口是框架窗口时才有意义,所以它们全部都被禁止掉了。点击 Finish,完成整个向导。 正如你所希望的,AppWizard 对于对话框应用生成的代码是相当简单的。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 并创建一个单线程单元(single-threaded apartment)。这对于要掌控 ActiveX 控件的对话框的是必需的,如果你的应用不使用 COM,你可以安全地把
尽管此对话框还相当的赤贫,但你还是可以立即编链并运行它:
此示例工程将演示如何把变量挂接到控件上。下面是应用的样子,又多了几个控件。你在后续的讨论中可以回来参看此图。 由于应用使用了一个列表视图(list view)控件,所以需要改动一下对 AtlInitCommonControls ( ICC_WIN95_CLASSES ); 这会比需要的多注册几个类,但它使我们在添加不同类型的控件时省却了记忆 使用控件封装类把一个成员变量联系到控件有好几种方法。有的使用了简单的 ATL 方法 1 – 附着一个 CWindow最简单的方法是声明一个 下面的代码演示了把变量关联到列表控件的全部三种方法: HWND hwndList = GetDlgItem(IDC_LIST); CListViewCtrl wndList1 (hwndList); // use constructor CListViewCtrl wndList2, wndList3; wndList2.Attach ( hwndList ); // use Attach method wndList3 = hwndList; // use assignment operator 记住, ATL 方法 2 – CContainedWindow
至于实际的类, 想搞定一个
在 ControlMania1 里,我们为 OK 按钮和 Exit 按钮各使用一个 现在我们来实践这些步骤。首先,向 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, 1), m_wndExitBtn(this, 2) { } 构造函数的参数是一个
最后,为每个 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;
}
}
如果你要使用 CContainedWindowT<CButton> m_wndOKBtn; 然后 当你把光标移动到按钮上的时候,你就可以看到
ATL 方法 3 – 子类化方法 3 致力于创建一个 ControlMania1 使用此方法来子类化主对话框中的 About 按钮。下面是 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 方法 1 – DDX_CONTROLWTL DDX(对话框数据交换)工作起来很像 MFC,而且可以相当轻松地把变量连接到控件上。首先,你需要像在前一个例子中一样,有一个 为了向 class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg> { //... }; 接下来在类中创建一个 DDX 映射,它与 MFC 应用中 ClassWizard 生成的 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 CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg> { //... 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; }; 最后,在 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;
}
如果你运行 ControlMania1 工程,你会看到所有的这些子类化都在起作用。在编辑框上右击会弹出消息框,在按钮上的光标像在前面显示的一样会改变其形状。 WTL 方法 2 – DDX_CONTROL_HANDLEWTL 7.1 中添加的一个新特性是 如果你还在使用 WTL 7.0,你可以使用以下宏定义一个可以和 #define DDX_CONTROL_IMPL(x) \ class x##_ddx : public CWindowImpl<x##_ddx, x> \ { public: DECLARE_EMPTY_MSG_MAP() }; 然后你就可以这样写: DDX_CONTROL_IMPL(CListViewCtrl) 这样你就有了一个名为 更多 DDX 的内容DDX 可以,当然,实际上也确实是作数据交换的。WTL 支持在编辑框和字符串变量间交换字符串数据。它也可以将字符串解析为一个数字,并将该数据在整数型或者浮点型变量间传送。它还支持向/从 DDX 宏每个 DDX 宏都会展开为一个真正工作的对
WTL 7.1 中还加入了另外一个用于浮点类型的宏:
注意,如果在你的应用中使用了 #define _ATL_USE_DDX_FLOAT
这是因为出于代码大小的优化,缺省对于浮点的支持是被禁止掉了的。 关于 DoDataExchange() 的更多信息你可以象在 MFC 中调用 BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE,
UINT nCtlID = (UINT)-1 );
参数:
如果控件被成功更新, 使用 DDX为了使用 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 按钮的处理器中,我们首先调用 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 ); } 如果你在编辑框中输入了非数字文本, 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 例子,我们添加一个复选框来演示 此 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; }; 在 void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl ) { // Transfer data from the controls to member variables. if ( !DoDataExchange(true) ) return; //... if ( m_bShowMsg ) MessageBox ( _T("DDX complete!"), _T("ControlMania1"), MB_ICONINFORMATION ); } 示例工程中还有使用其他 处理来自控件的通知在 WTL 中处理通知与 API 级编程类似。控件以 在父窗口中处理通知以 消息映射宏要处理
示例:
也有处理
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );
LRESULT func ( NMHDR* phdr ); 处理器的返回值用作消息的结果。这与 MFC 不同,MFC 中处理器接受一个 我们要为 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
}
此处理器并未使用 反射通知如果你有一个 如果你要将通知反射回控件类,你只需向对话框的消息映射中添加一个宏 class CMainDlg : public ... { public: BEGIN_MSG_MAP_EX(CMainDlg) //... NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) REFLECT_NOTIFICATIONS() END_MSG_MAP() }; 这一宏添加了一些代码到消息映射中,可以处理任何先前的宏都没有处理的通知消息。代码将检查消息的 反射的消息共有 18 个:
在控件类中,你可以仅为感兴趣的反射消息添加处理器,然后在最后加上 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 宏我们仅仅看到了一个用于反射消息的 WTL 宏,即 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 ); }; 如果你检点一下示例代码中的 ControlMania1 对话框,就会发现有一个像上面一样处理了 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 并点击树中的 +/- 按钮,你就可以看到处理器在工作 – 一旦你展开了一个节点,就再也不能折叠回去了。 拾零对话框字体如果你和我一样对用户界面吹毛求疵而又正好在使用 Win 2000 或者 XP,你就可能会对对话框为什么使用的是 MS Sans Serif 字体而不是 Tahoma 字体而感到奇怪。其实是因为 VC 6 实在是太古老了,它生成的资源文件在 NT 4 上可以很好地工作,但对于 NT 的后续版本却不能。你可以修正这一问题,不过需要手动编辑资源文件。 你需要对资源文件中存在的每个对话框做三样改动:
不幸的是,如果你又改动并保存(译者注:在集成环境的资源编辑器里)了资源,前两项改动会丢失,你需要再次修改。下面是对话框改动之前的一个示例: 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 字体。 在 VC 7 里,要使用正确的字体你只需要在对话框编辑器里改变一个设置: 当你把 Use System Font 改为 True 时,编辑器会帮你把字体改成 MS Shell Dlg。 _ATL_MIN_CRT正如在 VC Forum FAQ 中讲到的,ATL 具有一个优化功能,可以使你创建无需链接 C 运行时库(CRT)的应用程序。这一优化通过在预处理选项中添加 下一步在第五部分里,会涵盖以下知识,对话框数据验证(DDV)、WTL 中的新控件以及诸如属主绘制(owner draw)和定制绘制(custom draw)这样的高级用户界面特性。 修订历史2003 年 4 月 27 日:首次发布 |

近期评论