Quantcast
Channel: cyotek.com Blog Summary Feed
Viewing all articles
Browse latest Browse all 559

Dragging items in a ListBox control with visual insertion guides

$
0
0

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

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.

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

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

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.


Viewing all articles
Browse latest Browse all 559

Trending Articles