ExcelVBAのクラスモジュールって何?という人向けの使い方まとめ

なかなか上手に使いこなせなかったクラスモジュールが、ようやく頭の中で氷解してきた気がするので、自分のためにまとめてみました。めちゃくちゃ長いです。
目次
- はじめに
- VBAクラスモジュールの簡単な使い方
- クラスを便利に使う
- プロパティに制限をつける
- 制限のないプロパティ設定/取得を簡単に書く
- クラスモジュールでサブルーチンを使う
- コンストラクタとデストラクタ
- もっとクラスモジュールを活用したい
1. はじめに
VBAにもクラスはあるのですが、常々あんまり活用できてないなーと思ってました。それが先日、データベースから吐き出したCSVファイルを2次元配列に格納したのをきっかけに、2次元配列の構造をよくよく考えたら、これこそクラスで実現できるところなんじゃ…? と思い至り、やっっと自分の中で曖昧だった部分がすっきりした気がするので、その経緯を書き綴っておきます。
今回の思いに至るまで、VBAでクラスモジュールの記事を多数扱うthomさんのブログを読み漁り、twitterに突撃にしてご本人に質問したら快く教えてくださって、本当に本当にお世話になりました。
記事中でもその都度ご紹介させていただきますが、まずはこの場にて、厚くあつーく、御礼申し上げます!!! thomさん、ありがとうございましたー! ヽ(゜´Д`)ノ゜。
2. VBAクラスモジュールの簡単な使い方
まずは基礎的なところから。新規のxlsmファイルをVBEで開いて、標準モジュールとクラスモジュールを作成してみましょう。
「挿入」から「標準モジュール」と「クラスモジュール」をクリックして、
このように標準とクラスのモジュールができました。
慣れてきたらクラスの名前を任意のものに変更すると更に可読性も良くなるんですが、今回は説明のため、デフォルトのClass1で進めていきます。
2-1. クラスを書く
作成したClass1に、以下のように書いてみます。
Private Value_ As Integer '値を受け渡す変数 '「Value」というプロパティの設定プロシージャ Property Let Value(ByVal new_Value As Integer) Value_ = new_Value End Property '「Value」というプロパティの取得プロシージャ Property Get Value() As Integer Value = Value_ End Property
Class1に「Value」という数値型のプロパティ(属性)の設定・取得を「するための」記述をします。設定するProperty Letと、取得するProperty Getプロシージャ、その間の値を受け渡すためのValue_という変数。ひとつの属性に対して、この3つがワンセットです。(例外もありますが、とりあえず基本として。)
オブジェクト型のプロパティを設定したい場合は、LetではなくSetになります。
この時点ではまだピンと来ないかもしれませんが、Class1にこの記述をすると、こんなイメージになります。
Class1さんの運営する配車センターです。現在は、Class1さんが「この形の車、配車うけたまわります」と言っているだけで、実際にはまだ、車はありません。
標準モジュールからClass1さんにお願いして、車を手配してもらいましょう。
2-2. 標準モジュールからクラスを使う
Module1にこのように書きます。
Sub Test1() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Value = 100 'Valueプロパティの設定 Debug.Print obj1.Value 'obj1のValueプロパティの取得 Set obj1 = Nothing 'インスタンスの破棄 End Sub
これが一連の流れです。ひとつずつイメージ図と一緒に見ていきましょう。
インスタンスの生成
Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成
インスタンスというのは「実体」という意味です。Class1さんにお願いしてobj1という車を1台用意してもらいました。
プロパティの設定
obj1.Value = 100 'Valueプロパティの設定
ここで、さっきClass1に書いたProperty Letプロシージャが呼び出され、手配してもらった車の「Value」という荷台に荷物が載せられます。
プロパティの取得
Debug.Print obj1.Value 'obj1のValueプロパティの取得
荷物を載せたあと荷台の中身を尋ねると、Class1のProperty Getプロシージャが呼びだされ、何が載っているのか教えてくれます。
インスタンスの破棄
Set obj1 = Nothing 'インスタンスの破棄
使わなくなったobj1という車を、荷物を捨てて廃車にしちゃいます。本物の車だったらその都度廃車にするなんてもったいなさすぎですが、プログラム上では使わなくなったものを残しておくと、不具合を起こしたり効率が悪くなったりする場合もあるので、片付けてキレイにします。
追記:正しくは「インスタンスへの参照の破棄」
この記事では全体的に「インスタンスの破棄」と書いてしまっているのですが、実際にはこの記述では参照を破棄しているのであって、直接オブジェクトを破棄しているわけではありません。こちらの記事でthomさんが解説してくださってます!
参照されなくなったオブジェクトは最終的に破棄されます。ここから先は「参照」が省略されてるんだなーと思って読んでいただけると幸いです!
ちなみに、破棄の記述は場合によって省略できます。上記の例はひとつのプロシージャ内だけで使うものであり、その場合はプログラムが終了する際に自動的に破棄されるので、この1文は書かなくても問題ありません。
ただし、もちろん省略できない場合(プロシージャを超えて大きな範囲で使うなど)もありますので、省略されていたとしても、「破棄という手順がある」ということだけは覚えておきましょう。
参考
破棄の記述については「省略できるところは省略しちゃおう派」と「明示的にわかりやすくするため書こう派」など、いろんな考え方があります。後者は、チームでプログラミングするときなどに「みんながわかりやすく」というルールがあったりしますからね。
2-3. どう便利なの?
さて、ここまでがひととおり基礎的な使い方なのですが、これだとイマイチ便利に感じないですよね。ひとつ変数を用意して、それに中身を入れればいいだけなのに、なんでわざわざクラスを作ってめんどくさいことをするの? って思っちゃいます。
クラスは、ひとつの車にひとつのプロパティ(属性)を持つだけだったら、特に必要性を感じません。プロパティを複数持っていて、その形の車がたくさん必要というときにはじめて、「なにこれ便利!!!」が実感できるのです。
3. クラスを便利に使う
それでは今度は、Class1さんの配車センターの車をグレードアップしてみましょう。
3-1. 複数のプロパティを持つクラスを作る
さっきの例を参考にして、今度はClass1に3つのプロパティの設定/取得のプロシージャを書いてみます。
Private Name_ As String '名称 Private Price_ As Integer '値段 Private Number_ As Integer '個数 '「Name」というプロパティの設定プロシージャ Property Let Name(ByVal new_Name As String) Name_ = new_Name End Property '「Name」というプロパティの取得プロシージャ Property Get Name() As String Name = Name_ End Property '「Price」というプロパティの設定プロシージャ Property Let Price(ByVal new_Price As Integer) Price_ = new_Price End Property '「Price」というプロパティの取得プロシージャ Property Get Price() As Integer Price = Price_ End Property '「Number」というプロパティの設定プロシージャ Property Let Number(ByVal new_Number As Integer) Number_ = new_Number End Property '「Number」というプロパティの取得プロシージャ Property Get Number() As Integer Number = Number_ End Property
Class1さんの配車センターが新装開店しました。プロパティが3つ載せられる車を手配してくれます。
3-2. クラスを使った複数のインスタンスを作ってみる
では今度は、この形の車をClass1さんに複数用意してもらいましょう。標準モジュールに以下を書きます。
Sub Test2() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Name = "いちご" 'Nameプロパティの設定 obj1.Price = 150 'Priceプロパティの設定 obj1.Number = 5 'Numberプロパティの設定 Dim obj2 As Class1 '宣言 Set obj2 = New Class1 'インスタンスの生成 obj2.Name = "りんご" obj2.Price = 200 obj2.Number = 3 Dim obj3 As Class1 '宣言 Set obj3 = New Class1 'インスタンスの生成 obj3.Name = "みかん" obj3.Price = 180 obj3.Number = 10 'プロパティを取得 'インスタンスの破棄 End Sub
18行目までのコードで、3台の車を手配してもらって、それぞれのプロパティに荷物を載せることができました。
図で見るとこんなイメージです。ただ、これだと載せただけなので、取得と破棄も以下のように追記してみましょう。
Sub Test2() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Name = "いちご" 'Nameプロパティの設定 obj1.Price = 150 'Priceプロパティの設定 obj1.Number = 5 'Numberプロパティの設定 Dim obj2 As Class1 '宣言 Set obj2 = New Class1 'インスタンスの生成 obj2.Name = "りんご" obj2.Price = 200 obj2.Number = 3 Dim obj3 As Class1 '宣言 Set obj3 = New Class1 'インスタンスの生成 obj3.Name = "みかん" obj3.Price = 180 obj3.Number = 10 Debug.Print obj1.Name, obj1.Price, obj1.Number 'プロパティを取得 Debug.Print obj2.Name, obj2.Price, obj2.Number Debug.Print obj3.Name, obj3.Price, obj3.Number Set obj1 = Nothing 'インスタンスの破棄 Set obj2 = Nothing Set obj3 = Nothing End Sub
こうして20~22行目のDebug.Printを通ることで、イミディエイトウィンドウに以下のように表示されます。
このように、ひとつのオブジェクトに対して名前や値段などの好きな形のプロパティを自分で設定して使うことができて、更にそれをたくさん作ることができるんです。
Range("A1").ValueやRange("A1").Addressのようなことを、自分で作れちゃうわけです。内容を端的にあらわすプロパティ名にしておけば、書くときに直感的にわかりやすいですし、書いた後の可読性も高いですよね。
3-3. 2次元配列と比較してみる
ここまで例として挙げたデータは、2次元配列でも表せます。
| Name | Price | Number |
|---|---|---|
| いちご | 150 | 5 |
| りんご | 200 | 3 |
| みかん | 180 | 10 |
試しに、このデータを2次元配列で表現してみましょう。
配列は0から数えるので、モノの数が3つ、それぞれのプロパティ(属性)が3つで、ary(2, 2)という2次元配列に格納したとします。この中のデータをひとつ指定したいとき、配列にNameやPriceなどの要素名は保持できないので、
- 1つめのモノの名前は? →
ary(0, 0) - 2つめのモノの個数は? →
ary(1, 2) - 3つめのモノの値段は? →
ary(2, 1)
このように要素の位置を数値で指定しなければなりません。Numberの要素は何番目だっけ…、実際コードに書いた後も、どの要素を表しているのかわかりにくいです。
しかも配列はあらかじめ、格納する要素の数がいくつなのかを先に指定して定義してやらなくちゃいけないので、要素数が不確定な場合はめんどくさいんです。
しかしクラスモジュールを使ってプロパティを作り、インスタンスを生成して書いてみると、
- 1つめのモノの名前は? →
obj1.Name - 2つめのモノの個数は? →
obj2.Number - 3つめのモノの値段は? →
obj3.Price
このように書けるので、めちゃめちゃわかりやすい!!! のです。しかも必要なときに必要なだけ作ることができる。とっても扱いやすいです。
そして更にスゴイところはココ。クラスを使ってインスタンスを生成した変数は、
自分で作ったプロパティが候補として出てくるんです! すてき!!
4. プロパティに制限をつける
さて、使い方が分かってきたところで、もうちょっと突っ込んだ特性を見ていきましょう。
4-1. 「隠蔽」されているプロパティ
クラスモジュールに、以下のような「Name」というプロパティの設定/取得をするPropertyプロシージャがあったとします。
Private Name_ As String '名称 '「Name」というプロパティの設定プロシージャ Property Let Name(ByVal new_Name As String) Name_ = new_Name End Property '「Name」というプロパティの取得プロシージャ Property Get Name() As String Name = Name_ End Property
Propertyプロシージャ間の値を受け渡す変数Name_は、Privateで宣言されており、このクラスモジュール内でしか使えません。
変数の適用範囲に関する過去記事
そして、どのモジュールでもそうですが、プロシージャのPublicは省略することができます。逆に言えば、プロシージャの先頭に何も記述されていない場合はPublicと判断されます。
つまり、先ほどのコードはこういうことになります。
Private Name_ As String '名称 '「Name」というプロパティの設定プロシージャ Public Property Let Name(ByVal new_Name As String) Name_ = new_Name End Property '「Name」というプロパティの取得プロシージャ Public Property Get Name() As String Name = Name_ End Property
プロパティの値が入っているName_はPrivate変数なので、クラスモジュールの外側からは直接操作することができません。外部からは、PublicであるLet/Getのプロシージャを介さないとプロパティを操作できないのです。
図を使って説明してみます。
このクラスモジュールを外部のモジュールから使う時、プロパティをセットするときは、PrivateであるName_には直接アクセスできません。PublicであるProperty Letプロシージャを使って、入れてもらうんです。
取り出すときは、これもまたPrivate変数であるName_から直接持ってこれないので、PublicであるProperty Getプロシージャを使って中身を教えてもらいます。
ちょっとまどろっこしいような気がしちゃいますね。でもこれは、オブジェクト指向プログラミングの説明で言われる、「隠蔽」と呼ばれる手法なんです。
このイラストで考えてみましょう。この中で、「車にどんな荷台(プロパティ)をいくつ設けるか?」ということを決めるのは、Class1さんです。Class1さんは責任感の強い車屋さんで、「うちから配車する車に関しての責任は全部自分で持ちます!」というスタンスなので、車に関することはまるっと全部引き受けます。
この場合、実務では「Class1」というデフォルトの名前から「Car」などの自分を表す名前にしておけば、もっとわかりやすくなります。
さらに、Class1さんは、この車に対するいろいろを「隠蔽」させることができます。これは別に意地悪とかじゃなくて、目的は「安全性」です。使う人のために「危ない使い方ができないように制限する」というニュアンスです。
そうしておくと、どうなるでしょう? Module1さんは「配車してもらって、使うだけ」なので、すっごく楽ですよね。車に関することは、何も考えなくても良いわけですから。
このように、クラス側で特定の機能に関する構造や仕様を定義し、外部から隠蔽することを「カプセル化」と呼びます。そうすると、使う側では「コードがシンプルになる」「クラス側に修正があってもこっちは何もしなくても良い」「エラー処理はクラス側でやってくれる」などなど、メリットだらけなのです。
VBAはオブジェクト指向に必要な「カプセル化」「継承」「ポリモーフィズム」という3大要素が全部揃っていないので、非オブジェクト指向言語です。でも、「カプセル化」だけは実装できるんです。
参考(違う言語とかでも、概念は一緒のはず。)
さて、さきほどのコードは値を「隠蔽」させる形はしていますが、ただ値を引き渡しているだけです。この形にもっと処理を加えることで、値を「隠蔽」させる意義が出てきます。
4-2. プロパティを読み取り専用にする
それでは、プロパティに制限を設けてみましょう。さきほどのクラスモジュールのコードを、このようにしてみます。
'「Name」というプロパティの取得プロシージャ Property Get Name() As String Name = "ぶどう" End Property
Property Getプロシージャだけにしてみました。こうすると、Nameプロパティは取得はできても設定はできなくなるので、読み取り専用となります。
したがって、標準モジュールにこのように書いても、以下のハイライト部分でエラーになります。
Sub Test3() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Name = "いちご" 'Nameプロパティの設定 Debug.Print obj1.Name 'obj1のNameプロパティの取得 Set obj1 = Nothing 'インスタンスの破棄 End Sub
クラスモジュールには、値をセットするプロシージャがありませんからね。
図にするとこんな感じ。Letプロシージャがなければ、値を保持するPrivate変数も必要ありません。ただしこの場合、インスタンスをいくつ作成しても、Nameプロパティはすべて「ぶどう」になっちゃいます。
4-3. プロパティを一度だけ設定可能にする
インスタンスごとに違う名前を設定したい、でも上書きできないようにしたい! ということもできます。
Private Name_ As String '名称
'「Name」というプロパティの設定プロシージャ
Property Let Name(ByVal new_Name As String)
If Name_ = "" Then
Name_ = new_Name
Else
Err.Raise 10000, Description:="一度設定した名前は変更できません。"
End If
End Property
'「Name」というプロパティの取得プロシージャ
Property Get Name() As String
Name = Name_
End Property
基本形のProperty Let(設定)プロシージャにIf文が入りました。Name_の中身をチェックして、空じゃなかったら(既に設定されていたら)エラーを出すことができます。
標準モジュールにこのように書くと、最初の「いちご」は設定できても、更に「りんご」を上書きしようとするとエラーになります。
Sub Test4() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Name = "いちご" 'Nameプロパティの設定 obj1.Name = "りんご" '再度Nameプロパティの設定 Debug.Print obj1.Name 'obj1のNameプロパティの取得 Set obj1 = Nothing 'インスタンスの破棄 End Sub
Class1に書いたエラーメッセージが表示されます。
図にするとこんな感じ。このように、Private変数を用意して値の設定/取得を別でやるというのは、一見まどろっこしいようにも思えますが、他のモジュールから引き渡された値を、そのまま格納する前にチェックする工程を作ることができるんです。
使い方はいろいろで、教えてくださったthomさんからは
LetにIf文を持たせることで、設定できる文字数や、数値の範囲などを判定させ、不正な処理をハンドリングできます。
というお言葉もいただいております!
これにより、たとえばものすごーーく長い文字列だとか、すごーーーく大きな数値だとか、プログラムに悪影響を与えそうな意地悪な値を渡されても、それをハネることができるので、安全性が増しますね!
プロパティに「月」を持たせたい、なんて場合を考えてみると…、数値以外はダメ、1~12の範囲内じゃないとダメ、整数じゃないとダメ、などのチェック処理をしなきゃいけないですよね。これをProperty Letプロシージャ側に書いてあげれば、実行側のプロシージャはとても簡潔になります。
5. 制限のないプロパティ設定/取得を簡単に書く
上述したように、Propertyプロシージャは制限を設けて隠蔽させることでメリットがあるわけですが、制限不要なプロパティを作りたい場合は、実はもっと簡単に書けるんです。
5-1. Public変数で簡易的なプロパティに
まずは、クラスモジュールでのPropertyプロシージャの基本形。(何度も書いてますが、おさらい。)
Private Name_ As String '名称 '「Name」というプロパティの設定プロシージャ Property Let Name(ByVal new_Name As String) Name_ = new_Name End Property '「Name」というプロパティの取得プロシージャ Property Get Name() As String Name = Name_ End Property
この形は、ただ値を引き渡しているだけなので、設定も取得も無制限で行えます。
図にするとこんな感じ。
Letプロシージャに、値をチェックする条件をつけないのであれば、隠蔽させる意味はそんなにありません。そんな場合、
Public Name As String
と、代わりに書くことができます。
わたしは、thomさんのブログではじめてこれを見たとき、あまりの短さに、マジかっっ!!!Σ(゚д゚lll)ってなりましたw
図にすると、こういう感じですね。変数をPublicにしちゃえ! と。
この書き方はプロパティに制限をつけられないので、外部から自由にされて困る場合は危ないです。でも、制限を設ける必要のないプロパティに限れば、効率的にこの書き方をするというのも、選択肢の1つじゃないかなと。
ここでまたthomさんからのひとこと。
VBAでは制限のないPropertyに意味はないので、Public変数で作ってしまったりしますが、VB.NETなら、クラスを継承することでプロシージャのオーバーライドが使えるので、あえてPropertyプロシージャにしておく意味はあります。
VBAならではってこと、結構ありますよね。
5-2. 制限の有無で書きわける
というわけで、せっかく短く書ける方法があるのだから、読み書き制限を設けないプロパティと制限を設けるプロパティで、書き方を変えてやってみましょう。クラスモジュールに、以下のように書いてみます。
Public Name As String '名称(Name)プロパティ Public Price As Integer '値段(Price)プロパティ Public Number As Integer '個数(Number)プロパティ '売上(Sale)プロパティは取得のみ Property Get Sale() As Integer Sale = Price * Number '値段×個数の値を使う End Property
NamePriceNumberプロパティは自由に設定/取得ができますが、Saleプロパティは計算した値を取得するGETプロシージャしかないので、値を書き替えることはできません。
これで標準モジュールにこのように書けば、それぞれのプロパティが簡単に取得できます。
Sub Test5() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Name = "いちご" 'Nameプロパティの設定 obj1.Price = 150 'Priceプロパティの設定 obj1.Number = 5 'Numberプロパティの設定 Debug.Print obj1.Name, obj1.Price, obj1.Number, obj1.Sale 'プロパティを取得 Set obj1 = Nothing 'インスタンスの破棄 End Sub
売上であるobj1.Saleは、obj1.Price×obj1.Numberと書いても同じ値をとることはできますが、クラスにプロパティとして登録してしまったほうがスマートに扱えますよね。
6. クラスモジュールでサブルーチンを使う
ここまで、Class1を配車センターに例えて、クラスモジュールでプロパティを設定/取得する方法について書いてきました。でも、重要なことがあります。このままでは、この車は荷物を載せ降ろしするだけで、動かないんです。
せっかくなら、いろんな荷物を載せて動いて欲しいですよね。その「動き」は、標準モジュールでよく使う、サブルーチンを使います。(もちろん関数も使えます。)
サブルーチンをクラスに書くと、Class1さんの配車センターが更にグレードアップし、みんなに同じ動きをさせることができます。この車を複数台手配して動かしてみると、載っている荷物がそれぞれ違うので、省コードで多様な結果を得ることができるんです。
書き方は、クラスに書いたサブルーチンを、実行側のモジュールでオブジェクト名.サブルーチン名と、ドットで区切って指定します。プロパティの書き方と同じですが、
- Range(“A1”).Name → Range(“A1”)「の」Name(名詞 → 属性/プロパティ)
- Range(“A1”).Select → Range(“A1”)「を」Select(動詞 → 働き/メソッド)
ちょうどこのように、サブルーチンで書いたものは、上記の「メソッド」になります。
6-1. 引数を指定して同じ処理をさせる
それでは例として、クラスモジュールに以下のように書いてみます。
Public Name As String '名称(Name)プロパティ Public Price As Integer '値段(Price)プロパティ Public Number As Integer '個数(Number)プロパティ Sub Work(ByVal deco As String, ByVal n As Integer) Name = deco & Name & deco '指定文字で挟む Price = Price * 1.08 '税込みの値段へ Number = Number * n '指定数をかける End Sub
制限を設けないプロパティを3つと、Workという名前のサブルーチンを1つ作ってみました。サブルーチンでは、2つの引数を持ってくるようにします。
次に標準モジュールへ以下のように書きます。
Sub Test6()
Dim obj1 As Class1 '宣言
Set obj1 = New Class1 'インスタンスの生成
obj1.Name = "いちご" 'Nameプロパティの設定
obj1.Price = 150 'Priceプロパティの設定
obj1.Number = 5 'Numberプロパティの設定
Dim obj2 As Class1
Set obj2 = New Class1
obj2.Name = "りんご"
obj2.Price = 200
obj2.Number = 3
Dim obj3 As Class1
Set obj3 = New Class1
obj3.Name = "みかん"
obj3.Price = 180
obj3.Number = 10
Debug.Print obj1.Name, obj1.Price, obj1.Number 'プロパティを取得
Debug.Print obj2.Name, obj2.Price, obj2.Number
Debug.Print obj3.Name, obj3.Price, obj3.Number
Set obj1 = Nothing 'インスタンスの破棄
Set obj2 = Nothing
Set obj3 = Nothing
End Sub
3つのインスタンスを生成してプロパティを指定しました。このコードでは、まずは設定したプロパティを、なにもせずイミディエイトウィンドウへ書き出します。
こんなかんじに、書いた内容がそのまま出てきますよね。それではここに、以下のハイライト部分を追記してみましょう。
Sub Test6() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.Name = "いちご" 'Nameプロパティの設定 obj1.Price = 150 'Priceプロパティの設定 obj1.Number = 5 'Numberプロパティの設定 Dim obj2 As Class1 Set obj2 = New Class1 obj2.Name = "りんご" obj2.Price = 200 obj2.Number = 3 Dim obj3 As Class1 Set obj3 = New Class1 obj3.Name = "みかん" obj3.Price = 180 obj3.Number = 10 obj1.Work "☆", 5 'クラスのサブルーチン処理 obj2.Work "▲", 2 obj3.Work "□", 4 Debug.Print obj1.Name, obj1.Price, obj1.Number 'プロパティを取得 Debug.Print obj2.Name, obj2.Price, obj2.Number Debug.Print obj3.Name, obj3.Price, obj3.Number Set obj1 = Nothing 'インスタンスの破棄 Set obj2 = Nothing Set obj3 = Nothing End Sub
プロパティを設定した後、3つのオブジェクトそれぞれに、さっき作ったサブルーチンWorkの処理をさせます。それぞれに違う引数を与えています。
実行してみると、Workを通った後にDebug.Printするので、こんな結果になります。名前に装飾、値段が×1.08、個数が指定値で乗算されているのがわかりますね。このように、3つとも同じサブルーチンを通っていても、プロパティの値も、指定した引数も違うので、それぞれ違った結果が得られます。
6-2. 複数のプロパティの設定をまとめて行う
ここまでプロパティに値を設定するときは1つずつ記述していましたが、サブルーチンを使って、複数のプロパティを一度に設定することもできます。
クラスモジュールに以下のように書いてみます。
Private Name_ As String '名称 Private Price_ As Integer '値段 Private Number_ As Integer '個数 '3つのプロパティの設定プロシージャ Sub SetProp(ByVal new_Name As String, ByVal new_Price As Integer, ByVal new_Number As Integer) Name_ = new_Name Price_ = new_Price Number_ = new_Number End Sub '「Name」プロパティの取得プロシージャ Property Get Name() As String Name = Name_ End Property '「Price」プロパティの取得プロシージャ Property Get Price() As Integer Price = Price_ End Property '「Number」プロパティの取得プロシージャ Property Get Number() As Integer Number = Number_ End Property
これまではプロパティが3つあったら、設定用と取得用プロシージャがそれぞれ3つずつありましたよね。このうち3つのLet(Set)プロシージャを、このように1つのサブルーチンで書くこともできます。
Public Name As String '名称 Public Price As Integer '値段 Public Number As Integer '個数 '3つのプロパティの設定プロシージャ Sub SetProp(ByVal new_Name As String, ByVal new_Price As Integer, ByVal new_Number As Integer) Name = new_Name Price = new_Price Number = new_Number End Sub
無制限のプロパティで良ければ、変数をPublicで宣言しちゃえばGetがなくても使えます。
このサブルーチンを標準モジュールから使う場合は、
Sub Test7() Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンスの生成 obj1.SetProp "いちご", 150, 5 '3つのプロパティの設定 Dim obj2 As Class1 Set obj2 = New Class1 obj2.SetProp "りんご", 200, 3 Dim obj3 As Class1 Set obj3 = New Class1 obj3.SetProp "みかん", 180, 10 Debug.Print obj1.Name, obj1.Price, obj1.Number 'プロパティを取得 Debug.Print obj2.Name, obj2.Price, obj2.Number Debug.Print obj3.Name, obj3.Price, obj3.Number Set obj1 = Nothing 'インスタンスの破棄 Set obj2 = Nothing Set obj3 = Nothing End Sub
このように書きます。クラスに書いたサブルーチンの引数の型に合うように、プロパティの値を引数として指定します。プロパティがたくさんあっても1行で書けるので楽ですよね。
6-3. イベントプロシージャを共通化する
もうひとつ、クラスモジュールとサブルーチンの組み合わせを使って、ユーザーフォームのイベントを共通化してみましょう。
イベントプロシージャは通常1コントロールにつき1つなので、複数同じ処理をしたいときに同じことを何度も書かなければならなくなります。これをクラスを使って共通化させると便利です!
またしてもthomさんの記事を参考にしております。ありがとうございます!
昔カレンダーコントロールを作る記事を書いたのですが、今回はそれを例に「フォーム上のいずれかのラベルがクリックされたら、そのコントロール名とキャプションを取得」するコードを書いてみます。
ラベル番号がこのように並んでいるフォームがあって、
最終的にはこんな形のユーザーフォームがあるとします。
実際のカレンダーコントロールでは曜日や年月もラベルのため、Label1~42だけに適用させるために配列を使ったのですが、ここではもっと汎用的に、すべてのコントロールをループさせて、ラベルだけコレクションに格納してみます。
このフォームをCalenderFormという名前だとして、まず標準モジュールへ以下を書きます。
Sub Test8() CalenderForm.Show End Sub
フォームを表示するだけです。次にクラスモジュールへ以下を書きます。
Public WithEvents Target As MSForms.Label 'イベント補足用の変数 Private Sub Target_Click() 'クリックされたときの処理 Debug.Print Target.Name, Target.Caption 'コントロール名と、そのキャプションを取得 Unload CalenderForm 'フォームの破棄 End Sub
WithEventsでラベルオブジェクト変数を宣言しておきます。イベントが発生したときに通るプロシージャは変数名_イベントという名前で、共通化させたい内容を書いておきます。
上の例では、ラベルオブジェクト変数をPublicで宣言していますが、Privateで隠蔽させる形をこちらの記事で紹介しています。
最後にCalenderFormのフォームモジュールへ以下を書きます。
Private labelCollection As Collection 'コレクションの宣言
Private Sub UserForm_Initialize() 'Formが開くとき
Set labelCollection = New Collection 'コレクション生成
Dim ctrl As Control 'コントロール変数の宣言
Dim obj As Class1 'インスタンス変数を宣言
For Each ctrl In Me.Controls 'フォーム内のコントロールをループ
If TypeName(ctrl) = "Label" Then 'コントロールのタイプがラベルだったら
Set obj = New Class1 'インスタンスの生成
Set obj.Target = ctrl 'コントロールをセット
labelCollection.Add obj 'コレクションへ追加
Set obj = Nothing 'インスタンス破棄
End If
Next
End Sub
7~14行目でフォーム内のすべてのコントロールをループさせ、ラベルだったときだけ9~12行へ入り、コレクションへ追加していきます。インスタンス変数objはループの中で何度も使われるため、その都度破棄しないとならないので、12行目は省略できません。
これで、さきほど標準モジュールに書いたプロシージャを実行してカレンダーフォームを表示させ、
試しに「20」をクリックしてみると、
イミディエイトウィンドウに、クリックされたコントロール名とそのキャプションが入って、フォームが閉じます。ラベル以外のコントロールをクリックしても何も起こりません。
7. コンストラクタとデストラクタ
クラスモジュールの機能として、インスタンスの生成時と破棄時に自動で呼び出されるプロシージャがあります。生成時に呼び出されるプロシージャをコンストラクタ、破棄時に呼び出されるプロシージャをデストラクタと呼びます。
7-1. 書き方
クラスモジュールに以下のように書くと、インスタンスが生成されたときに呼び出されます。
'コンストラクタ Private Sub Class_Initialize() Debug.Print "コンストラクタが呼び出されました" End Sub
インスタンスが生成されたときにこのプロシージャを通るので、ここへプロパティの値を書いておくと初期値を設定することができます。
コンストラクタに引数を渡せる言語の場合、インスタンス生成と同時に値の代入までまとめて書くことができるのですが、VBAはコンストラクタに引数が渡せないので、基本的にはこの中に書いた値しか使えません。初期値というより規定値という感じかも。
VBAで引数つきコンストラクタを実現させるためにいろんな手法を開発している上級者の方々もいらっしゃいますので興味があれば調べてみてください!
デストラクタの場合は以下のように書きます。インスタンスが破棄されたときに呼び出されます。
'デストラクタ Private Sub Class_Terminate() Debug.Print "デストラクタが呼び出されました" End Sub
自動で破棄されないオブジェクトの確実な後始末や、ログを残したいときなどに、ここに処理を書いておきます。
7-2. 呼び出されるタイミング
なお、オブジェクト変数の作り方によって、コンストラクタとデストラクタの呼び出されるタイミングが変わるということを補足しておきます。
ここまで、宣言とインスタンス生成で1行ずつ、このように書いていました。
Dim obj1 As Class1 '宣言 Set obj1 = New Class1 'インスタンス生成
これを、以下のように1行でまとめて書くこともできます。
Dim obj1 As New Class1 '宣言と生成を一緒に
1行で書けるので楽なんですが、ちょっと使い方に癖があります。
こちらのサイトにあるとおり、VBAはオブジェクトに実際にアクセスが発生した際にインスタンスが生成されるため、書き方によってコンストラクタを通るタイミングが違うんです。そして、宣言等の記述があっても実際にインスタンスが生成されなかったオブジェクトを破棄した場合はデストラクタを通らないので、注意が必要です。
…って思っていたら、宣言とNewを1行で書くの、なんと公式では非推奨らしい…!
一緒に書いてしまうことで、上述のようにコンストラクタ/デストラクタのタイミングが変わったり、Nothingで破棄したオブジェクトが参照できてしまったりと、不安定な挙動を見せるようです。というわけで、これから勉強していこうと思っている方は宣言とNewを分けて書く方向で覚えるのがお勧めです。
8. もっとクラスモジュールを活用したい
ものすっごい長い記事を、ここまで読んでいただいてありがとうございます。
一応、自分の中の溜まりに溜まったインプットは吐き出せたかな、という感じですが、これを踏まえて実務にどう活かすかを考えていきたいですね。
自分の業務で一番活かせそうなのはデータベースに絡めた使い方かなと思っているので、そのへんをまた書きたいと思っているのですが、わたしはひとつの(しかもIT系でない)企業にずっと属している人間なので、他の方のクラスモジュールの使い方には大変刺激を受けます。
クラスモジュールでできることはまだまだ想像もつかないくらいあるんでしょうが、まずは自分の業務で実用的だと思えることから頑張っていきたいと思います!
そして最後にもう一度、thomさんありがとうございました!!
追記:そんなthom神の書いたクラス入門記事もとっても勉強になりますので、合わせてどうぞ。
私の解説とは逆向きに、Public変数→Sub・Functionによるアクセサ→アクセサ専用のProperty構文 という順番で、ううむなるほどこれは理解しやすい…! と唸ってしまいました。私の記事でピンと来なかった方はぜひご参照ください!
8-1. クラスモジュールを使った例
その後、クラスを使って書いてみた記事を追記していきます。
このブログでの記事
- ExcelVBAでクラスモジュールを活用してCSVファイルをコレクションに格納する
- 構造体+コレクションで、プロパティを動的に指定して計算する方法を考える
- [VBA]ExcelグラフをPowerPointへ任意の位置・大きさで貼り付ける
ご参考になれば幸いです。個人的には、同じ構造のオブジェクトをコレクションに格納してループで回すっていうのが本当に便利だと思います。
他の方の記事
- クラスモジュール活用 カテゴリーの記事一覧 – t-hom’s diary
- クラスモジュールで遊ぶ(1) : 趣味のプログラムあれこれ
- クラスモジュールへの取り組み インデックス – Powerpoint VBAを使おう!
他の方の記事でクラスモジュールのめっちゃ参考になりそうなところも追記していきます!
ほかの入門記事はこちら
- ExcelVBA入門第0回 始める前に
- ExcelVBA入門第1回 動かしてみる
- ExcelVBA入門第2回 とりあえず覚えておくべきこと
- ExcelVBA入門第3回 変数の宣言
- ExcelVBA入門第4回 RangeとCells
- ExcelVBA入門第5回 ステップ実行
- ExcelVBA入門第6回 If ~ End Ifステートメント
- ExcelVBA入門第7回 インデントとコメントアウト
- ExcelVBA入門第8回 繰り返し処理
- ExcelVBA入門第9回 5種類のモジュールの違い
- ExcelVBA入門第10回 3種類のプロシージャと命名規則
- ExcelVBA入門第11回 スコープ(適用範囲)
- これからExcelのマクロを始めたいという方に!簡単な練習問題作りました。
- 私がExcelVBAでよく使う便利なコード・スニペットまとめ
- プログラム初心者さんへ贈る、エラーが起きたら試してみて欲しいこと
- ExcelVBAのクラスモジュールって何?という人向けの使い方まとめ
書籍を執筆しています。






14件のコメント
こんにちわ。
ExcelVBAとAccessの連携を調べていた所このHPを拝見しました。
お陰で上手く連携出来て助かりました。
クラスのお話が出ていたので…
個人的にクラスで取っ付き易いのはWithEvents絡みかなと考えております。
説明が上手く出来ないので参考ページを張っておきます。
・VB中学校 Visual Basic 6.0 初級講座
http://homepage1.nifty.com/rucio/main/shokyu/jugyou30.htm
・hatena chips クラスで自作イベントを実装する
http://hatenachips.blog34.fc2.com/blog-entry-174.html
主にAfterUpdateイベントにて入力後の内容をTrimしたり、INSERT文を仕込んで入力ログを取るのに使ったりしています。
Accessのフォームに連携させると、とても便利です。
問題は、周りの同僚が付いていけないので難しいクラスを実装できない事です…
イナカモノさん、コメントありがとうございます!(わたしも田舎者ですw)
Accessへの連携、お役に立て光栄です! やはりWithEvents絡みはもっと使えそうですね。わたしは業務ではExcelVBAを扱うことが多いのですが、AccessVBAならEventで動かすこと多いですもんね。参考になります、ありがとうございます(*゜ω゜*)
追記:Propertyプロシージャだけじゃダメだろうと思って、サブルーチンの説明も追加しました。せっかくなのでイベントについても書かせていただきました!
こんばんわ、イナカモノです。追記、とても参考になりました。
「Private LblClc As Collection~」の辺りは全く知りませんでした。
thomさんのブログはマメにチェックしていたのですが…
「ExcelVBAでControlオブジェクトのChangeイベントを一括制御する方法」も
更新されているのを見つけました。
私も40個位のTextBoxの入力ログを取る方法が分からなくて泣きながら
Excelのセルに「=”Private Sub ” & TextBox1 & “_Exit(略”」と
書いて無理やりコードを生成して誤魔化した覚えがあります。
今日、まだそのフォームが動いているのを見つけて変な汗が出ました…
さて本題ですが、ExcelVBAではWithEventsでExitが使えないと知り、
昔のリベンジがてら、無理やり汎用クラスを書いてみました。
自分のブログも無く、吐き出す場所が無いのですいませんがここで…長くなりますがご容赦下さい。
気が付かない間違いもあるかも…
一番左のシートのセルA~C列に入力ログを入れていきます。
考えとしては、他の入力オブジェクトに入力をしたら、前の入力オブジェクトの
データを入れるという考えです(MicroSoftのHPで前そういう内容を見かけたので)
1:Form
Private ChkChg As Collection 'コレクションの宣言 Private Sub UserForm_Initialize() 'Formが開くとき Set ObjPublic = Nothing ThisWorkbook.Sheets(1).Range("A:C").ClearContents Set ChkChg = New Collection 'コレクション生成 Dim Ctrl As Control 'コントロール変数の宣言 For Each Ctrl In Me.Controls 'フォーム内のコントロールをループ Select Case TypeName(Ctrl) Case "CheckBox", "ComboBox", "ListBox", "OptionButton", "TextBox" 'コントロールのタイプがチェックボックスだったら Dim obj As New Class 'インスタンス作成 obj.セット Ctrl 'コントロールをセット ChkChg.Add obj 'コレクションへ追加 Set obj = Nothing 'インスタンス破棄 Case Else End Select Next End Sub2:Module
3:Class
Public WithEvents chk As MSForms.CheckBox Public WithEvents cmb As MSForms.ComboBox Public WithEvents lst As MSForms.ListBox Public WithEvents opt As MSForms.OptionButton Public WithEvents txt As MSForms.TextBox Public Sub セット(ByRef Con As MSForms.Control) Select Case TypeName(Con) Case "CheckBox" Set chk = Con Case "ComboBox" Set cmb = Con Case "OptionButton" Set opt = Con Case "ListBox" Set lst = Con Case "TextBox" Set txt = Con Case Else Err.Raise 1000, "EventControl", "想定外のコントロールです。" End Select End Sub Private Sub chk_Change() Call 共通イベント(chk) End Sub Private Sub chk_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) Select Case KeyCode Case vbKeyTab, vbKeyTab + vbKeyShift, vbKeyUp, vbKeyDown, vbKeyLeft, vbKeyRight Case Else Call 共通イベント(txt) End Select End Sub Private Sub cmb_Change() Call 共通イベント(cmb) End Sub Private Sub cmb_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) Select Case KeyCode Case vbKeyTab, vbKeyTab + vbKeyShift, vbKeyUp, vbKeyDown, vbKeyLeft, vbKeyRight Case Else Call 共通イベント(cmb) End Select End Sub Private Sub lst_Click() Call 共通イベント(lst) End Sub Private Sub lst_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) Select Case KeyCode Case vbKeyTab, vbKeyTab + vbKeyShift, vbKeyUp, vbKeyDown, vbKeyLeft, vbKeyRight Case Else Call 共通イベント(lst) End Select End Sub Private Sub opt_Click() Call 共通イベント(opt) End Sub Private Sub opt_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) Select Case KeyCode Case vbKeyTab, vbKeyTab + vbKeyShift, vbKeyUp, vbKeyDown, vbKeyLeft, vbKeyRight Case Else Call 共通イベント(opt) End Select End Sub Private Sub txt_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) Select Case KeyCode Case vbKeyTab, vbKeyTab + vbKeyShift, vbKeyUp, vbKeyDown, vbKeyLeft, vbKeyRight Case vbKeyReturn If txt.EnterKeyBehavior = True Then Call 共通イベント(txt) Case Else Call 共通イベント(txt) End Select End Sub Private Sub 共通イベント(C As MSForms.Control) If ObjPublic Is Nothing Then CountObjPublic = 1 Set ObjPublic = C Else If ObjPublic.Name <> C.Name Then ThisWorkbook.Sheets(1).Cells(CountObjPublic, "A").Value = ObjPublic.Name ThisWorkbook.Sheets(1).Cells(CountObjPublic, "B").Value = ObjPublic.Value ThisWorkbook.Sheets(1).Cells(CountObjPublic, "C").Value = Now CountObjPublic = CountObjPublic + 1 Set ObjPublic = C Else End If End If End Sub Private Sub Class_Terminate() If Not ObjPublic Is Nothing Then ThisWorkbook.Sheets(1).Cells(CountObjPublic, "A").Value = ObjPublic.Name ThisWorkbook.Sheets(1).Cells(CountObjPublic, "B").Value = ObjPublic.Value ThisWorkbook.Sheets(1).Cells(CountObjPublic, "C").Value = Now Set ObjPublic = Nothing Else End If End Subううわぁぁぁすごいコードありがとうございます! コード表示のため一部整形させていただきましたのでご了承くださると幸いです。こういうコードって結構需要がありますので是非ブログはじめてみてはいかがでしょうか(ΦωΦ)フフフ…
涙なしでは語れないエピソード、わたしも心当たりがありすぎます。あるあるですよね!! そして息が長いところもあるある…(;´Д`)
こんばんわ、イナカモノです。
コードの整形、有難う御座いました。自分のコード見てちょっと感動してました。
ブログはどうしようかと暫く悩んでいたのですが
仕事の小話とかで多少はネタがあり、前向きに考え中です。
最近、私事で忙しいのですぐには無理ですがいずれは…
出来たとしてもAccessとExcel(とVBA)の話が多くなりそうなので
アクセス数は完全に度外視の趣味サイトになりそうです(;´・ω・)
ではまた、いずれどこかで…
こんばんは! コード修正承りました。(修正事項はコメント欄には必要ないかと思って非公開にさせていただいちゃいますね。)綺麗に整形されていると自分のコードでも結構良さげに見えるものだと、わたしもいつも思ってますw
AccessとExcelとVBA、まさにうちですよ! 言語としてはマイナー感ありますが利用者はとても多いので需要はあるものだと実感しています。ブログ始めたら是非教えてくださいねー!ヽ(゚∀゚)ノ
先日はありがとうございました(@^∇^@)
おかげさまで出来ました!
表はテーブル名や名前定義ではできないようで、範囲指定にてできました。
皆にも喜ばれちょっと鼻高々~( ^ ≧^)フフーン
(って、ほとんどyouさんが作ってくれたんだけどw)
あのあと、ちょっと忙しすぎて出来た御礼も言えず申し訳ないです。
この投稿もとってもわかりやすくていいですね!
考え方がわかり易く、空っぽの頭でも(⌒^⌒)b なるほど!ってなります。
さて、週末は仕事のこと忘れて遊びましょう!!
naoさん、コメントありがとうございます。できたんですねー! よかったです!
この記事から、例のパワポコピペのコードにつなげて記事にしたいと思っているのですがなかなかたどり着けず…。。ゆっくり書いていきます(*`・ω・)ゞ わたしも今週末は娘の運動会がんばってきますー!
60の手習いでクラスの勉強を始めました(男性)。こちらのサイトを参考にユーザーフォームのコントロールを制御するコードを何とか作ることができるようになりました。とても助かっています。クラスはまだまだ自由に使いこなせていませんが、ボケ防止も兼ねて頑張りたいと思っています。
ところで、説明の順序でちょっとまごつくことがありました。
「ExcelVBAのクラスモジュールって何?という人向けの使い方まとめ」 “18行目までのコードで、3台の車を手配してもらって、それぞれのプロパティに荷物を載せました。”辺りから実行結果のあとにこのような説明がされるような書き方になっています。次の”この後20~22行目のDebug.Printを通ると、イミディエイトウィンドウに、このように表示されます。”も同様です。以下、何箇所か同じような説明があります。
“この後20~22行目のDebug.Printを通ると、イミディエイトウィンドウに、このように表示されます。”と言ったあとにイミディエイトウィンドウの表示があった方が、思考が逆転しなくていいのですが。このように感じるのはひょっとしたら私だけなのかもしれません。もし、お時間があればご検討ください。
hattonさん、コメントありがとうございます。幅広い層の方に読んでもらえて、また他の記事もお役に立てているようでたいへん光栄です。
記事へのご指摘、ありがとうございます。確かに前後していてわかりにくい表現だと思いましたので、一部修正いたしました。この記事がさらなるステップアップへのお手伝いになれたら嬉しいです!
追記:お名前変更、承知いたしました!
ものすごくよく分かりました。
クラスは喰えない、と諦めて10年以上でしたが、お陰様ですっかり使えるようになりました。
分かりやすい解説に感謝します!感激して泣きそう。
あざらちさん、コメントありがとうございます。
私もかなり長い時間「クラスは喰えない」と思っておりましたので、同じ気持ちだった方のお役に立てましてほんとうに嬉しいです! こちらこそありがたいお言葉に泣きそうです~~!
凄い 誠にありがとうございました。
Platinum Supportさん、コメントありがとうございます。お役に立てたのであれば嬉しいです!
コメントは承認制ですので、反映までしばらくお待ち下さい。(稀にスパムの誤判定にて届かないこともあるようですので、必要な際はお問い合わせからお願い致します。)
YouTubeでQ&Aコンテンツを企画しています
運営しているYouTubeチャンネルで、ご相談やご質問を募集しています。動画のコメントやお問い合わせページからお気軽にご相談をお寄せください。