用户可排序列表框
一个允许通过拖放重新排序项目的列表框控件。
引言
此控件允许用户通过拖放方式重新排序 ListBox
项目。它支持标准的 Items
属性(包含字符串和自定义对象)以及 DataSource
属性,前提是数据源实现了 IList
接口(例如 BindingList
)。
背景
网络上有很多允许用户对列表框进行排序的代码片段,但大多数只支持字符串列表(或其他硬编码的类型)。我的方法基于 BFree 在 StackOverflow 上的回答:http://stackoverflow.com/a/805267/540761。
为什么原生方法不够用?
上面链接中的代码很简单,只要我们只使用字符串(或更通用地,某种特定类型的)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;
这里唯一需要解释的是 OnSelectedIndexChanged(e)
。我们需要它,因为当我们处理 MouseDown
时,SelectedIndexChanged
不会被触发。
处理项目的移动非常简单
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);
//(1)
Point point = PointToClient(new Point(e.X, e.Y));
int index = IndexFromPoint(point); //destination index
//(2)
if (index < 0) index = Items.Count - 1;
//(3a)
if (index > sourceIndex)
{
Items.Insert(index + 1, Items[sourceIndex]);
Items.RemoveAt(sourceIndex);
}
//(3b)
else
{
Items.Insert(index, Items[sourceIndex]);
Items.RemoveAt(sourceIndex + 1);
}
//(4)
SelectedIndex = index;
}
关于此代码的一些注释
- 我们没有像
OnMouseDown
方法那样简单的方法来指示被放置项目的索引。但是,我们可以使用继承的IndexFromPoint
方法,它会给我们想要的结果。唯一需要记住的是将e.X
和e.Y
转换为客户端坐标。 - 在这一行,我们必须决定如何处理将项目放在列表框的最后一个元素下方(因为你不能将项目拖出列表框,
IndexFromPoint
返回 -1 的唯一情况是当用户将项目放在最后一个项目下方时)。处理这种情况最直观的方法是将目标索引设置为列表中的最后一个索引。 - 当我们有了源索引和目标索引后,就可以移动项目了。首先,我们通过在
Items
中再次插入Items[sourceIndex]
来复制一个项目,然后删除“原始的”一个。如果目标index
大于(在下方)源,那么从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
替换所有 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() //this is the constructor
{
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)
{
//moved index is at e.index2 position
//or simply at userSortableListBox1.SelectedIndex
//previously it was at e.index1 position
}
在下载部分,我包含了一个包含编译好的控件的 .dll 文件,因此你可以将其添加到项目的引用中并开箱即用地使用它。
关注点
我不确定为什么在使用列表框的 MouseDown
事件时 SelectedIndexChanged
不会被触发。但是,我使用了一个变通方法,我的控件只是从 MouseDown
中触发 SelectedIndexChanged
事件。