编辑
2025-02-06
C# 应用
00
请注意,本文编写于 89 天前,最后修改于 89 天前,其中某些信息可能已经过时。

目录

创建自定义比较器
重写SortableVirtualListView
使用控件
结论

在Windows Forms应用程序中,ListView是一个非常实用的控件,用于显示数据列表。但默认情况下,ListView并不支持点击列头进行排序。本文将介绍如何开发一个可点击列头排序的ListView控件。

创建自定义比较器

ListViewItemSorter 类是一个用于排序 ListView 控件中项目的自定义比较器。它实现了 IComparer<ListViewItem> 接口,可以按照指定的列、排序顺序和数据类型对 ListViewItem 进行排序。

C#
public class ListViewItemSorter : IComparer<ListViewItem> { private int _columnIndex; private SortOrder _sortOrder; private ColumnDataType _dataType; public ListViewItemSorter(int columnIndex, SortOrder sortOrder, ColumnDataType dataType) { _columnIndex = columnIndex; _sortOrder = sortOrder; _dataType = dataType; } public int Compare(ListViewItem x, ListViewItem y) { string textX = x.SubItems[_columnIndex].Text; string textY = y.SubItems[_columnIndex].Text; int result; switch (_dataType) { case ColumnDataType.Number: if (double.TryParse(textX, out double numX) && double.TryParse(textY, out double numY)) { result = numX.CompareTo(numY); } else { result = string.Compare(textX, textY, StringComparison.OrdinalIgnoreCase); } break; case ColumnDataType.Date: if (DateTime.TryParse(textX, out DateTime dateX) && DateTime.TryParse(textY, out DateTime dateY)) { result = dateX.CompareTo(dateY); } else { result = string.Compare(textX, textY, StringComparison.OrdinalIgnoreCase); } break; case ColumnDataType.Text: default: result = string.Compare(textX, textY, StringComparison.OrdinalIgnoreCase); break; } return _sortOrder == SortOrder.Ascending ? result : -result; } }
  1. 构造函数接受三个参数:
    • columnIndex:要排序的列的索引
    • sortOrder:排序顺序(升序或降序)
    • dataType:列数据的类型(数字、日期或文本)
  2. Compare 方法实现了实际的比较逻辑:
    • 根据指定的列索引获取要比较的文本
    • 根据数据类型进行相应的比较:
      • 对于数字类型,尝试将文本转换为 double 进行比较
      • 对于日期类型,尝试将文本转换为 DateTime 进行比较
      • 对于文本类型或无法转换的情况,使用字符串比较
  3. 比较结果会根据指定的排序顺序进行调整(升序或降序)

重写SortableVirtualListView

SortableVirtualListView 类是一个扩展的 ListView 控件,它提供了高效的大数据集处理和排序功能。

C#
public class SortableVirtualListView : ListView { private ColumnHeader _sortedColumn = null; private SortOrder _sortOrder = SortOrder.None; private List<ListViewItem> _items = new List<ListViewItem>(); private List<int> _sortedIndices = new List<int>(); private Dictionary<int, ColumnDataType> _columnTypes = new Dictionary<int, ColumnDataType>(); private const int MaxSortItems = 100000; private ProgressBar _progressBar; private BackgroundWorker _backgroundWorker; public event EventHandler DataLoadCompleted; public SortableVirtualListView() { this.DoubleBuffered = true; this.VirtualMode = true; this.VirtualListSize = 0; InitializeProgressBar(); InitializeBackgroundWorker(); } protected override void OnRetrieveVirtualItem(RetrieveVirtualItemEventArgs e) { if (e.ItemIndex >= 0 && e.ItemIndex < _items.Count) { int actualIndex = _sortedIndices.Count > e.ItemIndex ? _sortedIndices[e.ItemIndex] : e.ItemIndex; e.Item = _items[actualIndex]; } } private void InitializeProgressBar() { _progressBar = new ProgressBar { Dock = DockStyle.Bottom, Visible = false, Height = 20 }; this.Controls.Add(_progressBar); } private void InitializeBackgroundWorker() { _backgroundWorker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; _backgroundWorker.DoWork += BackgroundWorker_DoWork; _backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged; _backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted; } public void LoadItems(IEnumerable<ListViewItem> items) { if (_backgroundWorker.IsBusy) { _backgroundWorker.CancelAsync(); } _progressBar.Visible = true; _backgroundWorker.RunWorkerAsync(items); } private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { var items = (IEnumerable<ListViewItem>)e.Argument; List<ListViewItem> loadedItems = new List<ListViewItem>(); int totalItems = items.Count(); int count = 0; foreach (var item in items) { if (_backgroundWorker.CancellationPending) { e.Cancel = true; return; } loadedItems.Add(item); count++; if (count % 1000 == 0 || count == totalItems) { int progressPercentage = (int)((double)count / totalItems * 100); _backgroundWorker.ReportProgress(progressPercentage); } } e.Result = loadedItems; } private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { _progressBar.Value = e.ProgressPercentage; } private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (!e.Cancelled && e.Error == null) { SetItems((List<ListViewItem>)e.Result); } _progressBar.Visible = false; DataLoadCompleted?.Invoke(this, EventArgs.Empty); } public void SetColumnDataType(int columnIndex, ColumnDataType dataType) { _columnTypes[columnIndex] = dataType; } protected override void OnColumnClick(ColumnClickEventArgs e) { base.OnColumnClick(e); ColumnHeader clickedColumn = this.Columns[e.Column]; if (_sortedColumn == null) { _sortOrder = SortOrder.Ascending; } else if (_sortedColumn == clickedColumn) { _sortOrder = _sortOrder == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending; } else { _sortOrder = SortOrder.Ascending; } _sortedColumn = clickedColumn; ColumnDataType dataType = _columnTypes.ContainsKey(e.Column) ? _columnTypes[e.Column] : ColumnDataType.Text; SortItems(e.Column, _sortOrder, dataType); this.Refresh(); UpdateColumnHeaders(); } private void SortItems(int columnIndex, SortOrder sortOrder, ColumnDataType dataType) { // 使用 LINQ 对索引进行排序,而不是对实际项目进行排序 IEnumerable<int> query = Enumerable.Range(0, _items.Count); switch (dataType) { case ColumnDataType.Number: query = sortOrder == SortOrder.Ascending ? query.OrderBy(i => GetDoubleValue(_items[i].SubItems[columnIndex].Text)) : query.OrderByDescending(i => GetDoubleValue(_items[i].SubItems[columnIndex].Text)); break; case ColumnDataType.Date: query = sortOrder == SortOrder.Ascending ? query.OrderBy(i => GetDateTimeValue(_items[i].SubItems[columnIndex].Text)) : query.OrderByDescending(i => GetDateTimeValue(_items[i].SubItems[columnIndex].Text)); break; case ColumnDataType.Text: default: query = sortOrder == SortOrder.Ascending ? query.OrderBy(i => _items[i].SubItems[columnIndex].Text) : query.OrderByDescending(i => _items[i].SubItems[columnIndex].Text); break; } _sortedIndices = query.Take(MaxSortItems).ToList(); } private double GetDoubleValue(string text) { return double.TryParse(text, out double result) ? result : double.MinValue; } private DateTime GetDateTimeValue(string text) { return DateTime.TryParse(text, out DateTime result) ? result : DateTime.MinValue; } public void SetItems(List<ListViewItem> items) { _items = items; _sortedIndices = Enumerable.Range(0, Math.Min(items.Count, MaxSortItems)).ToList(); this.VirtualListSize = items.Count; this.Refresh(); } private void UpdateColumnHeaders() { foreach (ColumnHeader column in this.Columns) { if (column == _sortedColumn) { column.Text = column.Text.TrimEnd('▲', '▼') + (_sortOrder == SortOrder.Ascending ? " ▲" : " ▼"); } else { column.Text = column.Text.TrimEnd('▲', '▼'); } } } } public enum ColumnDataType { Text, Number, Date }
  1. 虚拟模式:使用 ListView 的虚拟模式来处理大量数据,提高性能。
  2. 异步数据加载:
    • 使用 BackgroundWorker 在后台异步加载数据。
    • 包含进度条显示加载进度。
    • 提供 DataLoadCompleted 事件通知数据加载完成。
  3. 自定义排序:
    • 支持点击列头进行排序。
    • 可以为每列指定数据类型(文本、数字、日期)。
    • 使用索引排序而不是直接排序项目,提高大数据集的排序效率。
  4. 列头排序指示:
    • 在列头显示排序方向(升序/降序)。
  5. 性能优化:
    • 使用双缓冲减少闪烁。
    • 限制最大排序项目数量(MaxSortItems)以处理超大数据集。

使用控件

C#
public partial class Form1 : Form { private SortableVirtualListView sortableListView; private const int ItemCount = 1000000; // 100万条数据 public Form1() { InitializeComponent(); InitializeSortableListView(); LoadLargeDataSet(); } private void InitializeSortableListView() { sortableListView = new SortableVirtualListView { Dock = DockStyle.Fill, View = View.Details, FullRowSelect = true, GridLines = true }; sortableListView.Columns.Add("ID", 80); sortableListView.Columns.Add("Name", 150); sortableListView.Columns.Add("Value", 100); sortableListView.Columns.Add("Date", 120); sortableListView.SetColumnDataType(0, ColumnDataType.Number); sortableListView.SetColumnDataType(1, ColumnDataType.Text); sortableListView.SetColumnDataType(2, ColumnDataType.Number); sortableListView.SetColumnDataType(3, ColumnDataType.Date); sortableListView.DataLoadCompleted += SortableListView_DataLoadCompleted; this.Controls.Add(sortableListView); } private void LoadLargeDataSet() { Random random = new Random(); DateTime startDate = new DateTime(2000, 1, 1); var items = Enumerable.Range(0, ItemCount).Select(i => new ListViewItem(new[] { i.ToString(), $"Item {i}", random.Next(1, 1000).ToString(), startDate.AddDays(random.Next(0, 8000)).ToString("yyyy-MM-dd") })); sortableListView.LoadItems(items); } private void SortableListView_DataLoadCompleted(object sender, EventArgs e) { this.Text = "Large Data Set Loaded"; } }

image.png

image.png

结论

通过以上步骤,我们成功创建了一个可点击列头排序的ListView控件。这个自定义控件不仅支持点击列头进行排序,还能显示排序方向,极大地提高了用户体验。您可以根据需要进一步扩展这个控件,例如添加自定义的排序算法或者支持不同数据类型的排序。

本文作者:rick

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!