WPF進階技巧和實戰07--自定義元素02


系列文章鏈接

在01節中,研究了如何開發自定義控件,下節開始考慮更特殊的選擇:派生自定義面板以及構建自定義繪圖

創建自定義面板

創建自定義面板是一種比較常見的自定義控件開發子集,面板可以駐留一個或多個子元素,並且實現了特定的布局邏輯以恰當地安排子元素。常見的基本類型的面板:StackPanel、DockPanel、WrapPanel、Canvas,Grid,TabPanel,ToolBarPverflowPanel,VirtualizingPanel。

兩步布局過程

每個面板都有相同的功能:負責改變子元素尺寸和安排子元素的兩步布局過程。第一個階段是測量階段,這個階段決定其子元素希望具有多大的尺寸。第二個階段是排列階段,這個階段為每個控件指定邊界。

可以通過重寫函數MeasureOverride()和ArrangeOverride(),來添加自己的邏輯。

  1. 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屬性來請求尺寸。

  1. 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無外觀控件的原則。一旦使用了繪圖邏輯,就會使得控件的可視化外觀不能通過控件模板來定制。

更好的方法是設計單獨的繪制自定義內容的元素,然后再控件的默認模板內部使用自定義元素。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM