【wpf】自定義gridlinedecorator給listview畫網格

【wpf】自定義gridlinedecorator給listview畫網格

經常看見有人問在使用wpf的listview的時候,怎樣能夠有網格線的效果。

最后更新 2024/2/4 上午5:21
大佛脚下
预计阅读 9 分钟
分类
WPF
标签
.NET WPF 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
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2025/1/26

wpf 藉助自定義 xml 文件實現國際化

本文詳細居間了在wpf程式中使用自定義xml文件實現國際化的方法,包括安裝必備nuget包、動態獲取語言列表、動態切換語言、在代碼和xaml界面中使用翻譯字符串等內容,同時提供了源碼連結,幫助開發者輕鬆實現wpf應用的國際化。

继续阅读