感谢 rgqancy 指出的 Bug,已经修正
先給個效果圖:

使用時的代碼:
<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>
-----------------------正文-------------------------------
經常看見有人問在使用 wpf 的 listview 的時候,怎樣能夠有網格線的效果。例如http://www.bbniu.com/forum/thread-1090-1-1.html
對這個問題,首先能想到的解決辦法是,在 gridviewcolumn 的 celltemplate 中,放上一個 border,然後設置 border 的 borderbrush 和 borderthickness。例如:
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1" UseLayoutRounding="True">
<TextBlock Text="{Binding Id}"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
但是,很快你會發現,border 不能隨著列寬的變化而變化,就像這樣:

而且,即使將 listview 的 horizontalcontentalignment 置為 stretch,也不能起到作用。必須在 listviewitem 上設置 horizontalcontentalignment="true"。因此,必須添加一個 listviewitem 的樣式,統一指定:
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
但問題還是沒有解決,因為 border 不能填滿整個 cell,就像這樣:

於是,你得小心的設置各個 border 的 margin,來讓它們“恰好”都連在一起,看上去就像是連續的線條。也許調整 margin 還不夠,還得修改 listviewitem 的模板;模板修改好了,發現創建這麼多的 border 性能又跟不上;最頭大的是,每個 column 都要指定一次 celltemplate,萬一哪天邊線的顏色要統一調整一下……
因此,這種辦法固然可行,操作起來其實麻煩的要死。
有沒有一種方式,可以直接在 listview 上“畫線”呢?固然,我們可以自己寫一個 listview,在 onrender 裡面畫線什麼的,但理想的情況還是能夠在可以不改動任何現有控制項的條件下,實現這個畫網格的功能。同時,這個網格線的顏色可以隨意調整就更好了。
因此,總的要求如下:
可以畫網格
不用改動 listview,或者自己寫 listview
可以調整網格的顏色
如果對設計模式熟悉的話,“不改動現有代碼,增加新的功能”,應該馬上能夠想到裝飾器模式。其實,wpf 中本身就有 decorator 這個控制項,而常用的 border 就是一個 decorator,可以幫助控制項畫背景色,畫邊線等等。
因此,如果能夠有這麼一個 decorator,把 listview 往裡面一放,就能有畫線的功能,豈不快哉?不過,這裡我並不打算直接繼承 decorator 來修改,因為 wpf 提供的 decorator 是針對所有 uielment 的,而我們只想針對 listview。
gridlinedecorator 直接繼承自 frameworkelement,並且通過重載 visualchild 和 logicalchild 相關的代碼來顯示其包裝的 listview。
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
}
}
其中,target 是一個屬性,類型是 listview,還有一個_guidlinesvisual,則是用於繪製網格的 drawingvisual。有人可能會問,為什麼不直接重載 onrender 方法,在裡面畫線呢?
理由是,重載 onrender 方法畫線,當 listview 設置了背景後,會將我們畫的線蓋住。這是因為控制項的背景是在模板中放了一個 border 來繪製的,border 也是在 onrender 中繪製的,它後繪製,我們的先繪製,會將我們畫的線給蓋住。同時,你會發現,當 listview 的 column 改變大小的時候,並不會引起 gridlinedecorator 重繪,所以網格線無法同步變化。
其實,gridlinedecorator 裡面的 getvisualchild 重載也非常講究:
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,接著才是_gridlinesvisual。 不過,即使是使用 drawingvisual,也會有 column 寬度改變無法通知重繪的問題。解決這個問題有好幾個思路:
- 監聽一下 gridviewcolumn 的寬度變化
- 監聽 compositiontarget.rendering 事件
第一個辦法,不可行,因為 gridviewcolumn 的寬度變化事件你找不到,第二個辦法是可行,不過效率嘛……
在經過一番研究之後,終於找到了一個可行的辦法,監聽 scrollviewer 的 scrollchanged 事件,因為 listview 內部是放置了兩個 scrollviewer,一個用於顯示 header,一個用於顯示 items。當 column 的寬度變化時,會觸發 scrollviewer 的 scrollchanged 事件。
因此,在構造函數裡面:
public GridLineDecorator()
{
this.AddVisualChild(_gridLinesVisual);
this.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(OnScrollChanged));
}
畫線的邏輯,主要就是遍歷所有的 container(其實是 listviewitem),計算其相對於 gridlinedecorator 的位移,算出橫線和縱線的坐標和長度,畫線。代碼比較多,大家可以下載以後自己看。
細心的童鞋可能會發現,有時候底部的線條在 listviewitem 顯示不完整時,沒有畫到最下端,這是由於 listview 做了 virtualize 處理。大家可以設置 virtualizingstackpanel.isvirtualizing="false"來強制繪製。
附代碼:https://files.cnblogs.com/RMay/ListViewWithLines.zip
站長註:
原作者寫的很好,效果不錯,數據量小,比如幾千條,上面的方案完全沒問題;如果程式需要接收幾十萬數據(分頁接收),使用裝飾器的方式效率一般(可考慮如何優化),下面代碼可簡單添加水平線:
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="BorderThickness" Value="0 0 1 1" />
<Setter Property="BorderBrush" Value="Black" />
</Style>
</ListView.ItemContainerStyle>

- 原文標題:【wpf】自定義 gridlinedecorator 給 listview 畫網格
- 原文作者:大佛腳下
- 原文連結:https://www.cnblogs.com/RMay/archive/2010/12/27/1918048.html
- 原文示例代碼:https://files.cnblogs.com/RMay/ListViewWithLines.zip
- 最後示例:https://github.com/dotnet9/CsharpSocketTest