t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA Application.Runの代わりにCallByNameを利用したCollectionのバブルソート

以前、Collectionのバブルソートを記事にした。
thom.hateblo.jp

この記事ではCollectionに数値や文字列ではなくオブジェクトが入っていた場合も任意のプロパティを元に比較を行うことができる。プロパティの指定には別途以下のようなFunctionプロシージャを用意し、ソートのコードにそのプロシージャ名を文字列で渡すことでソートに利用するキーを渡すという仕組み。

Function SortKey1(V As Variant) As Double
    SortKey1 = V.Top
End Function

Function SortKey2(V As Variant) As String
    SortKey2 = V
End Function

ソートするコードではApplication.Runメソッドでこれらのソートキーを呼び出し、オブジェクト同士の比較する際のプロパティに使用する。

さて、この記事は公開からすでに1年前が経過したが、最近kantokuさんがその記事を取り上げてくれたようだ。
kantoku.hatenablog.com

上記の記事はCATIAという3次元CADソフトでVBAマクロを扱ったものであるが、CATIAにはApplication.Runメソッドが存在せずお悩みの様子。

Application.RunはExcelが持つメソッドである。ほかにWordやPowerPointにもRunメソッドが存在するが、それはあくまで各アプリケーションに実装されているだけで、VBAの標準機能ではない。

そこで今回はApplication.Runの代わりにVBA標準のCallByName関数を使って汎用性のあるCollectionのソートを書いてみようと思う。

CallByNameはどうやら標準モジュール上のコードを呼ぶことはできないようなので、ソートキーはクラスモジュール上に実装することにした。

作り方

まずクラスモジュールを挿入し「SortKeys」という名称に変更する。
それから以下のコードを記述する。

Function SortKey1(V As Variant) As Double
    SortKey1 = V.Top
End Function

Function SortKey2(V As Variant) As String
    SortKey2 = V
End Function

次に標準モジュールを挿入し、まずはApplicatoin.Runの代わりとなるコードを挿入する。
ここではCallByName関数を利用しており、新規のSortKeysオブジェクトとそのメソッド名を利用してプロシージャ呼び出しを行っている。

Function GetSortKeyByName(procName As String, V As Variant) As Variant
    GetSortKeyByName = CallByName(New SortKeys, procName, VbMethod, V)
End Function

毎回SortKeysオブジェクトを作成しなおすのでかなりムダが多いけど、実用上は(今のところ)何の問題もない。

次にCollectionの要素を入れ替えるCollectionSwapプロシージャを追加。

Sub CollectionSwap(C As Collection, Index1 As Long, Index2 As Long)
    Dim Item1 As Variant, Item2 As Variant
    
    If IsObject(C.Item(Index1)) Then
        Set Item1 = C.Item(Index1)
    Else
        Let Item1 = C.Item(Index1)
    End If
    
    If IsObject(C.Item(Index2)) Then
        Set Item2 = C.Item(Index2)
    Else
        Let Item2 = C.Item(Index2)
    End If
    
    C.Add Item1, after:=Index2
    C.Remove Index2
    C.Add Item2, after:=Index1
    C.Remove Index1
End Sub

これは冒頭で紹介した記事からそのまま持ってきた。

次に実際にバブルソートを行うコード。

Sub CSort(C As Collection, SortKeyFunction As String)
    Dim i As Long, j As Long
    For i = 1 To C.Count
        For j = C.Count To i Step -1
            If GetSortKeyByName(SortKeyFunction, C(i)) _
                > GetSortKeyByName(SortKeyFunction, C(j)) Then
                    CollectionSwap C, i, j
            End If
        Next j
    Next i
End Sub

これもほぼ冒頭で紹介した記事から持ってきたものであるが、Application.Runの代わりに先ほど挿入したGetSortKeyByNameプロシージャを呼び出している。

これで準備完了。あとは使うだけ。

Excelの場合は以下のコードを挿入し、適当にシートにシェイプを配置してから実行すると、シェイプのTopプロパティの昇順にイミディエイトウインドウに出力される。

Sub CollectionSortSample()
    'ソート用データ1の準備
    Dim C1 As New Collection
    For Each s In ActiveSheet.Shapes
        C1.Add s
    Next
    
    'ソート用データ2の準備
    Dim C2 As New Collection
    C2.Add "E"
    C2.Add "A"
    C2.Add "C"
    C2.Add "D"
    C2.Add "B"
    C2.Add "F"

    'ソート呼び出し
    CSort C1, "SortKey1"
    CSort C2, "SortKey2"
    
    'ソート結果出力1
    Dim k As Long
    For k = 1 To C1.Count
        Debug.Print (C1(k).Name)
    Next
    
    'ソート結果出力2
    Dim l As Long
    For l = 1 To C2.Count
        Debug.Print (C2(l))
    Next
End Sub

もっとシンプルに改良する

CallByName関数を利用すると、そもそもSortKeyを別の関数に持たせなくても直接オブジェクトのプロパティ名を文字列で呼び出すことができる。わざわざクラスモジュールを作る必要もない。
プロパティを備えたオブジェクトをソートする場合にはそちらのほうがシンプルで便利だ。

まず、CollectionSwapプロシージャはこれまで通り。

Sub CollectionSwap(C As Collection, Index1 As Long, Index2 As Long)
    Dim Item1 As Variant, Item2 As Variant
    
    If IsObject(C.Item(Index1)) Then
        Set Item1 = C.Item(Index1)
    Else
        Let Item1 = C.Item(Index1)
    End If
    
    If IsObject(C.Item(Index2)) Then
        Set Item2 = C.Item(Index2)
    Else
        Let Item2 = C.Item(Index2)
    End If
    
    C.Add Item1, after:=Index2
    C.Remove Index2
    C.Add Item2, after:=Index1
    C.Remove Index1
End Sub

CSortは少し変更した。

Sub CSort(C As Collection, Optional SortKey)
    Dim i As Long, j As Long
    For i = 1 To C.Count
        For j = C.Count To i Step -1
            If IsMissing(SortKey) Then
                If C(i) > C(j) Then
                    CollectionSwap C, i, j
                End If
            Else
                If CallByName(C(i), SortKey, VbGet) _
                    > CallByName(C(j), SortKey, VbGet) Then
                        CollectionSwap C, i, j
                End If
            End If
        Next j
    Next i
End Sub

SortKeyをOptionalで渡し、もし省略された場合は中身を直接比較、SortKeyが文字列で渡された場合はそれをCallByNameで取り出して比較する。

IsMissingで判定をするためSortKeyはVariant型にしているが、これは文字列が渡される想定だ。

Optional SortKey As String = ""として、空文字判定でも良い。普段はそうするんだけれど、たまにはIsMissingも使ってあげようかなと。

さて、準備に必要なコードは上記の2点のみ。
GetSortKeyByNameも要らないし、SortKey1、SortKey2も要らなくなる。

上記のコードを使ったサンプルがこちら

Sub CollectionSortSample()
    'ソート用データ1の準備
    Dim C1 As New Collection
    For Each s In ActiveSheet.Shapes
        C1.Add s
    Next
    
    'ソート用データ2の準備
    Dim C2 As New Collection
    C2.Add "E"
    C2.Add "A"
    C2.Add "C"
    C2.Add "D"
    C2.Add "B"
    C2.Add "F"

    'ソート呼び出し
    CSort C1, "Top"
    CSort C2
    
    'ソート結果出力1
    Dim k As Long
    For k = 1 To C1.Count
        Debug.Print (C1(k).Name)
    Next
    
    'ソート結果出力2
    Dim l As Long
    For l = 1 To C2.Count
        Debug.Print (C2(l))
    Next
End Sub

シェイプを比較する際は、Topプロパティで比較したいのでSortKeyに直接"Top"を渡している。また、文字列の比較の場合は、単にSortKeyを省略するだけで良い。

SortKey取得用の関数がなくなってスッキリ。以前より良いコードになった。
ただし、関数を使用するバージョンと違い、オブジェクトがGetプロパティを実装している前提になる。

たとえば自作のオブジェクトでPropertyプロシージャでなくPublic変数で実装されているようなものは使えないし、オブジェクトのFunction呼び出し結果で比較したい場合はCallByNameの第三引数はVbGetではなくVbMethodになる。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。