【WPF】自定义GridLineDecorator给ListView画网格
| 大佛脚下

感谢 rgqancy 指出的 Bug,已经修正

先给个效果图:

img

使用时的代码:

<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 不能随着列宽的变化而变化,就像这样:

img

而且,即使将 ListView 的 HorizontalContentAlignment 置为 Stretch,也不能起到作用。必须在 ListViewItem 上设置 HorizontalContentAlignment="True"。因此,必须添加一个 ListViewItem 的样式,统一指定:

<Style TargetType="ListViewItem">
    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>

但问题还是没有解决,因为 Border 不能填满整个 Cell,就像这样:

img

于是,你得小心的设置各个 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

        /// <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 宽度改变无法通知重绘的问题。解决这个问题有好几个思路:

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

img

  • 原文标题:【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
网站统计
网站创建
5年
文章分类
7个
文章总计
471篇
文章原创
106篇(22.51%)