Arrangeable FlowLayout Panel

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