WPF控件开发(1) TextBox占位符

在twitter-bootstrap中有这么一个功能:

我们如何在WPF也实现类似这种写法:

<TextBox local:placeholder="请输入筛选条件..." />

 

首先熟悉一点WPF的人都知道,placeholder在这里是一个附加属性,而这个附加属性的类型是String。

第一种实现方式


首先我们想到的可能是这样:

 1 public static string GetPlaceholder1(DependencyObject obj)
 2 {
 3     return (string)obj.GetValue(Placeholder1Property);
 4 }
 5 public static void SetPlaceholder1(DependencyObject obj, string value)
 6 {
 7     obj.SetValue(Placeholder1Property, value);
 8 }
 9 public static readonly DependencyProperty Placeholder1Property =
10     DependencyProperty.RegisterAttached("Placeholder1", typeof(string), typeof(TextBoxHelper),
11         new UIPropertyMetadata(string.Empty, new PropertyChangedCallback(OnPlaceholder1Changed)));
12 public static void OnPlaceholder1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
13 {
14     TextBox txt = d as TextBox;
15     if (txt == null || e.NewValue.ToString().Trim().Length == 0) return;
16     RoutedEventHandler loadHandler = null;
17     loadHandler = (s1, e1) =>
18     {
19         txt.Loaded -= loadHandler;
20         if (txt.Text.Length == 0)
21         {
22             txt.Text = e.NewValue.ToString();
23             txt.FontStyle = FontStyles.Italic;
24             txt.Foreground = Brushes.Gray;
25         }
26     };
27     txt.Loaded += loadHandler;
28     txt.GotFocus += (s1, e1) =>
29     {
30         if (txt.Text == e.NewValue.ToString())
31         {
32             txt.Clear();
33             txt.FontStyle = FontStyles.Normal;
34             txt.Foreground = SystemColors.WindowTextBrush;
35         }
36     };
37     txt.LostFocus += (s1, e1) =>
38     {
39         if (txt.Text.Length == 0)
40         {
41             txt.Text = e.NewValue.ToString();
42             txt.FontStyle = FontStyles.Italic;
43             txt.Foreground = Brushes.Gray;
44         }
45     };
46 }
Placeholder1

 

基本存在以下几个问题:

  • 获得焦点取消了占位符,与需求(twitter-bootstrap)不符,这里需要当输入内容以后才取消占位符;
  • 如果这里某人恰巧也输入了”请输入筛选条件...”,当再次获得焦点的时候就会清空输入的内容,当然这一条完全可以再给一个标示去判断,比如在GotFocus的判断中,增加一个:
  •  1 txt.GotFocus += (s1, e1) =>
     2 {
     3     if (txt.Text == e.NewValue.ToString()
     4      && txt.FontStyle == FontStyles.Italic
     5      && txt.Foreground == Brushes.Gray)
     6     {
     7         txt.Clear();
     8         txt.FontStyle = FontStyles.Normal;
     9         txt.Foreground = SystemColors.WindowTextBrush;
    10     }
    11 };

     

  • 接之而来的问题就是如果TextBox恰巧需要设置FontStyle或Foreground,此时就无能为力;
  • 其实最重要的问题还是,当我没有输入内容时,获取TextBox的Text属性,总有一个我不需要的值,或许加个判断可以搞定,但是这不是一个好的方式;

使用装饰器实现-1


上面实现方式所带来的弊端,可以使用装饰器解决。
首先定义一个装饰器,它可能是这样:

 1 public class PlaceholderAdorner1 : Adorner
 2 {
 3     string _placeholder;
 4     public PlaceholderAdorner1(UIElement ele, string placeholder)
 5         : base(ele)
 6     {
 7         _placeholder = placeholder;
 8     }
 9     protected override void OnRender(DrawingContext drawingContext)
10     {
11         TextBox txt = this.AdornedElement as TextBox;
12         if (txt == null || !txt.IsVisible || string.IsNullOrEmpty(_placeholder)) return;
13         this.IsHitTestVisible = false;
14         drawingContext.DrawText(
15             new FormattedText
16             (
17                 _placeholder, 
18                 CultureInfo.CurrentCulture,
19                 txt.FlowDirection,
20                 new Typeface(txt.FontFamily, FontStyles.Italic, txt.FontWeight, txt.FontStretch),
21                 txt.FontSize,
22                 Brushes.Gray
23             ),
24             new Point(4, 2));
25     }
26 }

 

思路很简单,记录下这个占位符,然后给TextBox指定位置画出来。
这时候,附加属性可能是这样:

 1 public static string GetPlaceholder2(DependencyObject obj)
 2 {
 3     return (string)obj.GetValue(Placeholder2Property);
 4 }
 5 public static void SetPlaceholder2(DependencyObject obj, string value)
 6 {
 7     obj.SetValue(Placeholder2Property, value);
 8 }
 9 public static readonly DependencyProperty Placeholder2Property =
10     DependencyProperty.RegisterAttached("Placeholder2", typeof(string), typeof(TextBoxHelper), 
11         new UIPropertyMetadata(string.Empty, new PropertyChangedCallback(OnPlaceholder2Changed)));
12 public static void OnPlaceholder2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
13 {
14     TextBox txt = d as TextBox;
15     if (txt == null || e.NewValue.ToString().Trim().Length == 0) return;
16     RoutedEventHandler loadHandler = null;
17     loadHandler = (s1, e1) =>
18     {
19         txt.Loaded -= loadHandler;
20         var lay = AdornerLayer.GetAdornerLayer(txt);
21         if (lay == null) return;
22         Adorner[] ar = lay.GetAdorners(txt);
23         if (ar != null)
24         {
25             for (int i = 0; i < ar.Length; i++)
26             {
27                 if (ar[i] is PlaceholderAdorner1)
28                 {
29                     lay.Remove(ar[i]);
30                 }
31             }
32         }
33         if (txt.Text.Length == 0)
34             lay.Add(new PlaceholderAdorner1(txt, e.NewValue.ToString()));
35     };
36     txt.Loaded += loadHandler;
37     txt.TextChanged += (s1, e1) =>
38     {
39         bool isShow = txt.Text.Length == 0;
40         var lay = AdornerLayer.GetAdornerLayer(txt);
41         if (lay == null) return;
42         if (isShow)
43         {
44             lay.Add(new PlaceholderAdorner1(txt, e.NewValue.ToString()));
45         }
46         else
47         {
48             Adorner[] ar = lay.GetAdorners(txt);
49             if (ar != null)
50             {
51                 for (int i = 0; i < ar.Length; i++)
52                 {
53                     if (ar[i] is PlaceholderAdorner1)
54                     {
55                         lay.Remove(ar[i]);
56                     }
57                 }
58             }
59         }
60     };
61 }
Placeholder2

 

可以看到,附加属性只是对于装饰器的删除和添加,别无其他。
运行看看效果,都还不错,第一种实现所出现的问题也都能解决,但是随即发现一个问题:

  • 当隐藏(设置TextBox的Visibility为Hidden时),发现TextBox隐藏了,但是装饰器还存在。

使用装饰器实现-2


针对上述的问题,首先分析问题原因,首先知道OnRender方法是在UIElement中定义,看方法的注释中,意思似乎是只在布局改变时才调用,从OnRender方面下手几乎不可能。
回想WinForm出现此类情况的解决方案,无非是调用Invalidate之类的,但是存在一个时机的问题。
如果当TextBox的Visibility改变时,能获取通知,上述问题就可以解决,而且直接可以删除改装饰器。
鉴于WPF中的触发器能得到某个属性改变的通知,那么我们自己肯定也能得到。

 1 public PlaceholderAdorner1(UIElement ele, string placeholder)
 2     : base(ele)
 3 {
 4     _placeholder = placeholder;
 5     var dpd = DependencyPropertyDescriptor.FromProperty(UIElement.VisibilityProperty, typeof(UIElement));
 6     dpd.AddValueChanged(ele, new EventHandler((s1, e1) =>
 7     {
 8         this.InvalidateVisual();
 9     }));
10 }

 

测试发现,上述问题确实已经解决。
当然,除了可以在装饰器中重绘一次,还可以直接在属性改变时直接删除该装饰器。

 1 var dpd = DependencyPropertyDescriptor.FromProperty(UIElement.VisibilityProperty, typeof(UIElement));
 2 dpd.AddValueChanged(txt, new EventHandler((s1, e1) =>
 3 {
 4     bool isShow = txt.Text.Length == 0 && txt.Visibility == Visibility.Visible;
 5     var lay = AdornerLayer.GetAdornerLayer(txt);
 6     if (lay == null) return;
 7     if (isShow)
 8     {
 9         lay.Add(new PlaceholderAdorner1(txt, e.NewValue.ToString()));
10     }
11     else
12     {
13         Adorner[] ar = lay.GetAdorners(txt);
14         if (ar != null)
15         {
16             for (int i = 0; i < ar.Length; i++)
17             {
18                 if (ar[i] is PlaceholderAdorner1)
19                 {
20                     lay.Remove(ar[i]);
21                 }
22             }
23         }
24     }
25 }));

 

这段代码当然直接可以写在OnPlaceholder2Changed事件处理函数中。

装饰器的另一种实现方式


由于某次手动生成触发器的时候,发现注册属性改变通知和触发器还是有一定的不同的(具体问题有时间再提)。
所以对于这种实现方式总是心有余悸。
所幸装饰器除了使用OnRender方法,还有别的实现方式。

 1 public class PlaceholderAdorner2 : Adorner
 2 {
 3     private VisualCollection _visCollec;
 4     private TextBlock _tb;
 5     private TextBox _txt;
 6     public PlaceholderAdorner2(UIElement ele, string placeholder)
 7         : base(ele)
 8     {
 9         _txt = ele as TextBox;
10         if (_txt == null) return;
11         Binding bd = new Binding("IsVisible");
12         bd.Source = _txt;
13         bd.Mode = BindingMode.OneWay;
14         bd.Converter = new BoolToVisibilityConverter();
15         this.SetBinding(TextBox.VisibilityProperty, bd);
16         _visCollec = new VisualCollection(this);
17         _tb = new TextBlock();
18         _tb.Style = null;
19         _tb.FontSize = _txt.FontSize;
20         _tb.FontFamily = _txt.FontFamily;
21         _tb.FontWeight = _txt.FontWeight;
22         _tb.FontStretch = _txt.FontStretch;
23         _tb.FontStyle = FontStyles.Italic;
24         _tb.Foreground = Brushes.Gray;
25         _tb.Text = placeholder;
26         _tb.IsHitTestVisible = false;
27         _visCollec.Add(_tb);
28     }
29     protected override int VisualChildrenCount
30     {
31         get
32         {
33             return _visCollec.Count;
34         }
35     }
36     protected override Size ArrangeOverride(Size finalSize)
37     {
38         _tb.Arrange(new Rect(new Point(4, 2), finalSize));
39         return finalSize;
40     }
41     protected override Visual GetVisualChild(int index)
42     {
43         return _visCollec[index];
44     }
45 }

 

代码不难理解,就是给TextBox上面又放了一个TextBlock,并且把TextBlock的Visibility属性和TextBox的Visibility属性绑定。

最后附上文中demo下载:https://files.cnblogs.com/nanqi/NanQi.Controls.Placeholder.zip

end.

posted on 2013-06-03 08:57  南琦  阅读(4015)  评论(1编辑  收藏  举报