在Web上,有许多代码片段允许用户对ListBox进行排序,但大多数只支持字符串列表(或任何其他硬编码的列表)。本文介绍的方法基于StackOverflow上BFree的回答,但进行了扩展,以支持更复杂的数据类型和数据源绑定。
为什么原生方法不够用?
上述链接中的代码在只使用Items属性(字符串或更一般的单一类型)时是简单且有效的。但有时候,使用DataSource(例如,为了在多个控件中使用相同的项目列表)以及拥有一个与项目类型无关的控件是更好的选择。为此,将创建一个继承自System.Windows.Forms.ListBox的UserSortableListbox类。
为了保持代码简单,做了两个重要的假设:
拖放功能
要允许用户通过拖放重新排序项目,需要处理三个事件:重新排序的开始(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;
}
代码注释:
重新创建了基本解决方案。唯一的优势是代码中不再有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文件,所以可以直接添加到项目引用中,并立即使用它。