當前位置: 妍妍網 > 碼農

【WPF】自訂GridLineDecorator給ListView畫網格

2024-02-04碼農

感謝 rgqancy [1] 指出的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裏面畫線什麽的,但理想的情況還是能夠在可以不改動任何現有控制項的條件下,實作這個畫網格的功能。同時,這個格線的顏色可以隨意調整就更好了。

因此,總的要求如下:

  1. 可以畫網格

  2. 不用改動ListView,或者自己寫ListView

  3. 可以調整網格的顏色

如果對設計模式熟悉的話,「不改動現有程式碼,增加新的功能」,應該馬上能夠想到裝飾器模式。其實,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
///


/// GridLineBrush Dependency Property
///

public static readonly DependencyProperty GridLineBrushProperty =
DependencyProperty.Register( "GridLineBrush" , typeof (Brush), typeof (GridLineDecorator),
new FrameworkPropertyMetadata(Brushes.LightGray,
new PropertyChangedCallback(OnGridLineBrushChanged)));
///
/// Gets or sets the GridLineBrush property. This dependency property
/// indicates ....
///

public Brush GridLineBrush
{
get { return (Brush)GetValue(GridLineBrushProperty); }
set { SetValue(GridLineBrushProperty, value ); }
}
///
/// Handles changes to the GridLineBrush property.
///

private static void OnGridLineBrushChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
((GridLineDecorator)d).OnGridLineBrushChanged(e);
}
///
/// Provides derived classes an opportunity to handle changes to the GridLineBrush property.
///

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 ();
}
# 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寬度改變無法通知重繪的問題。解決這個問題有好幾個思路:

  1. 監聽一下GridViewColumn的寬度變化

  2. 監聽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.ItemContainer style >
< style TargetType = "{x:Type ListViewItem}" >
< Setter Property = "BorderThickness" Value = "0 0 1 1" />
< Setter Property = "BorderBrush" Value = "Black" />
style >
ListView.ItemContainer style >

  • 原文標題:【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

  • 參考資料

    [1]

    rgqancy: http://www.cnblogs.com/rgqancy/