感谢 rgqancy 指出的 Bug,已经修正
Give me a renderings first:

Code at the time of use:
<l:GridLineDecorator>
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding Id}"/>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
</GridView>
</ListView.View>
</ListView>
</l:GridLineDecorator>
---------------------Text---------------------------------------
I often see people asking how to have the grid effect when using WPF's ListView. For example, www.bbniu.com/forum/thread-1090-1-1.html
The first solution that can be thought of to solve this problem is to put a Border in the CellTemplate of the GridViewColumn, and then set the BorderBrush and BorderThickness of the Border. For example:
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1" UseLayoutRounding="True">
<TextBlock Text="{Binding Id}"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
However, you will soon find that Border cannot change with the column width, like this:

Moreover, even if the HorizontalContentAlignment of ListView is set to Stretch, it will not work. HorizontalContentAlignment="True" must be set on the ListViewItem. Therefore, you must add a ListViewItem style to uniformly specify:
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
But the problem is still not solved because Border cannot fill the entire Cell, like this:

Therefore, you have to carefully set the margins of each Border so that they are all "just" connected together and look like continuous lines. Maybe adjusting Margin wasn't enough, so I had to modify the template of ListViewItem; after the template was modified, I found that the performance of creating so many Border couldn't keep up with it. The biggest thing is that CellTemplate must be specified once for each Column, in case the color of the border needs to be adjusted uniformly...
Therefore, although this method is feasible, it is actually extremely troublesome to operate.
Is there a way to "draw lines" directly on ListView? Of course, we can write our own ListView and draw lines or something in OnRender, but ideally we can implement this grid drawing function without changing any existing controls. At the same time, it would be better if the color of this grid line could be adjusted at will.
Therefore, the general requirements are as follows:
You can draw a grid
Don't need to change the ListView, or write your own ListView
You can adjust the color of the grid
If you are familiar with design patterns and "add new functions without changing existing code", you should be able to think of decorator patterns immediately. In fact, WPF itself has a Decorator control, and the commonly used Border is a Decorator, which can help the control draw background colors, draw boundaries, etc.
Therefore, wouldn't it be great if you could have such a Decorator and put ListView in it to have the function of drawing lines? However, I don't plan to directly inherit the Decorator to modify it here, because the Decorator provided by WPF is for all UIElements, and we only want to target ListView.
The GridLineDecorator inherits directly from the FrameworkElement and displays its wrapped ListView by overloading VisualChild and LogicalChild-related code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Threading;
namespace ListViewWithLines
{
[ContentProperty("Target")]
public class GridLineDecorator : FrameworkElement
{
private ListView _target;
private DrawingVisual _gridLinesVisual = new DrawingVisual();
private GridViewHeaderRowPresenter _headerRowPresenter = null;
public GridLineDecorator()
{
this.AddVisualChild(_gridLinesVisual);
this.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(OnScrollChanged));
}
#region GridLineBrush
/// <summary>
/// GridLineBrush Dependency Property
/// </summary>
public static readonly DependencyProperty GridLineBrushProperty =
DependencyProperty.Register("GridLineBrush", typeof(Brush), typeof(GridLineDecorator),
new FrameworkPropertyMetadata(Brushes.LightGray,
new PropertyChangedCallback(OnGridLineBrushChanged)));
/// <summary>
/// Gets or sets the GridLineBrush property. This dependency property
/// indicates ....
/// </summary>
public Brush GridLineBrush
{
get { return (Brush)GetValue(GridLineBrushProperty); }
set { SetValue(GridLineBrushProperty, value); }
}
/// <summary>
/// Handles changes to the GridLineBrush property.
/// </summary>
private static void OnGridLineBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GridLineDecorator)d).OnGridLineBrushChanged(e);
}
/// <summary>
/// Provides derived classes an opportunity to handle changes to the GridLineBrush property.
/// </summary>
protected virtual void OnGridLineBrushChanged(DependencyPropertyChangedEventArgs e)
{
DrawGridLines();
}
#endregion
#region Target
public ListView Target
{
get { return _target; }
set
{
if (_target != value)
{
if (_target != null) Detach();
RemoveVisualChild(_target);
RemoveLogicalChild(_target);
_target = value;
AddVisualChild(_target);
AddLogicalChild(_target);
if (_target != null) Attach();
InvalidateMeasure();
}
}
}
private void GetGridViewHeaderPresenter()
{
if (Target == null)
{
_headerRowPresenter = null;
return;
}
_headerRowPresenter = Target.GetDesendentChild<GridViewHeaderRowPresenter>();
}
#endregion
#region DrawGridLines
private void DrawGridLines()
{
if (Target == null) return;
if (_headerRowPresenter == null) return;
var itemCount = Target.Items.Count;
if (itemCount == 0) return;
var gridView = Target.View as GridView;
if (gridView == null) return;
// 获取drawingContext
var drawingContext = _gridLinesVisual.RenderOpen();
var startPoint = new Point(0, 0);
var totalHeight = 0.0;
// 为了对齐到像素的计算参数,否则就会看到有些线是模糊的
var dpiFactor = this.GetDpiFactor();
var pen = new Pen(this.GridLineBrush, 1 * dpiFactor);
var halfPenWidth = pen.Thickness / 2;
var guidelines = new GuidelineSet();
// 画横线
for (int i = 0; i < itemCount; i++)
{
var item = Target.ItemContainerGenerator.ContainerFromIndex(i) as ListViewItem;
if (item != null)
{
var renderSize = item.RenderSize;
var offset = item.TranslatePoint(startPoint, this);
var hLineX1 = offset.X;
var hLineX2 = offset.X + renderSize.Width;
var hLineY = offset.Y + renderSize.Height;
// 加入参考线,对齐到像素
guidelines.GuidelinesY.Add(hLineY + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(hLineX1, hLineY), new Point(hLineX2, hLineY));
drawingContext.Pop();
// 计算竖线总高度
totalHeight += renderSize.Height;
}
}
// 画竖线
var columns = gridView.Columns;
var headerOffset = _headerRowPresenter.TranslatePoint(startPoint, this);
var headerSize = _headerRowPresenter.RenderSize;
var vLineX = headerOffset.X;
var vLineY1 = headerOffset.Y + headerSize.Height;
foreach (var column in columns)
{
var columnWidth = column.GetColumnWidth();
vLineX += columnWidth;
// 加入参考线,对齐到像素
guidelines.GuidelinesX.Add(vLineX + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(vLineX, vLineY1), new Point(vLineX, totalHeight));
drawingContext.Pop();
}
drawingContext.Close();
}
#endregion
#region Overrides to show Target and grid lines
protected override int VisualChildrenCount
{
get { return Target == null ? 1 : 2; }
}
protected override System.Collections.IEnumerator LogicalChildren
{
get { yield return Target; }
}
protected override Visual GetVisualChild(int index)
{
if (index == 0) return _target;
if (index == 1) return _gridLinesVisual;
throw new IndexOutOfRangeException(string.Format("Index of visual child '{0}' is out of range", index));
}
protected override Size MeasureOverride(Size availableSize)
{
if (Target != null)
{
Target.Measure(availableSize);
return Target.DesiredSize;
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Target != null)
Target.Arrange(new Rect(new Point(0, 0), finalSize));
return base.ArrangeOverride(finalSize);
}
#endregion
#region Handle Events
private void Attach()
{
_target.Loaded += OnTargetLoaded;
_target.Unloaded += OnTargetUnloaded;
}
private void Detach()
{
_target.Loaded -= OnTargetLoaded;
_target.Unloaded -= OnTargetUnloaded;
}
private void OnTargetLoaded(object sender, RoutedEventArgs e)
{
if (_headerRowPresenter == null)
GetGridViewHeaderPresenter();
DrawGridLines();
}
private void OnTargetUnloaded(object sender, RoutedEventArgs e)
{
DrawGridLines();
}
private void OnScrollChanged(object sender, RoutedEventArgs e)
{
DrawGridLines();
}
#endregion
}
}
Among them, Target is an attribute of type ListView, and there is also a_guidLinesVisual, which is DrawingVisual used to draw grids. Some people may ask, why not just overload the OnRender method and draw lines in it?
The reason is that if the OnRender method is overloaded to draw lines, when ListView sets the background, it will cover the lines we drew. This is because the background of the control is drawn with a Border placed in the template. The Border is also drawn in OnRender. It is drawn later, and our drawing first will cover the line we drew. At the same time, you will find that when the Column of a ListView changes size, it does not cause the GridLineDecorator to redraw, so the grid lines cannot change synchronously.
In fact, the GetVisualChild overload in GridLineDecorator is also very particular:
protected override Visual GetVisualChild(int index)
{
if (index == 0) return _target;
if (index == 1) return _gridLinesVisual;
throw new IndexOutOfRangeException(string.Format("Index of visual child '{0}' is out of range", index));
}
ListView is returned first, followed by_gridLinesVisual. However, even if you use DrawingVisual, there will be a problem that the Column width changes and cannot be notified of redrawing. There are several ideas to solve this problem:
- Listen for changes in the width of the GridViewColumn
- Monitor CompositionTarget.Rendering events
The first method is not feasible because you cannot find the width change event of the GridViewColumn. The second method is feasible, but it is efficient...
After some research, I finally found a feasible way to listen for the ScrollChanged event of ScrollViewer, because two ScrollViewer are placed inside the ListView, one for displaying the Header and the other for displaying Items. When the width of a Column changes, the ScrollChanged event of ScrollViewer is triggered.
Therefore, in the constructor:
public GridLineDecorator()
{
this.AddVisualChild(_gridLinesVisual);
this.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(OnScrollChanged));
}
The logic of drawing lines is mainly to traverse all Containers (actually ListViewItems), calculate their displacement relative to the GridLineDecorator, calculate the coordinates and lengths of horizontal and vertical lines, and draw lines. There is a lot of code, so you can download it and read it for yourself.
Careful children's shoes may find that sometimes the bottom line is not drawn to the bottom when the ListViewItem display is incomplete. This is because ListView has been Virtualized. You can set Virtualizing StackPanel.IsVirtualizing="False" to force drawing.
Attached code: files.cnblogs.com/RMay/ListViewWithLines.zip
** Webmaster's note: **
The original author wrote it very well, the effect is good, and the amount of data is small, such as thousands of items. The above solution is completely fine; if the program needs to receive hundreds of thousands of data (paging reception), the way to use the decorator is generally efficient (consider how to optimize), the following code can simply add horizontal lines:
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="BorderThickness" Value="0 0 1 1" />
<Setter Property="BorderBrush" Value="Black" />
</Style>
</ListView.ItemContainerStyle>

- Original title: [WPF] Customize GridLineDecorator to draw a grid for ListView
- Original author: At the foot of the Buddha
- Original link: www.cnblogs.com/RMay/archive/2010/12/27/1918048.html
- Original example code: files.cnblogs.com/RMay/ListViewWithLines.zip
- Last example: github.com/dotnet9/CsharpSocketTest