Dragging items in a ListBox control with visual insertion guides
In my last post, I described how to drag and drop items to
reorder a ListView
control. This time I'm going to
describe the exact same technique, but this time for the more
humble ListBox
.
Getting Started
The code below assumes you are working in a new class named
ListBox
that inherits from System.Windows.Forms.ListBox
.
As it's only implementation details that are different between the two versions, I'll include the pertinent code and point out the differences but that's about it. As always a full example project is available from the link at the end of the article.
As with the previous article, you must set
AllowDrop
totrue
on anyListBox
you wish to make use of this functionality.
Drawing on a ListBox
Just like the ListView
, the ListBox
control is a native
control that is drawn by the operating system and so overriding
OnPaint
doesn't work. The ListBox
also has a unique
behaviour of built in owner draw support, so you have to make
sure your painting works with all modes.
Fortunately, the exact same method of painting I used with the
ListView
works fine here too - that is, I capture WM_PAINT
messages and use Graphics.FromControl
to get something I can
work with.
The only real difference is getting the boundaries of the item
to draw due to the differences in the API's of the two controls
- the ListView
uses ListViewItem.GetBounds
whilst the
ListBox
version is ListView.GetItemRectangle
.
private void DrawInsertionLine()
{
if (this.InsertionIndex != InvalidIndex)
{
int index;
index = this.InsertionIndex;
if (index >= 0 && index < this.Items.Count)
{
Rectangle bounds;
int x;
int y;
int width;
bounds = this.GetItemRectangle(this.InsertionIndex);
x = 0; // aways fit the line to the client area, regardless of how the user is scrolling
y = this.InsertionMode == InsertionMode.Before ? bounds.Top : bounds.Bottom;
width = Math.Min(bounds.Width - bounds.Left, this.ClientSize.Width); // again, make sure the full width fits in the client area
this.DrawInsertionLine(x, y, width);
}
}
}
Flicker flicker flicker
The ListBox
is a flickery old beast when owner draw is being
used. Unlike the ListView
control where I just invalidate the
entire control and trust the double buffering, unfortunately
setting double buffering on the ListBox
seems to have no
effect and it flickers like crazy as you drag things around.
To help combat this, I've added a custom Invalidate
method
that accepts the index of a single item to redraw. It also
checks if an insertion mode is set, and if so adjusts the bounds
of the rectangle to include the next/previous item (otherwise,
bits of the insertion guides will be left behind as it tries to
flicker free paint). It will then invalidate only that specific
rectangle and reduce overall flickering. It's not perfect but
it's a lot better than invalidating the whole control.
protected void Invalidate(int index)
{
if (index != InvalidIndex)
{
Rectangle bounds;
bounds = this.GetItemRectangle(index);
if (this.InsertionMode == InsertionMode.Before && index > 0)
{
bounds = Rectangle.Union(bounds, this.GetItemRectangle(index - 1));
}
else if (this.InsertionMode == InsertionMode.After && index < this.Items.Count - 1)
{
bounds = Rectangle.Union(bounds, this.GetItemRectangle(index + 1));
}
this.Invalidate(bounds);
}
}
When you call
Control.Invalidate
it does not trigger an immediate repaint. Instead it sends aWM_PAINT
message to the control to do a paint when next possible. This means multiple calls toInvalidate
with custom rectangles will more than likely have them all combined into a single large rectangle, thus repainting more of the control that you might anticipate.
Initiating a drag operation
Unlike theListView
control and its ItemDrag
event, the
ListBox
doesn't have one. So we'll roll our own using similar
techniques to those I've described before.
protected int DragIndex { get; set; }
protected Point DragOrigin { get; set; }
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button == MouseButtons.Left)
{
this.DragOrigin = e.Location;
this.DragIndex = this.IndexFromPoint(e.Location);
}
else
{
this.DragOrigin = Point.Empty;
this.DragIndex = InvalidIndex;
}
}```
When the user first presses a button, I record both the position of the cursor and which item is under it.
```csharp
protected override void OnMouseMove(MouseEventArgs e)
{
if (this.AllowItemDrag && !this.IsDragging && e.Button == MouseButtons.Left && this.IsOutsideDragZone(e.Location))
{
this.IsDragging = true;
this.DoDragDrop(this.DragIndex, DragDropEffects.Move);
}
base.OnMouseMove(e);
}
private bool IsOutsideDragZone(Point location)
{
Rectangle dragZone;
int dragWidth;
int dragHeight;
dragWidth = SystemInformation.DragSize.Width;
dragHeight = SystemInformation.DragSize.Height;
dragZone = new Rectangle(this.DragOrigin.X - (dragWidth / 2), this.DragOrigin.Y - (dragHeight / 2), dragWidth, dragHeight);
return !dragZone.Contains(location);
}
As it would be somewhat confusing to the user (not to mention
rude) if we suddenly initiated drag events whenever they click
the control and their mouse wiggles during it, we check to see
if the mouse cursor has moved sufficient pixels away from the
drag origin using metrics obtained from SystemInformation
.
If the user has dragged the mouse outside this region, then we
call DoDragDrop
to initialize the drag and drop operation.
Updating the insertion index
In exactly the same way as with the ListView
version, we can
use the DragOver
event to determine which item the mouse is
hovered over, and from there calculate if this is a "before" or
"after" action.
protected override void OnDragOver(DragEventArgs drgevent)
{
if (this.IsDragging)
{
int insertionIndex;
InsertionMode insertionMode;
Point clientPoint;
clientPoint = this.PointToClient(new Point(drgevent.X, drgevent.Y));
insertionIndex = this.IndexFromPoint(clientPoint);
if (insertionIndex != InvalidIndex)
{
Rectangle bounds;
bounds = this.GetItemRectangle(insertionIndex);
insertionMode = clientPoint.Y < bounds.Top + (bounds.Height / 2) ? InsertionMode.Before : InsertionMode.After;
drgevent.Effect = DragDropEffects.Move;
}
else
{
insertionIndex = InvalidIndex;
insertionMode = InsertionMode.None;
drgevent.Effect = DragDropEffects.None;
}
if (insertionIndex != this.InsertionIndex || insertionMode != this.InsertionMode)
{
this.Invalidate(this.InsertionIndex); // clear the previous item
this.InsertionMode = insertionMode;
this.InsertionIndex = insertionIndex;
this.Invalidate(this.InsertionIndex); // draw the new item
}
}
base.OnDragOver(drgevent);
}
The logic is the same, just the implementation differences in
getting the hovered item (use ListBox.IndexFromPoint
and the
item bounds). I've also added a dedicated InsertionMode.None
option this time, which is mainly so I don't unnecessarily
invalidate larger regions that I wanted as described in "Flicker
flicker flicker" above.
If the mouse leaves the confines of the control, then we use the
DragLeave
event to reset the insertion status. Again no
differences per se, I set the insertion mode now, and I also
call Invalidate
first with the current index before resetting
it.
protected override void OnDragLeave(EventArgs e)
{
this.Invalidate(this.InsertionIndex);
this.InsertionIndex = InvalidIndex;
this.InsertionMode = InsertionMode.None;
base.OnDragLeave(e);
}
Handling the drop
When the user releases the mouse, the DragDrop
event is
raised. Here, we'll do the actual removal and re-insertion of
the source item.
protected override void OnDragDrop(DragEventArgs drgevent)
{
if (this.IsDragging)
{
try
{
if (this.InsertionIndex != InvalidIndex)
{
int dragIndex;
int dropIndex;
dragIndex = (int)drgevent.Data.GetData(typeof(int));
dropIndex = this.InsertionIndex;
if (dragIndex < dropIndex)
{
dropIndex--;
}
if (this.InsertionMode == InsertionMode.After && dragIndex < this.Items.Count - 1)
{
dropIndex++;
}
if (dropIndex != dragIndex)
{
object dragItem;
dragItem = this.Items[dragIndex];
this.Items.Remove(dragItem);
this.Items.Insert(dropIndex, dragItem);
this.SelectedItem = dragItem;
}
}
}
finally
{
this.Invalidate(this.InsertionIndex);
this.InsertionIndex = InvalidIndex;
this.InsertionMode = InsertionMode.None;
this.IsDragging = false;
}
}
base.OnDragDrop(drgevent);
}
Just as simple as the ListView
version!
Sample Project
An example demonstration project with an extended version of the above code is available for download from the link below.
Update History
- 2014-07-27 - First published
- 2020-11-21 - Updated formatting
Related articles you may be interested in
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
ListBoxInsertionDragDemo.zip
|
Sample project for the dragging items in a ListBox control with visual insertion guides blog post. |
1.0.0.0 | 27/07/2014 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Misiu
#
Hi Richard, First of all thank You for great article, once again it helped me a lot. Right now I'm trying to create something similar for Datagridview control, but I have no idea where should I start. You've created two extended components: ListView and ListBox, maybe DataGridView won't be a problem to You. Maybe You could point me where I can start with this? Many thanks for all Your great work
Schorge
#
Nice Drag 'n Drop, but, how i can access Listbox Columns and Edit any Row ?
Richard Moss
#
Hello,
You can't, at least automatically - this is not functionality present in a ListBox control (which hearkens all the way back to Windows 3.1 at least). For this sort of functionality, you'd have to roll your own inline editing functionality. Alternatively, look at a ListView control, this supports built in automatic editing although I think that is only for the first column.
As another alternative, look into something like the DataGrid which fully supports assorted editors for each cell.
Regards;
Richard Moss