在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中的静态内部类

    静态内部类是 static 修饰的内部类,这种内部类的特点是: 1. 静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问 2. 如果外部类的静态成员与内部类的成 ...

  2. 多台服务器共享session问题(2)

    多台服务器共享session问题  转载自:https://www.cnblogs.com/lingshao/p/5580287.html 在现在的大型网站中,如何实现多台服务器中的session数据 ...

  3. Docker与数据:三种挂载方式

    操作系统与存储 操作系统中将存储定义为 Volume(卷) ,这是对物理存储的逻辑抽象,以达到对物理存储提供有弹性的分割方式.另外,将外部存储关联到操作系统的动作定义为 Mount(挂载). Dock ...

  4. Java数八大据类型的拓展

    public class 数据类型拓展问题 { public static void main(String[] args) { //================================= ...

  5. 【算法】使用Golang实现加权负载均衡算法

    背景描述 如下图所示,负载均衡做为反向代理,将请求方的请求转发至后端的服务节点,实现服务的请求. 在nginx中可以通过upstream配置server时,设置weight表示对应server的权重. ...

  6. 硕盟type-c转接头|四合一多功能扩展坞

    硕盟SM-T54是一款 TYPE C转HDMI+VGA+USB3.0+PD3.0四合一多功能扩展坞,支持四口同时使用,您可以将含有USB 3.1协议的电脑主机,通过此产品连接到具有HDMI或VGA的显 ...

  7. DevExpress Silverlight DXChart特效总结

    1.  主题修改 引用  xmlns:core=http://schemas.devexpress.com/winfx/2008/xaml/core 在Grid中添加core:ThemeManager ...

  8. Ubuntu中类似QQ截图的截图工具并实现鼠标右键菜单截图

    @ 目录 简介: 安装: 设置快捷键: 实现鼠标右键菜单截图: 简介: 在Windows中用惯了强大易用的QQ截图,会不习惯Ubuntu中的截图工具. 软件名为火焰截图,功能类似QQ截图,可以设置快捷 ...

  9. 截断误差VS舍入误差

     截断误差:是指计算某个算式时没有精确的计算结果,如积分计算,无穷级数计算等,使用极限的形式表达的,显然我们只能截取有限项进行计算,此时必定会有误差存在,这就是截断误差. 舍入误差:是指由于计算机表示 ...

  10. AS插件快速生成javabean

    https://blog.csdn.net/u010227042/article/details/103803198