使用拖放方法排序ListBox项目

在Web上,有许多代码片段允许用户对ListBox进行排序,但大多数只支持字符串列表(或任何其他硬编码的列表)。本文介绍的方法基于StackOverflow上BFree的回答,但进行了扩展,以支持更复杂的数据类型和数据源绑定。

为什么原生方法不够用?

上述链接中的代码在只使用Items属性(字符串或更一般的单一类型)时是简单且有效的。但有时候,使用DataSource(例如,为了在多个控件中使用相同的项目列表)以及拥有一个与项目类型无关的控件是更好的选择。为此,将创建一个继承自System.Windows.Forms.ListBox的UserSortableListbox类。

为了保持代码简单,做了两个重要的假设:

  • UserSortableListbox始终是用户可排序的(没有禁用拖放的可能性)
  • 唯一支持的SelectionMode是SelectionMode.One

拖放功能

要允许用户通过拖放重新排序项目,需要处理三个事件:重新排序的开始(MouseDown事件)、移动元素(DragOver)和放下项目(DragDrop)。在MouseDown事件后启动拖放机制:

protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (SelectedItem == null) { return; } sourceIndex = SelectedIndex; OnSelectedIndexChanged(e); DoDragDrop(SelectedItem, DragDropEffects.Move); }

sourceIndex在类中定义如下:

private int sourceIndex = -1;

处理项目移动非常简单:

protected override void OnDragOver(DragEventArgs e) { base.OnDragOver(e); e.Effect = DragDropEffects.Move | DragDropEffects.Scroll; }

最有趣的部分是,在DragDrop事件发生后,可以移动被放下的项目到正确的位置。以下是OnDragDrop方法的第一个版本:

protected override void OnDragDrop(DragEventArgs e) { base.OnDragDrop(e); Point point = PointToClient(new Point(e.X, e.Y)); int index = IndexFromPoint(point); if (index < 0) index = Items.Count - 1; if (index > sourceIndex) { Items.Insert(index + 1, Items[sourceIndex]); Items.RemoveAt(sourceIndex); } else { Items.Insert(index, Items[sourceIndex]); Items.RemoveAt(sourceIndex + 1); } SelectedIndex = index; }

代码注释:

  • 没有简单的方法来指示放下项目的索引,如在MouseDown方法中。但是,可以使用继承的IndexFromPoint方法,它将给提供想要的。唯一需要记住的是将e.X和e.Y转换为客户坐标。
  • 在这一行中,必须决定如何处理将项目放下在ListBox最下方的情况(因为不能将项目拖出ListBox,IndexFromPoint返回-1的唯一情况是用户将项目放下在最后一个项目下方)。处理这种情况的最直观方式是将目标索引设置为列表的最后一个索引。
  • 当有了源索引和目标索引后,可以移动项目。首先,通过在Items中再次插入Items[sourceIndex]来复制一个项目,然后移除“原始的一个”。如果目标索引大于(低于)源索引,从sourceIndex移除会影响目标索引,所以在index + 1处插入。类似地,当目标索引小于(高于)源索引时,在index位置插入会影响sourceIndex,所以必须在sourceIndex + 1处移除。
  • 移除了之前选择的项目,所以是时候在它的新位置重新选择它了。

重新创建了基本解决方案。唯一的优势是代码中不再有e.Data.GetData()。幸运的是,添加DataSource支持现在真的很简单。只需要找到一个DataSource和Items字段的公共类(或接口),它将允许操作其元素,特别是提供Count、Insert和RemoveAt方法。

Items具有ObjectCollection类型,它实现了IList、ICollection和IEnumerable。因为IList接口正是要找的,可以假设DataSource将实现它,将创建一个名为items的变量,用这个类型替换OnDragDrop方法中的所有Items,这将完成工作并允许在UserSortableListbox中使用DataSource。

IList items = DataSource != null ? DataSource as IList : Items;

更多功能

为了使控件更有用,可以添加一个Reorder事件,当用户移动项目时会触发:

public class ReorderEventArgs : EventArgs { public int index1, index2; } public delegate void ReorderHandler(object sender, ReorderEventArgs e); public event ReorderHandler Reorder;

index1和index2是移动项目的源和目标索引。以下是完整的OnDragDrop方法,包括DataSource支持和Reorder事件:

protected override void OnDragDrop(DragEventArgs e) { base.OnDragDrop(e); Point point = PointToClient(new Point(e.X, e.Y)); int index = IndexFromPoint(point); IList items = DataSource != null ? DataSource as IList : Items; if (index < 0) index = items.Count - 1; if (index != sourceIndex) { if (index > sourceIndex) { items.Insert(index + 1, items[sourceIndex]); items.RemoveAt(sourceIndex); } else { items.Insert(index, items[sourceIndex]); items.RemoveAt(sourceIndex + 1); } if (null != Reorder) Reorder(this, new ReorderEventArgs() { index1 = sourceIndex, index2 = index }); } SelectedIndex = index; }

保持实现简单

如上所述,假设拖放不能被禁用,并且在使用控件时只有SelectionMode.One可用,所以应该在设计器中隐藏AllowDrop和SelectionMode,并在构造函数中设置适当的值:

[Browsable(false)] new public bool AllowDrop { get { return true; } set { } } [Browsable(false)] new public SelectionMode SelectionMode { get { return SelectionMode.One; } set { } } public UserSortableListBox() { base.AllowDrop = true; base.SelectionMode = SelectionMode.One; }

当然,可以添加刚刚禁用的属性的支持。如果想允许禁用移动项目,只需要在OnMouseMove的开头检查AllowDrop(或另一个新属性),然后执行或不执行DoDragDrop()。

支持其他选择模式更复杂,但仍然简单。不仅要移动一个项目并有一个sourceIndex,还要添加一个sourceIndex[]数组,它将在OnMouseDown中从SelectedIndices复制,以及primarySourceIndex,它将包含点击的项目(也在OnMouseDown中,可以从IndexFromPoint获得,无需转换坐标)。然后,在OnDragDrop方法中,使用(primarySourceIndex - index)位置移动所有项目:sourceIndex[i]处的项目将移动到sourceIndex[i] + primarySourceIndex - index位置。

使用代码

使用这个控件就像使用标准的ListBox一样简单。Reorder事件在设计器中可用,可以轻松处理:

userSortableListBox1.Reorder += new synek317.Controls.UserSortableListBox.ReorderHandler(this.optionsListBox_Reorder); void optionsListBox_Reorder(object sender, UserSortableListBox.ReorderEventArgs e) { // 移动的索引在e.index2位置 // 或者简单地在userSortableListBox1.SelectedIndex // 之前它在e.index1位置 }

在下载部分,包含了一个编译好的控件的.dll文件,所以可以直接添加到项目引用中,并立即使用它。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485