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

避免 GetDlgItem

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (21投票s)

2000年5月17日

CPOL
viewsIcon

478346

了解如何避免使用 GetDlgItem。


引言与概述

当你学习用 C 语言编写 Windows 程序时,你学习了 GetDlgItem。这是在处理对话框时获取控件句柄的方法。在 C 语言中,这是获取控件窗口句柄的唯一方法。在 C++/MFC 中,有更好的方法。使用 GetDlgItem 在 C++/MFC 中就像用汇编语言编写 Windows 程序一样没有意义。是的,我知道有一些受虐狂喜欢用汇编语言编写 Windows 程序。但很少有情况是真正 *需要* 的。也许在 DSP 算法的内循环中,你可以嵌入一些花哨的 Pentium III 指令,但除此之外,这是疯狂的。GetDlgItem 也是如此。实际上,使用原始的 GetDlgItem 比使用汇编代码的理由稍微多一点,但这些理由代表了罕见且非常不常见的情况。

我的观点是,如果你一年写一个以上的 GetDlgItem,你可能没有正确使用 C++/MFC。幸运的是,有一种更好、更优雅、更安全的方法来通过 MFC 访问控件。

请注意,你应该几乎从不使用对话框中的 UpdateData。如果你使用它,你应该只在无模式对话框中使用它。据我所知,在模态对话框中使用 UpdateData 绝对 *没有* 理由。我在 另一篇文章 中详细讨论了这个问题。但这里介绍的技术对于避免使用 UpdateData 也很重要。一个简单的规则:如果你在模态对话框中调用 UpdateData,你就没有正确使用 MFC。


创建控件变量

要避免使用 GetDlgItem,你必须创建一个 *控件变量* 来代表你的控件。你可以为任何 ID 不是 IDC_STATIC 的控件创建控件变量。由于一种似乎是不可救药的疯狂,你需要经历一个不幸的复杂步骤(下面会讨论)来为一组单选按钮(除了第一个)创建控件变量;我无法理解为什么这被认为是好事,但微软似乎对如何使用单选按钮有特殊的想法。 

要创建控件变量,你需要激活 ClassWizard,选择“成员变量”选项卡,在列表框中高亮显示你想要变量的控件,然后单击“添加变量”按钮。你将看到一个窗口,与下图(已填写)非常相似。

在“成员变量名”窗口中,输入你的成员变量名。窗口最初预填充了“m_”。我发现这立即令人困惑。对于某些控件,你可以同时拥有一个 *控件* 变量和一个 *值* 变量。我使用“m_”作为值变量(这是一种我很少使用的功能),并使用“c_”作为控件变量的前缀。相信我:如果你开始使用 m_ 来表示控件变量,你很快就会后悔。我经历过,我做过。这就是为什么我有了一个新的约定。它有效。其他约定,请参阅 下面的 讨论。


转到“类别”窗口。对于某些控件,你只能有一个“控件”变量,这是唯一的选项。对于其他控件,例如编辑控件,你可以有一个“控件”或“值”变量。默认是“值”,所以你必须像我在图中那样选择“控件”作为类别。选择了“控件”变量后,你就可以选择类型了。如果你有一个直接从基类派生的类,就像我从 CEdit 派生了 CNumericEdit 一样,ClassWizard 会识别它并将其作为“变量类型”窗口中的选项之一提供。由于 c_Count 是一个表示计数的编辑控件,所以我选择了我的 CNumericEdit 类作为变量类型。

不幸的是,如果你的类是从你派生的一个类派生的,而这个类又从基类派生,ClassWizard 就无法处理(ClassWizard 无法处理许多你希望做的常见事情……)。在这种情况下,只需选择基类类型,稍后需要手动编辑。

这会给你一个所选类型的变量。如果你查看你的 .h 文件,你会发现已经添加了一个声明。

   CNumericEdit c_Count;

你有责任确保在对话框头文件之前包含适当的头文件(在此例中是 NumericEdit.h),以便用户定义的控件类型是已知的。

如果你(像我通常那样)有多个派生级别才能获得有用的控件(例如,我有一个花哨的 ComboBox 类 CSmartCombo,还有一个显示带图标的选项的类 CImageCombo,它派生自 CSmartCombo,而 CSmartCombo 又派生自 CComboBox),你无法指定这个类。如果我想要一个 CImageCombo 控件,我只需要告诉它 CComboBox 是变量类型,然后我手动修改头文件。很傻,但 ClassWizard 并不是一个你愿意让它在有扫帚在旁边时去搬水的水巫……

完成此操作后,你还会注意到另一个特性:在 .cpp 文件中,一直有一个函数,现在它有了一行。

void CMyClass::DoDataExchange(CDataExchange * pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CMyClass)
    DDX_Control(pDX, IDC_COUNT, c_Count);
    //}}AFX_DATA_MAP
}

与大多数 //{{ //}} 风格的 ClassWizard 注释一样,你不应该自己修改分隔块的内容,除非你愿意承担后果。DDX_Control 行的作用是将控件的 HWND 映射到变量,在这种情况下,IDC_COUNT 被映射到 c_Count。是的,它是在底层使用 GetDlgItem 来完成的。

在你的 OnInitDialog 处理程序调用 CDialog::OnInitDialog 之后,这些变量就可以使用了。因此,而不是写:

CButton * button = (CButton *)GetDlgItem(IDC_BUTTON);
if(button->GetCheck( ) == BST_CHECKED) ...

你可以写:

if(c_Button.GetCheck( ) == BST_CHECKED) ...

在我的例子中,我可以写:

if(c_Count.GetWindowInt() == 0) ...

因为 GetWindowInt 是我的 CNumericEdit 类的成员函数。我甚至可以写:

c_Count.SetWindowText(37);

因为我在我的类中重载了 SetWindowText,使其可以接受整数值。

这代表了拥有数百个 GetDlgItem 转换所带来的问题之一:如果你子类化了一个控件怎么办?你必须找到所有使用该控件的 GetDlgItem 并更改转换。例如,假设你覆盖了 CListBox 派生类的 AddString 来重新计算水平范围并调用 SetHorizontalExtent,并因此也覆盖了 ResetContent 来将水平范围设置为 0。如果你然后将一个普通的 CListBox 变量更改为你的新 CHorzListBox 类,你必须找到所有转换为 CListBox 的地方,并只更改那些适用于你的新 CHorzListBox 类的转换。很丑陋,不是吗?而如果你使用控件变量,你只需更改声明中的变量类型,所有重载和继承都会正常工作。好多了。这才是 C++ 应有的使用方式。


陷阱与注意事项

这并非没有问题。使用 GetDlgItem 时,其中一些问题已经存在,而另一些则是 ClassWizard 有限的世界观造成的。

我们已经提到 ClassWizard 无法处理多于一个派生级别。这很傻,但我自从 ClassWizard 在 16 位 MFC 中引入以来就一直在抱怨,但没有人理会。

单选按钮变量

另一个内在问题,代表了微软某种奇怪的哲学观点,是你不能为单选按钮创建控件变量。这没有意义。他们有一种奇怪的想法,认为你操作单选按钮的唯一方法是通过索引。这完全不够。因此,你必须通过一些严重的扭曲来为你的单选按钮获取控件变量。

你必须做的第一件事是回去将 *所有* 单选按钮标记为具有 WS_GROUP 样式。只有具有 WS_GROUP 样式的单选按钮才能拥有控件变量。然而,如果你将所有单选按钮都标记为 WS_GROUP,创建控件变量,然后移除 WS_GROUP 属性,一切都能正常工作,谢谢。为什么我们必须经历这些额外的步骤完全没有意义,但就像派生类问题一样,我多年来一直为此抱怨而没有效果。我的问题是我总是忘记回去撤销所有 WS_GROUP 属性,所以当我第一次运行程序时,我发现我所有的单选按钮都变成了一个按钮组。糟糕。$#%! 修复并重新编译/重新链接。

未初始化的控件

这个问题实际上已经存在了,你可能已经遇到过。但对于那些没有遇到过的人来说,可能会发生什么。

在对话框创建期间或 DDX_Control 初始化期间,控件可能会生成消息。这些消息通常有处理程序,它们希望假设控件已存在。它们可能不存在。你对 DDX_Control 初始化顺序没有真正的控制权。考虑两个简单的例子。

示例 1:调整控件大小

你可以创建一个带有调整大小边框的对话框。使用这个调整大小边框,你可以拖动对话框边缘来调整它的大小。一个典型的用法可能是如果你在对话框中有一个列表框,最方便的位置是在对话框底部。当你水平或垂直拉伸对话框时,你希望拉伸列表框,使其充满整个对话框。看起来很简单。

void CMyDialog::OnSize(UINT nType, int cx, int cy)
{
    CDialog::OnSize(nType, cx, cy);
    CRect r;
    GetClientRect(&r);
    CRect lb;
    c_ListBox.GetWindowRect(&lb);
    ScreenToClient(&lb);
    c_ListBox.SetWindowPos(NULL, 0, 0, r.Width(), r.Height() - lb.top,
                           SWP_NOMOVE | SWP_NOZORDER);
}

不幸的是,OnSize 例程可以被非常早地调用;WM_SIZE 是创建窗口时发送的前五个消息之一。这意味着在创建对话框时,会调用 OnSize 处理程序,*但此时还没有创建任何控件!* 结果是 c_ListBox.GetWindowRect 在 Windows 深处导致了一个访问冲突。丑陋。

示例 2:带自动伙伴的旋转控件。

如果你有一个 CSpinCtrl,并且它的“自动伙伴”属性已设置,这意味着它将在其值更改时设置其伙伴控件(例如编辑控件)的值。这也意味着在创建旋转控件时,它会将该控件设置为 0。这会生成一个 WM_CHANGEWM_UPDATE 序列给编辑控件。如果你正在监听此事件,并想根据更改来更改其他控件的状态,如果你尝试访问其他控件,而该控件尚未定义,则会导致访问冲突。

这两个问题都可以通过向对话框类添加一个额外的变量来解决。添加一个保护变量“BOOL initialized”。在对话框类的构造函数中,将“initialized”设置为 FALSE。(注意,我 *不* 对不是公共的、由 DoModal 调用者设置或读取的类变量使用 m_ 前缀。我编程多年来,并不认为这个约定有用。而且,我永远不会在我定义的任何变量中使用匈牙利命名法,无论在哪里,任何时候。但这又是另一个话题)。你 *必须* 在构造函数中设置它,*而不是* 在 OnInitDialog 中设置,因为当你到达 OnInitDialog 中的赋值时,已经太晚了。为了防止访问无效变量的可能性,你只需检查 initialized 变量。例如,

void CMyDialog::OnSize(UINT nType, UINT cx, UINT cy)
{
    CDialog::OnSize(nType, cx, cy);
    if(!initialized)
        return;
    // ... whatever else you want to do
}

命名约定

微软对命名约定有一些奇怪的想法。匈牙利命名法是在 C 语言还没有原型,因此也没有跨模块检查的时代发明的。在 C 语言还没有真正成为一门编程语言的时代,它可能还有些用处。现在它毫无价值,应该避免。我发现用于成员变量的 m_ 约定同样疯狂。我只在有限的情况下使用它,特别是在与控件关联的值上。我只需要为从外部传入对话框的值,或者返回给调用者的控件的值变量。由于我很少这样做,我的代码中 m_ 变量的出现率非常非常低。

我还有其他约定。例如,我给 *控件* 变量加上“c_”前缀。这表示它是一个控件变量。使用不同于“m_”的约定是必要的,因为某些控件可以同时拥有控件变量和值变量。此外,为某些控件(如编辑控件)的标题命名通常很有用。如果你需要启用/禁用这些控件,这很有用。考虑一个编辑控件 c_Text,它有一个静态控件作为标题。我使用“x_”前缀来表示标题。这给世界带来了极大的理智。如果我想通过传递值来预加载文本控件,我会创建一个值变量 m_Text。要访问控件,我创建一个控件变量 c_Text。要访问其标题,我创建一个控件 x_Text。我 *从不* 在对话框内访问 m_ 变量,除非我提供了一个“重置到初始状态”按钮,将控件重置为其初始值。没有必要访问值变量。如果访问,我只读取它。

这意味着 DDX 调用看起来会是这样的:

DDX_Control(pDX, IDC_TEXT, c_Text);
DDX_Text(pDX, IDC_TEXT, m_Text);
DDX_Control(pDX, IDC_TEXT_CAPTION, x_Text);

然后,要启用或禁用文本控件及其对应的标题,你可以这样写:

void CMyDialog::updateControls( )
{
     BOOL enable;
     enable = ...
     c_Text.EnableWindow(enable);
     x_Text.EnableWindow(enable);
}

看这有多清晰。易于编写,易于理解,易于维护。那个 updateControls 方法是什么?那是 另一篇文章


摘要

GetDlgItem 方法在编写 MFC 代码中的实用性有限。如果你使用它,你很有可能没有正确使用 MFC。当然也有例外,我在 关于对话框控件管理的文章 中展示了一个。但这种情况很少见。一个编写良好的 MFC 应用程序不会有任何不必要的 GetDlgItem 实例,也不会在任何模态对话框中使用 UpdateData,永远。


这些文章中表达的观点是作者的观点,不代表,也不被微软认可。

发送邮件至newcomer@flounder.com提出关于本文的问题或评论。
版权所有 © 1999CompanyLongName保留所有权利。
www.flounder.com/mvp_tips.htm
© . All rights reserved.