一个验证编辑控件






4.20/5 (5投票s)
2000年11月4日

129622

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
返回一个布尔值 TRUE
或 FALSE
来指示该值是否有效。
为此,我使用一个用户定义的、实际上是已注册的窗口消息来通知父窗口。有关详细信息,请参阅我关于消息管理的论文。在这种情况下,我使用一个静态类成员变量,我在类中声明为:
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); }
这只会接受显示的字符,并调用超类处理程序,或者简单地发出蜂鸣声并返回,从而丢弃字符。
摘要
本文总结了我广泛用于我构建的应用程序的一组技术。我没有在其他地方看到过这些想法的记录,因此这似乎是一个很好的主题。
这些文章中表达的观点是作者的观点,不代表,也不被微软认可。