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.
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.
Downloads
- ListBoxInsertionDragDemo.zip (14.71 KB)
All content Copyright (c) by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is https://www.cyotek.com/blog/dragging-items-in-a-listbox-control-with-visual-insertion-guides?source=rss.