Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Manual Reordering of Items inside a ListView

0.00/5 (No votes)
9 Jan 2008 1  
This article shows how to implement drag&drop inside a ListView and how to enable custom painting in a ListView.

Sample Image - ListViewCustomReorder.gif

Introduction

In another community, someone asked whether it is possible to implement something like drag&drop inside a ListView.

The person claimed to have searched for an example but didn't find any. Only articles dealing with dragging items into or out of a ListView, but not dragging and dropping items within the same ListView.

This question raised my interest, so I built a quick&dirty solution to try it out. After the basics worked, I added a few "bells and whistles" (like showing the insertion point in the ListView), but we'll come to this later on.

Basic Idea

Each drag and drop operation is mainly just a combination of handling three events:

  • MouseDown: Find out if something that can be dragged has been clicked on, and save the reference for further use.
  • MouseMove: Show the user some feedback whether or not the item can be dropped at the mouse position.
  • MouseUp: Finalize the drag operation and feed the item to the target.

System.Windows.Forms.Control has some methods and events to deal with drag and drop operations. If you want to dig deeper into drag and drop, you should read about Control.DoDragDrop(), Control.GiveFeedback(), and the Drag* events. MSDN has some nice tutorials on this topic.

Implementation - First Leg

The first thing we have to do is to remember which item is being dragged:

// The LVItem being dragged
private ListViewItem _itemDnD = null;

I chose a ListViewItem so that we can easily access all its properties later on. Likewise, you could just save the index of the item as an int, but then, you'd always have to access the ListView's Item collection, and once the item has been removed from the ListView, you won't find it at all.

Also, remember that _itemDnD is just a reference to the object, so the memory consumption is minimal, as well.

Like stated above, to achieve our goal, we have to handle the following events of our ListView.

MouseDown

The ListView has a very convenient method to find the ListViewItem at a certain location: GetItemAt(), so we'll use it to find out which item the user has clicked on.

_itemDnD = listView1.GetItemAt(e.X, e.Y);
// if the LV is still empty, no item will be found anyway,
// so we don't have to consider this case

Not much else to do here - just remember which item was clicked on, and you're done.

MouseMove

Once the user actually moves the mouse while having pressed a mouse button, the actual drag operation is performed.

if (_itemDnD == null)
    return;

// Show the user that a drag operation is happening
Cursor = Cursors.Hand;

For simplicity reasons, I just chose one of the predefined Cursors to show that a dragging operation is in progress.

For a start, that's sufficient as well. We'll give additional feedback later on.

MouseUp

Now, we're at the point where the actual work is done. The user has released the mouse button, so we have to find out where the item has been dropped. The GetItemAt() method is called once again. Afterwards, the item is removed from the ListView and inserted at the new location.

if (_itemDnD == null)
    return;
 
// use 0 instead of e.X so that you don't have
// to keep inside the columns while dragging
ListViewItem itemOver = listView1.GetItemAt(0, e.Y);
 
if (itemOver == null)
    return;
 
listView1.Items.Remove(_itemDnD);
listView1.Items.Insert(itemOver.Index, _itemDnD);
 
Cursor = Cursors.Default;

The only problem is that this algorithm isn't sophisticated enough to decide whether to insert before or after the item we dropped our ListViewItem on. Right now, we're always inserting before the item we released the mouse over.

There's also a problem now when we drop the item on itself: first, it's being removed from the Items collection, and then we want to insert it before itself. At this moment, the item has an Index of -1 (i.e., it isn't part of an Items collection), so we'll get an exception here that we cannot insert a ListViewItem before Index position -1.

Don't worry, we'll fix this later on.

First Summary

Apart from the few flaws I mentioned above, we've already implemented drag and drop operations within a ListView. You see - it's not that difficult.

Now, on to the "bells and whistles" I mentioned earlier:

Implementation - Second Leg

Insertion Before/After an Item

Right now, we always insert before the item we released the mouse button over, so it's not possible to make an item become the last item of the ListView.

To improve the handling, I chose to find out whether the item has been dropped on the upper or lower half of the target item. If it's been dropped on the upper half, we'll insert before the target item, otherwise after the target item.

Finding out where exactly the mouse has been released is being accomplished with the help of ListViewItem.GetBounds(). This method allows you to query the rectangle the ListViewItem occupies. I've altered the MouseUp event handler accordingly:

// use 0 instead of e.X so that you don't have
// to keep inside the columns while dragging
ListViewItem itemOver = listView1.GetItemAt(0, e.Y);
 
if (itemOver == null)
    return;
 
Rectangle rc = itemOver.GetBounds(ItemBoundsPortion.Entire);
 
// find out if we insert before or after the item the mouse is over
bool insertBefore;
if (e.Y < rc.Top + (rc.Height / 2))
    insertBefore = true;
else
    insertBefore = false;
 
if (_itemDnD != itemOver)
// if we dropped the item on itself, nothing is to be done
{
    if (insertBefore)
    {
        listView1.Items.Remove(_itemDnD);
        listView1.Items.Insert(itemOver.Index, _itemDnD);
    }
    else
    {
        listView1.Items.Remove(_itemDnD);
        listView1.Items.Insert(itemOver.Index + 1, _itemDnD);
    }
}

Please note that I also checked whether the item is being dropped on itself. In this case, we don't have to do anything. With this modification, we can place the item before the first or after the last ListViewItem in the ListView.

Giving Feedback by Extending the ListView

Now that we can decide where exactly to insert the item, a little more feedback would be nice, don't you think? I was thinking of drawing a colored line where the item will be inserted if you release the button. But, how to achieve this goal? We can find out whether to insert before or after the current item in the MouseMove event, just like we already do in MouseUp, so that's not the problem.

What is a little complicated, though, is how to perform the actual drawing onto the ListView.

Usually, to perform some extra painting on a control, you can derive from the control, override OnPaint(), call the OnPaint() of your base class, and then perform your own additional painting.

Unfortunately, the ListView doesn't call OnPaint() (or OnPaintBackground()), because it's still just a wrapper around the CommonControls ListView (has been this way since .NET 1.0).

Nevertheless, there is a way to accomplish custom painting on the ListView. Every control has a protected method WndProc() in which you can get access to all the Windows messages the control receives. This is a very powerful method; you're right at the pulse of the control, so to speak. Mess it up and your control is dead (doesn't work as expected or work at all); play it right and you can do almost anything with the control. Since OnPaint() usually is being called when the WM_PAINT message is received, we can catch this message here and perform our painting.

The overridden method in a ListView derived class looks like this:

protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
 
    if (m.Msg == WM_PAINT)
    {
        if (LineBefore >= 0 && LineBefore < Items.Count)
        {
            Rectangle rc = Items[LineBefore].GetBounds(ItemBoundsPortion.Entire);
            DrawInsertionLine(rc.Left, rc.Right, rc.Top);
        }
 
        if (LineAfter >= 0 && LineBefore < Items.Count)
        {
            Rectangle rc = Items[LineAfter].GetBounds(ItemBoundsPortion.Entire);
            DrawInsertionLine(rc.Left, rc.Right, rc.Bottom);
        }
    }
}

WM_PAINT, in this case, is a constant I defined with the corresponding value (0x000f) taken from one of the Windows header files (WinUser.h). You can find them in a subdirectory of your Visual Studio installation, for example.

LineBefore and LineAfter are two new properties I added to the new ListView. They can be set to an item index to make the ListView draw an insertion line before or after this item.

The painting of the insertion line has been encapsulated in a private method DrawInsertionLine(), if you wonder where this comes from.

Now that the ListView actually can display insertion lines, the rest is just a matter of setting the right properties and telling the ListView to repaint itself (so that a WM_PAINT is sent and the painting is performed) by calling its Invalidate() method.

Summary

I hope I could show you how drag and drop operations inside a ListView can be accomplished with a few simple steps.

The basic task isn't very hard, but as soon as you want to implement a little more sophisticated control, you should know a little bit about message handling in .NET (and Windows) controls.

History

  • 17th June, 2006: Version 1.0 - Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here