Avalonia开发实践(二)——开发带边框的Grid

一、开发背景

在实际开发工作中,常常会用到Grid进行布局。为了美观考虑,会给每个格子加上边框,如下图:

原生的Grid虽然有ShowGridLines属性可以控制显示格子之间的线,但线的样式不能定义,可以说此功能非常鸡肋。接下来我们自己动手实现Grid中的网格线!

二、设计思路

虽然Grid自带的格子线非常拉胯,但它的实现方式为我们提供了宝贵的思路。

首先,Grid继承自Panel,而在Panel中,Render方法已经密封了,所以想在Grid中利用Render方法进行边框绘制这条路就走不通了。

官方的做法是,定义一个GridLinesRenderer,继承自Control。将GridLinesRenderer添加到Grid的VisualChildren中,在GridLinesRenderer的Render方法中实现对Grid的边框绘制。

三、实现过程

1、定义边框绘制类

/// <summary>
/// GridLineStyle
/// </summary>
internal class GridLinesRenderer : Control
{
    private Pen _borderPen;
    private Size _lastArrangeSize;

    public override void Render(DrawingContext context)
    {
        base.Render(context);
        var grid = this.GetVisualParent<Grid>();
        if (grid == null || !grid.ShowGridLines)
            return;

        if (_borderPen == null)
        {
            _borderPen = new Pen(grid.GridLineBrush, grid.GridLineWidth, lineCap: PenLineCap.Round);
        }
        else
        {
            _borderPen.Brush = grid.GridLineBrush;
            _borderPen.Thickness = grid.GridLineWidth;
        }

        // 获取行高、列宽数据
        var rowHeightArr = new double[Math.Max(grid.RowDefinitions.Count, 1)];
        var colWidthArr = new double[Math.Max(grid.ColumnDefinitions.Count, 1)];
        if (grid.RowDefinitions.Count == 0)
        {
            rowHeightArr[0] = _lastArrangeSize.Height;
        }
        else
        {
            for (int i = 0; i < grid.RowDefinitions.Count; i++)
            {
                rowHeightArr[i] = grid.RowDefinitions[i].ActualHeight;
            }
        }
        if (grid.ColumnDefinitions.Count == 0)
        {
            colWidthArr[0] = _lastArrangeSize.Width;
        }
        else
        {
            for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
            {
                colWidthArr[i] = grid.ColumnDefinitions[i].ActualWidth;
            }
        }

        // 绘制内边框
        var _lastOffsetX = 0d;
        var _currentOffsetX = colWidthArr[0];
        for (int i = 1; i < colWidthArr.Length; ++i)
        {
            if (_lastOffsetX != _currentOffsetX && colWidthArr[i] > 0)
            {
                DrawGridLine(
                    context,
                    _currentOffsetX, 0.0,
                    _currentOffsetX, _lastArrangeSize.Height);
                _lastOffsetX = _currentOffsetX;
                _currentOffsetX += colWidthArr[i];
            }
        }

        var _lastOffsetY = 0d;
        var _currentOffsetY = rowHeightArr[0];
        for (int i = 1; i < rowHeightArr.Length; ++i)
        {
            if (_lastOffsetY != _currentOffsetY && rowHeightArr[i] > 0)
            {
                DrawGridLine(
                    context,
                    0.0, _currentOffsetY,
                    _lastArrangeSize.Width, _currentOffsetY);
                _lastOffsetY = _currentOffsetY;
                _currentOffsetY += rowHeightArr[i];
            }
        }

        // 绘制外边框
        double radiusX = grid.CornerRadius;
        double radiusY = grid.CornerRadius;
        Rect rect = new Rect(_lastArrangeSize).Deflate(grid.GridLineWidth / 2);
        if (radiusX == 0.0 && radiusY == 0.0)
        {
            var rectangleGeometry = new RectangleGeometry(rect);
            context.DrawGeometry(null, _borderPen, rectangleGeometry);
        }
        else
        {
            StreamGeometry streamGeometry = new StreamGeometry();
            using (StreamGeometryContext streamGeometryContext = streamGeometry.Open())
            {
                GeometryHelper.DrawRoundedCornersRectangle(streamGeometryContext, rect, radiusX, radiusY);
            }
            context.DrawGeometry(null, _borderPen, streamGeometry);
        }
    }

    private void DrawGridLine(
        DrawingContext drawingContext,
        double startX,
        double startY,
        double endX,
        double endY)
    {
        var start = new Point(startX, startY);
        var end = new Point(endX, endY);
        drawingContext.DrawGeometry(null, _borderPen, new RectangleGeometry(new Rect(start, end).Deflate(0.5)));
    }

    internal void UpdateRenderBounds(Size arrangeSize)
    {
        _lastArrangeSize = arrangeSize;
        InvalidateVisual();
    }
}

2、自定义Grid,重写ArrangeOverride方法,每次布局变化时触发边框重绘方法

/// <summary>
/// Grid
/// </summary>
public class Grid : Avalonia.Controls.Grid
{
    private GridLinesRenderer _gridLinesRenderer;

    /// <summary>
    /// Defines the <see cref="ShowGridLines"/> property.
    /// </summary>
    public new bool ShowGridLines
    {
        get { return (bool)GetValue(ShowGridLinesProperty); }
        set { SetValue(ShowGridLinesProperty, value); }
    }

    /// <summary>
    /// Defines the <see cref="ShowGridLinesProperty"/> property.
    /// </summary>
    public static readonly new StyledProperty<bool> ShowGridLinesProperty = AvaloniaProperty.Register<Grid, bool>("ShowGridLines");

    /// <summary>
    /// Defines the <see cref="GridLineBrush"/> property.
    /// </summary>
    public IBrush GridLineBrush
    {
        get { return (IBrush)GetValue(GridLineBrushProperty); }
        set { SetValue(GridLineBrushProperty, value); }
    }

    /// <summary>
    /// Defines the <see cref="GridLineBrushProperty"/> property.
    /// </summary>
    public static readonly StyledProperty<IBrush> GridLineBrushProperty = AvaloniaProperty.Register<Grid, IBrush>("GridLineBrush");

    /// <summary>
    /// Defines the <see cref="GridLineWidth"/> property.
    /// </summary>
    public double GridLineWidth
    {
        get { return (double)GetValue(GridLineWidthProperty); }
        set { SetValue(GridLineWidthProperty, value); }
    }

    /// <summary>
    /// Defines the <see cref="GridLineWidthProperty"/> property.
    /// </summary>
    public static readonly StyledProperty<double> GridLineWidthProperty = AvaloniaProperty.Register<Grid, double>("GridLineWidth", 1d);

    /// <summary>
    /// Defines the <see cref="CornerRadius"/> property.
    /// </summary>
    public float CornerRadius
    {
        get { return (float)GetValue(CornerRadiusProperty); }
        set { SetValue(CornerRadiusProperty, value); }
    }

    /// <summary>
    /// Defines the <see cref="CornerRadiusProperty"/> property.
    /// </summary>
    public static readonly StyledProperty<float> CornerRadiusProperty = AvaloniaProperty.Register<Grid, float>("CornerRadius");

    private GridLinesRenderer EnsureGridLinesRenderer()
    {
        if (ShowGridLines && _gridLinesRenderer == null)
        {
            _gridLinesRenderer = new GridLinesRenderer();
            VisualChildren.Add(_gridLinesRenderer);
        }

        if (!ShowGridLines && _gridLinesRenderer != null)
        {
            VisualChildren.Remove(_gridLinesRenderer);
            _gridLinesRenderer = null;
        }
        return _gridLinesRenderer;
    }

    /// <summary>
    /// ArrangeOverride
    /// </summary>
    /// <param name="arrangeSize"></param>
    /// <returns></returns>
    protected override Size ArrangeOverride(Size arrangeSize)
    {
        var size = base.ArrangeOverride(arrangeSize);
        var gridLinesRenderer = EnsureGridLinesRenderer();
        gridLinesRenderer?.UpdateRenderBounds(arrangeSize);
        return size;
    }
}

这样,一个具有边框绘制功能的Grid就完成了。

四、进阶思考

有时Grid并不是所有单元格都用得上的,可能还涉及跨行跨列的情况,这时就需要根据每个子元素的空间占据大小来绘制边框了。我们可以记录Grid每个单元格的布局参数,再遍历每个子元素,用合并单元格的思路来绘制内部边框。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值