感谢 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里面画线什么的,但理想的情况还是能够在可以不改动任何现有控件的条件下,实现这个画网格的功能。同时,这个网格线的颜色可以随意调整就更好了。

因此,总的要求如下:

1、可以画网格

2、不用改动ListView,或者自己写ListView

3、可以调整网格的颜色

如果对设计模式熟悉的话,“不改动现有代码,增加新的功能”,应该马上能够想到装饰器模式。其实,WPF中本身就有Decorator这个控件,而常用的Border就是一个Decorator,可以帮助控件画背景色,画边线等等。

因此,如果能够有这么一个Decorator,把ListView往里面一放,就能有画线的功能,岂不快哉?不过,这里我并不打算直接继承Decorator来修改,因为WPF提供的Decorator是针对所有UIElment的,而我们只想针对ListView。

GridLineDecorator直接继承自FrameworkElement,并且通过重载VisualChild和LogicalChild相关的代码来显示其包装的ListView。

GridLineDecorator

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"来强制绘制。

附代码:http://files.cnblogs.com/RMay/ListViewWithLines.zip

转载于:https://www.cnblogs.com/RMay/archive/2010/12/27/1918048.html

【WPF】自定义GridLineDecorator给ListView画网格相关推荐

  1. WPF自定义空心文字

    原文:WPF自定义空心文字 首先创建一个自定义控件,继承自FrameworkElement,"Generic.xaml"中可以不添加样式. 要自定义空心文字,要用到绘制格式化文本F ...

  2. WPF自定义LED风格数字显示控件

    WPF自定义LED风格数字显示控件 原文:WPF自定义LED风格数字显示控件 版权声明:本文为博主原创文章,转载请注明作者和出处 https://blog.csdn.net/ZZZWWWPPP1119 ...

  3. WPF自定义仪表盘控件

    WPF自定义仪表盘控件 一.前言 二.功能实现 一.前言 在学习和工作中使用WPF时,都离不开自定义控件的使用,今天分享一个自己在学习过程中使用到的一个自定义仪表盘控件,感觉挺不错的,在这里分享给大家 ...

  4. WPF 自定义标题栏 自定义菜单栏

    原文:WPF 自定义标题栏 自定义菜单栏 自定义标题栏 自定义列表,可以直接修改WPF中的ListBox模板,也用这样类似的效果.但是ListBox是不能设置默认选中状态的. 而我们需要一些复杂的UI ...

  5. [WPF]自定义鼠标指针

    [WPF]自定义鼠标指针 原文:[WPF]自定义鼠标指针 [WPF]自定义鼠标指针 周银辉 看看WPF Cursor类的两个构造函数吧: public Cursor(Stream cursorStre ...

  6. Android自定义Adapter的ListView的思路及代码

    Android自定义Adapter的ListView的思路及代码,需要的朋友可以参考一下 width="650" height="200" align=&quo ...

  7. WPF自定义命令(转)

    WPF自定义命令 自定义命令,可以分为两个层次来理解 1.声明自己的RoutedCommand实例,这个层次比较浅 2.从实现ICommand接口开始,这个才算的上真正的自定义命令 自定义命令的目的是 ...

  8. WPF 自定义 MessageBox (相对完善版 v1.0.0.6)

    基于WPF的自定义 MessageBox. 众所周知WPF界面美观.大多数WPF元素都可以简单的修改其样式,从而达到程序的风格统一.可是当你不得不弹出一个消息框通知用户消息时(虽然很不建议在程序中频繁 ...

  9. WPF 自定义BarChartControl(可左右滑动的柱状图)

    WPF 自定义BarChartControl(可左右滑动的柱状图) 原文:WPF 自定义BarChartControl(可左右滑动的柱状图) 自定义可左右滑动.拖拽滑动的平面柱状图 在做这种样式控件之 ...

最新文章

  1. java中包的_Java中的包
  2. 自动人脸识别基本原理
  3. oracle 添加登陆数据库触发器--记录IP 地址
  4. aop切面排除某个类_AOP 你看这一篇就够了
  5. 听说最近你读过不少书
  6. 爱是相互的,这样才是平衡
  7. 音视频技术开发周刊 | 192
  8. 将url参数字符串转成数组
  9. 基于以太坊的测试链发布一个智能合约
  10. P2P中的NAT穿越方案简介
  11. 随想录(scons编译)
  12. Button switch..case 语句监听按钮点击的方法。。下面这方法好。
  13. ie型lfsr_什么是PRBS
  14. 单列变双列css_css – 右对齐双列布局丢失水平滚动条
  15. Office 2013集成SP1
  16. linux结课考试试题,Linux认证考试课后基础试题及答案
  17. 教师管理系统_ER图_功能图_数据字典_数据库脚本
  18. VTK实现三维地质建模
  19. db_recovery_file_dest与log_archive_dest、log_archive_dest_n
  20. Protel (DXP2004sp2) 许可协议认证过程

热门文章

  1. shell脚本实现一个彩色进度条
  2. iphone7p配置参数详情_华为mate40标准版参数配置-参数详情
  3. c++ 链表_链表(单向链表的建立、删除、插入、打印)
  4. 程序员被沦陷!国内程序员真的饱和了?
  5. 聊一聊你了解的程序猿是什么样的?
  6. 2020受欢迎的20个JavaScript 库
  7. 互联网躺平学 ,从后端到前端有多吃香?
  8. 有哪些必看的前端 JS 库?
  9. 华为p20pro投屏到笔记本_新荣耀笔记本与微软系统合作,网友:一碰即传投屏功能还有吗...
  10. 火狐浏览器百度网盘服务器响应,火狐浏览器打不开百度网盘怎么解决?解决百度网盘打不开的步骤分享...