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

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

WPF进阶技巧和实战07--自定义元素02的更多相关文章

  1. WPF进阶技巧和实战07--自定义元素01

    完善和扩展标准控件的方法: 样式:可使用样式方便地重用控件属性的集合,甚至可以使用触发器应用效果 内容控件:所有继承自ContentControl类的控件都支持嵌套的内容.使用内容控件,可以快速创建聚 ...

  2. WPF进阶技巧和实战03-控件(3-文本控件及列表控件)

    系列文章链接 WPF进阶技巧和实战01-小技巧 WPF进阶技巧和实战02-布局 WPF进阶技巧和实战03-控件(1-控件及内容控件) WPF进阶技巧和实战03-控件(2-特殊容器) WPF进阶技巧和实 ...

  3. WPF进阶技巧和实战03-控件(4-基于范围的控件及日期控件)

    系列文章链接 WPF进阶技巧和实战01-小技巧 WPF进阶技巧和实战02-布局 WPF进阶技巧和实战03-控件(1-控件及内容控件) WPF进阶技巧和实战03-控件(2-特殊容器) WPF进阶技巧和实 ...

  4. WPF进阶技巧和实战06-控件模板

    逻辑树和可视化树 System.Windows.LogicalTreeHelper System.Windows.Media.VisualTreeHelper 逻辑树类(LogicalTreeHelp ...

  5. WPF进阶技巧和实战08-依赖属性与绑定01

    依赖项属性 定义依赖项属性 注意:只能为依赖对象(继承自DependencyObject的类)添加依赖项属性.WPF中的元素基本上都继承自DependencyObject类. 静态字段 名称约定(属性 ...

  6. WPF进阶技巧和实战08-依赖属性与绑定02

    将元素绑定在一起 数据绑定最简单的形式是:源对象是WPF元素而且源属性是依赖项属性.依赖项属性内置了更改通知支持,当源对象中改变依赖项属性时,会立即更新目标对象的绑定属性. 元素绑定到元素也是经常使用 ...

  7. WPF进阶技巧和实战09-事件(1-路由事件、鼠标键盘输入)

    理解路由事件 当有意义的事情发生时,有对象(WPF的元素)发送的用于通知代码的消息,就是事件的核心思想.WPF通过事件路由的概念增强了.NET事件模型.事件由允许源自某个元素的事件由另一个元素引发.例 ...

  8. WPF进阶技巧和实战09-事件(2-多点触控)

    多点触控输入 多点触控输入和传统的基于比的输入的区别是多点触控识别手势,用户可以移动多根手指以执行常见的操作,放大,旋转,拖动等. 多点触控的输入层次 WPF允许使用键盘和鼠标的高层次输入(例如单击和 ...

  9. WPF进阶技巧和实战03-控件(5-列表、树、网格02)

    数据模板 样式提供了基本的格式化能力,但是不管如何修改ListBoxItem,他都不能够展示功能更强大的元素组合,因为了每个ListBoxItem只支持单个绑定字段(通过DisplayMemberPa ...

随机推荐

  1. java线程池 面试题(精简)

    什么是线程池? 线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理. 如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线 ...

  2. WebAPI 自定义过滤

    自定义filter 类过滤 ------------------------------------------------------------------------- public class ...

  3. vim conf文件配色

    VIM conf文件配色 一.配置文件 1.下载Nginx配置文件的语法文件:nginx.vim wget http://www.vim.org/scripts/download_script.php ...

  4. ubuntu下配置JDK的一些坑点

    ubuntu下配置JDK的一些坑点 在centos下的JDK配置: 在ubuntu下的话,要修改两个地方: 在/etc/enviornment中配置! 在/etc/profile中配置! 写在最后: ...

  5. Struts中整合的强大Ognl学习(一)

    测试使用了一个JavaBean的User,User中的Address单独封装再形成了一个JavaBean: 为了测试静态方法和静态变量调用,写了一个Util方法: 因为测试Ognl功能过多所以直接使用 ...

  6. sparksql的三种join实现

    join 是sql语句中的常用操作,良好的表结构能够将数据分散在不同的表中,使其符合某种范式,减少表冗余,更新容错等.而建立表和表之间关系的最佳方式就是Join操作. sparksql作为大数据领域的 ...

  7. SpringBoot笔记(1)

    一.Spring能做什么 微服务 响应式开发 分布式云开发 web开发 无服务开发(云) 批处理业务等 二.SpringBoot作用 能快速创建出生产级别的Spring应用 SpringBoot是整合 ...

  8. Java特性和优势

    Java特性和优势 简单性 面向对象性 可移植性 高性能 分布式 动态性 多线程 安全性 健壮性

  9. SciPy笔记

    一.简介 SciPy 是一个开源的 Python 算法库和数学工具包.Scipy 是基于 Numpy 的科学计算库,用于数学.科学.工程学等领域,很多有一些高阶抽象和物理模型需要使用 Scipy.Sc ...

  10. Ordering the Soldiers 题解

    CodeChef:ORDERS 简化题意: \(n\) 个人排队,给定每个人需要向左移动几个,求最终排列. 即还原逆序对. 错误想法 既然知道每个人向左移动 \(a_i\) 个,那就相当于让他的排名 ...