在互联网上浏览时,经常会遇到各种类型的文本框,比如eBay的搜索框,它是一个带有提示文字的文本框,如“在这里输入搜索字符串”。当点击该框时,提示文字会消失,留下一个空的文本框供输入搜索字符串。微软甚至在其Atlas网站上提供了一个名为TextBoxWatermark的AJAX示例控件。最近,这种类型的控件开始出现在Windows应用程序中,如Outlook 2007、IE7以及Firefox 2.0。这种控件非常方便,因为它基本上将文本框和标签的功能合二为一,而不会占用大量的屏幕空间。在Web上,这种巧妙的功能通常通过使用JavaScript来处理,但在Windows上,需要自己想办法提供相同的功能。
在Web应用程序中,开发者通常会在文本框的onBlur和onFocus事件中添加JavaScript代码,以放置提示文字。开发者还必须意识到表单提交(或ASP.NET中的“回发”)可能会导致提示文字包含在提交数据中,因此代码必须对此有所了解。此外,JavaScript代码被迫操作文本框的value属性以实现这一点,因此即使是JavaScript解决方案也有一种“hack”的感觉。
标准的WinForms文本框不支持此功能,因此这个类将通过继承System.Windows.Forms.TextBox并自行处理提示文字的显示来解决这个不足。
覆盖WndProc方法可能看起来有些过度,但在这种情况下,代码相对简单。对于这个控件,只需要关注WM_SETFOCUS和WM_KILLFOCUS消息,它们应该与GotFocus和SetFocus事件相同,以及WM_PAINT:
protected override void WndProc(ref System.Windows.Forms.Message m)
{
switch (m.Msg)
{
case WM_SETFOCUS:
_drawPrompt = false;
break;
case WM_KILLFOCUS:
_drawPrompt = true;
break;
}
base.WndProc(ref m);
// 只有在WM_PAINT事件和_drawPrompt为true时才绘制提示文字
if (m.Msg == WM_PAINT && _drawPrompt && this.Text.Length == 0 && !this.GetStyle(ControlStyles.UserPaint))
DrawTextPrompt();
}
DrawTextPrompt方法做了大部分工作。它确定了要绘制提示的客户端矩形区域,并根据HorizontalAlignment进行任何偏移,然后使用TextRenderer在矩形区域内绘制PromptText:
protected virtual void DrawTextPrompt(Graphics g)
{
TextFormatFlags flags = TextFormatFlags.NoPadding | TextFormatFlags.Top | TextFormatFlags.EndEllipsis;
Rectangle rect = this.ClientRectangle;
// 根据HorizontalAlignment偏移矩形区域,否则显示看起来有点奇怪
switch (this.TextAlign)
{
case HorizontalAlignment.Center:
flags = flags | TextFormatFlags.HorizontalCenter;
rect.Offset(0, 1);
break;
case HorizontalAlignment.Left:
flags = flags | TextFormatFlags.Left;
rect.Offset(1, 1);
break;
case HorizontalAlignment.Right:
flags = flags | TextFormatFlags.Right;
rect.Offset(0, 1);
break;
}
// 使用TextRenderer绘制提示文字
TextRenderer.DrawText(g, _promptText, this.Font, rect, _promptColor, this.BackColor, flags);
}
在处理这个控件时,第一个想法是覆盖OnGotFocus/OnLostFocus方法,只是交换Text和PromptText值(除了改变ForeColor)。这立即被证明是一个糟糕的想法,因为一旦测试它,就注意到设计时的Text属性突然被PromptText替换了(ForeColor也被PromptForeColor替换了)。这有效地移除了开发者设置默认Text值的能力,所以是时候尝试新的方法了。
在考虑了一些替代方案之后,决定覆盖OnPaint方法,并手动绘制提示覆盖在TextBox区域上。这将解决尝试操作Text/ForeColor属性的“hack”性质。所以开发了DrawTextPrompt函数,并在OnPaint中添加了一个调用。不幸的是,这也被证明是一个有问题的解决方案(已经留下了代码,以便可以自己测试)。提示根本无法正确地绘制在TextBox上,显示出各种奇怪的行为,如消失的文字、错误的字体等。第一个想法是错误地编写了DrawTextPrompt,但一个简单的测试表明它确实正确地绘制了提示。
所以在更加努力之后,终于决定覆盖WndProc。知道必须弄清楚哪个消息是解决方案的关键(WM_PAINT显然是一个选择,但那不是OnPaint应该做的吗?)。经过几个小时的挫败,没有其他候选人,决定尝试看看WM_PAINT是否会有不同的结果。所以添加了对DrawTextPrompt的调用,天哪,它起作用了!!!还没有解释为什么WM_PAINT起作用而OnPaint不起作用,但可以自己试试,通过取消注释构造函数中的SetStyle行。工作理论是SetStyle调用禁用了还没有考虑到的额外行为。
在浏览代码时,注意所有调用Invalidate()的地方。这是为了让控件在设计时属性变化时重新绘制自己。控件在设计时应与运行时表现相同。如果Text属性中有值,PromptText将不会显示。如果更改PromptText、PromptForeColor或TextAlign属性,控件应立即更改。
还添加了一个FocusSelect布尔属性(自从VB3以来就渴望的一个功能,当时继承不是一个选项),当设置为true时,当控件获得焦点时,将选择控件中的所有文本。
正如用户评论中指出的,Windows XP及更高版本有一个消息可以发送到TextBox控件,几乎可以实现相同的目标,那就是EM_SETCUEBANNER。使用此消息有一些优点:
但也有一些缺点: