zoukankan      html  css  js  c++  java
  • 水印管理器:2、Win32实现

    花了一些时间,把一条简单的SendMessage进行了包装,制作了一个用于水印管理的组件,先看一下图。


    这两个类是实现水印管理器的主要类型,分别是Cuer和CueInfo。Cuer就是我所说的水印管理器,为了和DIY的进行区分,没有称之为WatermarkManager。CueInfo保存了水印的信息,包括文字和一个选项。

    先来说CueInfo,它有两个主要属性,分别是Text和HideOnFocus,Text很明显,就是水印的文字,而HideOnFocus是表示是否需要在控件获得焦点的时候隐藏水印文字,默认为true。至于另外两个属性,Control表示当前CueInfo应用在哪个控件上,Manager表示当前CueInfo是由哪个Cuer组件来管理的,这两个属性是只读的,由Cuer组件维护。

    Win32版水印管理器的所有功能都由Cuer这个类来实现,它从Component继承,并且实现了IExtenderProvider接口。下面对Cuer组件的几个公开成员做一个简要的说明。
    public static bool IsSupported(Control control);
    此方法的功能是测试指定的控件是否受Cuer组件支持,由于受到EM_SETCUEBANNER消息只能支持TextBox控件的限制,目前支持的控件只有TextBox和ComboBox。
    public void SetCue(Control control, CueInfo cue);
    它的功能是为控件指定水印信息,包括文字内容,以及一个是否需要自动隐藏的选项。
    public CueInfo GetCue(Control control);
    根据指定的控件,获取其相应的水印信息,如果没有找到则返回一个空值。
    public bool Enabled { get; set; }
    对于这个属性,我想我不要多说了,当设置为false以后,所有水印信息全部消失。

    先来段例子,有个感性的认识。假设有一个窗体用于用户登录,有两个控件,一个是用于输入用户名的tbUsername,另一个是用于输入密码的tbPassword,分别设置文字“User name”和“Password”作为它们的水印文本,请看以下代码。

    Cuer cuer1 = new Cuer();

    CueInfo cue 
    = new CueInfo();
    cue.Text 
    = "User name";
    cuer1.SetCue(tbUsername, cue);
    cue 
    = new CueInfo();
    cue.Text 
    = "Password";
    cuer1.SetCue(tbPassword, cue);

    是不是很简单呢,简单的几行代码就可以实现水印的效果。好了,以上我对这两个主要的类Cuer和CuerInfo进行了说明,接下来我将把这个组件实现细节中几个关键问题进行一些说明。

    控件(Control)与水印信息(CueInfo)的对应

    这点不难理解,因为Cuer是在Control外部进行操作的,要将Control与CueInfo一一对应,就必须借助一个字典,以Control为键,CueInfo为值,我采用的是泛型字典类Dictionary<Control, CueInfo>。字典的维护主要在SetCue方法中完成,假如参数cue的值为空就从字典中移除,非空就加入字典。不过事情并不会这么简单,在对字典进行操作时还有一些额外的问题要考虑,想想前面提到的CueInfo的Control和Manager属性,这两个属性是由Cuer进行维护的,因此在添加和移除时,也必须同时对这两个属性进行维护。

    SetCue方法会检查cue参数是否已经被管理(也就是CueInfo.Manager属性非空),无论是否由当前Cuer组件管理,都会引发InvalidOperationException。如果不进行这样的限制,可能会造成几个控件共享一个CueInfo实例,修改一下先前的示例,参考以下代码:

    CueInfo cue = new CueInfo();
    cue.Text 
    = "User name";
    cuer1.SetCue(tbUsername, cue);
    cue.Text 
    = "Password";
    cuer1.SetCue(tbPassword, cue);

    这样的程序到底这个cue变量应该代表用户名呢还是密码呢,呵呵,总之我能想到的是两个输入框都显示了Passowrd的水印文字。我不清楚这种限制是否会对实际的应用造成影响,至少在我编写Demo时没有碰到过麻烦。

    看起来这样的对应关系应该是比较可靠的了,不过没有考虑控件的动态移除。假如在运行时控件因为某些原因要从窗体中移除掉,那么在字典中会保留一个无用的控件引用,虽然可能不会造成太大的影响,但会使程序看起来不那么严密。Control.Disposed事件可以通知我们控件的非托管资源被释放了,从某种意义上来看,就是它被称除了,只要捕获这个事件并在处理程序中将其从字典中移除就可以了。具体的实现请参考源代码中的SetCue方法和control_Disposed方法。

    关于ComboBox控件

    前面我说过,EM_SETCUEBANNER只支持TextBox控件,但为什么又可以支持ComboBox呢?关于这个问题我在先前的随笔中明说过,ComboBox实际上会在内部嵌入一个原生EDIT控件,也就是TextBox控件,那么只要能够获取到这个EDIT控件,也同样可以由这个API来实现水印的功能。不过还是有遗憾的是,ComboBox有三种风格(DropDownStyle),分别是Simple、DropDown和DropDownList,前面两种都是允许用户在控件内输入文字的,而第三种是一个纯粹的下拉列表,不能输入,因此在它内部是没有EDIT控件的,所以不能在DropDownList的ComboBox上实现水印功能。但我觉得这个没有问题,因为对于下拉列表,有没有水印提示已经不那么重要了。

    关于如何获取ComboBox的内部EDIT控件,先前的程序处理得过于简单,在这里需要进行修正一下,先看代码:

            private static bool GetChildCallback(IntPtr hWnd, ref IntPtr lParam)
            {
                StringBuilder clsname 
    = new StringBuilder(1024);
                GetClassName(hWnd, clsname, clsname.Capacity);
                
    if (string.Compare(clsname.ToString(), "EDIT"true== 0)
                {
                    lParam 
    = hWnd;
                    
    return false;
                }
                
    return true;
            }

     这是EnumChildWindows的回调函数,在这个回调过程中,必须判断当前正在枚举的这个句柄所表示的控件是否为EDIT。API函数GetClassName可获取句柄所表示窗口的类,如果它是EDIT,说明这是一个TextBox,直接返回false以终止后续的枚举。这样做的原因是,当ComboBox的风格设置为Simple时,会在它内部生成两个子窗口,而且EDIT的位置是在第2个,因此如果按照原先的方式所返回的结果是肯定不正确的。

    在控件上应用水印效果

    首先要把握的一个是时机,也就是什么时候需要应用水印效果。我一共找了5个时机来应用水印效果:

    • 在SetCue方法中,当CueInfo添加到字典之后,将水印应用到控件上。
    • 在CueInfo的Text属性或HideOnFocus属性的值发生更改以后,并且CueInfo已经受到某个Cuer的管理时。
    • 当Cuer.Enabled属性的值发生更改以后,会将当前正在管理的(也就是字典中的)所有CueInfo重新应用一次。
    • 在所管理的控件的HandleCreated事件发生后,重新应用水印。

    这里需要着重说明的是最后一种情况。HandleCreated事件是在控件的句柄被创建以后发生的,有两种情况需要用到这个事件,一种是在调用SetCue方法时,控件的句柄还没有创建,这个可以通过Control.IsHandleCreated属性来检测,原因是控件还没有被加载;另一种情况是在控件已经加载的情况下,句柄被重新创建了。第二种情况比较要命,因为它会导致水印无法显示,这点我在开发的过程中已经遇到了,ComboBox的DropDownStyle默认值是DropDown,如果设置为Simple,控件的句柄会被重新创建,如果不捕获这个事件水印就失效了。

    时机找到了,接下来就该干正事了,调用SendMessage发送EM_SETCUEBANNER消息,将水印信息应用在控件上。EM_SETCUEBANNER消息有两个参数,wParam为0时表示在控件接受到焦点时自动隐藏水印,1表示只有在用户开始输入时才隐藏。lParam就是要显示的文字,这个不用多解释了。Cuer.ApplyCue用于实现这个功能,首先根据指定的CueInfo获取控件的句柄,如果句柄的值不是IntPtr.Zero,就把CueInfo的Text和HideOnFocus属性的值分别转换为lParam和wParam,再发送消息。GetHandle方法封装了获取控件句柄的逻辑,如果指定的控件是TextBox就直接返回它的Handle属性,如果是ComboBox,就根据之前所说的方法获取它的句柄。

    文章到此为止,已经把Cuer实现原理的几个主要问题做了解释,接下来就是设计时支持功能的开发。考虑到文章太长容易引起阅读疲劳,我会再另起一篇文章再说明。有兴趣的朋友可以点击这里下载源程序和演示程序,下图是演示程序的截图。(解释一下,因为这个组件我有打算要发到CodeProject上,所以源码里没有写过一个中文字,敬请各位看官谅解 :D)



    (未完待续)
  • 相关阅读:
    C#中将dll汇入exe,并加壳
    很不错的在线格式转换网站
    Eclipse快捷键大全
    win7休眠的开启与关闭方法
    C#实现注册码
    Microsoft.CSharp.targets不存在解决方法
    数据库>SQL Server2005>第4季SQL从入门到提高>2SQL Server使用
    main函数名字写错,写成mian等等的错误提示
    CSS选择器
    斐波那契数的实现
  • 原文地址:https://www.cnblogs.com/effun/p/1551548.html
Copyright © 2011-2022 走看看