65.9K
CodeProject 正在变化。 阅读更多。
Home

面向 MFC 程序员的 WTL,第五部分 - 高级对话框 UI 类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (85投票s)

2003年4月27日

18分钟阅读

viewsIcon

477788

downloadIcon

6147

使用实现高级对话框 UI 元素的新的 WTL 类

目录

第五部分介绍

在上一篇文章中,我们看到了一些 WTL 对话框和控件的特性,它们与相应的 MFC 类相似。在本文中,我们将介绍一些实现更高级 UI 功能的新 WTL 类:所有者绘制和自定义绘制、新的 WTL 控件、UI 更新和对话框数据验证 (DDV)。

专用的所有者绘制和自定义绘制类

由于所有者绘制和自定义绘制控件在 GUI 工作中非常常见,WTL 提供了混合类来处理一些繁重的工作。我们将在接下来的部分中逐一介绍它们,同时开始上一篇示例项目 ControlMania2 的续集。如果您正在通过 AppWizard 创建项目来跟随学习,请务必将此对话框设置为无模式。它必须是无模式的,UI 更新才能正常工作。我将在UI 更新部分详细说明这一点。

COwnerDraw

所有者绘制涉及处理多达四条消息:WM_MEASUREITEMWM_DRAWITEMWM_COMPAREITEMWM_DELETEITEM。atlframe.h 中定义的 COwnerDraw 类简化了您的代码,因为您不需要为所有这些消息编写消息处理程序。相反,您将消息链式传递给 COwnerDraw,它会调用您在类中实现的重写函数。

如何链式传递消息取决于您是否将消息反射到控件。以下是 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()
};

请注意,映射的主部分处理 WM_* 消息,而 ALT_MSG_MAP(1) 部分处理反射版本 OCM_*。所有者绘制通知类似于 WM_NOTIFY,您可以选择在控件的父级中处理它们,或将它们反射回控件。如果您选择前者,您可以直接将消息链式传递给 COwnerDraw

// 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 );
};

但是,如果您希望控件处理消息,则需要使用 CHAIN_MSG_MAP_ALT 宏将消息链式传递到 ALT_MSG_MAP(1) 部分

// C++ class that implements an owner-drawn button

class CMyButton : public CWindowImpl<CMyButton, CButton>,
                  public COwnerDraw<CMyButton>, ...
{
  BEGIN_MSG_MAP(CMyButton)
    //...

    CHAIN_MSG_MAP_ALT(COwnerDraw<CMyButton>, 1)
    DEFAULT_REFLECTION_HANDLER()
  END_MSG_MAP()
 
  void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

COwnerDraw 解包随消息发送的参数,然后调用您类中的实现函数。在上面的示例中,这些类实现了 DrawItem(),当 WM_DRAWITEMOCM_DRAWITEM 链式传递到 COwnerDraw 时会调用此函数。您可以重写的方法有

void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
int  CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);
void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct);

如果由于某种原因您不想在重写中处理消息,您可以调用 SetMsgHandled(false),消息将传递给消息映射中可能存在的任何其他处理程序。

对于 ControlMania2,我们将从 ControlMania1 中的树控件开始,并添加一个所有者绘制按钮,并在按钮类中处理反射的 WM_DRAWITEM。这是资源编辑器中的新按钮

 [Owner-drawn button 1 - 7K]

现在我们需要一个实现此按钮的类

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 );
};

DrawItem() 使用 GDI 调用,如 BitBlt() 在按钮面上绘制图片。此代码应该很容易理解,因为 WTL 类名和方法与 MFC 再次相似。

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);
}

这是按钮的外观

 [Owner-drawn button - 19K]

CCustomDraw

CCustomDraw 的工作方式与 COwnerDraw 类似,您可以通过类似的方式处理 NM_CUSTOMDRAW 消息并进行链式传递。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);

所有默认处理程序都返回 CDRF_DODEFAULT,因此只有在需要进行自己的绘制或返回不同值时才需要重写方法。

您可能已经注意到,在上一张屏幕截图中,“Dawn”显示为绿色。这是通过使用一个新类(源代码中的 CBuffyTreeCtrl)实现的,该类派生自 CTreeCtrl,它将消息链式传递给 CCustomDraw 并重写 OnPrePaint()OnItemPrePaint()。当树被填充时,“Dawn”节点的项目数据被设置为 1,并且 OnItemPrePaint() 检查该值并在找到时更改文本颜色。

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;
}

COwnerDraw 一样,您可以在自定义绘制消息处理程序中调用 SetMsgHandled(false),以将消息传递给消息映射中的任何其他处理程序。

新的 WTL 控件

WTL 有一些自己的新控件,它们要么改进了其他包装器(如 CTreeViewCtrlEx),要么提供了内置控件中没有的新功能(如 CHyperLink)。

CBitmapButton

WTL 的 CBitmapButton,在 atlctrlx.h 中声明,使用起来比 MFC 版本更容易。WTL 类使用图像列表而不是四个单独的位图资源,这意味着您可以将多个按钮图像保存在一个位图中,并稍微减少 GDI 使用量。如果您有很多图形并且您的应用程序在 Windows 9x 上运行,这尤其好,因为使用大量单独的图形会很快耗尽 GDI 资源并导致系统崩溃。

CBitmapButton 是一个派生自 CWindowImpl 的类,它具有许多功能,包括:控件的自动调整大小、3D 边框的自动生成、热跟踪支持以及每个按钮的多个图像以适应控件的各种状态。

在 ControlMania2 中,我们将使用一个 CBitmapButton 以及我们之前创建的所有者绘制按钮。我们首先向 CMainDlg 添加一个名为 m_wndBmpBtnCBitmapButton 成员。然后,我们以通常的方式将其连接到新按钮,可以通过调用 SubclassWindow() 或使用 DDX。我们将位图加载到图像列表中,然后告诉按钮使用该图像列表。我们还告诉按钮图像列表中的哪个图像对应哪个控件状态。这是 OnInitDialog() 中设置按钮的部分

    // 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 );

默认情况下,按钮拥有图像列表,因此 OnInitDialog() 不得删除它创建的图像列表。这是新按钮的默认状态。请注意控件如何调整大小以精确适应图像的大小。

 [WTL bitmap button - 19K]

由于 CBitmapButton 是一个非常有用的类,我将在此处介绍其公共方法。

CBitmapButton 方法

CBitmapButtonImpl 包含实现按钮的所有代码,但除非您需要重写方法或消息处理程序,否则可以将 CBitmapButton 用于您的控件。

CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE,
                  HIMAGELIST hImageList = NULL)

构造函数设置按钮的扩展样式(不要与其窗口样式混淆),并可以分配一个图像列表。通常默认值就足够了,因为您可以使用其他方法设置这两个属性。

BOOL SubclassWindow(HWND hWnd)

SubclassWindow() 被重写以执行子类化并初始化类保留的内部数据。

DWORD GetBitmapButtonExtendedStyle()
DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle,
                                   DWORD dwMask = 0)

CBitmapButton 支持一些影响按钮外观或操作的扩展样式

BMPBTN_HOVER
启用热跟踪。当光标在按钮上方时,它将以焦点状态绘制。
BMPBTN_AUTO3D_SINGLE, BMPBTN_AUTO3D_DOUBLE
自动在图像周围生成 3D 边框,并在按钮获得焦点时生成焦点矩形。此外,如果您未提供按下状态的图像,则会自动为您生成一个。BMPBTN_AUTO3D_DOUBLE 会生成稍粗的边框。
BMPBTN_AUTOSIZE
使按钮调整自身大小以匹配图像大小。此样式是默认样式。
BMPBTN_SHAREIMAGELISTS
如果设置,按钮对象不会销毁用于保存按钮图像的图像列表。如果未设置,图像列表将由 CBitmapButton 析构函数销毁。
BMPBTN_AUTOFIRE
如果设置,单击按钮并按住鼠标按钮会生成重复的 WM_COMMAND 消息。

调用 SetBitmapButtonExtendedStyle() 时,dwMask 参数控制哪些样式受到影响。使用默认值 0 使新样式完全替换旧样式。

HIMAGELIST GetImageList()
HIMAGELIST SetImageList(HIMAGELIST hImageList)

使用 GetImageList()SetImageList() 将图像列表与按钮关联,或获取当前与按钮关联的图像列表。

int  GetToolTipTextLength()
bool GetToolTipText(LPTSTR lpstrText, int nLength)
bool SetToolTipText(LPCTSTR lpstrText)

CBitmapButton 支持在鼠标悬停在按钮上方时显示工具提示。调用 GetToolTipText()SetToolTipText() 来获取或设置要在工具提示中显示的文本。

void SetImages(int nNormal, int nPushed = -1,
               int nFocusOrHover = -1, int nDisabled = -1)

调用 SetImages() 告诉按钮图像列表中的哪个图像用于哪个按钮状态。参数都是图像列表的基于 0 的索引。nNormal 是必需的,但其他是可选的。传递 -1 表示没有对应状态的图像。

CCheckListViewCtrl

atlctrlx.h 中定义的 CCheckListViewCtrl 是一个派生自 CWindowImpl 的类,它实现了一个包含复选框的列表视图控件。这与 MFC 的 CCheckListBox 不同,后者使用列表框,而不是列表视图。CCheckListViewCtrl 非常简单,因为该类自身添加的功能很少。但是,它确实引入了一个新的辅助类 CCheckListViewCtrlImplTraits,它类似于 CWinTraits,但有一个第三个模板参数,即用于控件的扩展列表视图样式。如果您不定义自己的 CCheckListViewCtrlImplTraits 集,该类默认使用这些样式:LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT

这是一个使用不同扩展列表视图样式和使用这些特征的新类的示例特征定义。(请注意,您必须在扩展列表视图样式中包含 LVS_EX_CHECKBOXES,否则您将收到断言失败消息。)

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)

当您子类化现有列表视图控件时,SubclassWindow() 会查看关联的 CCheckListViewCtrlImplTraits 类中的扩展列表视图样式并将其应用于控件。特征类的前两个模板参数(窗口样式和扩展窗口样式)未使用。

BOOL GetCheckState(int nIndex)
BOOL SetCheckState(int nItem, BOOL bCheck)

这些方法实际上在 CListViewCtrl 中。SetCheckState() 接受一个项目索引和一个布尔值,指示是选中还是取消选中该项目。GetCheckState() 仅接受一个索引并返回该项目的当前选中状态。

void CheckSelectedItems(int nCurrItem)

此方法接受一个项目索引。它切换该项目的选中状态(该项目必须被选中),并将所有其他选中项目的选中状态更改为匹配。您可能不会自己使用此方法,因为当单击复选框或用户按下空格键时,CCheckListViewCtrl 会处理选中项目。

这是 CCheckListViewCtrl 在 ControlMania2 中的样子

 [Check list ctrl - 22K]

CTreeViewCtrlEx 和 CTreeItem

这两个类通过包装 HTREEITEM 使树控件功能更易于使用。CTreeItem 对象保留一个 HTREEITEM 和一个指向包含该项的树控件的指针。然后,您可以使用 CTreeItem 对该项执行操作;您无需在每次调用中都引用树控件。CTreeViewCtrlEx 类似于 CTreeViewCtrl,但其方法处理 CTreeItem 而不是 HTREEITEM。因此,例如,当您调用 InsertItem() 时,它会返回一个 CTreeItem 而不是 HTREEITEM。然后您可以使用 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 );

CTreeItem 有一个与每个接受 HTREEITEMCTreeViewCtrl 方法对应的方法,就像 CWindow 包含与接受 HWND 的 API 对应的方法一样。请查看 ControlMania2 代码,它演示了 CTreeViewCtrlExCTreeItem 的更多方法。

CHyperLink

CHyperLink 是一个派生自 CWindowImpl 的类,它可以子类化一个静态文本控件并使其成为可点击的超链接。CHyperLink 自动处理链接的绘制(遵循用户的 IE 颜色偏好),并且还支持键盘导航。类 CHyperLinkImplCHyperLink 的基类,包含实现链接的所有代码,但除非您需要重写方法或消息处理程序,否则您可以坚持使用 CHyperLink 作为您的控件。

CHyperLink 控件的默认行为是在单击链接时在默认浏览器中启动 URL。如果子类化的静态控件具有 WS_TABSTOP 样式,您还可以使用 Tab 键将焦点移到控件,然后按空格键或回车键单击链接。当光标悬停在链接上时,CHyperLink 还会显示工具提示。默认情况下,CHyperLink 使用静态控件的文本作为 URL 和工具提示文本的默认值,但您可以如下所述通过方法调用更改这些属性。

在 WTL 7.1 中,CHyperLink 添加了许多功能;这些新功能通过扩展样式启用。样式及其用法在方法列表之后进行解释。

CHyperLink 方法

这些是您通常会使用的 CHyperLink 方法。还有其他用于计算控件大小、解析链接文本等的方法;您可以在 atlctrlx.h 中查看该类以查看完整列表。

CHyperLinkImpl ( DWORD dwExtendedStyle = HLINK_UNDERLINED )
CHyperLink()

CHyperLinkImpl 构造函数接受要应用于控件的扩展样式。CHyperLink 缺少匹配的构造函数,但您可以使用 SetHyperLinkExtendedStyle() 设置这些样式。

BOOL SubclassWindow(HWND hWnd)

SubclassWindow() 被重写以执行子类化,然后初始化类保留的内部数据。如果您通过 DDX_CONTROL 将超链接变量与静态控件关联,则会自动为您调用此函数,或者您可以自己调用它以手动子类化控件。

DWORD GetHyperLinkExtendedStyle()
DWORD SetHyperLinkExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)

获取或设置控件的扩展样式。您必须在调用 SubclassWindow()Create() 之前设置扩展样式,以便控件知道如何绘制文本。

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)

获取或设置当光标悬停在链接上时在工具提示中显示的文本。但是,这些方法只能用于具有 HLINK_COMMANDBUTTONHLINK_NOTIFYBUTTON 扩展样式的链接。有关工具提示的更多信息,请参阅下文。

这是 ControlMania2 对话框中“纯”超链接控件的外观

 [WTL hyperlink - 21K]

URL 通过 OnInitDialog() 中的此调用设置

    m_wndLink.SetHyperLink ( _T("https://codeproject.org.cn/") );

CHyperLink 扩展样式

新的 WTL 7.1 功能通过设置相应的扩展样式位来启用。样式是

HLINK_UNDERLINED
链接文本将带下划线。这是默认行为。
HLINK_NOTUNDERLINED
链接文本永远不会带下划线。
HLINK_UNDERLINEHOVER
链接文本仅当光标悬停在链接上时才带下划线。
HLINK_COMMANDBUTTON
当链接被点击时,控件会向控件的父窗口发送 WM_COMMAND 消息(通知码设置为 BN_CLICKED)。
HLINK_NOTIFYBUTTON
当链接被点击时,控件会向控件的父窗口发送 WM_NOTIFY 消息(通知码设置为 NM_CLICK)。
HLINK_USETAGS
控件只将 <a> 标签内的文本视为链接,其他文本正常绘制。
HLINK_USETAGSBOLD
HLINK_USETAGS 相同,但 <a> 标签内的文本以粗体绘制。当设置此样式时,下划线扩展样式将被忽略,并且链接文本永远不会带下划线。
HLINK_NOTOOLTIP
控件将不显示工具提示。

如果既未设置 HLINK_COMMANDBUTTON 也未设置 HLINK_NOTIFYBUTTON 样式,则 CHyperLink 对象在单击时调用其 Navigate() 方法。Navigate() 调用 ShellExecuteEx() 在默认浏览器中启动 URL。如果您想在单击链接时执行其他操作,请设置 HLINK_COMMANDBUTTONHLINK_NOTIFYBUTTON,然后处理控件发送的通知消息。

其他 CHyperLink 细节

您可以在静态控件上设置 SS_CENTERSS_RIGHT 样式,以使超链接文本居中或右对齐。但是,如果控件具有 HLINK_USETAGSHLINK_USETAGSBOLD 样式,则这些位将被忽略,文本始终左对齐。

如果您使用 CHyperLink 打开 URL(即,您没有设置 HLINK_COMMANDBUTTONHLINK_NOTIFYBUTTON),则无法使用 SetToolTipText() 更改工具提示文本。但是,您可以通过 CHyperLink 成员 m_tip 直接访问工具提示控件,并使用 AddTool() 设置文本

  m_wndLink.m_tip.AddTool ( m_wndLink, _T("Clickety!"), &m_wndLink.m_rcLink, 1 );

请注意,这里与 WTL 7.0 存在一个突破性更改:WTL 7.1 中 CHyperLink 使用工具 ID 1。在 WTL 7.0 中,ID 与窗口句柄相同,您可以使用 m_tip.UpdateTipText() 更改文本。我在 WTL 7.1 中使用 UpdateTipText() 没有任何成功;上面的代码复制了 CHyperLink::Init() 最初设置工具提示的操作。

由于一些绘制问题,HLINK_USETAGSHLINK_USETAGSBOLD 样式最好在链接文本始终在一行上时使用。绘制代码会查找 <a> 标签内的文本,并将文本分成三部分:标签之前、标签之内、标签之后。但是,如果某一部分文本需要断字,它将错误地换行。我已经在 ControlMania2 的一个单独对话框中对此进行了说明

 [Link drawing problems - 13K]

您还应确保 HLINK_UNDERLINEHOVER 未与 HLINK_USETAGSBOLD 一起设置,因为那将导致链接文本后面出现一些空白,如上方的第一个超链接所示。

UI 更新对话框控件

对话框中的 UI 更新控件比 MFC 中容易得多。在 MFC 中,您需要了解未文档化的 WM_KICKIDLE 消息以及如何处理它和触发控件更新。在 WTL 中,没有这样的技巧,尽管 AppWizard 中有一个 bug 需要您添加一行代码。

首先要记住的是,对话框必须是无模式的。这是必要的,因为 CUpdateUI 要完成其工作,您的应用程序需要控制消息循环。如果您将对话框设置为模式,系统会处理消息循环,因此空闲处理程序不会被调用。由于 CUpdateUI 在空闲时执行其工作,因此没有空闲处理意味着没有 UI 更新。

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()
//...

};

请注意,CMainDlg 派生自 CUpdateUI 并具有一个更新 UI 映射。OnInitDialog() 包含以下代码,这在之前的框架窗口示例中应该很熟悉

    // register object for message filtering and idle updates

    CMessageLoop* pLoop = _Module.GetMessageLoop();
    ATLASSERT(pLoop != NULL);
    pLoop->AddMessageFilter(this);
    pLoop->AddIdleHandler(this);
 
    UIAddChildWindowContainer(m_hWnd);

这次,我们不是调用 UIAddToolbar()UIAddStatusBar(),而是调用 UIAddChildWindowContainer()。这告诉 CUpdateUI 我们的对话框包含需要更新的子窗口。如果您查看 OnIdle(),您可能会怀疑缺少某些东西

BOOL CMainDlg::OnIdle()
{
    return FALSE;
}

您可能期望此处有一个额外的 CUpdateUI 方法调用来执行实际更新,您说得对,应该有;AppWizard 遗漏了一行代码。您需要将以下行添加到 OnIdle()

BOOL CMainDlg::OnIdle()
{
    UIUpdateChildWindows();
    return FALSE;
}

为了演示 UI 更新,当您单击左侧位图按钮时,右侧按钮将被启用或禁用。因此,首先,我们使用标志 UPDUI_CHILDWINDOW 在更新 UI 映射中添加一个条目,以指示该条目适用于子窗口

    BEGIN_UPDATE_UI_MAP(CMainDlg)
        UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)
    END_UPDATE_UI_MAP()

然后,在左按钮的处理程序中,我们调用 UIEnable() 来切换另一个按钮的启用状态

void CMainDlg::OnAlysonODBtn ( UINT uCode, int nID, HWND hwndCtrl )
{
    UIEnable ( IDC_ALYSON_BMPBTN, !m_wndBmpBtn.IsWindowEnabled() );
}

DDV

WTL 的对话框数据验证 (DDV) 支持比 MFC 的更简单。在 MFC 中,您需要为 DDX(将数据传输到变量)和 DDV(验证数据)创建单独的宏。在 WTL 中,一个宏同时完成这两项工作。WTL 包含基本的 DDV 支持,使用 DDX 映射中的以下宏

DDX_TEXT_LEN
执行类似 DDX_TEXT 的 DDX,并验证字符串的长度(不包括空终止符)小于或等于指定的限制。
DDX_INT_RANGEDDX_UINT_RANGE
它们执行类似 DDX_INTDDX_UINT 的 DDX,并且它们验证数字是否在给定的最小值和最大值之间。
DDX_FLOAT_RANGE
执行类似 DDX_FLOAT 的 DDX,并验证数字是否在给定的最小值和最大值之间。
DDX_FLOAT_P_RANGE (WTL 7.1 新增)
执行类似 DDX_FLOAT_P 的 DDX,并验证数字是否在给定的最小值和最大值之间。

这些宏的参数与相应的非验证宏类似,另外带有一个或两个参数,指示可接受的范围。DDX_TEXT_LEN 接受一个参数,即允许的最大长度。其他宏接受两个额外的参数,指示允许的最小值和最大值。

ControlMania2 有一个 ID 为 IDC_FAV_SEASON 的编辑框,它与成员变量 m_nSeason 绑定。

 [Season selector edit box - 26K]

《巴菲》有七季,所以季的合法值为 1 到 7,DDV 宏如下所示

    BEGIN_DDX_MAP(CMainDlg)
    //...

        DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
    END_DDX_MAP()

OnOK() 调用 DoDataExchange() 来验证季节编号。m_nSeason 作为 DoDataExchange() 中完成的工作的一部分被填充。

处理 DDV 失败

如果控件的数据验证失败,CWinDataExchange 会调用可重写的函数 OnDataValidateError(),并且 DoDataExchange() 返回 falseOnDataValidateError() 的默认实现只是发出蜂鸣声,因此您可能希望提供更友好的错误指示。OnDataValidateError() 的原型是

void OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data );

_XData 是一个结构体,CWinDataExchange 用它填充有关输入数据和允许范围的详细信息。这是该结构体的定义

struct _XData
{
    _XDataType nDataType;
    union
    {
        _XTextData textData;
        _XIntData intData;
        _XFloatData floatData;
    };
};

nDataType 指示联合体中哪个成员有意义。其可能的值是

enum _XDataType
{
    ddxDataNull = 0,
    ddxDataText = 1,
    ddxDataInt = 2,
    ddxDataFloat = 3,
    ddxDataDouble = 4
};

在我们的例子中,nDataType 将是 ddxDataInt,这意味着 _XData 中的 _XIntData 成员被填充。_XIntData 是一个简单的结构体

struct _XIntData
{
    long nVal;
    long nMin;
    long nMax;
};

我们的 OnDataValidateError() 重写会显示一条错误消息,告诉用户允许的范围是什么

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 以查看 _XData 结构体中的其他数据类型 - _XTextData_XFloatData

调整对话框大小

WTL 最早引起我注意的一点是它对可调整大小对话框的内置支持。不久前,我撰写了一篇关于这个主题的文章,请参阅该文章了解更多详情。总而言之,您将 CDialogResize 类添加到对话框的继承列表中,在 OnInitDialog() 中调用 DlgResize_Init(),然后将消息链式传递给 CDialogResize

接下来

在下一篇文章中,我们将探讨在对话框中托管 ActiveX 控件以及如何处理控件触发的事件。

参考文献

使用 WTL 内置的对话框调整大小类 - Michael Dunn

在 WTL 中使用 DDX 和 DDV - Less Wright

版权和许可

本文为版权所有材料,(c)2003-2005 Michael Dunn。我知道这并不能阻止人们在网络上复制它,但我还是要说。如果您有兴趣翻译本文,请给我发电子邮件告知我。我预计不会拒绝任何人翻译的许可,我只是想知道翻译情况,以便在此处发布链接。

本文随附的演示代码已发布到公共领域。我以这种方式发布它,以便代码能够惠及所有人。(我没有将文章本身发布到公共领域,因为文章仅在 CodeProject 上提供有助于提高我的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,请给我发一封电子邮件告知我(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),但这不是必需的。在您自己的源代码中注明出处也值得赞赏,但不是必需的。

修订历史

2003 年 4 月 28 日:文章首次发布。
2005 年 12 月 31 日:更新以涵盖 WTL 7.1 中的更改。

系列导航:« 第四部分(对话框和控件) | » 第六部分(托管 ActiveX 控件)

© . All rights reserved.