t-hom’s diary

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

VBA 自作のCollectionクラスをFor Eachでまわす裏ワザ

以前以下の記事で書いたものは、デフォルトプロパティの設定だった。
thom.hateblo.jp

しかしその後、海外のサイトでさらにすごいテクニックを発見。
http://www.papwalker.com/ref101/ccol.html

かいつまんで説明する。

まずクラスモジュールを作成し任意のクラス名にしておく。
ここではMyCollectionというクラス名にした。

そして以下のコードを張り付ける。

※2015/10/03 「項目」メンバがSubになっている間違いがありましたのでFunctionに修正しました。

Dim 内部コレクション As Collection

Private Sub Class_Initialize()
    Set 内部コレクション = New Collection
End Sub

Public Sub 追加(対象, Optional キー, Optional Before, Optional After)
    内部コレクション.Add 対象, キー, Before, After
End Sub

Public Function 項目(Index)
    項目 = 内部コレクション.Item(Index)
End Function

Public Function NewEnum() As IEnumVARIANT
         Set NewEnum = 内部コレクション.[_NewEnum]
End Function

プロジェクトエクスプローラーからMyCollectionクラスを選択し、ファイルメニューからファイルのエクスポートをクリックしてデスクトップなどに保存する。
エクスポートしたら、同じくファイルメニューからMyCollectionクラスを解放しておく。(解放時にエクスポートするか聞かれるが、いいえを選択する。)

MyCollection.clsファイルをメモ帳などのテキストエディタで開くと、VBE上では見えないAttribute(属性)などが見える。

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "MyCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Dim 内部コレクション As Collection

Private Sub Class_Initialize()
    Set 内部コレクション = New Collection
End Sub

Public Sub 追加(対象, Optional キー, Optional Before, Optional After)
    内部コレクション.Add 対象, キー, Before, After
End Sub

Public Function 項目(Index)
    項目 = 内部コレクション.Item(Index)
End Function

Public Function NewEnum() As IEnumVARIANT
         Set NewEnum = 内部コレクション.[_NewEnum]
End Function

項目とNewEnumの下に、それぞれ以下のような属性を書き加える。

Public Function 項目(Index)
Attribute Value.VB_UserMemId = 0
    項目 = 内部コレクション.Item(Index)
End Function

Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
         Set NewEnum = 内部コレクション.[_NewEnum]
End Function

上書き保存し、VBEのファイルメニューからこれをインポートする。
VBエディタ上からは書き加えたAttributeは見えない。
これでMyCollectionクラスの準備は完成。

後は標準モジュールに以下のようなコードを書いて実行してみると、

Sub test()
    Dim C As New MyCollection
    C.追加 "Hello"
    C.追加 "Good Bye"
    Debug.Print C(1)
    For Each x In C
        Debug.Print x
    Next
End Sub

普通のコレクションと同じように使えていることが分かる。
このテクニックを使うと、コレクションの機能を自由に拡張できる。

例えば、中身を配列にして返すToArrayメソッド、内容をソートするSortメソッド、特定の型のみを許可するジェネリクスもどきなども実装できる。

難点はコレクションでは非表示メソッドであるNewEnumが見えてしまうこと。
非表示にするAttributeもありそうな気がするが、調査はまた今度にする。

For Each文はNewEnumの有無で繰り返し可能なオブジェクトかどうかを判定しているとのこと。
Collectionも非表示オブジェクトとして[_NewEnum]を持っているので、MyCollectionのNewEnumでは内部コレクションの[_NewEnum]を返している。

参照設定でOLE Automationが外れているとこのコードは動かない。
IEnumVARIANTを普通のVariantに書き換えればOLE Automationが参照されていなくても動作したので、そちらの方が確実かもしれない。

IEnumVARIANTはOLE Automationライブラリで定義されている非表示のインターフェースのようだ。
オブジェクトブラウザでstdoleを選択して非表示のメンバーを表示させ、検索すると出てくる。

f:id:t-hom:20150921112138p:plain

ひょっとしてImplements IEnumVARIANTで各種メソッドを実装すればイテレータブルなオリジナルオブジェクトを作成できるかもしれないと思ったが、サポートされていないバリアント型を引数にとるメソッドがあるため、不正なインターフェースとされてしまうようだ。IUnknown型で同じことをしようと思ったが、こちらもうまくいかなかった。

何がしたかったかというと、コレクションの内部動作を人に説明するために、動的配列で疑似コレクションを作ろうと思ったのだ。
ただ、配列を返すプロパティを作らずに直接For Eachでオブジェクトを直接回せるようなものはうまく作れなかった。

なお、今回紹介したテクニックは以下の書籍(英語)でも紹介されている。

VBA Developer's Handbook

VBA Developer's Handbook

この書籍ではクラスモジュールの使い方などあまり知られていない高度なテクニックが非常に丁寧に解説されており、英語を読める方にはイチオシのバイブルである。

以上

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