65.9K
CodeProject 正在变化。 阅读更多。
Home

用户可排序列表框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (5投票s)

2012年1月30日

CPOL

5分钟阅读

viewsIcon

22670

downloadIcon

1039

一个允许通过拖放重新排序项目的列表框控件。

UserSortableListboxExample

引言

此控件允许用户通过拖放方式重新排序 ListBox 项目。它支持标准的 Items 属性(包含字符串和自定义对象)以及 DataSource 属性,前提是数据源实现了 IList 接口(例如 BindingList)。

背景

网络上有很多允许用户对列表框进行排序的代码片段,但大多数只支持字符串列表(或其他硬编码的类型)。我的方法基于 BFree 在 StackOverflow 上的回答:http://stackoverflow.com/a/805267/540761

为什么原生方法不够用?

上面链接中的代码很简单,只要我们只使用字符串(或更通用地,某种特定类型的)Items 属性,它就很好用。有时使用 DataSource(例如,将同一项目列表用于多个控件)并包含非字符串项会很方便。拥有一个不依赖于项目类型的控件也是一个好主意。为了实现这一点,我们将创建一个继承自 System.Windows.Forms.ListBoxUserSortableListbox 类。

保持简单

为了保持代码的简洁,我决定假设两个重要的方面

  • UserSortableListbox 始终是用户可排序的(无法禁用拖放)
  • 唯一支持的 SelectionModeSelectionMode.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;
}

关于此代码的一些注释

  1. 我们没有像 OnMouseDown 方法那样简单的方法来指示被放置项目的索引。但是,我们可以使用继承的 IndexFromPoint 方法,它会给我们想要的结果。唯一需要记住的是将 e.Xe.Y 转换为客户端坐标。
  2. 在这一行,我们必须决定如何处理将项目放在列表框的最后一个元素下方(因为你不能将项目拖出列表框,IndexFromPoint 返回 -1 的唯一情况是当用户将项目放在最后一个项目下方时)。处理这种情况最直观的方法是将目标索引设置为列表中的最后一个索引。
  3. 当我们有了源索引和目标索引后,就可以移动项目了。首先,我们通过在 Items 中再次插入 Items[sourceIndex] 来复制一个项目,然后删除“原始的”一个。如果目标 index 大于(在下方)源,那么从 sourceIndex 删除会影响目标索引,因此我们在 index + 1 处插入。类似地,当目标 index 小于(在上方)源时,在位置索引处插入会影响 sourceIndex,因此我们必须在 sourceIndex + 1 处删除。
  4. 我们删除了之前选中的项目,现在是时候在它的新位置重新选中它了。

我们已经重新创建了基本解决方案。唯一的优点是代码中不再有 e.Data.GetData()。幸运的是,现在添加 DataSource 支持非常简单。我们只需要找到一个 DataSourceItems 字段的通用类(或接口),它允许我们操作其元素,特别是提供 CountInsertRemoveAt 方法。Items 的类型是 ObjectCollection,它实现了 IListICollectionIEnumerable。因为 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;

index1index2 是被移动项目的源索引和目标索引。这是完整的 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,因此我们应该在设计器中隐藏 AllowDropSelectionMode,并在构造函数中设置适当的值

[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 事件。

© . All rights reserved.