系列文章鏈接
- WPF進階技巧和實戰01-小技巧
- WPF進階技巧和實戰02-布局
- WPF進階技巧和實戰03-控件(1-控件及內容控件)
- WPF進階技巧和實戰03-控件(2-特殊容器)
- WPF進階技巧和實戰03-控件(5-列表、樹、網格01)
- WPF進階技巧和實戰03-控件(5-列表、樹、網格02)
- WPF進階技巧和實戰03-控件(5-列表、樹、網格03)
- WPF進階技巧和實戰03-控件(5-列表、樹、網格04)
- WPF進階技巧和實戰04-資源
- WPF進階技巧和實戰05-樣式與行為
- WPF進階技巧和實戰06-控件模板
- WPF進階技巧和實戰07--自定義元素01
- WPF進階技巧和實戰07--自定義元素02
- WPF進階技巧和實戰08-依賴屬性與綁定01
- WPF進階技巧和實戰08-依賴屬性與綁定02
- WPF進階技巧和實戰08-依賴屬性與綁定03
在01節中,研究了如何開發自定義控件,下節開始考慮更特殊的選擇:派生自定義面板以及構建自定義繪圖
創建自定義面板
創建自定義面板是一種比較常見的自定義控件開發子集,面板可以駐留一個或多個子元素,並且實現了特定的布局邏輯以恰當地安排子元素。常見的基本類型的面板:StackPanel、DockPanel、WrapPanel、Canvas,Grid,TabPanel,ToolBarPverflowPanel,VirtualizingPanel。
兩步布局過程
每個面板都有相同的功能:負責改變子元素尺寸和安排子元素的兩步布局過程。第一個階段是測量階段,這個階段決定其子元素希望具有多大的尺寸。第二個階段是排列階段,這個階段為每個控件指定邊界。
可以通過重寫函數MeasureOverride()和ArrangeOverride(),來添加自己的邏輯。
- MeasureOverride()方法
這個方法決定了每個子元素希望多大的空間。會遍歷子元素集合,並調用每個子元素的Measure()發放來控制子元素的最大可用空間。最后,面板返回所有子元素所需的空間。
public static readonly DependencyProperty DiameterProperty = DependencyProperty.Register(
"Diameter", typeof(double), typeof(FixLampCirclePanel), new FrameworkPropertyMetadata(170.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
public double Diameter
{
get => (double)GetValue(DiameterProperty);
set => SetValue(DiameterProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
if (Children.Count == 0) return new Size(Diameter, Diameter);
var newSize = new Size(Diameter, Diameter);
foreach (UIElement element in Children)
{
element.Measure(newSize);
}
return newSize;
}
元素調用Measure()方法之后才會渲染自身,后續在子元素執行計算時,才會使用DesiredSize屬性來請求尺寸。
- ArrangeOverride()方法
測量完所有尺寸后,就需要排列所有子元素。Arrange()方法來實現這個過程。
public static readonly DependencyProperty KeepVerticalProperty = DependencyProperty.Register(
"KeepVertical", typeof(bool), typeof(FixLampCirclePanel), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure));
public bool KeepVertical
{
get => (bool)GetValue(KeepVerticalProperty);
set => SetValue(KeepVerticalProperty, value);
}
public static readonly DependencyProperty OffsetAngleProperty = DependencyProperty.Register(
"OffsetAngle", typeof(double), typeof(FixLampCirclePanel), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
public double OffsetAngle
{
get => (double)GetValue(OffsetAngleProperty);
set => SetValue(OffsetAngleProperty, value);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (base.Children.Count == 0) return finalSize;
//第一個放在中間,第一個移動半徑為0即可,其余的均分布
var perDeg = 360.0 / (Children.Count - 1);
var radius = 0.0;
for (int i = 0; i < Children.Count; i++)
{
if (i != 0) radius = Diameter / 2;
UIElement element = base.Children[i];
var centerX = element.DesiredSize.Width / 2.0;
var centerY = element.DesiredSize.Height / 2.0;
var angle = perDeg * i + OffsetAngle;
var transform = new RotateTransform
{
CenterX = centerX,
CenterY = centerY,
Angle = KeepVertical ? 0 : angle
};
element.RenderTransform = transform;
var r = Math.PI * angle / 180.0;
var x = radius * Math.Cos(r);
var y = radius * Math.Sin(r);
var rectX = x + finalSize.Width / 2 - centerX;
var rectY = y + finalSize.Height / 2 - centerY;
element.Arrange(new Rect(rectX, rectY, element.DesiredSize.Width, element.DesiredSize.Height));
}
return finalSize;
}
Canvas面板的副本
Canvas面板在希望的位置放置子元素,並且為子元素設置他們希望的尺寸。所以不需要計算如何分割可用空間,所以為每個子元素提供無線的空間。同時,返回值是空的Size對象,所以面板是不請求任何空間,而是由您明確地為Canvas面板指定尺寸,或者將其放置到布局容器中進行拉伸以填充整個容器可用的空間。
protected override Size MeasureOverride(Size constraint)
{
Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (UIElement internalChild in base.InternalChildren)
{
internalChild?.Measure(availableSize);
}
return default(Size);
}
ArrangeOverride()方法通過附加屬性(Left,Right,Top,Bottom)來確定每個子元素的位置。
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement internalChild in base.InternalChildren)
{
if (internalChild == null)
{
continue;
}
double x = 0.0;
double y = 0.0;
double left = Canvas.GetLeft(internalChild);
if (!Double.IsNaN(left))
{
x = left;
}
else
{
double right = Canvas.GetRight(internalChild);
if (!Double.IsNaN(right))
{
x = arrangeSize.Width - internalChild.DesiredSize.Width - right;
}
}
double top = Canvas.GetTop(internalChild);
if (!Double.IsNaN(top))
{
y = top;
}
else
{
double bottom = Canvas.GetBottom(internalChild);
if (!Double.IsNaN(bottom))
{
y = arrangeSize.Height - internalChild.DesiredSize.Height - bottom;
}
}
internalChild.Arrange(new Rect(new Point(x, y), internalChild.DesiredSize));
}
return arrangeSize;
}
更好的WrapPanel
在傳統的WrapPanel中添加強制換行的功能,可以通過自定義控件來實現。首先要添加強制換行附加屬性。沒有使用常規屬性封裝器封裝這個屬性,因為不在定義他們的同一個類中設置它,而是使用兩個靜態方法。
public static readonly DependencyProperty LineBreakBeforeProperty =
DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(WrapBreakPanel),
new FrameworkPropertyMetadata() { AffectsArrange = true, AffectsMeasure = true });
public static void SetLineBreakBefore(UIElement element, bool value)
{
element.SetValue(LineBreakBeforeProperty, value);
}
public static bool GetLineBreakBefore(UIElement element)
{
return (bool)element.GetValue(LineBreakBeforeProperty);
}
自定義繪圖元素
在WPF中,這些類位於元素樹的最底層,通過單獨的文本、形狀、位圖來執行渲染。
OnRender()方法
需要執行自定義渲染,就必須重寫OnRender()方法,該方法繼承自UIElement基類。一些空間使用OnRender()方法繪制可視化細節並在其上疊加其他元素形成組合。Border類是OnRender()方法中繪制邊框,Panel類是在OnRender()方法中繪制背景。兩者都支持子內容,並且這些子內容在自定義的繪圖之上進行渲染。
OnRender()方法接收一個DrawingContext對象,使用這個對象進行繪制操作。OnRender()方法中不能顯示的創建和關閉DrawingContext對象,因為幾個不同的OnRender()方法使用相同的DrawingContext對象,在開始繪制時,WPF會自動創建DrawingContext對象,並且當不再需要時自動關閉該對象。
OnRender()方法實際上並沒有繪制在屏幕上,而是繪制在DrawingContext對象上,然后WPF緩存這些信息。WPF來決定何時需要重新繪制並使用DrawingContext對象創建內容。WPF無縫地管理繪制和刷新的過程,由用戶來定義內容。
自定義繪圖元素
下面的例子通過RadialGradientBrush畫刷繪制陰影背景,中心點跟隨鼠標移動。
public class CustomDrawnElement : FrameworkElement
{
public Color BackgroundColor { get => (Color)GetValue(BackgroundColorProperty); set => SetValue(BackgroundColorProperty, value); }
public static readonly DependencyProperty BackgroundColorProperty =
DependencyProperty.Register("BackgroundColor", typeof(Color), typeof(CustomDrawnElement),
new FrameworkPropertyMetadata(Colors.Yellow) { AffectsRender = true });
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
this.InvalidateVisual();
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
this.InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Rect rect = new Rect(0, 0, base.ActualWidth, ActualHeight);
drawingContext.DrawRectangle(GetForegroundBrush(), null, rect);
}
private Brush GetForegroundBrush()
{
if (!IsMouseOver)
{
return new SolidColorBrush(BackgroundColor);
}
else
{
RadialGradientBrush brush = new RadialGradientBrush(Colors.White, BackgroundColor);
Point point = Mouse.GetPosition(this);
Point newPoint = new Point(point.X / base.ActualWidth, point.Y / base.ActualHeight);
brush.GradientOrigin = newPoint;
brush.Center = newPoint;
return brush;
}
}
}
創建自定義元素
在WPF中,切記不要再控件中進行自定義繪圖,會破壞WPF無外觀控件的原則。一旦使用了繪圖邏輯,就會使得控件的可視化外觀不能通過控件模板來定制。
更好的方法是設計單獨的繪制自定義內容的元素,然后再控件的默認模板內部使用自定義元素。
