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.

The demonstration project in action
The demonstration project in action

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 to true on any ListBox 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.

csharp
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.

csharp
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 a WM_PAINT message to the control to do a paint when next possible. This means multiple calls to Invalidate 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.

csharp
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.

csharp
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.

csharp
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.

csharp
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

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


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

Reply

# Schorge

Nice Drag 'n Drop, but, how i can access Listbox Columns and Edit any Row ?

Reply

# 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

Reply