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

一个验证编辑控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (5投票s)

2000年11月4日

viewsIcon

129622

downloadIcon

1569

一个信息丰富、面向用户的验证编辑控件。

问题

DDV 机制太原始,几乎无法使用。它只在“确定”按钮(或者更具体地说,在伴随它的UpdateData调用时)进行验证,这使得验证时机太晚。它还发出的错误消息与程序中的表示有关,而不是与问题域相关。因此,我避免使用这种玩具机制,而是编写了自己的验证代码。我还发现,当我允许在有错误的情况下点击“确定”按钮时,这尤其令人恼火;这违反了 GUI 设计的基本原则。因此,DDV 不仅对用户来说很难使用,而且实际上违反了 GUI 设计准则,因为通常你必须点击一个实际上无效的按钮,然后 DDV 机制才能被调用!

当然,这使得编程更容易。但目标不是让应用程序易于编程,而是让应用程序易于使用。我相信我的技术对后者有显著的贡献。它们并不更容易编程这一事实,无关紧要。

这个特定的示例很有用,因为它说明了几个有用的技术。这些技术包括:

  • 演示如何逐个字符地验证输入
  • 即时提供关于输入正确性的有用反馈
  • 演示如何使用 ToolTip 指出“确定”按钮被禁用的原因。

我在构建信息丰富、面向用户的验证方法时,以某种形式或另一种形式使用了所有这些技术。

解决方案

我包含的第一个内容是一个验证编辑控件。这个特定的控件解决了一个许多用户要求的问题:一个可以验证浮点数输入的编辑控件。但是,您可以将 FSM 替换为可以验证日期、时间、社会安全号码或任何其他可解析的文本形式的 FSM。验证不必局限于简单的解析,尽管从每次更改都会触发验证这一事实可以清楚地看出,您不希望每次都进行某种数据库查找。在这种情况下,您更有可能在 WM_KILLFOCUS (OnKillFocus) 事件中进行验证。但这与此控件要解决的问题不同。

我的处理方式是处理反射的 WM_COMMAND/EN_CHANGE 消息。当内容更改时,我会读取整个字符串并重新解析它。这是与传统的 getch 式输入方法论上的变化,在这种输入中,您可以简单地对每个按键运行 FSM。在 Windows 中,按键顺序与编辑控件的内容顺序无关,因为用户可以将输入插入符定位在字符串中的任何位置。因此,每次都必须从头开始重新解析整个字符串。

语法检查

在这种情况下,我使用(atof 函数指定的语法的子集)解析浮点数。(我不接受 D 或 d 作为指数指示符)。

[空格] [符号] [数字] [.数字] [{ e | E} [符号] 数字]

(实际上,我不允许用户输入空格,但稍后会讨论这一点)。解析是通过有限状态机 (FSM) 完成的,该 FSM 接收当前状态和当前字符,并根据指示下一个状态的表进行解码。该表被编码为 switch 语句中的一系列 case 语句。在每次匹配时,我可以执行以下一项或多项操作:

  • “吃掉”字符,将其从输入流中移除,或将其留给下一个状态处理
  • 设置下一个状态
  • 设置一个指示符,表示字符串是完成的、不完整的还是错误的。

这被编码如下(部分):通过设置一个指向预定义画笔的指针来设置“指示符”。如果画笔被设置为“错误”指示符,循环就会结束。

int state = S0;
for(int i = 0; brush != &errorBrush && i < s.GetLength();)
   { /* scan string */
    TCHAR ch = s[i];
    switch(MAKELONG(state, ch))
       { /* states */
        case MAKELONG(S0, _T(' ')):
        case MAKELONG(S0, _T('\t')):
           i++;
          continue;
        case MAKELONG(S0, _T('+')):
        case MAKELONG(S0, _T('-')):
           i++;
           brush = &partialBrush;
           state = IPART;
           continue;
      case MAKELONG(S0, _T('0')):
           ¤
           ¤
           ¤
      case MAKELONG(S0, _T('9')):
           state = IPART;
           continue;
      case MAKELONG(S0, _T('.')):
           i++;
           state = FPART;
           brush = &partialBrush;
           continue;
      case MAKELONG(S0, _T('E')):
      case MAKELONG(S0, _T('e')):
           i++;
           state = ESIGN;
           brush = &partialBrush;
          continue;
      case MAKELONG(IPART, _T('0')):
           ¤
           ¤
           ¤
      case MAKELONG(IPART, _T('9')):
           i++;
           brush = &OKBrush;
           continue;
      case MAKELONG(IPART, _T('.')):
           i++;
           brush = &OKBrush;
           state = FPART;
           continue;
      case MAKELONG(IPART, _T('e')):
      case MAKELONG(IPART, _T('E')):
           i++;
           brush = &partialBrush;
           state = ESIGN;
           continue;
      case MAKELONG(FPART, _T('0')):
      case MAKELONG(FPART, _T('9')):
           i++;
           brush = &OKBrush;
           continue;
       case MAKELONG(FPART, _T('e')):
       case MAKELONG(FPART, _T('E')):
           i++;
           brush = &partialBrush;
           state = ESIGN;
           continue;
      case MAKELONG(ESIGN, _T('+')):
      case MAKELONG(ESIGN, _T('-')):
           i++;
           brush = &partialBrush;
           state = EPART;
           continue;
      case MAKELONG(ESIGN, _T('0')):
      case MAKELONG(ESIGN, _T('1')):
           ¤
           ¤
           ¤
      case MAKELONG(ESIGN, _T('9')):
           state = EPART;
           continue;
      case MAKELONG(EPART, _T('0')):
           ¤
           ¤
           ¤
      case MAKELONG(EPART, _T('9')):
           i++;
           brush = &OKBrush;
           continue;
      default:
           brush = &errorBrush;
           continue;
     } /* states */
   } /* scan string */

要吸收一个字符,我只需增加指针 (i++)。您可以创建类似的表来解析日期、时间或您可以定义的任何其他字段。

值检查

语法正确的数值可能不满足其他标准。例如,信用卡号采用一种验证算法,其中一个数字(通常是低位数字)是前面数字的函数。多年前一种常见的方案是将数字相加,然后将结果值从 9 中减去,然后使用结果数字作为低位数字。您可以设置范围检查,或验证月份中的日期不超过所选月份的有效范围(例如,没有 2 月 31 日)。

在我的示例程序中,我将值限制为绝对值大于 1.0,正值小于等于 8192.0f,负值大于等于 -16384.0f。我们如何将值范围检查耦合到基本验证中?答案是,每当获得语法上有效的数字时,我就会向父窗口发送一条消息,要求它验证控件。从 SendMessage 返回一个布尔值 TRUEFALSE 来指示该值是否有效。

为此,我使用一个用户定义的、实际上是已注册的窗口消息来通知父窗口。有关详细信息,请参阅我关于消息管理的论文。在这种情况下,我使用一个静态类成员变量,我在类中声明为:

static UINT UWM_CHECK_VALUE;

我在 .cpp 文件中通过执行以下操作来初始化它:

UINT CFloatingEdit::UWM_VALID_CHANGE = ::RegisterWindowMessage( 
  _T("UWM_VALID_CHANGE-{6FE8A4C1-AE33-11d4-A002-006067718D04}"));

我通过将以下行放在父窗口的 MESSAGE_MAP 中来响应此消息。请注意,消息请求遵循魔法 ClassWizard 注释。

//}}AFX_MSG_MAP
ON_REGISTERED_MESSAGE(CFloatingEdit::UWM_CHECK_VALUE, OnCheckValue)

我已将此消息的参数定义为如下所示:

/***************************************************************************
*                                UWM_CHECK_VALUE
* Inputs:
*        WPARAM: MAKELONG(GetDlgCtrlID(), EN_CHANGE)
*        LPARAM: (LPARAM)(HWND): Window handle
* Result: BOOL
*        TRUE if value is acceptable
*        FALSE if value has an error
* Effect: 
*        If the value is FALSE, the window is marked as an invalid value
*        If the value is TRUE, the window is marked as a valid value
* Notes:
*        This message is sent to the parent of the control as a consequence
*        of the EN_CHANGE notification, but only if the value 
*        is syntactically correct. It may be sent at other times as well
***************************************************************************/

显示更改

为了指示状态并向用户提供即时反馈,我修改了控件的背景颜色。我选择白色作为空控件,红色作为无效值,黄色作为到目前为止语法正确但尚未完全有效的,以及绿色作为满足所有标准的。这些示例如下:

编辑控件是空的。它显示为正常的编辑背景,在此计算机上是白色的。
编辑控件具有语法上有效的值,该值与任何范围约束匹配(在本例中,未启用范围检查)。
编辑控件具有尚未完成的值。它尚未在语法上有效,但到目前为止的内容是正确的。
编辑控件的值在语法上不正确。无论如何向该值添加内容都无法使其正确。

控件更新

我需要响应验证的变化来更新控件。这意味着我需要启用或禁用控件(例如“确定”按钮),只要状态发生变化。为了做到这一点,我向父窗口发送一条通知,指示验证状态已发生变化。这是另一个已注册的窗口消息。

/**************************************************************************
*                                UWM_VALID_CHANGE
* Inputs:
*    WPARAM: (WPARAM)MAKELONG(GetDlgCtrlID(), BOOL) 
*                     Flag indicating new valid state
*    LPARAM: Window handle
* Result: LRESULT
*    Logically void, 0, always
* Effect: 
*    Notifies the parent that the validity of the input value has changed
**************************************************************************/

当我收到此消息时,我会调用以下处理程序:

LRESULT CValidatorDlg::OnValidChange(WPARAM, LPARAM lParam)
   {
    CWnd::FromHandle((HWND)lParam)->InvalidateRect(NULL);
    updateControls();

    return 0;
   } // CValidatorDlg::OnValidChange

注意开头的 InvalidateRect。我必须强制重绘整个控件,当有效状态发生变化时,否则显示会略有倾斜,只有字母后面的背景会被重绘。当您阅读代码时,您会发现其他几个完整的控件重绘,它们确保了正确的显示。这些是必要的,因为 Windows 在最小化控件重绘方面做得非常好,如果您在没有进行此重绘的情况下键入如下所示的值,结果会非常奇怪。您甚至可能注意到,一些如果从左到右键入会产生有效值的值,现在会以红色显示无效值。这是因为根本没有进行 InvalidateRect。另请注意,字符单元格之外的背景部分是无效的。

工具提示

我发现当对话框上有许多控件时,其中几个控件会影响某个控件(例如“确定”按钮)是否启用,使用 ToolTip 来指定解释该控件为何未启用的文本通常非常有用。通常有几个原因。在这种情况下,问题是显示哪个原因。ToolTip 的可显示字符串长度有限,超出该长度的字符将不会显示。我的策略是采用一种涉及基于规则的系统的方法,该系统检查条件并显示禁用该控件的第一个条件。有时我会安排这些规则,以便显示最常见或最容易修复的条件。

要启用 ToolTips,您必须在 OnInitDialog 处理程序中调用 EnableToolTips(TRUE) 函数。此外,我通过使用回调函数来执行 ToolTip。这意味着我必须在 MESSAGE_MAP 中,在魔法注释之外添加以下消息处理程序:

//}}AFX_MSG_MAP
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipNotify)

处理程序定义如下:

BOOL CValidatorDlg::OnToolTipNotify(UINT, NMHDR * pNMHDR, LRESULT *)
   {
    TOOLTIPTEXT * pTTT = (TOOLTIPTEXT *)pNMHDR;
    HWND ctl = (HWND)pNMHDR->idFrom;

    UINT msg = 0; // set to message ID of text to display

    if(pTTT->uFlags & TTF_IDISHWND)
       { /* display request */
        UINT id = ::GetDlgCtrlID(ctl);
        switch(id)
           { /* id */
            case IDC_SAMPLE:
                 // This is the 'OK' button
                 // We search for reasons it might be disabled
                 // Only the first reason counts.
                 // Our limit is the tooltip text length, so we
                 // present the errors in the order we think the
                 // user might most easily fix them. For example,
                 // the tab order

在每个控件的每个 case 中,我放置规则,或者调用一个计算规则的函数。例如,为了使消息更清晰,我在 CFloatingEdit 控件中有两个内部状态函数,其中一个告诉我值在语法上是否有效,另一个告诉我值在语义上是否有效。例程 IsValid 仅当值在语法上语义上都有效时才返回 TRUE。这允许我更详细地报告值无效的原因。对于语义检查,我调用子项有效性检查使用的相同函数,该函数返回一个字符串 ID,用于在发生错误时显示字符串,或者返回 0 表示没有错误。最终,此值存储在变量 msg 中。如果此值为 0,则不显示任何内容,并且我从处理程序返回 FALSE;否则,我设置传递的结构中的一些字段,然后返回 TRUE

if(msg == 0)
   return FALSE;
pTTT->lpszText = MAKEINTRESOURCE(msg);
pTTT->hinst = AfxGetResourceHandle();
return TRUE;

这会导致显示存储在 msg 中的字符串 ID 指定的字符串作为 ToolTip。

在实际应用程序中,文本会更具信息性,例如,“温度值格式不正确”,但对其进行泛化应该很明显。

输入限制

为了进一步降低错误的可能性,我禁用了无效字符。所以对于我的浮点控件,我只允许数字、加号和减号、小数点以及字母“e”和“E”。还有退格键。别忘了退格键!

一个常见的建议是“将其放在对话框的 PreTranslateMessage 处理程序中”。对我来说,这毫无意义;它违反了抽象和面向对象的多个问题。对我来说,将此放在想要过滤字符的控件中更有意义。为此,我编写了一个如下所示的处理程序,它出现在我的子类对话框中。当使用 ClassWizard 添加 WM_CHAR 处理程序时,它会被创建,我只需要填充显示的代码。

void CFloatingEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
   {
    switch(nChar)
       { /* validate */
    case _T('+'):
    case _T('-'):
    case _T('.'):
    case _T('E'):
    case _T('e'):
    case _T('0'):
    case _T('1'):
    case _T('2'):
    case _T('3'):
    case _T('4'):
    case _T('5'):
    case _T('6'):
    case _T('7'):
    case _T('8'):
    case _T('9'):
    case _T('\b'):
       break;
    default:
       MessageBeep(0);
       return;
       } /* validate */
    CEdit::OnChar(nChar, nRepCnt, nFlags);
}

这只会接受显示的字符,并调用超类处理程序,或者简单地发出蜂鸣声并返回,从而丢弃字符。

摘要

本文总结了我广泛用于我构建的应用程序的一组技术。我没有在其他地方看到过这些想法的记录,因此这似乎是一个很好的主题。


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

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