zoukankan      html  css  js  c++  java
  • 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无外观控件的原则。一旦使用了绘图逻辑,就会使得控件的可视化外观不能通过控件模板来定制。

    更好的方法是设计单独的绘制自定义内容的元素,然后再控件的默认模板内部使用自定义元素。

  • 相关阅读:
    硬件基础---拆装机
    Dynamics CRM 2015 Update 1 系列(4): 自己定义主键
    Heroku第三方服务接入指南(二)
    ssh 免密登录
    CentOS 7 NAT软路由
    Nginx升级
    挖矿病毒
    安装 epel-release
    hydra 安装和使用
    Linux 安全信息查看
  • 原文地址:https://www.cnblogs.com/vigorous/p/15185263.html
Copyright © 2011-2022 走看看