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

设计 Windows 控件 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (53投票s)

2002 年 10 月 24 日

Ms-RL

16分钟阅读

viewsIcon

254790

如何设计一个商业级质量的 Windows 控件。

引言

这是关于创建 Windows 控件的三个系列文章中的第一篇。第一篇文章(本文)将解释如何设计一个好的控件,第二篇将通过一个逐步创建新控件的过程深入讲解编码细节,第三篇将介绍控件的问答方面以及如何测试和验证 Windows 控件。

我们开始吧…

设计控件

我将尝试给大家一些关于如何设计更好控件的建议。我从事 GUI 开发已经将近 10 年了,我所有的写作都源于我自己的经验。

在 CodeProject 等网站上有很多不错的控件代码,但它们根本无法使用。乍一看,它们似乎能完美地集成到您开发的项目中,并且能满足您的所有需求。您下载它,稍作调整,然后将其集成到您的应用程序中,老板会很高兴您能按时完成项目。

不幸的是,在开发商业应用程序时,这很少是故事的结局。即使新控件通过了您的 QA 部门(您有 QA 部门,不是吗?),最终支持团队还是会报告问题。

自定义控件中最常见的问题是

  • 不完整甚至缺失键盘处理
  • 在更改显示设置后外观失真
  • 不符合用户 Windows 设置
  • 在更改字体大小设置后尺寸不正确

这些情况确实会发生,并且会发生在您的客户身上。开发人员最大的错误是在开发人员的计算机上测试应用程序。永远不要这样做。拿秘书的电脑、您阿姨的旧 486,或者甚至安装一个没有最新服务包的干净操作系统,然后选择不同的显示方案。另外,还要测试其他操作系统语言——记住,办公室外面还有一个世界。

提示:大多数开发人员没有资源在各种计算机上干净地安装(clean-install)各种操作系统,有一个简单但高效的解决方案:虚拟机软件,如 VM-Ware(http://www.vmware.com [^])。它们价格合理,非常适合开发人员的测试环境。

这些错误源于(几乎)所有这些控件都是为特定目的开发的。仅此而已。很少有程序员会付出额外的努力来完成设计并交付一个能够通过所有测试的完全功能的控件。即使是许多商业 GUI 库在这方面也做得不好。

关于控件和控件设计的进一步阅读

http://msdn.microsoft.com/library/en-us/shellcc/platform/commctls/common/user.asp

http://msdn.microsoft.com/nhp/default.asp?contentid=28000443

明智地选择您的武器

当您打算编写自己的控件时,请在编写任何代码之前停下来,弄清楚您是否真的需要创建一个自定义控件,或者它是否可以用标准的 Windows 控件或它们的组合来解决。

Windows 提供了丰富的控件,几乎可以满足任何用途,并且它们 99% 没有错误。您的自定义控件只有经过大量的使用、测试和调试才能达到这个水平。通常需要更改设计才能使用标准控件实现所需的结果,但这通常是值得的。您可以获得稳定性和符合标准。您的用户不需要学习如何使用您的新控件;他们熟悉 Windows 控件。如果您认为您的用户必须有一个带有不同背景颜色和不同字体的按钮,请出去问问您的用户。他们可能喜欢色彩鲜艳的外观,但如果您让他们面对一个标准的解决方案,他们会同意标准解决方案更易于使用。

记住:可用性和定制性很少兼得。

好的,所以您决定必须拥有一个非同寻常的什么什么控件?没问题,让我们继续,找出如何正确地做。

弄清楚您应用程序的平均用户的目标受众是谁。这决定了您如何表示数据,如何标记它,它必须是什么样的。

让我们举一个(糟糕的)例子:颜色选择器。大多数颜色选择器控件提供各种方法来选择或操作颜色。一种表示方法是 HSB/HSL 模型。在这个模型(以及其他模型)中,颜色由三个数值表示,称为 H、S 和 B(色相、饱和度和亮度)。现在问问自己,有多少普通家庭用户知道这三个字母的意思?

 看这张图。这张截图来自一款广泛使用的文本处理应用程序。“Sat:”是一个有意义的标签吗?(顺便说一句,在这个特定的应用程序中,有足够的空间写出完整的单词而不是缩写。)

如果开发人员在编写代码之前多思考一下,他们可能会想出更好、更直观的解决方案。

好的,回到我们的设计。既然您了解了您的受众,就可以清楚地决定表示方式。请始终牢记,普通用户不想要一个酷炫的控件,而是想要一个易于处理、无需按 F1 或阅读手册就能理解的控件。如果它满足了这些要求,并且仍然外观酷炫,那么您离老板给你加薪又近了一步。

关于键盘的一点说明:像 Windows 一样处理按键——上就是上,下就是下。如果您使用 Enter 键来选择一个项目,您的用户将无法理解。MSDN 有关于按键及其建议用法的很好列表。

在开始编码之前,请制作一个控件的模型。打开您喜欢的绘图程序,草绘控件。将位图放在您的应用程序中。看看它的外观,问问别人有什么看法。他们明白它是什么吗?他们会如何使用它?

如果您的模型通过了这个简单的测试,您几乎就可以开始编码了。

“船长,向前看”

现在是时候考虑控件的使用及其导航了。在大多数情况下,您凭直觉就知道如何用鼠标使用它。但是,键盘呢?许多用户(包括我)喜欢使用键盘进行导航,仅仅因为它更快,仅仅因为您需要在附近的字段中输入内容,或者仅仅因为计算机没有连接鼠标。

键盘导航总是包括显示焦点。焦点是用户正在与之交互的控件部分的视觉反馈。这是用户唯一了解他们正在做什么的线索。再次查看 Windows 控件,看看焦点是如何显示的。按钮显示一个虚线矩形并改变边框,编辑控件显示插入位置的插入符,菜单显示一个彩色的选择。模仿这种行为。普通用户已经习惯了。

我想向您展示一个 Windows 控件中的小缺陷,这个缺陷几乎让所有(键盘)用户都感到困惑,并且看起来像是一个“被遗忘”的功能:ListView 控件中的焦点矩形。当它包含项目但没有项目被选中,并且用户通过 Tab 键导航到列表时,列表确实获得了焦点,但它没有显示焦点矩形。对用户来说,这看起来就像没有任何控件获得焦点。一个简单的解决方案是拦截 WM_SETFOCUS 消息,并在没有项目被选中或聚焦时,将 LVIS_FOCUS 状态设置为第一个列表项。

如果您第一次做不好,别担心——即使是微软也不总是做对。(MS:我们应该如何使用 Word XP 文件打开对话框中“打开”按钮的下拉功能?)始终征求他人对您的控件的意见,并询问他们是否理解。观察他们使用它。

不要忘记,如果您的控件或应用程序失去焦点,您必须移除焦点。有一些窗口消息您应该处理,因为它们可能会改变焦点状态或样式。为了让事情更复杂,这些消息在不同的 Windows 版本上有所不同。

焦点管理意味着保留一些状态变量并保持其最新。这可能是控件逻辑中棘手的一部分。根据控件的复杂性,您甚至可能需要以多种形式显示焦点——当用户与文本交互时显示插入符,在按钮类型的区域上显示虚线,等等。

另一个重要的焦点视觉反馈——这次是鼠标焦点——是热跟踪。热跟踪意味着当用户将鼠标悬停在某个项目或区域上时,改变其视觉外观。一个很好的例子是 Internet Explorer 工具栏中的按钮。这里,搜索按钮正常状态和热跟踪状态。

当您实现此类功能时,请遵守已安装操作系统和 IE 版本的 Windows 设置和功能。这将使您的控件看起来更像“Windows”,因为它像所有其他标准控件一样运行和响应。

Windows API 函数 GetSystemMetrics()SystemParametersInfo() 是您最好的朋友。它们提供了您所需的所有信息。如果您将这些信息缓存到自己的变量中,您就必须处理 Windows 在用户更改设置时发送的更改通知。查看 MSDN 中的 WM_xxxCHANGED 通知。

妈,为什么它这么大?

您是在以像素为单位计算控件的大小吗?您是在特定位置绘图吗?别这么做。当您将控件放置在对话框上时,它以对话框单位进行测量。对话框单位根据显示设置进行缩放。当用户选择一个大默认字体时,对话框会变大,您的控件也会变大。试试看;在显示属性中选择“大字体”。您的控件现在看起来怎么样?

始终相对于计算大小和位置。如果您要显示简单的图标图形,请考虑使用 TrueType 字体。Windows 还使用字体来显示任何窗口上的符号。最小化、最大化、关闭图标以及滚动箭头只是自定义字体中的字母。Outlook 使用自定义字体来显示附件、优先级和其他符号。为什么?因为它会缩放。位图不会缩放。而且,输出文本比显示位图甚至 WMF 文件要容易十倍。

如果您创建与 Windows 项目相关的项,请模仿 Windows。使用 GetSystemMetrics() 来检索相应 Windows 元素的尺寸、维度或深度。例如,当您绘制 3D 元素时,可以通过调用 GetSystemMetrics(SM_CXEDGE)GetSystemMetrics(SM_CYEDGE) 来获取正确的大小。这确保了您的控件在所有配置和 Windows 版本中看起来都正确。

当您必须显示位图或图标时,请非常小心透明度和背景颜色。程序员常常忘记图标的背景色不是灰色的。您可以在许多 Windows 2000 之前的应用程序中注意到这一点,当时 Microsoft 使用不同的灰色作为对话框和窗口的背景。当您在更改颜色(或在 Windows 2000/XP 下)运行这些应用程序时,您会注意到图标具有不同的灰色。

因此,使背景颜色透明并正确设置透明颜色。通过更改显示设置中的 3D 对象颜色,您可以轻松检查是否设置正确。

永远不要假设颜色,因为用户会更改它。始终使用 GetSysColor()GetSysColorBrush() 来获取当前活动的颜色。我关于度量的说法也适用于颜色:它们可能随时更改。利用 WM_xxxCHANGED 通知。

HWND、WPARAM 和 LPARAM

您不能发布虚函数。

程序员将如何使用您的控件?您会回答:通过实现该控件的类。

不完全是正确的答案。我知道,这是常见且最方便的方式。但看看 Windows 控件——MFC 程序员常常忘记 Windows API 中没有名为 SetExtendedStyle() 的函数。这只是 MFC 类函数,它执行一个简单的 SendMessage() 并带有一些参数。

实现一个基于消息的接口而不是基于类的接口需要付出一些努力。但这有几个优点

  • 异步调用(使用 PostMessage
  • 对其进行子类化,并在没有源代码的情况下修改/扩展行为
  • 使用 Spy++ 或 Winspector[^] 等工具跟踪消息
  • 与其他编程语言和工具集成

让我们来看看传统的实现方式

class CMyControl
{
// ... some declarations here  
  void SetColor(RGB c) { m_color = c; Invalidate(); }
  RGB GetColor() { return m_color; }
private:  
  RGB m_color;
};

仅此而已。故事结束。虽然这是最简单、最快的实现方式,但也是最不灵活的。即使您将 SetColor/GetColor 函数声明为 virtual,您也得不到多少好处。基于消息的实现的所有优点都不在这里。

现在,用基于消息的实现来实现相同的目标

// in the main header file MyControl.h
#define MC_SETCOLOR     (MC_BASEMSG + 1)
#define MC_GETCOLOR     (MC_BASEMSG + 2)

 

// in the class definition file MyControl_impl.h
class CMyControlImpl
{
// ... some declarations here
  void SetColor(RGB c) { SendMessage(MC_SETCOLOR, 0, (LPARAM)c); }  
RGB GetColor() { return SendMessage(MC_GETCOLOR); }
   
// if this is using MFC we have here the message handler declarations
  afx_msg LRESULT OnMsgSetColor(UINT, WPARAM, LPARAM);
 
private:
  RGB m_color;
};

 

// in the class implementation file MyControl_impl.cpp
 
BEGIN_MESSAGE_MAP(CMyControlImpl, CWnd)
  //{{AFX_MSG_MAP(CMyControlImpl)  
  ON_MESSAGE(MC_SETCOLOR, OnMsgSetColor)
  ON_MESSAGE(MC_GETCOLOR, OnMsgGetColor)  
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()
 
LRESULT CMyControlImpl::OnMsgSetColor(UINT, WPARAM c, LPARAM)
{
  m_color = (RGB)c;
  Invalidate();
}
 
LRESULT CMyControlImpl::OnMsgGetColor(UINT, WPARAM, LPARAM)
{
  return (LRESULT) m_color;
}

这需要编写更多的代码。但是使用该控件的任何开发人员只需要第一个文件 MyControl.h,其中包含所有消息定义。如果您的控件很复杂,或者您有足够的时间和动力,那么您可以提供一个包装类和/或宏。

#define MyControl_SetColor (hwndMC, c) \
    (int)SNDMSG((hwndMC), MC_SETCOLOR, (WPARAM)(c), 0L)
#define MyControl_GetColor (hwndMC) \
    (int)SNDMSG((hwndMC), MC_GETCOLOR, 0, 0L)

class CMyControl
{
publicvoid SetColor(RGB c) { MyControl_SetColor(m_hWnd, c); }  
  RGB GetColor() { return MyControl_GetColor(m_hWnd); }
  }

这正是 MFC 为所有标准 Windows 控件所做的(其中还包含一些额外的错误检查)。看看 MFC 源文件中的 CEdit 类。学习他们是如何做的。另外,看看 VC include 目录中的 commctrl.h 文件。该文件包含所有 Windows 控件的定义。

使用这种方法,您可以获得前面提到的所有优势。由于所有定义和额外的头文件,这需要更多的工作。最终,如果您想创建高质量的商业控件,就无法避免这个额外的步骤。

如果您写过控件,您就会知道调试 GUI 代码的痛苦。通过基于消息的方法,事情会变得更容易——连接 Spy++ 或更好的 Winspector[^] 到您的控件,然后查看消息流。您可以轻松地发现任何发送错误的消息、任何越界的参数以及任何返回值。

最后一点可以增强您的控件:不要直接调用公开的函数。让我用我们的示例来演示这一点。

假设我们有一个名为 UpdateData() 的函数,该函数在某个时候需要设置颜色,因此它直接调用 OnMsgSetColor(),甚至更糟的是,直接修改 m_color 变量。如果您不跟踪到 UpdateData() 函数,您将永远无法弄清楚为什么控件会改变颜色。如果此函数发送一个 MC_SETCOLOR 消息,那么颜色为什么会改变就会显而易见。

当然,对于更复杂的函数,您可能需要定义通过指针传递给控件的结构。即使您需要传递超过 2 个参数,您也需要将它们挤进一个结构中,或者设法使用 short int 参数,并使用 MAKEWPARAM()MAKELPARAM() 宏传递它们。

谁会“打印”控件?

嗯,可能没有多少人这样做。我同意“打印”这个词对于这个目的来说不是最好的选择。像“渲染”这样的词可能更合适。

到现在为止,您可能已经意识到我正在谈论 WM_PRINTWM_PRINTCLIENT 消息。它们与普通的 WM_PAINT 消息没有太大区别,但它们可能在没有无效区域的情况下被调用。应用程序可以通过发送 WM_PRINTWM_PRINTCLIENT 消息,随时要求控件将自己绘制到给定的 DC 中。

实现这些消息只需要将您的绘图代码移到一个单独的函数中,该函数接受一个 DC 作为输入参数。WM_PRINTWM_PRINTCLIENT 之间的区别在于,前者应该渲染整个控件窗口(包括非客户端区域),而后者应该只渲染客户端区域。

实现这些消息对于支持一些较新的 Windows API 函数(如 AnimateWindow())是必需的。

终端服务感知

终端服务不再是一个时髦的词,而是许多公司和所有 Windows XP 用户的现实。它们在任何 Windows XP 安装中都默认可用,只是现在它们被称为远程桌面。Windows NT 4.0 终端服务和 Citrix 终端服务器也可用,可能还有其他一些名称。不要低估它在企业中的普及程度。这绝对是您想添加到 Windows 控件中的一项功能。

在您计划使您的控件支持 TS 时,有几件事需要考虑:绘图代码必须经过优化,并且鼠标交互应尽量少。通过检测 TS 会话并处理 WM_WTSESSION_CHANGE 消息,控件可以在终端会话中使用时最小化图形效果。诸如悬停之类的进阶功能可以禁用;备受欢迎的扁平控件,它会在激活时显示/隐藏边框,可能会恢复正常操作,等等。

通过仔细规划,可以在不破坏对 Win9x 系统的兼容性的情况下添加 TS 意识。

现代架构之类的东西

随着每个新的 Windows 版本,Microsoft 都会为现有控件添加一些新功能、新的窗口消息、新的样式。只要您仔细规划您的控件,其中许多功能都很容易实现。

这些新功能之一是 UI 状态反馈。您可以在“显示设置 / 外观 / 效果”中打开或关闭它。它被称为“除非我按下 Alt 键,否则隐藏用于键盘导航的下划线字母”。此项的上下文帮助已经揭示了其含义比这个笨拙的标题所暗示的要多。

这个特殊功能隐藏在三个新的窗口消息后面:WM_QUERYUISTATEWM_UPDATEUISTATEWM_CHANGEUISTATE。这些消息只是获取或设置一个位图,指定要关闭或打开哪个功能。您的控件所要做的就是绘制控件或其视觉反馈时考虑这些位。

另一个重要的事情是 Windows XP 的主题支持。完全支持主题需要大量工作,如果您的控件使用默认的 Windows 元素(按钮、滚动条、下拉指示器等)作为窗口的一部分,则必须实现主题支持。没有捷径。否则,您的控件在客户应用程序的所有主题控件之间会显得很难看。幸运的是,在 CodeProject 等网站上有一些辅助类,您可以在您的实现中轻松使用。

结论

编写好的控件是一项艰苦的工作。设计控件的接口需要时间。您为自己的应用程序创建的控件永远不会是一个完整的解决方案。它只实现了您需要的功能。下一个使用您的控件的程序员可能对接口或环境有非常不同的要求。如果您打算发布您的控件,请仔细审查接口和代码,并添加缺失的功能。

任何 Set… 函数都应该有一个相应的 Get… 函数。即使您的应用程序不需要它,其他人也会需要。如果您的控件已经提供了该功能,那么它是可重用的。否则,它只是一个代码片段,需要大量工作。

如果您的控件使用常量或 #defined 测量值,请考虑它们是否可以配置。不要仅仅因为您不需要更多就施加限制。

 

暂时就到这里。我将根据您的反馈和任何其他想法更新本文。希望您在下一部分发表时继续阅读。

© . All rights reserved.