I found this piece of code and when I ran it, I was surprised how usable it actually was.
At the time, I needed something that would allow me to sort an array of things by dragging and dropping. The things I was working with were photos, but this class handles pretty much everything.
You can grab an item and drag it into a new position. A caret shows you where the new position will be. Even better, you can multi-select items and drag them to a new position. When you need the new order, just iterate through the child controls.
I don’t actually have a use for it right now, but I need to save this so I have it for the future.
Public Class ArrangeableFlowLayoutPanel
Inherits FlowLayoutPanel
Protected mouseDownPoint As Point
Protected insertCaret As PictureBox
Protected isMultiSelectOn As Boolean
Protected isRangeSelectOn As Boolean
Public Property AllowReordering As Boolean = True
Public Property CaretColor As Color = Color.Green
Public Property CaretWidth As Integer = 3
Public Property CaretPadding As Padding = New Padding(2, 0, 2, 0)
Public Property SelectionColor As Brush = Brushes.Black
Public Property SelectionWidth As Integer = 1
Friend Property SelectedControls As New Generic.List(Of Control)
Public Property DragTolerance As Integer = 40
Public Event ItemOrderChanged(sender As Object, e As EventArgs)
Public Sub New()
Me.AllowDrop = True
Me.AutoScroll = True
CreateCaret()
End Sub
Private Sub Form_Key(sender As Object, e As KeyEventArgs)
isMultiSelectOn = e.Control
isRangeSelectOn = e.Shift
End Sub
Private Sub ArrangeableFlowLayoutPanel_ControlAdded(sender As Object, e As ControlEventArgs) Handles Me.ControlAdded
AddHandler e.Control.MouseDown, AddressOf Item_MouseDown
AddHandler e.Control.MouseUp, AddressOf Item_MouseUp
AddHandler e.Control.MouseMove, AddressOf Item_MouseMove
AddHandler e.Control.Paint, AddressOf Item_Paint
End Sub
Private Sub ArrangeableFlowLayoutPanel_ControlRemoved(sender As Object, e As ControlEventArgs) Handles Me.ControlRemoved
RemoveHandler e.Control.MouseDown, AddressOf Item_MouseDown
RemoveHandler e.Control.MouseUp, AddressOf Item_MouseUp
RemoveHandler e.Control.MouseMove, AddressOf Item_MouseMove
RemoveHandler e.Control.Paint, AddressOf Item_Paint
If SelectedControls.Contains(e.Control) Then SelectedControls.Remove(e.Control)
End Sub
Private Sub ArrangeableFlowLayoutPanel_ParentChanged(sender As Object, e As EventArgs) Handles Me.ParentChanged
Dim f As Form
f = Me.FindForm
If f IsNot Nothing AndAlso Not f.KeyPreview Then f.KeyPreview = True
RemoveHandler Me.FindForm.KeyDown, AddressOf Form_Key
RemoveHandler Me.FindForm.KeyUp, AddressOf Form_Key
AddHandler Me.FindForm.KeyDown, AddressOf Form_Key
AddHandler Me.FindForm.KeyUp, AddressOf Form_Key
End Sub
Private Sub ArrangeableFlowLayoutPanel_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed
insertCaret.Dispose()
RemoveHandler Me.FindForm.KeyDown, AddressOf Form_Key
RemoveHandler Me.FindForm.KeyUp, AddressOf Form_Key
End Sub
Private Sub ArrangeableFlowLayoutPanel_DragDrop(sender As Object, e As DragEventArgs) Handles Me.DragDrop
Dim dropIndex As Integer
For i As Integer = 0 To SelectedControls.Count - 1
dropIndex = Me.Controls.GetChildIndex(insertCaret)
Me.Controls.SetChildIndex(SelectedControls(i), dropIndex + 1)
Next
Me.Controls.Remove(insertCaret)
RaiseEvent ItemOrderChanged(Me, New EventArgs)
End Sub
Private Sub ArrangeableFlowLayoutPanel_DragLeave(sender As Object, e As EventArgs) Handles Me.DragLeave
Dim topBorderY As Integer
Dim bottomBorderY As Integer
Dim mousePositionY As Integer
Dim hostForm As Form
Me.Controls.Remove(insertCaret)
hostForm = Me.FindForm
topBorderY = hostForm.PointToClient(Me.Parent.PointToScreen(Me.Location)).Y
bottomBorderY = Me.Height + topBorderY
mousePositionY = hostForm.PointToClient(MousePosition).Y
Do While mousePositionY >= bottomBorderY ' Below bottom of control
If Me.VerticalScroll.Value <= Me.VerticalScroll.SmallChange + Me.VerticalScroll.Maximum Then
Me.VerticalScroll.Value += Me.VerticalScroll.SmallChange
Else
Me.VerticalScroll.Value = Me.VerticalScroll.Maximum
End If
mousePositionY = hostForm.PointToClient(MousePosition).Y
Me.Refresh()
Loop
Do While mousePositionY <= topBorderY ' Above top of control
If Me.VerticalScroll.Value >= Me.VerticalScroll.SmallChange - Me.VerticalScroll.Minimum Then
Me.VerticalScroll.Value -= Me.VerticalScroll.SmallChange
Else
Me.VerticalScroll.Value = Me.VerticalScroll.Minimum
End If
mousePositionY = hostForm.PointToClient(MousePosition).Y
Me.Refresh()
Loop
End Sub
Private Sub ArrangeableFlowLayoutPanel_DragOver(sender As Object, e As DragEventArgs) Handles Me.DragOver
Dim ctl As Control
Dim dropControlPosition As Point
Dim dropIndex As Integer
If e.Data IsNot Nothing Then
e.Effect = DragDropEffects.Move
ctl = Me.GetChildAtPoint(Me.PointToClient(New Point(e.X, e.Y)))
If ctl IsNot Nothing AndAlso ctl IsNot insertCaret Then
dropControlPosition = ctl.PointToClient(New Point(e.X, e.Y))
If dropControlPosition.X <= ctl.Width \ 2 Then
dropIndex = Me.Controls.GetChildIndex(ctl) - 1
Else
dropIndex = Me.Controls.GetChildIndex(ctl) + 1
End If
If dropIndex < 0 Then dropIndex = 0
If Not Me.Controls.Contains(insertCaret) Then
insertCaret.Height = ctl.Height
Me.Controls.Add(insertCaret)
End If
Me.Controls.SetChildIndex(insertCaret, dropIndex)
End If
End If
End Sub
Private Sub Item_MouseDown(sender As Object, e As MouseEventArgs)
If e.Button = System.Windows.Forms.MouseButtons.Left Then
mouseDownPoint = e.Location
End If
End Sub
Private Sub Item_MouseUp(sender As Object, e As MouseEventArgs)
Dim ctl As Control
Dim startIndex As Integer
Dim endIndex As Integer
Dim newCtl As Control
If e.Button = System.Windows.Forms.MouseButtons.Left Then
ctl = DirectCast(sender, Control)
' Choosing individual items or the first of a range
If isMultiSelectOn OrElse (isRangeSelectOn And SelectedControls.Count = 0) Then
If SelectedControls.Contains(ctl) Then
SelectedControls.Remove(ctl)
Else
SelectedControls.Add(ctl)
End If
ctl.Invalidate()
' Choosing the end of a range
ElseIf isRangeSelectOn Then
startIndex = Me.Controls.GetChildIndex(SelectedControls(SelectedControls.Count - 1))
endIndex = Me.Controls.GetChildIndex(ctl)
For i As Integer = startIndex To endIndex Step CInt(IIf(startIndex < endIndex, 1, -1))
newCtl = DirectCast(Me.Controls(i), Control)
If Not SelectedControls.Contains(newCtl) Then
SelectedControls.Add(newCtl)
newCtl.Invalidate()
End If
Next
Else
SelectedControls.Clear()
SelectedControls.Add(ctl)
For Each c As Control In Me.Controls
c.Invalidate()
Next
End If ' single or multi-select
End If ' Left button only
End Sub
Private Sub Item_MouseMove(sender As Object, e As MouseEventArgs)
Dim ctl As Control
Dim rect As Rectangle
Dim rectPoint As Point
If AllowReordering AndAlso e.Button = System.Windows.Forms.MouseButtons.Left Then
ctl = DirectCast(sender, Control)
' create a range before dragging activates
rectPoint = New Point(mouseDownPoint.X, mouseDownPoint.Y)
rectPoint.Offset(0 - (Me.DragTolerance \ 2), 0 - (Me.DragTolerance \ 2))
rect = New Rectangle(rectPoint, New Size(Me.DragTolerance, Me.DragTolerance))
' See if we've dragged outside the tolerance area
If Not rect.Contains(e.Location) Then
' dragged item is not in selection, include it if ctrl is held
' otherwise, clear the selection and only use the dragged item
If Not SelectedControls.Contains(ctl) Then
If isMultiSelectOn Then
SelectedControls.Add(ctl)
ctl.Invalidate()
Else
SelectedControls.Clear()
SelectedControls.Add(ctl)
For Each c As Control In Me.Controls
c.Invalidate()
Next
End If ' Ctrl held down
End If ' Not in current selection
Me.DoDragDrop(SelectedControls, DragDropEffects.Move)
End If ' Outside drag buffer area
End If ' mouse button down
End Sub
Private Sub Item_Paint(sender As Object, e As PaintEventArgs)
Dim ctl As Control
ctl = DirectCast(sender, Control)
If SelectedControls.Contains(ctl) Then
' Draw outline
e.Graphics.DrawRectangle(New Pen(Me.SelectionColor, Me.SelectionWidth), Me.SelectionWidth \ 2, Me.SelectionWidth \ 2, ctl.Width - Me.SelectionWidth, ctl.Height - Me.SelectionWidth)
End If
End Sub
Private Sub CreateCaret()
insertCaret = New PictureBox
With insertCaret
.Name = "caret"
.Height = 1
.Width = CaretWidth
.Margin = CaretPadding
.Padding = New Padding(0)
.BackColor = Me.CaretColor
End With
End Sub
End Class