导语:
以前写文本框这些控件都会添加个描述,但每次都写一个TextBlock太难受了,还得注意对齐,所以就有了这篇文章。
1.先看实现效果
2.再看实现方法
(1)装饰器
WPF有个装饰器的功能,可以在UIElement上叠加一层装饰器用来显示绘制内容。见装饰器概述 - WPF .NET Framework | Microsoft Learn
代码如下
class DescriptionAdorner : Adorner
{
private string Description { get; set; }
private int FonrSize { get; set; }
private FormattedText formattedText { get; set; }
private double TextXOffset { get; set; }
private double TextYOffset { get; set; }
public PlacementMode Placement { get; set; }
public DescriptionAdorner(UIElement adornedElement, string description, int fontsize = 12)
: base(adornedElement)
{
Description = description;
FonrSize = fontsize;
formattedText = new FormattedText(
Description,
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface("Verdana"),
FonrSize,
Brushes.Black);
TextXOffset = 10;
TextYOffset = 0;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Rect adornedElementRect = new Rect(this.AdornedElement.RenderSize);
// Some arbitrary drawing implements.
SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green);
renderBrush.Opacity = 0.2;
Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1.5);
double renderRadius = 5.0;
if (string.IsNullOrEmpty(Description)) return;
var textWidth = formattedText.WidthIncludingTrailingWhitespace;
var textHeight = formattedText.Height;
double textX = 0; double textY = 0;
switch (Placement)
{
case PlacementMode.Left:
textX = adornedElementRect.Left - (textWidth + TextXOffset);
textY = adornedElementRect.Top + adornedElementRect.Height / 2 - textHeight / 2 + TextYOffset;
break;
case PlacementMode.Top:
textX = adornedElementRect.Left + TextXOffset;
textY = adornedElementRect.Top - textHeight + TextYOffset;
break;
case PlacementMode.Right:
textX = adornedElementRect.Right + TextXOffset;
textY = adornedElementRect.Top + adornedElementRect.Height / 2 - textHeight / 2 + TextYOffset;
break;
case PlacementMode.Bottom:
textX = adornedElementRect.Left + TextXOffset;
textY = adornedElementRect.Bottom + TextYOffset;
break;
default:
break;
}
drawingContext.DrawText(formattedText, new Point(textX, textY));
//drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopLeft, renderRadius, renderRadius);
//drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.TopRight, renderRadius, renderRadius);
//drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomLeft, renderRadius, renderRadius);
//drawingContext.DrawEllipse(renderBrush, renderPen, adornedElementRect.BottomRight, renderRadius, renderRadius);
}
}
上面的代码在Onrender中将Description文本直接绘制到了控件的对应位置,并通过Placement实现了上下左右四个方向的排列。
但光装饰器并不能很好的实现我的需求,所以又想到了附加属性。
(2)附加属性
代码如下
public class AttachDescriptionAttrib : UIElement
{
public static bool GetIsShowDescription(DependencyObject obj)
{
return (bool)obj.GetValue(IsShowDescriptionProperty);
}
public static void SetIsShowDescription(DependencyObject obj, bool value)
{
obj.SetValue(IsShowDescriptionProperty, value);
}
public static string GetDescription(DependencyObject obj)
{
return (string)obj.GetValue(DescriptionProperty);
}
public static void SetDescription(DependencyObject obj, string value)
{
obj.SetValue(DescriptionProperty, value);
}
public static int GetFontSize(DependencyObject obj)
{
return (int)obj.GetValue(FontSizeProperty);
}
public static void SetFontSize(DependencyObject obj, int value)
{
obj.SetValue(FontSizeProperty, value);
}
public static PlacementMode GetPlacement(DependencyObject obj)
{
return (PlacementMode)obj.GetValue(PlacementProperty);
}
public static void SetPlacement(DependencyObject obj, PlacementMode value)
{
obj.SetValue(PlacementProperty, value);
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.RegisterAttached("Placement", typeof(PlacementMode), typeof(AttachDescriptionAttrib), new PropertyMetadata(PlacementMode.Left, new PropertyChangedCallback(ReloadAdorner)));
// Using a DependencyProperty as the backing store for FontSize. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.RegisterAttached("FontSize", typeof(int), typeof(AttachDescriptionAttrib), new PropertyMetadata(12, new PropertyChangedCallback(ReloadAdorner)));
// Using a DependencyProperty as the backing store for Description. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.RegisterAttached("Description", typeof(string), typeof(AttachDescriptionAttrib), new PropertyMetadata(string.Empty, new PropertyChangedCallback(ReloadAdorner)));
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsShowDescriptionProperty =
DependencyProperty.RegisterAttached("IsShowDescription", typeof(bool), typeof(AttachDescriptionAttrib), new UIPropertyMetadata(false, new PropertyChangedCallback(ReloadAdorner)));
private static void Elemt_Loaded(object sender, RoutedEventArgs e)
{
SetDesctiptionAdorner(sender as FrameworkElement, GetDescription(sender as DependencyObject), GetFontSize(sender as DependencyObject)
, GetPlacement(sender as DependencyObject));
}
public static void ReloadAdorner(DependencyObject d,DependencyPropertyChangedEventArgs e)
{
var elemt = d as FrameworkElement;
if (GetIsShowDescription(d) == false)
{
ClearDescriptionAdorner(elemt);
return;
}
if (elemt.IsLoaded)
SetDesctiptionAdorner(elemt, GetDescription(d), GetFontSize(d)
, GetPlacement(d));
else elemt.Loaded += Elemt_Loaded;
}
public static void SetDesctiptionAdorner(UIElement uIElement, string desc, int fontSize, PlacementMode placement)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(uIElement);
ClearDescriptionAdorner(uIElement);
adornerLayer.Add(new DescriptionAdorner(uIElement, desc, fontSize) { Placement = placement });
}
public static void ClearDescriptionAdorner(UIElement uIElement)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(uIElement);
if (adornerLayer == null) return;
var adorners = adornerLayer.GetAdorners(uIElement);
for (int i = 0; i < adorners?.Length; i++)
{
if (adorners[i] is DescriptionAdorner)
adornerLayer.Remove(adorners[i]);
}
}
}
添加装饰器的方法是SetDesctiptionAdorner,其他的附加属性只是为了更多的可控参数。
需要注意的是控件在Loaded后才能获取到AdornerLayer,所以在ReloadAdorner入口处给UIElement添加了Loaded事件回调来添加装饰器。
同时我们可能在控件加载完毕后改变属性,所以用IsLoaded来判断是否需要重新添加装饰器。
同时在SetDesctiptionAdorner中,添加装饰器前我们需要清理一下旧的装饰器,使用ClearDescriptionAdorner来完成。
代码很简单,主要是思路问题,可供参考。