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

GUI 属于谁?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (11投票s)

2000年5月17日

viewsIcon

71677

了解 GUI 定义的所有者以及 GUI 编程的陷阱

引言

程序员经常会遇到一个问题:“谁拥有GUI?”对此有几种答案,它们的确定性程度各不相同,但在Windows编程十年之后,我坚信有一条简单、至关重要的规则你必须遵守:你,程序员,并不拥有GUI。任何基于违反这条简单规则的决策,最终都会导致痛苦、混乱和无法维护的代码。本文探讨了GUI究竟属于谁,以及你如何保护自己免受他们的影响。

GUI的一个所有者是微软。GUI的设计指南规定了菜单和工具栏图标的某些属性。如果你的程序中的一个图标看起来像这样:那么它最好是与打开文件操作相关联,而不是与粘贴操作相关联。最左边的菜单应称为File(文件)。最右边的菜单应称为Help(帮助)。在MDI应用程序中,倒数第二个菜单应称为Window(窗口)。任何可编辑的应用程序中,第二个最左边的项目应为Edit(编辑)。在此之间,你有一些协商的余地,但如果你交付的产品将Edit放在右边,而将Help放在左边,你的用户将非常恼火。如果你将Exit(退出)放在Edit菜单上,你会显得很愚蠢。

所以,微软拥有GUI。

但又不完全是。我是一名顾问。对我来说,客户拥有GUI。当他们提出一个违反微软标准的建议时,我必须警告他们,但最终我可能不得不照他们说的做。当他们做一些非常愚蠢的事情时,我可能比仅仅涉及品味问题时更加坚持己见。当他们要求一些不易实现的东西,例如在对话框上有512个复选框时,我必须解释为什么这行不通,并提出一个可行的解决方案。

所以,客户拥有GUI。

当然,19世纪的编程理论家卡尔·冯·克劳塞维茨(Carl von Clausewitz,1780-1831)曾精辟地说:“没有哪个GUI设计能经受住与用户的首次接触”(我可能记错了一些细节,但它包含了所有关键思想)。你可能交付了一个GUI,但如果用户不喜欢它,你将不得不进行更改。作为一名顾问,我通过一个间接的层次来处理这个问题;最终用户向我的客户抱怨,客户向我抱怨,然后我们进行更改。(有时,当客户坚持按他们的方式做,而我告诉他们什么可行什么不可行时,我会得到证明——最终用户会准确地告诉他们对那个想法的看法。)

所以,最终用户拥有GUI。

但这还不是全部。我曾不得不根据竞争对手的菜单布局进行更改。当你试图进入那个市场并想吸引现有客户群时,如果你的GUI不会给最终用户带来学习成本,你可能会更有效。

你可以从两个方面理解这一点:营销部门拥有GUI,或者竞争对手拥有GUI。

不拥有GUI。

事实上,即使是你自己写的程序,你也不拥有它!随着你对程序的了解的深入,你对如何正确做事的看法也会不断演变。作为你自己程序的最终用户,你对GUI的所有权比作为同一应用程序的程序员要多。仅仅因为你是同一个人,也不能让你免于遵守第一条规则。

你拥有GUI的错觉会让你走上一条通往不可维护代码的弯路。情况可能会更糟:它会使持久化状态不仅变得没有意义,而且明显不正确。这意味着你保存在注册表、数据文件或其他地方的任何值,都不得以任何方式与GUI耦合。

映射编码

不要使用巧妙的编码是第一条规则。如果你想在持久化状态中存储黑色,就存储为RGB(0,0,0)。这是语言无关的。如果你能将位向量存储在DWORD中,那是最好的存储方式。存储一个对程序有意义的整数,特别是对于枚举表示。当你需要将其传输到GUI时,使用特定于GUI表示的映射。

当你要国际化时,问题会更加复杂。例如,举一个简单的例子,你有一个颜色下拉列表。黑色、绿色、红色和黄色。或者德语的Schwartz、Grün、Rot和Gelb。如果要求列表按颜色的字母顺序排列呢?你不能将列表中条目的偏移量与颜色值耦合。根本不要尝试。

假设客户突然想添加蓝色和橙色。如果你将黑色编码为持久化状态中的0,而突然偏移量0的颜色变成了1或7,你到底编码了什么颜色?如果你将内部表示与GUI基于完全偶然的细节(例如本周你选择了哪一类控件来表示它)以及该控件如何编码该值的琐碎偶然细节耦合,那你将陷入极大的麻烦。当然,你不会知道,直到你不得不实际更改控件,或控件的值集,或进行国际化,但相信我,你已经陷入麻烦了。这只是一个问题,即你何时发现自己陷入麻烦,以及你会发现自己陷入多大的麻烦。

列表框、ListCtrl和组合框问题

编程技术中的一个常见缺陷是将列表框或组合框索引的整数值与某种解释联系起来,而这种解释并非存储在列表框或组合框特定位置的完全偶然属性。一旦你这样做,你就失去了可维护性。

一个常见的解决方案是这样做:

static COLORREF colors[] = {
    RGB(0, 0, 0),
    RGB(255, 0, 0),
    RGB(0, 255, 0),
    RGB(0, 0, 255),
    ...
};

这样你就可以(例如,从资源编辑器中)预加载组合框,其中包含“黑色”、“红色”、“绿色”和“蓝色”。这太愚蠢了。为什么程序中的一组strings会必然与程序中其他地方的某个硬编码表具有一对一的位置对应关系,这一点只有微弱可理解。此外,这需要任何更改值顺序(例如在资源编辑器中)的人都必须知道与表的对应关系。我发现处理这种情况只有一种合理的方法,这种方法使其不受控件排序(或未排序)方式、值数量和文本表示的影响。我在我的关于组合框初始化的论文中讨论了这一点,所以在此不详细赘述,只总结设计问题。

如果你想将值与索引解耦,这意味着你必须弄清楚哪个组合框或列表框条目对应于特定的颜色。幸运的是,这很简单。你将颜色值附加到列表框或组合框的ItemData组件,并且你不使用SetCurSel(n)(对于n,即持久化状态中存储的偏移值)或SelectStringExact(name)(用于文本名称),而是简单地枚举控件的所有成员,并将每个ItemData与所需值进行比较。由于我们使用的是C++,创建一个为你执行此操作的子类非常简单。我甚至在我的CIDCombo中做到了这一点。

绝不、永远、在任何你能想象到的条件下,都不应该将持久化存储中的值与像GUI布局这样短暂的东西的任何偶然特性耦合。即使是对于程序内部维护的瞬态数据,也很少会考虑这样做,而且将对话框控件的表示与程序的任何其他组件耦合是完全没有意义的。为信息创建一个规范表示。当你需要将其呈现给用户时,在那个点(也仅在那一点)将其映射到GUI。当用户在GUI中指定值时,将其映射回规范表示。请注意,微软提供的映射通常相当弱。例如,对于对话框中的列表框和组合框,使用MFC,唯一的映射是从整数值到控件的索引。为什么任何应用程序都应该知道或关心某个值在特定控件中的表示,这让我费解。

你可以通过编写自己的DDX处理程序来绕过这个问题。我发现,在OnInitDialog处理程序中进行初始化同样高效。编写一两行额外的代码来创建一个高度可维护的程序,是我乐于进行的活动。

复选框技术

复选框经常会出现问题,尽管DDX对于将BOOL类型的值从应用程序翻译到对话框通常很有用。并非所有你想做的事情都必然由一个简单的BOOL值表示。通常,从应用程序角度来看最合理的表示是一个WORDDWORD中的一组位。虽然你可以为此编写自定义DDX,但这只能帮助你进入和退出对话框。你仍然需要一种方法来在对话框内操作值。不,不要在你的对话框中使用UpdateData。那样会让你发疯,我有一个关于那个主题的另一篇文章

例如,我有时有一个值,它被编码为一个位图,并反映到GUI中作为复选框。我编写两个映射函数:bitsToControlscontrolsToBits。如果你读过我关于不使用GetDlgItem的文章,你可能还记得我提出的原则:“如果你一年写两个以上的GetDlgItem,你可能没有正确使用MFC”。bitsToControlscontrolsToBits函数就是使用GetDlgItem有意义的那种函数,事实上,我每年写一次这样的函数。

typedef struct {
    UINT ctl;
    DWORD value;
} controlMap;

#define MASK_ACTIVE    0x80000000
#define MASK_REVERSE   0x00000001
#define MASK_AUTO      0x00000002
#define MASK_MALLEABLE 0x00008000

static controlMap mapping[] = {
    { IDC_ACTIVE, MASK_ACTIVE},
    { IDC_REVERSE, MASK_REVERSE},
    { IDC_AUTO, MASK_AUTO},
    { IDC_MALLEABLE, MASK_MALLEABLE},
    { 0, 0} // End of table
};

bitsToControlscontrolToBits的实现现在应该很明显了。

void CMyDialog::bitsToControls(DWORD bits)
{
    for(int i = 0; mapping[i].ctl != 0; i++)
    {
        CButton * button = (CButton *)GetDlgItem(mapping[i].ctl);
        button->SetCheck( bits & mapping[i].value ? BST_CHECKED : BST_UNCHECKED);
    }
}

DWORD CMyDialog::controlsToBits()
{
    DWORD result = 0;
    for(int i = 0; mapping[i].ctl != 0; i++)
    {
        CButton * button = (CButton *)GetDlgItem(mapping[i].ctl);
        if(button->GetCheck() == BST_CHECKED)
            result |= mapping[i].value;
    }
    return result;
}

好的,讲究一点——这里有两个GetDlgItem的实例,所以如果我想遵守自己的规则,我应该每两年只写一次这段代码。让我们不要走得太远……

因此,优点是,如果我必须将一个选项更改为一组互斥的单选按钮并将其编码为多个位,我可以自由地这样做。我不必依赖外部表示以任何方式符合GUI表示。在你的程序中使用对你的程序有意义的表示。在持久化存储中使用对持久化存储有意义的表示。并在需要时将其映射到GUI表示并从GUI表示映射回来。因此,这两个函数将构成你的自定义DDX实现的一部分。

单选按钮问题

当你使用微软的工具时,单选按钮是一个特别令人讨厌的对象。出于我无法理解的原因,他们认为你应该用整数来表示单选按钮的值。你在程序中看到的整数直接映射到对话框上的单选按钮组的偏移量。我无法理解这有什么意义。例如,假设我有一组选项:Big(大)、Medium(中)和Small(小)。按照微软的方法,我有一个编码,即0、1或2。现在假设我发现没有人购买medium尺寸,所以我将其停产。按照微软的方法,我现在有两个整数,0和1,分别代表BigSmall。1现在是Small,而不是Medium。这是我几十年来见过的最愚蠢的想法(我从事这个行业已有35年了)。唯一明智且正确的方法是始终让Big代表一个常量,Small代表另一个常量。代表Medium的值根本不再是合法值。但如果你使用微软的方法,旧的“Medium”值现在变成了“Small”,而编码“Small”的旧值根本不合法。而且,这些值没有任何理由代表单选按钮组的偏移量;如果我添加了六种尺寸,我可能想在下拉列表中表示这些值,而且,如果我发现我最初只有BigSmall,并且想在它们之间添加Medium呢?按照微软的策略,我会发现我的值与GUI耦合会总是选择错误的尺寸!此外,值到控件的映射必须在对话框内部完成,而不是在对话框外部;对话框的抽象接口是持久化存储表示,而由对话框将其映射到其控件表示。任何理解模块化抽象的人都会立即明白这一点。

我处理这个问题的方式是创建一个enum,或者一组#defines来表示值。值集是面向应用程序的。这些值被分配的方式旨在代表一个对持久化存储稳定且对代码稳定的表示。如果我需要在对话框中使用它们,我会在OnInitDialog中进行映射。在这种情况下,忽略了股票DDX的“便利性”;它在这个目的上是完全无用的。它比无用更糟。它对长期可维护性具有破坏性。在这个行业中,可维护性至关重要。事实上,股票DDX绝不应该用于处理列表框、组合框和单选按钮。它将GUI表示与应用程序表示耦合在一起,这是无法维护的。

快速编写一个不可维护的程序并没有什么优势。

虽然不广为人知,但DDX机制是可扩展的。如果你愿意,你可以编写自己的DDX方法来初始化列表框或组合框,并且这个自定义DDX将包含必要的映射。一个真正智能的自定义DDX将知道如何处理各种控件类型。

我处理单选按钮的方法是使用switch语句或解码表,将应用程序值映射到单选按钮控件ID。不是索引。是控件ID。索引意味着重新组织控件(添加或删除单选按钮)将保持索引的不变性,这是不可能的。它甚至允许模式迁移;如果你有一个无效值,你可以采取具体行动来处理它。

UINT id = IDC_WHATEVER; // default value
switch(applicationValue)
{
case value1:
    id = IDC_VALUE1;
    break;
case value2:
    id = IDC_VALUE2;
    break;
case whatever:
    id = IDC_WHATEVER;
    break;
}
CheckRadioButton(IDC_FIRST, IDC_LAST, id);

这有一个严重的缺点,就是你必须知道组中第一个控件的ID和组中最后一个控件的ID,所以如果你以影响组的第一个和最后一个值的方式更改单选按钮,你必须更改CheckRadioButton调用。这需要多一点工作,特别是如果你使用表而不是switch语句,但这也不会太困难。

微软方法的另一个严重缺点是,他们只允许你为一个组的第一个单选按钮(具有WS_GROUP标志的单选按钮)创建一个控件变量。你无法查询单个按钮的值。我觉得这也同样愚蠢。解决方法很繁琐,但很直接。

首先,在组中的每个单选按钮上设置WS_GROUP标志。然后转到ClassWizard,选择Member Variables选项卡,然后为每个单选按钮创建一个Control变量,例如(请注意,我对此使用了不同的前缀:c_表示“控件”,而“m_”表示“作为调用者传入的参数的变量”)

CButton c_Value1;
CButton c_Value2;
CButton c_Whatever;

然后,你可以回到对话框编辑器,并从除第一个单选按钮外的所有单选按钮中删除WS_GROUP标志(我总是忘记执行这一步,直到我运行对话框并发现我的单选按钮不是不相交的!)。

现在,如果你使用了我关于对话框控件管理的技术,你可以编写约束方程,例如

BOOL enable = c_Whatever.GetCheck() == BST_CHECKED;
c_WhateverText.EnableWindow(enable);

(这使得单选按钮右侧的文本输入框在单选按钮被选中时启用)。

摘要

我希望我已经指出了在MFC关于控件管理的内置机制上过度依赖的一些重大问题。最重要的是,我希望我已经清楚地说明了你,程序员,对GUI拥有最少的权利,并展示了你可以使用的技术,以有效地将GUI与面向应用程序的表示解耦。


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

你可以在下方留言,提出关于本文的问题或评论。

版权所有 © 1999 The Joseph M. Newcomer Co. 保留所有权利。
www.flounder.com/mvp_tips.htm

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.