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 }
基本存在以下几个问题:
- 获得焦点取消了占位符,与需求(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 }
可以看到,附加属性只是对于装饰器的删除和添加,别无其他。
运行看看效果,都还不错,第一种实现所出现的问题也都能解决,但是随即发现一个问题:
- 当隐藏(设置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.