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:
- Is the mouse over an item or whitespace
- Is the
View
in LargeIcon
Mode
- Is the insertion point Above or Below the item
- Has the pointer moved to a new location
- 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)
If IsNothing(MeItem) Then
Me.Invalidate(InvRect)
If Me.Items.Count > 0 Then e.Effect = DragDropEffects.None
OffItems = True
Else
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
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 LineStartPt_N <> LineStartPt Or OffItems Then
Me.Invalidate(InvRect)
Me.Update()
OffItems = False
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)
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
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()
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