一、开发背景
在实际开发工作中,常常会用到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)
{
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)
{
DrawGridLine(
context,
0.0, _currentOffsetY,
_lastArrangeSize.Width, _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每个单元格的布局参数,再遍历每个子元素,用合并单元格的思路来绘制内部边框。