转载自:一风
TextBox水印实现
使用Vista的用户都知道,在登录时输入用户名和密码的文本框都具有水印效果,在文本框里没有文字时,分别会以灰色显示“用户名”和“密码”,当输入内容以后这些说明文字就消失了。
由于目前手头上在开发的一个软件需要用到这样的功能,因此对水印的效果做了一些研究。很明显,水印的文字并不是真正的文字内容(也就是TextBox.Text属性),而是使用GDI绘制在TextBox控件表面上的图形。水印的实现并不复杂,创建一个类继承TextBox,当TextBox需要绘制时,先由TextBox完成自身的绘制,再根据当前TextBox中是否有内容来确定是否要绘制水印文本。
什么时候需要绘制呢,有过控件开发经验的人都知道,一般是在TextBox的Paint事件(OnPaint方法)里放入绘制水印文字的代码,但很快被否决,因为TextBox根本不会引发Paint事件。道理很简单,TextBox是.net对原生的Windows控件EDIT进行了封装,所有的控件绘制都是由Windows自己完成的,在.net的代码里是无法介入的,除非使用SetStyle方法设置UserPaint为true。通过UserPaint可以由.net代码来完全绘制控件,当然就可以引发Paint事件,但这种做法有太多的东西要考虑太多,并不是我要研究的主要方向。
那么,要怎么样才能在无法使用Paint事件的情况下,知道何时应该绘制水印文本呢?很简单,Windows Message,消息,强大的消息。Control类提供了WndProc方法用于控件开发人员处理消息,只要重载这个方法,并判断是否为绘制控件的消息WM_PAINT,就可以知道什么时候应该绘制水印了。
OK,接下来就简单了,下面的代码演示了一个简单的水印效果的实现。
protected override void WndProc(ref Message m)
{
// 由基类先处理消息,因为我们绘制水印的工作是在原生控件绘制完毕之后。
base.WndProc(ref m);
// 0x000F为常数WM_PAINT的实际值。
if (m.Msg == 0x000F)
{
// 此时已经抓到了绘制事件,开始绘制水印。
if (this.Text.Length == 0) // 只有在文本框里没有文字的情况下才绘制水印。
{
Brush brush = SystemBrushes.GrayText; // 使用灰色的文本色,即表示禁用状态的文字颜色。
Font font = this.Font; // 使用当前控件的字体。
Rectangle rect = this.ClientRectangle; // 在当前控件的客户区域绘制。
using (Graphics g = this.CreateGraphics())
{
g.DrawString("水印", font, brush, rect);
}
}
}
}
ComboBox水印实现
简要说明了如何在TextBox里实现水印效果。把同样的实现方法搬到ComboBox中不对了,虽然代码运行没有出现错误,但却达不到我们在TextBox上的应用效果,根本看不到水印。这是怎么回事呢?
与TextBox一样,ComboBox是对Windows的原生控件COMBOBOX的封装。通过使用Spy++查看ComboBox控件,不难发现其实ComboBox内还有一个窗口,而这个窗口才是真正用于编辑文字的,它是一个EDIT控件,ComboBox只是实现了下拉列表的功能。因此,要在ComboBox上实现水印的效果,必须要在它内部的EDIT原生控件上绘制,而不是在ComboBox上。由于内部的这个EDIT是Windows原生的,通过Control.Controls集合无法获取到它的,因此只能通过Windows API实现。
在Windows API中,EnumChildWindows这个函数可以通过回调的方式枚举指定窗口的所有子窗口,关于这个函数的使用在这里我就不详细说明了,有兴趣的可以参考MSDN里的相关说明。因为原生的COMBOBOX中只有一个子控件,因此要获取内部的这个EDIT并不困难。具体实现见以下代码,摘自我的提供的WatermarkComboBox类的源码。
1 /**//// <summary>
2 /// 获取内部EDIT的句柄。
3 /// </summary>
4 private void RetreiveEditControl()
5 {
6 IntPtr handle = new IntPtr();
7
8 EnumChildWindows(this.Handle, GetChildCallback, ref handle);
9
10 this._editHandle = handle;
11 }
12
13 /**//// <summary>
14 /// EnumChildWindows的回调函数。
15 /// </summary>
16 private bool GetChildCallback(IntPtr hWnd, ref IntPtr lParam)
17 {
18 // 因为原生COMBOBOX只有一个子控件,因此不用作任何判断直接返回。
19 lParam = hWnd;
20 return false;
21 }
从以上代码可以看出,_editHandle就是内部EDIT控件的句柄,这样,与TextBox水印的的绘制代码相比,只要做两个修改就可以了。
第一个修改的地方是获取绘制区域的Rectangle,因为EDIT不是.net控件,因此只能使用API函数GetClientRect,而不能直接使用this.ClientRectangle属性。
第二个修改的地方是Graphics的获取,改为使用Graphics.FromHwnd方法。
修改之后的代码如下:
1 Brush brush = SystemBrushes.GrayText;
2 Font font = this.Font;
3 RECT rect = new RECT();
4
5 // 通过API获取EDIT的客户区域大小。
6 GetClientRect(_editHandle, ref rect);
7
8 StringFormat stringFormat = new StringFormat();
9 stringFormat.Alignment = StringAlignment.Near;
10 stringFormat.LineAlignment = StringAlignment.Center;
11
12 // Graphics从EDIT的句柄获取。
13 using (Graphics g = Graphics.FromHwnd(_editHandle))
14 {
15 g.DrawString(_watermark, font, brush, rect.ToRectangle(), stringFormat);
16 }
17
18 // 释放非托管资源。
19 stringFormat.Dispose();
20
这样,在ComboBox上绘制水印的功能就完成了,不过还存在两个BUG。
1 在窗体出现时水印不会立刻显示,只有鼠标在上面移过以后才会显示。
2 水印的闪烁比较明显,特别是在启用了视觉主题以后。
以上的2个问题,我目前还不清楚是什么原因造成的,估计是和消息有关系。因为在WndProc方法中所处理的消息都是发给这个控件的,而EDIT并不等于ComboBox本身,因此可能会造成不正常的行为。要解决这个问题,看来只能从其它方面入手,先卖个关子,稍后的文章中我会提到这个问题的解决办法。
效果图如下:
未输入任何内容
输入了用户名之后
本文的演示程序和源代码请点击这里下载。