Let the user drag ListBox items in C#

[drag ListBox items]

This example uses drag-and-drop to allow the user to drag ListBox items from one position to another. To use the program, right-click on an item and then drag it to hits new position. If you drag an item beyond the last item in the list, the item is moved to the end of the list.

The techniques used here are actually relatively simple, although it can be hard to figure out exactly what you need to do.

AllowDrop

The first step is to set the ListBox control’s AllowDrop property to true. This is a very easy step to forget and, if you do, nothing happens and you’ll probably be left wondering why. This is such an important step that I decided to do it in the following Form_Load event handler instead of setting the property in the Form Designer.

private void Form1_Load(object sender, EventArgs e)
{
    lstAnimals.AllowDrop = true;
}

Tracking Data

Usually a program uses drag-and-drop to let the user drag text, images, or some other simple data from one control to another. It also usually doesn’t care where the data comes from or where it ends up. For this example, I want to add the following restrictions.

  • Allow only Move operations, not Copy or something else.
  • Do not drag something other than an item onto the ListBox.
  • Allow items to be dragged only to the control that started the drag.

The program uses the following DragItem class to keep track of the data and where it started.

public class DragItem
{
    public ListBox Client;
    public int Index;
    public object Item;

    public DragItem(ListBox client, int index, object item)
    {
        Client = client;
        Index = index;
        Item = item;
    }
}

This class simply stores the control that started the drag, the index where the dragged item began, and the item itself.

Notice that the Item field is a generic object. Many ListBox controls contain strings, but the list can actually hold any object. Using the object data type allows the DragItem object to represent any data.

The program uses the ListBox control’s MouseDown, DragEnter, DragOver, and DragDrop methods to control the drag.

MouseDown

When the user presses the right mouse button down over a ListBox item, the following code executes.

// On right mouse down, start the drag.
private void lst_MouseDown(object sender, MouseEventArgs e)
{
    ListBox lst = sender as ListBox;

    // Only use the right mouse button.
    if (e.Button != MouseButtons.Right) return;

    // Find the item under the mouse.
    int index = lst.IndexFromPoint(e.Location);
    lst.SelectedIndex = index;
    if (index < 0) return;

    // Drag the item.
    DragItem drag_item = new DragItem(lst, index, lst.Items[index]);
    lst.DoDragDrop(drag_item, DragDropEffects.Move);
}

This code first verifies that the user has pressed the right mouse button and exits if some other button is pressed.

Next, the code uses the control’s IndexFromPoint method to find the index of the item under the mouse. The code selects that item. Then if the index is -1, which indicates that the mouse is not over any item, the method exits.

Finally, if the user has pressed the right mouse button down over an actual item, the code creates a DragItem object holding the control, item index, and item. It then calls the control’s DoDragDrop method to start the drag.

DragEnter

When the drag enters the control, its DragEnter event fires. The event handler can decide whether the control should allow this kind of drop.

The following code shows the example’s DragEnter event handler.

// See if we should allow this kind of drag.
private void lst_DragEnter(object sender, DragEventArgs e)
{
    ListBox lst = sender as ListBox;

    // Allow a Move for DragItem objects that are
    // dragged to the control that started the drag.
    if (!e.Data.GetDataPresent(typeof(DragItem)))
    {
        // Not a DragItem. Don't allow it.
        e.Effect = DragDropEffects.None;
    }
    else if ((e.AllowedEffect & DragDropEffects.Move) == 0)
    {
        // Not a Move. Do not allow it.
        e.Effect = DragDropEffects.None;
    }
    else
    {
        // Get the DragItem.
        DragItem drag_item = (DragItem)e.Data.GetData(typeof(DragItem));

        // Verify that this is the control that started the drag.
        if (drag_item.Client != lst)
        {
            // Not the congtrol that started the drag. Do not allow it.
            e.Effect = DragDropEffects.None;
        }
        else
        {
            // Allow it.
            e.Effect = DragDropEffects.Move;
        }
    }
}

This method looks scarier but is really fairly simple. First, it checks that the drag is dragging a DragItem object. If this is some other kind of drag (for example, the user is dragging text or an image), the method sets e.Effect = DragDropEffects.None to not allow the drag on this control.

Next, the program determines whether the drag is a Move operation. If the drag is not a Move, the program does not allow it.

If the drag looks okay so far, the code then gets the DragItem object being dragged. If that object’s Client is different from the current control, the code again does not allow the drag.

Finally, if the drag contains DragItem data, is a Move, and was started by this control, then the code sets e.Effect = DragDropEffects.Move to allow the drag,

DragOver

The DragOver event occurs when the user moves the drag around on the control. The program can use that event to provide feedback such as showing where the item would land if dropped.

When the drag moves over the example’s ListBox, the following code executes.

// Select the item under the mouse during a drag.
private void lst_DragOver(object sender, DragEventArgs e)
{
    // Do nothing if the drag is not allowed.
    if (e.Effect != DragDropEffects.Move) return;

    ListBox lst = sender as ListBox;

    // Select the item at this screen location.
    lst.SelectedIndex =
        lst.IndexFromScreenPoint(new Point(e.X, e.Y));
}

This code first checks the current drag effect, which was set by the DragEnter method. If the effect is not Move, then this drag is not allowed so the code simply returns. It does not provide feedback because the drag is not allowed on this control.

If the effect is Move, the code gets the control that raised the event. It uses the IndexFromScreenPoint extension method described shortly to see what item is below the mouse and selects that item to highlight it.

The DragEventArgs object includes the mouse’s coordinates (e.X, e.Y) during the drag. Unfortunately those coordinates are relative to the screen instead of the current control. The following IndexFromScreenPoint extension method uses those coordinates to find the ListBox item under the mouse.

// Return the index of the item that is
// under the point in screen coordinates.
public static int IndexFromScreenPoint(this ListBox lst, Point point)
{
    // Convert the location to the ListBox's coordinates.
    point = lst.PointToClient(point);

    // Return the index of the item at that position.
    return lst.IndexFromPoint(point);
}

This method uses the ListBox control’s PointToClient method to convert the screen coordinates to the control’s coordinate system. It then uses the control’s IndexFromPoint method to get the index of the item beneath the mouse and returns that index.

Note that this method returns -1 if the mouse is below the last item in the ListBox.

DragDrop

When the user drops the drag on the control, the following event handler executes. Note that the event does not occur unless the drag is allowed. If the user drops the drag over a control that does not allow it, then the event does not occur.

// Drop the item here.
private void lst_DragDrop(object sender, DragEventArgs e)
{
    ListBox lst = sender as ListBox;

    // Get the ListBox item data.
    DragItem drag_item = (DragItem)e.Data.GetData(typeof(DragItem));

    // Get the index of the item at this position.
    int new_index =
        lst.IndexFromScreenPoint(new Point(e.X, e.Y));

    // If the item was dropped after all
    // of the items, move it to the end.
    if (new_index == -1) new_index = lst.Items.Count - 1;

    // Remove the item from its original position.
    lst.Items.RemoveAt(drag_item.Index);

    // Insert the item in its new position.
    lst.Items.Insert(new_index, drag_item.Item);

    // Select the item.
    lst.SelectedIndex = new_index;
}

This code gets the ListBox control that is catching the drop and the dragged DragInfo object. It calls the IndexFromScreenPoint method to get new_index, the index of the item on which the data is being dropped.

If new_index is -1, then the user is dropping beyond the last item in the ListBox. In that case, the code sets new_index to the index of the last item in the list so the item will be moved to the end.

The code then removes the item from its current position in the list and inserts it at the position below the mouse. The code finishes by selecting the dropped item.

Summary

You may have noticed that the event handlers shown here use their sender objects instead of hard-coded ListBox objects such as lstAnimals and lstFoods. That allows the event handlers to work with any number of controls. Both of this example’s ListBox controls use the same event handlers so the user can drag to rearrange items in either list. The DragEnter event handler prevents the user from dragging items from one list to another.

Download the example to see additional details and to experiment with it.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in controls, user interface and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.