[WPF] GridLine DecoratorでListViewのグリッドを描画する

[WPF] GridLine DecoratorでListViewのグリッドを描画する

WPFのListViewを使ってグリッド線をどうやって効果があるのかという質問をよく見かけます。

最后更新 2024/02/04 5:21
大佛脚下
预计阅读 7 分钟
分类
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を使ってグリッド線をどうやって効果があるのかという質問をよく見かけます。例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>

しかし、次のように列の幅によって境界が変更されないことがすぐにわかります。

img

また,ListViewのHorizontalContentAlignmentをStretchにしても機能しない. ListViewItemにHorizontalContentAlignment="True"を設定する必要があります。したがって、ListViewItemのスタイルを追加し、統一的に指定する必要があります:

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

しかし、境界線はセル全体を埋めることができないため、問題は解決しません。

img

したがって、各境界線のマージンを慎重に設定して、それらがすべて“正確に”接続され、連続した線のように見えるようにします。おそらくMarginを調整するだけでは十分ではなく、ListViewItemのテンプレートを変更する必要があります。テンプレートを修正したので、多くのボーダーを作成するパフォーマンスが追いつかないことがわかりました。最大のことは、各コラムにCellTemplateを1度指定する必要があります。

したがって、この方法は確かに実行可能ですが、操作は実際には面倒です。

ListViewに直接“線を引く”方法はありますか?もちろん、OnRender内に線を引くListViewを書いて自分で書くこともできますが、理想的には既存のコントロールを変更せずにグリッドを描画する機能を実装できます。また、グリッド線の色は自由に調整できると良いです。

一般的な要件は以下の通り。

  1. グリッドを描くことができる

  2. リストを変更したり、リストを書いたりする必要はない。

  3. グリッドのカラーを調整できる

“既存のコードを変更せずに新しい機能を追加する”デザインパターンに精通していれば、すぐにデコレータパターンを思い浮かべるはずです。実際、WPF自体にDecoratorコントロールがあり、一般的に使用されるBorderはDecoratorであり、背景色の描画や境界線の描画などを制御するのに役立ちます。

ですから、ListViewを中に入れるようなDecoratorがあれば、線を引く機能があるのは嬉しいことではないでしょうか。しかし、Decoratorを直接継承するつもりはありません。WPFはすべてのUIelment用のDecoratorを提供しており、ListView用のDecoratorのみを使用したいからです。

GridLine Decoratorは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が背景を設定した後に線を覆うからです。これは、コントロールの背景がテンプレートにボーダーを入れて描画され、ボーダーもOnRenderで描画され、その後描画すると、最初に描画した線がカバーされるためです。また、ListViewの列のサイズが変更されても、GridLine Decoratorが再描画されないため、グリッド線は同期して変更されません。

実際、GridLine Decoratorの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の幅変化イベントが見つからないため、実行不可能です。2番目の方法は実行可能ですが、効率的です。

ListViewにはヘッダー用とアイテム用の2つのScrollViewerがあるため、いくつかの研究の結果、ScrollViewerのScrollChangedイベントをリッスンする実行可能な方法が見つかりました。Columnの幅が変更されると、ScrollViewerのScrollChangedイベントがトリガーされます。

そのため、コンストラクタ内で:

public GridLineDecorator()
{
    this.AddVisualChild(_gridLinesVisual);
    this.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(OnScrollChanged));
}

線を引くロジックは、主にすべてのコンテナ(実際にはListViewItem)を通過し、GridLine Decoratorからの変位を計算し、水平線と垂直線の座標と長さを計算し、線を引くことです。もっと多くのコードをダウンロードして後で見ることができます。

注意深い子供の靴は、ListViewがVirtualize処理を行っているため、ListViewItem表示が不完全であるときに下部の線が一番下に描画されないことに気づくかもしれません。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] ListViewにグリッドを描画するGridLine Decorator
  • 投稿者:大仏の足元
  • 原文へのリンク:https//www.cnblogs.com/RMay/archive/2010/12/27/1918048.html
  • 原文のサンプルコード:https//files.cnblogs.com/RMay/ListViewWithLines.zip
  • 最后の例github.com/dotnet9/CsharpSocketTest
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2025/09/13

WPFからAvaloniaへの移行シリーズ:WPFプログラムをAvaloniaに移行する必要がある理由

ここ数年、当社のホストソフトウェアは主にWPFとWin Formで開発されてきました。これらのテクノロジーはWindowsプラットフォームで非常にうまく機能し、小規模なパイロット生産から今日の大規模なデリバリまでの段階を経てきました。しかし、ビジネスの成長と顧客のニーズの変化に伴い、単一のWindowsテクノロジースタックは私たちが乗り越えなければならないハードルになりました。

继续阅读
同分类 / 同标签 2025/01/26

WPFはカスタムXMLファイルで国際化を実現

この記事では、必要なNuGetパッケージのインストール、言語リストの動的取得、言語の動的切り替え、コードとxamlインターフェイスでの翻訳文字列の使用、開発者がWPFアプリケーションを簡単に国際化できるようにするソースコードへのリンクなど、WPFプログラムでカスタムXMLファイルを使用した国際化の方法について詳しく説明します。

继续阅读