This content has moved - please find it at https://devblog.cyotek.com.

Although these pages remain accessible, some content may not display correctly in future as the new blog evolves.

Visit https://devblog.cyotek.com.

Dragging items in a ListView control with visual insertion guides

I can't remember when it was I first saw something being dragged with an insertion mark for guidance. Whenever it was, it was a long long time ago and I'm just catching up now.

This article describes how to extend a ListView control to allow the items within it to be reordered, using insertion guides.

The demonstration project in action

Drag Drop vs Mouse Events

When I first decided that one of my applications needed the ability to move items around in a list, I knocked together some quick code by using the MouseDown, MouseMove and MouseUp events. It worked nicely but I ended up not using it, for the simple reason I couldn't work out how to change the cursor one of the standard drag/drop icons - such as Move, Scroll, or Link. So if anyone knows how to do this, I'd be happy to hear how (note I don't mean assigning a custom cursor, I mean using the true OS cursor). As it turns out, apart from the cursor business (and the incidental fact it looks... awful... if you enable shell styles), it's also reinventing a fairly large wheel as the ListView control already provides most of what we need.

An Elephant Never Forgets: The AllowDrop Property

I should probably have this tattooed upon my forehead as I think that whenever I try and add drag and drop to anything and it doesn't work, it's invariably because I didn't set the AllowDrop property of the respective object to true. And invariably it takes forever until I remember that that has to be done.

So... don't forget to set it!

Getting Started

The code below assumes you are working in a new class named ListView that inherits from System.Windows.Forms.ListView. You could do most of this by hooking into the events of an existing control, but that way leads to madness (and more complicated code). Or at least duplicate code, and more than likely duplicate bugs.

Drawing insertion marks

I originally started writing this article with the drag sections at the start, followed by the sections on drawing. Unfortunately, it was somewhat confusing to read as the drag is so heavily dependant on the drawing and insertion bits. So I'll talk about that first instead.

In order to draw our guides, and to know what to do when the drag is completed, we need to store some extra information - the index of the item where the item is to be inserted, and whether the item is to be inserted before or after the insertion item. Leaving behind the question on if that sentence even makes sense, on with some code!

public enum InsertionMode
{
  Before,

  After
}

protected int InsertionIndex { get; set; }

protected InsertionMode InsertionMode { get; set; }

protected bool IsRowDragInProgress { get; set; }

As we'll be drawing a nice guide so the user is clear on what is happening, we'll also provide the property to configure the colour of said guide.

[Category("Appearance")]
[DefaultValue(typeof(Color), "Red")]
public virtual Color InsertionLineColor
{
  get { return _insertionLineColor; }
  set { _insertionLineColor = value; }
}

We'll also need to initialize default values for these.

public ListView()
{
  this.DoubleBuffered = true;
  this.InsertionLineColor = Color.Red;
  this.InsertionIndex = -1;
}

Notice the call to set the DoubleBuffered property? This has to be done, otherwise your drag operation will be an epic exercise of Major Flickering. In my library code I use the LVS_EX_DOUBLEBUFFER style when creating the window, but in this example DoubleBuffered has worked just as well and is much easier to do.

Drawing on a ListView

The ListView control is a native control that is drawn by the operating system. In otherwords, overriding OnPaint isn't working to work.

So how do you draw on it? Well, you could always go even more old school than using Windows Forms in the first place, and use the Win32 to do some custom painting. However, it's a touch overkill and we can get around it for the most part.

Instead, we'll hook into WndProc, watch for the WM_PAINT message and then use the Control.CreateGraphics method to get a Graphics object bound to the window and do our painting that way.

private const int WM_PAINT = 0xF;

[DebuggerStepThrough]
protected override void WndProc(ref Message m)
{
  base.WndProc(ref m);

  switch (m.Msg)
  {
    case WM_PAINT:
      this.DrawInsertionLine();
      break;
  }
}

I'm not really sure that using CreateGraphics is the best way to approach this, but it seems to work and it was quicker than trying to recall all the Win32 GDI work I've done in the past.

Tip: The DebuggerStepThrough is useful for stopping the debugger from stepping into a method (including any manual breakpoints you have created). WndProc can be called thousands of times in a "busy" control, and if you're trying to debug and suddenly end up in here, it can be a pain.

The code for actually drawing the insertion line is in itself simple enough - we just draw a horizontal line with arrow heads at either side. We adjust the start and end of the line to ensure it always fits within the client area of the control, regardless of if the control is horizontally scrolled or the total width of the item.

private void DrawInsertionLine()
{
  if (this.InsertionIndex != -1)
  {
    int index;

    index = this.InsertionIndex;

    if (index >= 0 && index < this.Items.Count)
    {
      Rectangle bounds;
      int x;
      int y;
      int width;

      bounds = this.Items[index].GetBounds(ItemBoundsPortion.Entire);
      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);
    }
  }
}

private void DrawInsertionLine(int x1, int y, int width)
{
  using (Graphics g = this.CreateGraphics())
  {
    Point[] leftArrowHead;
    Point[] rightArrowHead;
    int arrowHeadSize;
    int x2;

    x2 = x1 + width;
    arrowHeadSize = 7;
    leftArrowHead = new[]
                    {
                      new Point(x1, y - (arrowHeadSize / 2)), new Point(x1 + arrowHeadSize, y), new Point(x1, y + (arrowHeadSize / 2))
                    };
    rightArrowHead = new[]
                      {
                        new Point(x2, y - (arrowHeadSize / 2)), new Point(x2 - arrowHeadSize, y), new Point(x2, y + (arrowHeadSize / 2))
                      };

    using (Pen pen = new Pen(this.InsertionLineColor))
    {
      g.DrawLine(pen, x1, y, x2 - 1, y);
    }

    using (Brush brush = new SolidBrush(this.InsertionLineColor))
    {
      g.FillPolygon(brush, leftArrowHead);
      g.FillPolygon(brush, rightArrowHead);
    }
  }
}

And that's all there is to that part of the code. Don't forget to ensure the control is double buffered!

Initiating a drag operation

The ListView control has an ItemDrag event that is automatically raised when the user tries to drag an item. We'll use this to initiate our own drag and drop operation.

protected override void OnItemDrag(ItemDragEventArgs e)
{
  if (this.Items.Count > 1)
  {
    this.IsRowDragInProgress = true;
    this.DoDragDrop(e.Item, DragDropEffects.Move);
  }

  base.OnItemDrag(e);
}

Note: The code snippets in this article are kept concise to show only the basics of the technique. In most cases, I've expanded upon this to include extra support (in this case for raising an event allowing the operation to be cancelled), please download the sample project for the full class.

When the DroDragDrop is called, execution will halt at that point until the drag is complete or cancelled. You can use the DragEnter, DragOver, DragLeave, DragDrop and GiveFeedback events to control the drag, for example to specify the action that is currently occurring, and to handle what happens when the user releases the mouse cursor.

Updating the insertion index

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.IsRowDragInProgress)
  {
    int insertionIndex;
    InsertionMode insertionMode;
    ListViewItem dropItem;
    Point clientPoint;

    clientPoint = this.PointToClient(new Point(drgevent.X, drgevent.Y));
    dropItem = this.GetItemAt(0, Math.Min(clientPoint.Y, this.Items[this.Items.Count - 1].GetBounds(ItemBoundsPortion.Entire).Bottom - 1));

    if (dropItem != null)
    {
      Rectangle bounds;

      bounds = dropItem.GetBounds(ItemBoundsPortion.Entire);
      insertionIndex = dropItem.Index;
      insertionMode = clientPoint.Y < bounds.Top + (bounds.Height / 2) ? InsertionMode.Before : InsertionMode.After;

      drgevent.Effect = DragDropEffects.Move;
    }
    else
    {
      insertionIndex = -1;
      insertionMode = this.InsertionMode;

      drgevent.Effect = DragDropEffects.None;
    }

    if (insertionIndex != this.InsertionIndex || insertionMode != this.InsertionMode)
    {
      this.InsertionMode = insertionMode;
      this.InsertionIndex = insertionIndex;
      this.Invalidate();
    }
  }

  base.OnDragOver(drgevent);
}

The code is a little long, but simple enough. We get the ListViewItem underneath the cursor. If there isn't one, we clear any existing insertion data. If we do have one, we check if the cursor is above or below half of the total height of the item in order to decide "before" or "after" status.

We also inform the underlying drag operation so that the appropriate cursor "Move" or "No Drag" is displayed.

Finally, we issue a call to Invalidate to force the control to repaint so that the new indicator is drawn (or the existing indicator cleared).

If the mouse leaves the confines of the control, then we use the DragLeave event to reset the insertion status. We don't need to use DragEnter as DragOver covers us in this case.

protected override void OnDragLeave(EventArgs e)
{
  this.InsertionIndex = -1;
  this.Invalidate();

  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.IsRowDragInProgress)
  {
    ListViewItem dropItem;

    dropItem = this.InsertionIndex != -1 ? this.Items[this.InsertionIndex] : null;

    if (dropItem != null)
    {
      ListViewItem dragItem;
      int dropIndex;

      dragItem = (ListViewItem)drgevent.Data.GetData(typeof(ListViewItem));
      dropIndex = dropItem.Index;

      if (dragItem.Index < dropIndex)
      {
        dropIndex--;
      }
      if (this.InsertionMode == InsertionMode.After && dragItem.Index < this.Items.Count - 1)
      {
        dropIndex++;
      }

      if (dropIndex != dragItem.Index)
      {
        this.Items.Remove(dragItem);
        this.Items.Insert(dropIndex, dragItem);
        this.SelectedItem = dragItem;
      }
    }

    this.InsertionIndex = -1;
    this.IsRowDragInProgress = false;
    this.Invalidate();
  }

  base.OnDragDrop(drgevent);
}

We reuse the InsertionIndex and InsertionMode values we calculated in OnDragOver and then determine the index of the new item from these. Remove the source item, reinsert it at the new index, then clear the insertion values, force and repaint and we're done. Easy!

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
ListViewInsertionDragDemo.zip
  • md5: 4168ad176fc85c10770000b0408f7cab

Sample project for the dragging items in a ListView control with visual insertion guides blog post.

1.0.0.0 27/07/2014 Download

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?

Styling with Markdown is supported

Comments

Gravatar

Miguel F. Augusto

# Reply

Hello Mr.Richard Moss,

very nice work on this blog, i only want to ask you if i can do the same in VBA in Excel, or Excel dont support this type of DRAG DROP Insertion Mark?

e-mail me : (edit by Cyotek Team: removed email address)

Ty, regards Miguel F. Augusto

Gravatar

Richard Moss

# Reply

Miguel,

Thanks for your comment. Unfortunately, I don't believe this will be entirely possible in VBA as this is based on Visual Basic 6, and I don't think the VB6 list view exposed all the necessary functionality. For example, .NET provides a WndProc override, making it trivial to intercept Windows messages. VB on the other hand, walled this away and made it very difficult - I remember countless times I made VB6 "disappear" by faulting when hooking messages when I used to work with VB.

So while I can't say for certain (it's been many a year since I touched VB, and even longer since I touched VBA (and then only for simple macros)), I don't think it's possible.

Regards;
Richard Moss

Gravatar

pc8181

# Reply

Hello Richard :

Thanks for your work !!!!

but i can row reorder with selected a single Item but how to do multiple selected items ?>.

for the reference : http://www.codeproject.com/Articles/4576/Drag-and-Drop-ListView-row-reordering?msg=5313848#xx5313848xx

Gravatar

Austin

# Reply

Thanks for the nice job. And I found a bug,I dragged 1 after 2, and then I drag 1 just let the clorline before or after itself, but it will move 1 to 3 if the colorline is on the bottom of the item 1.How to fix this bug? thank you

John

# Reply

This is Great.

Thanks for sharing this. Saved me a lot of time

Cheers mate!

Gravatar

Henrik

# Reply

Very nice work! Thank you for sharing...

regards Henrik

Motaz Alnuweiri

# Reply

Wow amazing!

Hello and thank you so much for sharing.

You can enable auto scroll when reach the top or bottom of the list:

protected override void OnDragOver(DragEventArgs drgevent) { ...

    // Scroll top or bottom for dragging item
    if (this.InsertionIndex > -1 && this.InsertionIndex < this.Items.Count)
    {
        EnsureVisible(this.InsertionIndex);
    }

    base.OnDragOver(drgevent);

}

Regards, Motaz