2013年(21)
分类: C#/.net
2013-07-12 16:31:35
Adorner Example – TextBox Watermark
Topic: Adorner Example – TextBox Watermark
标题: Adorner 示例 – TextBox 水印
本文章将会向你展示如何通过Adorner给一个adorned对象动态的增加和修改属性。在本文中,我们将采用Adoner给一个TextBox增加水印效果。
this will introduce you some technique that you might want to use to use Adorner to dynamically add/change the Properties/Contents of adorned elements.
In this example, we will use the Adorner to Adorn on the TextBox to give some default Watermark effect.
首先,我们创建一个TextBoxBehavior,通过它,我们给一个Textbox增加水印效果。
First, we shall create the necessary TextBoxBehavior, with which we can apply the adorner to one Textbox.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using TestWatermarkTextBoxAdorner.Adorners;
public static class TextBoxBehavior
{
#region Fields
// Using a DependencyProperty as the backing store for Watermark. This enables animation, styling, binding, etc...
public static readonly DependencyProperty WatermarkProperty =
DependencyProperty.RegisterAttached("Watermark", typeof(object), typeof(TextBoxBehavior), new UIPropertyMetadata(null, new PropertyChangedCallback(OnWatermarkChanged)));
// Using a DependencyProperty as the backing store for WatermarkForeground. This enables animation, styling, binding, etc...
public static readonly DependencyProperty WatermarkForegroundProperty =
DependencyProperty.RegisterAttached("WatermarkForeground", typeof(Brush), typeof(TextBoxBehavior), new UIPropertyMetadata(null, new PropertyChangedCallback(OnWatermarkForegroundChanged)));
#endregion Fields
#region Dependency Properties
public static object GetWatermark(DependencyObject obj)
{
return (object)obj.GetValue(WatermarkProperty);
}
public static void SetWatermark(DependencyObject obj, object value)
{
obj.SetValue(WatermarkProperty, value);
}
public static Brush GetWatermarkForeground(DependencyObject obj)
{
return (Brush)obj.GetValue(WatermarkForegroundProperty);
}
public static void SetWatermarkForeground(DependencyObject obj, Brush value)
{
obj.SetValue(WatermarkForegroundProperty, value);
}
#endregion Dependnecy Properties
#region Private methods
private static void OnWatermarkChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs changeArgs)
{
Control control = (Control)dependencyObject;
control.Loaded += new RoutedEventHandler(TextBoxBehavior.ControlLoaded);
if (dependencyObject is ComboBox || dependencyObject is TextBox)
{
control.GotKeyboardFocus += new KeyboardFocusChangedEventHandler(TextBoxBehavior.ControlGetKeyboardFocus);
control.LostFocus += new RoutedEventHandler(TextBoxBehavior.ControlLoaded);
}
if (!(dependencyObject is ItemsControl) || dependencyObject is ComboBox) return;
// the rest is not import as for now
}
private static void OnWatermarkForegroundChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs changeArgs)
{
Control control = (Control)dependencyObject;
if (control != null)
{
SetWatermarkForeground(control, TextBoxBehavior.GetWatermarkForeground(control));
}
}
private static void ControlLoaded(object sender, RoutedEventArgs e)
{
Control control = (Control)sender;
if (!TextBoxBehavior.ShouldShowWatermark(control)) return;
TextBoxBehavior.ShowWatermark(control);
}
private static bool ShouldShowWatermark(Control control)
{
if (control is ComboBox) return (control as ComboBox).Text == string.Empty;
if (control is TextBox) return (control as TextBox).Text == string.Empty;
if (control is ItemsControl) return (control as ItemsControl).Items.Count == 0;
else return false;
}
private static void RemoveWatermark(UIElement control)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer((Visual)control);
if (adornerLayer == null) return;
Adorner[] adorners = adornerLayer.GetAdorners(control);
if (adorners == null)
{
return;
}
foreach (var adorner in adorners)
{
if (adorner is WatermarkAdorner)
{
adorner.Visibility = Visibility.Hidden;
adornerLayer.Remove(adorner); // dangerous
}
}
}
private static void ShowWatermark(Control control)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer((Visual)control);
if (adornerLayer == null) return;
adornerLayer.Add((Adorner)new WatermarkAdorner((UIElement)control, TextBoxBehavior.GetWatermark((DependencyObject)control)));
if (TextBoxBehavior.GetWatermarkForeground(control) != null)
SetWatermarkForeground(control, GetWatermarkForeground(control));
}
private static void SetWatermarkForeground(Control control, Brush brush)
{
if (brush == null) throw new ArgumentNullException("brush");
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer((Visual)control);
if (adornerLayer == null) return;
Adorner[] adorners = adornerLayer.GetAdorners(control);
if (adorners != null)
{
foreach (Adorner adorner in adornerLayer.GetAdorners(control))
{
if (adorner is WatermarkAdorner)
{
((WatermarkAdorner)adorner).WatermarkForeground = brush;
break;
}
}
}
}
private static void ControlGetKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Control control = (Control)sender;
if (!TextBoxBehavior.ShouldShowWatermark(control)) return;
TextBoxBehavior.RemoveWatermark((UIElement)control);
}
#endregion Private Methods
}
关键点是在两个attached properties, 一个是Watermark and 另一个是WatermarkForeground.
在Watermark的PropertyChangedCallback回调中,我们会侦听TextBox的ControlGetKeyboardFocus 、LostFocus和Loaded事件。在Loaded事件的处理方法中,最基本的是测试是否可以显示水印,如果可以,显示。
The important things here are the two Attached Properties, one is called the “Watermark” and the other is called the “WatermarkForeground”.
One the PropertyChangedCallback of property “Watermark”, it try to attach event handler for the TextBox’s ControlGetKeyboardFocus and LostFocus as well as handler when the control is loaded.
On the ControlLoaded event handler, it basically do: check if the Control should show Watermark, and if it is does show, try to make it ShowWaterark..
代码的关键点ShowWatermark,它
The core part is in the ShowWatermark, which basically
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer((Visual)control);
然后 加入“WatermarkAdorner”到 AdornerLayer
And add the Custom Adorner “WatermarkAdorner” to the AdornerLayer.
adornerLayer.Add((Adorner)new WatermarkAdorner((UIElement)control, TextBoxBehavior.GetWatermark((DependencyObject)control)));
Watermark Adorner 层的数据如下
Here is the definition for the Watermark Adorner layer.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
public class WatermarkAdorner : Adorner
{
#region Fields
private readonly ContentPresenter _contentPresenter;
private Brush _watermarkForeground = null;
#endregion Fields
private Control Control
{
get
{
return (Control)this.AdornedElement;
}
}
public Brush WatermarkForeground
{
get
{
return _watermarkForeground;
}
set
{
_watermarkForeground = value;
if (_contentPresenter != null)
{
TextElement.SetForeground(_contentPresenter, _watermarkForeground);
}
}
}
#region Constructor
public WatermarkAdorner(UIElement adornedElement, object watermark)
: base(adornedElement)
{
this.IsHitTestVisible = false; // should not accept focus or Hit test
ContentPresenter contententPresenter1 = new ContentPresenter();
contententPresenter1.Content = watermark;
contententPresenter1.Opacity = 0.5; // makes this to be some kinds of configurable one
//Thickness thickness = this.Control.Margin;
double left = this.Control.Margin.Left + this.Control.Padding.Left + 4.0;
double top = this.Control.Margin.Top + this.Control.Padding.Top + 3.0;
double right = 0;
double bottom = 0;
Thickness thickness = new Thickness(left, top, right, bottom);
this._contentPresenter = contententPresenter1;
this.Margin = thickness;
if (this.Control is ItemsControl && !(this.Control is ComboBox))
{
this._contentPresenter.VerticalAlignment = VerticalAlignment.Center;
this._contentPresenter.HorizontalAlignment = HorizontalAlignment.Center;
}
Binding binding = new Binding("IsVisible")
{
Source = (object) adornedElement,
Converter = (IValueConverter) new BooleanToVisibilityConverter()
};
this.SetBinding(UIElement.VisibilityProperty, (BindingBase)binding);
}
#endregion Constructor
#region Override
// this is not necessary?
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index) // replace the Visual of the TextBox with the visual of the _contentPresenter;
{
return (Visual)this._contentPresenter;
}
protected override Size MeasureOverride(Size constraint)
{
this._contentPresenter.Measure(this.Control.RenderSize); // delegate the measure override to the ContentPresenter's Measure
return this.Control.RenderSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
this._contentPresenter.Arrange(new Rect(finalSize));
return finalSize;
}
#endregion Override
}
还有比较重要的地方在于该Adorner 层返回一个“ContentPresenter”给WPF visual element 系统。
The key part here is that it returns a “ContentPresenter” and this content presenter will be the one that get returned when inquired by the WPF visual element system.
所以, 如下的两个重载
So, the following two override from Adorner implementations.
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index) // replace the Visual of the TextBox with the visual of the _contentPresenter;
{
return (Visual)this._contentPresenter;
}
告知WPF visual element 系统该Adorner层只有一个子元素,且子元素是内部维护的“ContentPresenter”。
Tells the WPF visual tree system that the Adorner layer has one child, and the child is “ContentPresenter” that we have kept internally.
但是, 为了正确的显示ContentPresenter,我们必须告诉WPF visual element 系统如何measure 和arrange内容。
However, in order to render the ContentPresenter correctly, we have tell the WPF control system how we are going to measure and arrange the content.
protected override Size MeasureOverride(Size constraint)
{
this._contentPresenter.Measure(this.Control.RenderSize); // delegate the measure override to the ContentPresenter's Measure
return this.Control.RenderSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
this._contentPresenter.Arrange(new Rect(finalSize));
return finalSize;
}
这里做的是,当系统WPF询问Adorner层的大小的时候, 我们实际上用“ContentPresenter”作了measure 和arrange。
What is done here is that when the systems make inquiry on the size/arrange of the Adorner layer, the “ContentPresenter” is rendered with the desired Size and actual size hereby.
可以想到的一个改进是如果控件有style,那么水印颜色太难读了这样的话 ,我们可以使contentPresenter的前景色和修饰的控件的前景色一致。
An enhancement that we can think of is that for those controls that are styled, the default text’s foreground may be too illegible to read, you may want to make the contentPresenter’s foreground the same as the one used for the control.
var foregroundBinding = new Binding("Foreground")
{
Source = adornedElement,
Path = new PropertyPath("Foreground")
};
_contentPresenter.SetBinding(TextElement.ForegroundProperty, foregroundBinding);
这样是用该代码。
To use the code, here is the example.
xmlns=""
xmlns:x=""
xmlns:behavior="clr-namespace:TestWatermarkTextBoxAdorner.Behaviors"
Title="MainWindow"
Height="350" Width="525">
behavior:TextBoxBehavior.Watermark="Input user's name"
behavior:TextBoxBehavior.WatermarkForeground="Red"
TabIndex="0"
Grid.Row="0"
/>
引用:
References: