【VB.NET】コントロールを配置位置で並べ替え

下記のようにすることで、コントロールを配置位置でソートできるようにします。

Dim controlList As New List(Of Control)()
...
controlList.Sort(AddressOf CompareByPosition)

上記のソートを行うためのComparisonジェネリック デリゲートは下記の通りです。

Imports System.Windows.Forms
Imports System.Runtime.InteropServices

''' <summary>
''' コントロールを位置で比較する。
''' </summary>
''' <param name="x">比較元コントロール。</param>
''' <param name="y">比較先コントロール。</param>
''' <returns>比較結果。</returns>
''' <remarks><see cref="List(Of Control)"/>.<c>Sort</c> メソッドのパラメータとして指定可能。</remarks>
Public Shared Function CompareByPosition(ByVal x As Control, ByVal y As Control) As Integer
    ' 許容範囲
    Const AcceptableRange As Integer = 5

    ' 上座標で比較
    Dim ret As Integer = x.Top - y.Top
    If 0 < Math.Abs(ret) - AcceptableRange Then
        Return ret
    End If

    ' 左座標で比較
    ret = x.Left - y.Left
    If 0 < Math.Abs(ret) - AcceptableRange Then
        Return ret
    End If

    ' Zオーダーで比較
    ret = CompareByZOrder(x, y)
    If Not ret = 0 Then
        Return ret
    End If

    ' コントロール名で比較
    Return CompareByName(x, y)
End Function

''' <summary>
''' コントロールをZオーダーで比較する。
''' </summary>
''' <param name="x">比較元コントロール。</param>
''' <param name="y">比較先コントロール。</param>
''' <returns>比較結果。</returns>
Private Shared Function CompareByZOrder(ByVal x As Control, ByVal y As Control) As Integer
    Dim h As IntPtr = GetWindow(x.Handle, GetWindowCmd.GW_HWNDNEXT)

    While h <> IntPtr.Zero
        If h = y.Handle Then
            Return -1
        End If
        h = GetWindow(h, GetWindowCmd.GW_HWNDNEXT)
    End While

    h = GetWindow(x.Handle, GetWindowCmd.GW_HWNDPREV)
    While h <> IntPtr.Zero
        If h = y.Handle Then
            Return 1
        End If
        h = GetWindow(h, GetWindowCmd.GW_HWNDPREV)
    End While

    Return 0
End Function

''' <summary>
''' コントロールを名前で比較する。
''' </summary>
''' <param name="x">比較元コントロール。</param>
''' <param name="y">比較先コントロール。</param>
''' <returns>比較結果。</returns>
Private Shared Function CompareByName(ByVal x As Control, ByVal y As Control) As Integer
    ' コントロール名で比較
    Return String.Compare(x.Name, y.Name)
End Function

''' <summary>
''' 指定されたウィンドウと指定された関係(またはオーナー)にあるウィンドウのハンドルを返す。
''' </summary>
''' <param name="hwd">元ウィンドウのハンドル。</param>
''' <param name="uCmd">関係。</param>
''' <returns>関数が成功すると、ウィンドウのハンドルが返ります。指定した関係を持つウィンドウがない場合は、NULL が返ります。拡張エラー情報を取得するには、 関数を使います。</returns>
<DllImport("user32.dll")> _
Private Shared Function GetWindow(ByVal hwd As IntPtr, ByVal uCmd As Integer) As IntPtr
End Function

''' <summary>
''' GetWindow関数のコマンド。
''' </summary>
Private Enum GetWindowCmd
    ''' <summary>
    ''' 指定したウィンドウと同じ種類で最も高い Z オーダーを持つウィンドウのハンドルを取得します。<br/>
    ''' 指定したウィンドウが最前面ウィンドウの場合は、最も高い Z オーダーを持つ最前面ウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウがトップレベルウィンドウの場合は、最も高い Z オーダーを持つトップレベルウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウが子ウィンドウの場合は、最も高い Z オーダーを持つ兄弟ウィンドウのハンドルが返ります。
    ''' </summary>
    GW_HWNDFIRST = 0

    ''' <summary>
    ''' 指定したウィンドウと同じ種類で最も低い Z オーダーを持つウィンドウのハンドルを取得します。<br/>
    ''' 指定したウィンドウが最前面ウィンドウの場合は、最も低い Z オーダーを持つ最前面ウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウがトップレベルウィンドウの場合は、最も低い Z オーダーを持つトップレベルウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウが子ウィンドウの場合は、最も低い Z オーダーを持つ兄弟ウィンドウのハンドルが返ります。
    ''' </summary>
    GW_HWNDLAST = 1

    ''' <summary>
    ''' 指定したウィンドウより Z オーダーが 1 つ下のウィンドウのハンドルを取得します。<br/>
    ''' 指定したウィンドウが最前面ウィンドウの場合は、1 つ下の最前面ウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウがトップレベルウィンドウの場合は、1 つ下のトップレベルウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウが子ウィンドウの場合は、1 つ下の兄弟ウィンドウのハンドルが返ります。
    ''' </summary>
    GW_HWNDNEXT = 2

    ''' <summary>
    ''' 指定したウィンドウより Z オーダーが 1 つ上のウィンドウのハンドルを取得します。<br/>
    ''' 指定したウィンドウが最前面ウィンドウの場合は、1 つ上の最前面ウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウがトップレベルウィンドウの場合は、1 つ上のトップレベルウィンドウのハンドルが返ります。<br/>
    ''' 指定したウィンドウが子ウィンドウの場合は、1 つ上の兄弟ウィンドウのハンドルが返ります。
    ''' </summary>
    GW_HWNDPREV = 3

    ''' <summary>
    ''' 指定したウィンドウのオーナーウィンドウのハンドルを取得します。
    ''' </summary>
    ''' <remarks>
    ''' 詳細については、「Owned Windows」を参照してください。
    ''' </remarks>
    GW_OWNER = 4

    ''' <summary>
    ''' 指定したウィンドウが親ウィンドウの場合は、Z オーダーが一番上の子ウィンドウのハンドルを取得します。<br/>
    ''' それ以外の場合は、NULL が返ります。<br/>
    ''' この関数は、指定されたウィンドウの子ウィンドウだけを調べます。<br/>
    ''' それより下位の子孫は調べません。
    ''' </summary>
    GW_CHILD = 5

    ''' <summary>
    ''' Windows 2000:指定したウィンドウをオーナーとする有効なポップアップウィンドウのハンドルを取得します。<br/>
    ''' (この検索では、GW_HWNDNEXT で見つかる最初のウィンドウを使います。)<br/>
    ''' 有効なポップアップウィンドウがない場合は、指定したウィンドウのハンドルが返ります。<br/>
    ''' </summary>
    GW_ENABLEDPOPUP = 6
End Enum

許容範囲という値を設定していますが、これはこの範囲内の位置の差であれば同じ位置であるとみなすというものです。
許容範囲なしにしてしまうと、わずか1ピクセル揃っていないだけで並び順に影響してしまいます。

実際に使用する際にはコンテナを考慮して再帰的に呼び出す事によって、タブ順を自動制御するなどしています。