Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

gListView - ListView Control with Visual Drag and Drop FeedBack (VB.NET)

7 Apr 2009 0  
ListView has a pointer for drop location, alternating row coloring, and custom drag cursor.

Introduction

I needed a ListView that gave me more Drag and Drop feedback. The couple of examples I found were not complete enough for my needs. I began work on my own to fill in the gaps. I needed built-in reordering with a visual insertion pointer, auto scrolling, and a better example of what was being dragged. I had the initial stages working well, and then after developing the gCursor [^], it all came together. The gListView Inherits ListView and has these extra properties:

Properties and Enumerations

    Enum eAutoScroll
        None
        All
        Vertical
        Horizontal
    End Enum

Here is a list of the primary properties:

  • Public Property gCurrCursor() As gCursor

    Setup the gCursor

  • Public Property gCursorVisible() As Boolean

    Use or not use the gCursor

  • Public Property DropBarColor() As Color

    Color of the pointer for the insertion point

  • Public Property AutoScroll() As eAutoScroll

    What type of Auto Scroll to use

  • Public Property MatchFont() As Boolean

    When dropping the ListViewItem does it keep its original font or change to match this ListView's font

  • Public Property ColorRows() As Boolean

    Use the ColorRowA and ColorRowB to alternate the color of the rows

  • Public Property ColorRowA() As Color

    When ColorRows = True, the Item Color alternates between ColorRowA and ColorRowB

  • Public Property ColorRowB() As Color

    When ColorRows = True, the Item Color alternates between ColorRowA and ColorRowB

Reference Links

Some concepts used in this Project (and referenced later) are already explained in these articles:

Dragging Over the List

The big problem with drag and drop in a list, is pin pointing where exactly the item will end up when you let go of the mouse button. To determine where to draw the pointer and reference the insertion point, you need to know:

    1. Is the mouse over an item or whitespace
    2. Is the View in LargeIcon Mode
    3. Is the insertion point Above or Below the item
    4. Has the pointer moved to a new location
    5. Is the mouse near an edge to trigger scrolling

Add Insert Pointer

    Private Sub PaintPointer( _
        ByRef Mpt As Point, _
        ByRef MeItem As ListViewItem, _
        ByRef e As System.Windows.Forms.DragEventArgs)

        'Check if the mouse is over an Item
        If IsNothing(MeItem) Then
            'if there are 0 items let it add
            Me.Invalidate(InvRect)
            If Me.Items.Count > 0 Then e.Effect = DragDropEffects.None
            OffItems = True
        Else
            'Get the old pointer area to Invalidate
            If Me.View = Windows.Forms.View.LargeIcon Then
                InvRect = New Rectangle(LineStartPt.X - 6, LineStartPt.Y, _
                        12, MeItem.Bounds.Height + 15)
            Else
                InvRect = New Rectangle(LineStartPt.X, LineStartPt.Y - 6, _
                        MeItem.Bounds.Width, 13)
            End If

            Dim ItemRect As Rectangle = MeItem.Bounds
            Dim StrtPt As Integer
            Dim LineStartPt_N As Point
            Dim LineEndPt_N As Point
            Dim LineAbove_N As Boolean

            'determine the new pointer location and position
            If Me.View = Windows.Forms.View.LargeIcon Then
                If Mpt.X < ItemRect.Left + (ItemRect.Width / 2) Then
                    StrtPt = ItemRect.Left
                    LineAbove_N = True
                Else
                    StrtPt = ItemRect.Right
                    LineAbove_N = False
                End If
                LineStartPt_N = New Point(StrtPt - 1, ItemRect.Top - 1)
                LineEndPt_N = New Point(StrtPt - 1, ItemRect.Bottom - 1)
            Else
                If Mpt.Y < ItemRect.Top + (ItemRect.Height / 2) Then
                    StrtPt = ItemRect.Top
                    LineAbove_N = True
                Else
                    StrtPt = ItemRect.Bottom
                    LineAbove_N = False
                End If
                LineStartPt_N = New Point(ItemRect.Left - 1, StrtPt - 1)
                LineEndPt_N = New Point(ItemRect.Right - 1, StrtPt - 1)
            End If

            'if the Pointer has moved clear the old area and paint a new pointer 
            If LineStartPt_N <> LineStartPt Or OffItems Then
                Me.Invalidate(InvRect)
                Me.Update()
                OffItems = False

                'Set the new position
                LineStartPt = LineStartPt_N
                LineEndPt = LineEndPt_N
                LineAbove = LineAbove_N

                DrawThePointer(ItemRect)

                LineIndex = MeItem.Index
            End If
        End If
    End Sub

Auto Scroll the List

Especially when re-ordering a list, you need to be able to have the gListView scroll itself automatically when dragging. First determine which way to scroll and if it is allowed.

    Private Sub CheckScroller( _
        ByRef Mpt As Point, _
        ByRef MeItem As ListViewItem, _
        ByRef e As System.Windows.Forms.DragEventArgs)

        Dim ScrollMargin As Padding = New Padding
        If Me.View = Windows.Forms.View.Details Then
            ScrollMargin.Top = Me.TopItem.Bounds.Top + 5
        Else
            ScrollMargin.Top = Me.ClientRectangle.Top + 5
        End If
        ScrollMargin.Bottom = Me.ClientSize.Height - 5
        ScrollMargin.Left = 5
        ScrollMargin.Right = Me.ClientSize.Width - 5

        If Mpt.Y <= ScrollMargin.Top _
            AndAlso (_AutoScroll = eAutoScroll.All _
            Or _AutoScroll = eAutoScroll.Vertical) Then

            scrollDirection = 0
            scrollHorzVert = sHorzVert.Vert
            ScrollTimer.Start()
            _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollUp
            e.Effect = DragDropEffects.None
            If IsNothing(MeItem) Then _gCurrCursor.MakeCursor()
            Me.Invalidate(InvRect)

        ElseIf Mpt.Y >= ScrollMargin.Bottom _
            AndAlso (_AutoScroll = eAutoScroll.All _
            Or _AutoScroll = eAutoScroll.Vertical) Then

            scrollDirection = 1
            scrollHorzVert = sHorzVert.Vert
            ScrollTimer.Start()
            _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollDn
            e.Effect = DragDropEffects.None
            If IsNothing(MeItem) Then _gCurrCursor.MakeCursor()
            Me.Invalidate(InvRect)

        ElseIf Mpt.X <= ScrollMargin.Left _
            AndAlso (_AutoScroll = eAutoScroll.All _
            Or _AutoScroll = eAutoScroll.Horizontal) Then

            scrollDirection = 0
            scrollHorzVert = sHorzVert.Horz
            ScrollTimer.Start()
            _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollLeft
            e.Effect = DragDropEffects.None
            If IsNothing(MeItem) Then _gCurrCursor.MakeCursor()
            Me.Invalidate(InvRect)

        ElseIf Mpt.X >= ScrollMargin.Right _
            AndAlso (_AutoScroll = eAutoScroll.All _
            Or _AutoScroll = eAutoScroll.Horizontal) Then

            scrollDirection = 1
            scrollHorzVert = sHorzVert.Horz
            ScrollTimer.Start()
            _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollRight
            e.Effect = DragDropEffects.None
            If IsNothing(MeItem) Then _gCurrCursor.MakeCursor()
            Me.Invalidate(InvRect)

        Else
            _gCurrCursor.gScrolling = gCursor.eScrolling.No
            If ScrollTimer.Enabled Then _gCurrCursor.MakeCursor()
            ScrollTimer.Stop()
        End If
    End Sub

Then SendMessage using a Timer to scroll the control. If the mouse moves further away, speed up the scrolling if allowed, or stop scrolling if it gets too far away.

    Enum sHorzVert
        Horz
        Vert
    End Enum
    Private scrollHorzVert As sHorzVert
    Private scrollDirection As Integer
    Const SelLVColl As String = _
        "System.Windows.Forms.ListView+SelectedListViewItemCollection"
    Const LVItem As String = _
        "System.Windows.Forms.ListViewItem"

    Private WithEvents ScrollTimer As New Timer
    Private Const WM_HSCROLL As Integer = &H114S
    Private Const WM_VSCROLL As Integer = &H115S

    Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
    (ByVal hwnd As Integer, _
     ByVal wMsg As Integer, _
     ByVal wParam As Integer, _
     ByRef lParam As Object) As Integer

    Private Sub ScrollTimer_Tick(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles ScrollTimer.Tick

        Try
            If scrollHorzVert = sHorzVert.Vert Then
                If _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollDn Then
                    ScrollTimer.Interval = 300 - (10 * _
                    (Me.PointToClient(MousePosition).Y - _
                    Me.ClientSize.Height))
                ElseIf _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollUp Then
                    ScrollTimer.Interval = 300 + (10 * _
                    (Me.PointToClient(MousePosition).Y - _
                    (Me.Font.Height \ 2)))
                End If
            Else
                If _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollRight Then
                    ScrollTimer.Interval = 300 - (10 * _
                    (Me.PointToClient(MousePosition).X - Me.ClientSize.Width))
                ElseIf _gCurrCursor.gScrolling = gCursor.eScrolling.ScrollLeft Then
                    ScrollTimer.Interval = 300 + (10 * _
                    (Me.PointToClient(MousePosition).X))
                End If
            End If
        Catch ex As Exception
        End Try

        If MouseButtons <> Windows.Forms.MouseButtons.Left Or _
            Me.PointToClient(MousePosition).Y >= Me.ClientSize.Height + 30 Or _
            Me.PointToClient(MousePosition).Y <= Me.ClientRectangle.Top - 30 Or _
            Me.PointToClient(MousePosition).X <= -40 Or _
            Me.PointToClient(MousePosition).X >= Me.ClientSize.Width + 30 _
     Then
            ScrollTimer.Stop()
            _gCurrCursor.gScrolling = gCursor.eScrolling.No
            _gCurrCursor.MakeCursor()
        Else
            ScrollControl(Me, scrollDirection)
        End If

    End Sub

    Private Sub ScrollControl(ByRef objControl As Control, _
        ByRef intDirection As Integer)

        ' For intDirection, a value of 0 scrolls up and 1 scrolls down.
        If scrollHorzVert = sHorzVert.Horz Then
            SendMessage(objControl.Handle.ToInt32, _
                WM_HSCROLL, intDirection, VariantType.Null)
        Else
            SendMessage(objControl.Handle.ToInt32, _
                WM_VSCROLL, intDirection, VariantType.Null)
        End If
    End Sub

Putting It All Together in the DragOver Event

In the DragOver Event the KeyState is checked to see if the Control Key is being pressed to switch between Move and Copy DragDropEffects. Then after getting the Item under the mouse, call the CheckScroller and PaintPointer routines.

    Private Sub gListView_DragOver(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.DragEventArgs) _
        Handles Me.DragOver

        If e.Data.GetDataPresent(SelLVColl, False) _
          Or e.Data.GetDataPresent(LVItem, False) Then

            If (e.KeyState And 8) = 8 Then
                e.Effect = DragDropEffects.Copy
            Else
                e.Effect = DragDropEffects.Move
            End If

            Dim Mpt As Point = Me.PointToClient(New Point(e.X, e.Y))
            Dim MeItem As ListViewItem
            MeItem = CType(sender, ListView).GetItemAt(Mpt.X, Mpt.Y)

            If Not IsNothing(_gCurrCursor) _
              AndAlso _AutoScroll <> eAutoScroll.None Then
                CheckScroller(Mpt, MeItem, e)
            End If

            If IsNothing(_gCurrCursor) _
              OrElse _gCurrCursor.gScrolling = gCursor.eScrolling.No Then
                PaintPointer(Mpt, MeItem, e)
            End If

        Else
            e.Effect = DragDropEffects.None
        End If

    End Sub

Coloring the Rows

To alternate the color of the rows, I override the OnDrawItem to check if the row index is odd or even to change the BackColor accordingly.

    Protected Overrides Sub OnDrawItem( _
      ByVal e As System.Windows.Forms.DrawListViewItemEventArgs)
        
        MyBase.OnDrawItem(e)

        e.DrawDefault = True

        If _ColorRows Then
            If e.ItemIndex Mod 2 = 0 Then
                e.Item.BackColor = _ColorRowA
            Else
                e.Item.BackColor = _ColorRowB
            End If
        Else
            e.Item.BackColor = Me.BackColor
        End If

    End Sub

    Protected Overrides Sub OnDrawColumnHeader( _
      ByVal e As System.Windows.Forms.DrawListViewColumnHeaderEventArgs)
        MyBase.OnDrawColumnHeader(e)

        e.DrawDefault = True

    End Sub

The gCursor

I like to see not only where the item is going, but what item is going. The gCursor [^] lets you display what is being dragged for immediate feedback to the drag contents. I made the gCursor a built-in property of the gListView. Its properties can be set programmatically, but it can be a pain having to run the program every time to test the look of the gCursor. I added a UITypeEditor [^] to make this process easier. So when you add the gListViewControl.dll to your project, also add the gCursor.dll. Add a gListView to the Form and the gCursor to the component tray. Then just like adding an ImageList to a ListView, choose the gCursor from the dropdown in the property grid to add it to the gListView. You can change most of its properties there, but if you select the gCursor in the Component Tray, you can open the property Editor for a richer more complete experience.

gCursorUIEditor

Open the dropdown in the gCurrCursor property to associate a gCursor with the gListView.

After opening the Editor, any adjustments you make will reflect in the example. You can even drag it around to get an even better feel for it. Close the Editor to set the new gCursor.

Dropping on the List

Since you can't use the Items.Insert method to put an item at the end of the list or if it is empty, you have to determine whether to use the Add or Insert. Then determine if you have one item or multiple items and are they moving or copying.

    Private Sub gListView_DragDrop(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.DragEventArgs) Handles Me.DragDrop

        If e.Data.GetDataPresent(SelLVColl, False) _
            Or e.Data.GetDataPresent(LVItem, False) Then

            'Determine where and how to add the item
            Dim insertItem As Boolean = True
            Dim dragItem, dragItem_Clone, InsertAtItem As New ListViewItem
            If Me.Items.Count = 0 Then
                insertItem = False
            Else
                If LineAbove Then
                    InsertAtItem = Me.Items(LineIndex)
                Else
                    If LineIndex + 1 < Me.Items.Count Then
                        InsertAtItem = Me.Items(LineIndex + 1)
                    Else
                        InsertAtItem = Me.Items(LineIndex)
                        insertItem = False
                    End If
                End If
            End If

            Me.BeginUpdate()

            'Flip the view to refresh the indexing
            Dim lvViewState As View = Nothing
            lvViewState = Me.View
            Me.View = View.List

            If e.Data.GetDataPresent(SelLVColl, False) Then
                Dim DragItems As SelectedListViewItemCollection = _
                    CType(e.Data.GetData(SelLVColl), _
                    SelectedListViewItemCollection)
                If Not DragItems.Contains(InsertAtItem) Then
                    For Each dragItem In DragItems
                        dragItem_Clone = CType(dragItem.Clone, ListViewItem)
                        If _MatchFont Then
                            dragItem_Clone.Font = Me.Font
                            dragItem_Clone.ForeColor = Me.ForeColor
                        End If
                        If insertItem = False Then
                            Me.Items.Add(dragItem_Clone)
                        Else
                            Me.Items.Insert(InsertAtItem.Index, dragItem_Clone)
                        End If
                        If e.Effect = DragDropEffects.Move Then
                            dragItem.Remove()
                        End If
                    Next
                End If

            ElseIf e.Data.GetDataPresent(LVItem, False) Then
                dragItem = CType(e.Data.GetData(LVItem), ListViewItem)
                dragItem_Clone = CType(dragItem.Clone, ListViewItem)
                If _MatchFont Then
                    dragItem_Clone.Font = Me.Font
                    dragItem_Clone.ForeColor = Me.ForeColor
                End If
                If insertItem = False Then
                    Me.Items.Add(dragItem_Clone)
                Else
                    Me.Items.Insert(InsertAtItem.Index, dragItem_Clone)
                End If
                If e.Effect = DragDropEffects.Move Then
                    dragItem.Remove()
                End If
            End If

            Me.View = lvViewState
            Me.EndUpdate()
            InsertAtItem.EnsureVisible()
        End If

    End Sub

Using the gListView

After dropping a gListView on your Form, go to the ItemDrag Event where you would normally put the DoDragDrop Call. Add any changes to the gCursor just before the DoDragDrop, if you are using it. Take a look at Form2 to see how simple it can be to use.

    Private Sub GListView4_ItemDrag(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.ItemDragEventArgs) _
        Handles GListView4.ItemDrag

        Dim glist As gListView = CType(sender, gListView)
        Dim glistitem As ListViewItem = CType(e.Item, ListViewItem)
        With glist.gCurrCursor
            .gImage = CType(glist.LargeImageList.Images( _
                glist.SelectedItems(0).ImageKey), Bitmap)
            .gText = glistitem.Text & vbCrLf & glistitem.SubItems(1).Text
            .MakeCursor()
        End With

        glist.DoDragDrop(glist.SelectedItems(0), DragDropEffects.Move)

    End Sub

History

  • Version 1.0 - March 2009

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here