MFC 程序员的 WTL 教程 ( 5 ) — 高级对话框 UI 类
内容
第五部分简介在上一部分里,我们了解了一些关于对话框和控件的 WTL 特性,其工作方式与 MFC 中的对应类很相似。在本部分里,我们会介绍几个新的 WTL 类,它们实现了一些更加高级的 UI 特性:属主绘制(Owner draw)和定制绘制(Custom draw),新的 WTL 控件,UI 更新,以及对话框数据验证(DDV)。
属主绘制以及定制绘制的专用类由于属主绘制和定制绘制在 GUI 工作中已经变得相当的普遍,于是 WTL 提供几个嵌入类来处理这些烦人的事。接下来我们会逐一介绍它们,作为我们的上一个示例工程的续集,现在我们从 ControlMania2 开始。如果你是随着 AppWizard 来创建工程,就要确保你的对话框是非模态的。为了使 UI 更新能正常工作,这是必须的。在 UI 更新一节中,我会给出更多的细节。 COwnerDraw属主绘制包括对四个消息的处理: 如何串联消息取决于你是否把消息反射回了控件。下面是 template <class T> class COwnerDraw { public: BEGIN_MSG_MAP(COwnerDraw<T>) 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() }; 映射的主体节来处理 // C++ class for a dialog that contains owner-drawn controls class CSomeDlg : public CDialogImpl<CSomeDlg>, public COwnerDraw<CSomeDlg>, ... { BEGIN_MSG_MAP(CSomeDlg) //... CHAIN_MSG_MAP(COwnerDraw<CSomeDlg>) END_MSG_MAP() void DrawItem ( LPDRAWITEMSTRUCT lpdis ); }; 不过如果你希望控件处理消息,你就要使用
void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct); int CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct); void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct); 如果出于某些原因你不想处理某个消息,你可以调用 对于 ControlMania2,我们将从 ControlMania1 中的树控件开始,并添加一个属主绘制按钮,然后在按钮类中处理反射的 现在,我们还需要一个实现此按钮的类: class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>, public COwnerDraw<CODButtonImpl> { public: BEGIN_MSG_MAP_EX(CODButtonImpl) CHAIN_MSG_MAP_ALT(COwnerDraw<CODButtonImpl>, 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
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); 它们的缺省处理全都返回 你可能在最后一个截图上已经注意到了,“Dawn” 是用绿色显示的。这是由于使用了一个从 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; } 和在 新的 WTL 控件WTL 有它自己的几个新控件,有的是对其他封装类的增强(比如 CBitmapButtonWTL 的
在 ControlMania2 中,我们将在前面所创建的属主绘制按钮旁边创建一个 // 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 的方法
CBitmapButtonImpl 构造函数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) 调用 CCheckListViewCtrl
下面是一个特点(traits)定义的示例,使用了不同的扩展视图列表风格,以及一个使用这些特点的新类。(注意,必须在扩展列表视图风格中包括 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<CMyCheckListCtrl, CListViewCtrl, CMyCheckListTraits> { private: typedef CCheckListViewCtrlImpl<CMyCheckListCtrl, CListViewCtrl, CMyCheckListTraits> baseClass; public: BEGIN_MSG_MAP(CMyCheckListCtrl) CHAIN_MSG_MAP(baseClass) END_MSG_MAP() }; CCheckListViewCtrl 方法BOOL SubclassWindow(HWND hWnd) 当你子类化一个现有的列表视图控件时, BOOL GetCheckState(int nIndex) BOOL SetCheckState(int nItem, BOOL bCheck) 这两个方法实际上在 void CheckSelectedItems(int nCurrItem) 此方法接受一个条目索引。它切换该条目的勾选(Check)状态,该条目必须已经被选定(Select),并且同时改变其他所有已选定条目的选中状态。你自己通常不会使用此方法,因为 下面是 CTreeViewCtrlEx 和 CTreeItem通过封装 // 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, 37 ); // Using CTreeItems: CTreeItem ti, ti2; ti = m_wndTreeEx.InsertItem ( "baz", TVI_ROOT, TVI_LAST ); ti2 = ti.AddTail ( "yen", 0 ); ti2.SetData ( 42 );
CHyperLink
在 WTL 7.1 里,为 CHyperLink 方法这些是常用的 CHyperLinkImpl( DWORD dwExtendedStyle = HLINK_UNDERLINED ) CHyperLink()
BOOL SubclassWindow(HWND hWnd)
DWORD GetHyperLinkExtendedStyle() DWORD SetHyperLinkExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0) 获取或者设置控件的扩展风格。你必须在调用 bool GetLabel(LPTSTR lpstrBuffer, int nLength) bool SetLabel(LPCTSTR lpstrLabel) 获取或者设置控件要用的文本。如果你不设置标签文本,控件会把标签文本设置为静态控件的窗口文字。 bool GetHyperLink(LPTSTR lpstrBuffer, int nLength) bool SetHyperLink(LPCTSTR lpstrLink) 获取或者设置关联到控件的 URL。如果你不设置超链接,控件会把超链接设置为静态控件的窗口文字。 bool GetToolTipText(LPTSTR lpstrBuffer, int nLength) bool SetToolTipText(LPCTSTR lpstrToolTipText) 获取或者设置光标悬停于链接之上时显示在工具提示里的文本。不过,只有链接具有 下面是一个“普通的”超链接控件在 ControlMania2 对话框中看起来的样子: 其 URL 在 m_wndLink.SetHyperLink ( _T("http://www.codeproject.com/") );
CHyperLink 扩展风格新的 WTL 7.1 的特性可以通过设置相应的扩展风格位来启用。这些风格有:
如果既没有设置 CHyperLink 的其它细节你可以为静态控件设置 如果你使用 m_wndLink.m_tip.AddTool ( m_wndLink, _T("Clickety!"), &m_wndLink.m_rcLink, 1 ); 请注意,这儿有一个和 WTL 7.0 不兼容的改变: 由于一些绘制上的问题, 你应该确保 对话框控件的 UI 更新在对话框里进行控件的 UI 更新比在 MFC 中要简单得多。在 MFC 里,你必须知道未文档化的 第一件需要记住的事情是对话框必须是非模态的。这是因为要使 ControlMania2 的对话框是非模态的,其类定义的起始部分很像是一个框架窗口类: class CMainDlg : public CDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>, public CMessageFilter, public CIdleHandler { public: enum { IDD = IDD_MAINDLG }; BOOL PreTranslateMessage(MSG* pMsg); 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;
}
出于演示 UI 更新的目的,当你点击左边的位图按钮时,右边的按钮会被启用或者禁用。所以首先,我们在 UI 更新映射中添加一个入口,使用 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 ) { UIEnable ( IDC_ALYSON_BMPBTN, !m_wndBmpBtn.IsWindowEnabled() ); } DDVWTL 中对话框数据验证(DDV)的支持较 MFC 而言要简单一点。在 MFC 里,你要为 DDX(把数据传送到变量)和 DDV(验证数据)分别创建单独的宏,而在 WTL 里,一个宏就把这些全搞定。WTL 通过在 DDX 映射里使用下面的宏来提供基本的 DDV 支持:
这些宏的参数与对应的无验证功能的宏很像,附带上了一两个表明可接受范围的参数。 ControlMania2 有一个 ID 为 IDC_FAV_SEASON 的编辑框,并绑定到了成员变量 由于 Buffy 共有七季,故其合法值是 1 到 7(译者注:Buffy,全称“Buffy the Vampire Slayer”,是一部美国电视连续剧,中译名《捉鬼者巴菲》),故 DDV 宏看起来是这样: BEGIN_DDX_MAP(CMainDlg)
//...
DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
END_DDX_MAP()
处理 DDV 失败如果一个控件的数据验证失败了, 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 ); GotoDlgCtrl ( GetDlgItem(nCtrlID) ); } 查看 atlddx.h 可以看到 改变对话框的大小WTL 引起我注意的首要的几件事情之一就是其对可变大小的对话框的内建支持。此前,我曾就此主题写过一篇文章,欲知详情请参考该文。概要地讲,你要做的就是把 下一步在下一篇文章中,我们来看一下在对话框中掌控 ActiveX 控件,以及如何处理控件激发的事件。 参考资料Using WTL’s Built-in Dialog Resizing Class – Michael Dunn Using DDX and DDV with WTL – Less Wright 修订历史2003 年 4 月 28 日:首次发布 |

近期评论