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:
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);
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;
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;
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:
ListViewItem itemOver = listView1.GetItemAt(0, e.Y);
if (itemOver == null)
return;
Rectangle rc = itemOver.GetBounds(ItemBoundsPortion.Entire);
bool insertBefore;
if (e.Y < rc.Top + (rc.Height / 2))
insertBefore = true;
else
insertBefore = false;
if (_itemDnD != itemOver)
{
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