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