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

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

なかなか上手に使いこなせなかったクラスモジュールが、ようやく頭の中で氷解してきた気がするので、自分のためにまとめてみました。めちゃくちゃ長いです。


目次

  1. はじめに
  2. VBAクラスモジュールの簡単な使い方
    1. クラスを書く
    2. 標準モジュールからクラスを使う
    3. どう便利なの?
  3. クラスを便利に使う
    1. 複数のプロパティを持つクラスを作る
    2. クラスを使った複数のインスタンスを作ってみる
    3. 2次元配列と比較してみる
  4. プロパティに制限をつける
    1. 「隠蔽」されているプロパティ
    2. プロパティを読み取り専用にする
    3. プロパティを一度だけ設定可能にする
  5. 制限のないプロパティ設定/取得を簡単に書く
    1. Public変数で簡易的なプロパティに
    2. 制限の有無で書きわける
  6. クラスモジュールでサブルーチンを使う
    1. 引数を指定して同じ処理をさせる
    2. 複数のプロパティの設定をまとめて行う
    3. イベントプロシージャを共通化する
  7. コンストラクタとデストラクタ
    1. 書き方
    2. 呼び出されるタイミング
  8. もっとクラスモジュールを活用したい
    1. クラスモジュールを使った例

1. はじめに

VBAにもクラスはあるのですが、常々あんまり活用できてないなーと思ってました。それが先日、データベースから吐き出したCSVファイルを2次元配列に格納したのをきっかけに、2次元配列の構造をよくよく考えたら、これこそクラスで実現できるところなんじゃ…? と思い至り、やっっと自分の中で曖昧だった部分がすっきりした気がするので、その経緯を書き綴っておきます。

今回の思いに至るまで、VBAでクラスモジュールの記事を多数扱うthomさんのブログを読み漁り、twitterに突撃にしてご本人に質問したら快く教えてくださって、本当に本当にお世話になりました。

記事中でもその都度ご紹介させていただきますが、まずはこの場にて、厚くあつーく、御礼申し上げます!!! thomさん、ありがとうございましたー! ヽ(゜´Д`)ノ゜。

2. VBAクラスモジュールの簡単な使い方

まずは基礎的なところから。新規のxlsmファイルをVBEで開いて、標準モジュールとクラスモジュールを作成してみましょう。

160914-01

「挿入」から「標準モジュール」と「クラスモジュール」をクリックして、

160914-02

このように標準とクラスのモジュールができました。慣れてきたらクラスの名前を任意のものに変更すると更に可読性も良くなるんですが、今回は説明のため、デフォルトの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にこの記述をすると、こんなイメージになります。

160914-03

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 'インスタンスの生成
160914-04

インスタンスというのは「実体」という意味です。Class1さんにお願いしてobj1という車を1台用意してもらいました。

プロパティの設定

obj1.Value = 100 'Valueプロパティの設定
160914-05

ここで、さっきClass1に書いたProperty Letプロシージャが呼び出され、手配してもらった車の「Value」という荷台に荷物が載せられます。

プロパティの取得

Debug.Print obj1.Value 'obj1のValueプロパティの取得
160914-06

荷物を載せたあと荷台の中身を尋ねると、Class1のProperty Getプロシージャが呼びだされ、何が載っているのか教えてくれます。

インスタンスの破棄

Set obj1 = Nothing 'インスタンスの破棄
160914-07

使わなくなった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
160914-08

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台の車を手配してもらって、それぞれのプロパティに荷物を載せることができました。

160914-09

図で見るとこんなイメージです。ただ、これだと載せただけなので、取得と破棄も以下のように追記してみましょう。

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を通ることで、イミディエイトウィンドウに以下のように表示されます。

160914-10

このように、ひとつのオブジェクトに対して名前や値段などの好きな形のプロパティを自分で設定して使うことができて、更にそれをたくさん作ることができるんです。

Range("A1").ValueRange("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

このように書けるので、めちゃめちゃわかりやすい!!! のです。しかも必要なときに必要なだけ作ることができる。とっても扱いやすいです。

そして更にスゴイところはココ。クラスを使ってインスタンスを生成した変数は、

160914-11

自分で作ったプロパティが候補として出てくるんです! すてき!!

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プロシージャを使って中身を教えてもらいます。

ちょっとまどろっこしいような気がしちゃいますね。でもこれは、オブジェクト指向プログラミングの説明で言われる、「隠蔽」と呼ばれる手法なんです。

160914-09

このイラストで考えてみましょう。この中で、「車にどんな荷台(プロパティ)をいくつ設けるか?」ということを決めるのは、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

したがって、標準モジュールにこのように書いても、上記のハイライト部分でエラーになります。

160914-12

クラスモジュールには、値をセットするプロシージャがありませんからね。

図にするとこんな感じ。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

標準モジュールにこのように書くと、最初の「いちご」は設定できても、更に「りんご」を上書きしようとするとエラーになります。

160914-13

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を配車センターに例えて、クラスモジュールでプロパティを設定/取得する方法について書いてきました。でも、重要なことがあります。このままでは、この車は荷物を載せ降ろしするだけで、動かないんです。

せっかくなら、いろんな荷物を載せて動いて欲しいですよね。その「動き」は、標準モジュールでよく使う、サブルーチンを使います。(もちろん関数も使えます。)

160914-14

サブルーチンをクラスに書くと、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つのインスタンスを生成してプロパティを指定しました。このコードでは、まずは設定したプロパティを、なにもせずイミディエイトウィンドウへ書き出します。

160914-10

こんなかんじに、書いた内容がそのまま出てきますよね。それではここに、以下のハイライト部分を追記してみましょう。

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の処理をさせます。それぞれに違う引数を与えています。

160914-15

実行してみると、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さんの記事を参考にしております。ありがとうございます!

カレンダーコントロールを作る記事を書いたのですが、今回はそれを例に「フォーム上のいずれかのラベルがクリックされたら、そのコントロール名とキャプションを取得」するコードを書いてみます。

140804-16

ラベル番号がこのように並んでいるフォームがあって、

140804-24

最終的にはこんな形のユーザーフォームがあるとします。実際のカレンダーコントロールでは曜日や年月もラベルのため、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行目は省略できません。

これで、さきほど標準モジュールに書いたプロシージャを実行してカレンダーフォームを表示させ、

140804-27

試しに「20」をクリックしてみると、

160914-16

イミディエイトウィンドウに、クリックされたコントロール名とそのキャプションが入って、フォームが閉じます。ラベル以外のコントロールをクリックしても何も起こりません。

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. クラスモジュールを使った例

その後、クラスを使って書いてみた記事を追記していきます。

このブログでの記事

ご参考になれば幸いです。個人的には、同じ構造のオブジェクトをコレクションに格納してループで回すっていうのが本当に便利だと思います。

公開日:2016/09/14
更新日:2018/11/20

14件のコメント

  1. イナカモノ より:

    こんにちわ。
    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のフォームに連携させると、とても便利です。
    問題は、周りの同僚が付いていけないので難しいクラスを実装できない事です…

    • *you より:

      イナカモノさん、コメントありがとうございます!(わたしも田舎者ですw)

      Accessへの連携、お役に立て光栄です! やはりWithEvents絡みはもっと使えそうですね。わたしは業務ではExcelVBAを扱うことが多いのですが、AccessVBAならEventで動かすこと多いですもんね。参考になります、ありがとうございます(*゜ω゜*)

      追記:Propertyプロシージャだけじゃダメだろうと思って、サブルーチンの説明も追加しました。せっかくなのでイベントについても書かせていただきました!

  2. イナカモノ より:

    こんばんわ、イナカモノです。追記、とても参考になりました。
    「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 Sub
    

    2:Module

    Public ObjPublic As Object
    Public CountObjPublic As Long
    

    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
    
    • *you より:

      ううわぁぁぁすごいコードありがとうございます! コード表示のため一部整形させていただきましたのでご了承くださると幸いです。こういうコードって結構需要がありますので是非ブログはじめてみてはいかがでしょうか(ΦωΦ)フフフ…

      涙なしでは語れないエピソード、わたしも心当たりがありすぎます。あるあるですよね!! そして息が長いところもあるある…(;´Д`)

  3. イナカモノ より:

    こんばんわ、イナカモノです。
    コードの整形、有難う御座いました。自分のコード見てちょっと感動してました。

    ブログはどうしようかと暫く悩んでいたのですが
    仕事の小話とかで多少はネタがあり、前向きに考え中です。
    最近、私事で忙しいのですぐには無理ですがいずれは…
    出来たとしてもAccessとExcel(とVBA)の話が多くなりそうなので
    アクセス数は完全に度外視の趣味サイトになりそうです(;´・ω・)

    ではまた、いずれどこかで…

    • *you より:

      こんばんは! コード修正承りました。(修正事項はコメント欄には必要ないかと思って非公開にさせていただいちゃいますね。)綺麗に整形されていると自分のコードでも結構良さげに見えるものだと、わたしもいつも思ってますw

      AccessとExcelとVBA、まさにうちですよ! 言語としてはマイナー感ありますが利用者はとても多いので需要はあるものだと実感しています。ブログ始めたら是非教えてくださいねー!ヽ(゚∀゚)ノ

  4. nao より:

    先日はありがとうございました(@^∇^@)
    おかげさまで出来ました!
    表はテーブル名や名前定義ではできないようで、範囲指定にてできました。
    皆にも喜ばれちょっと鼻高々~( ^ ≧^)フフーン
    (って、ほとんどyouさんが作ってくれたんだけどw)

    あのあと、ちょっと忙しすぎて出来た御礼も言えず申し訳ないです。

    この投稿もとってもわかりやすくていいですね!
    考え方がわかり易く、空っぽの頭でも(⌒^⌒)b なるほど!ってなります。

    さて、週末は仕事のこと忘れて遊びましょう!!

    • *you より:

      naoさん、コメントありがとうございます。できたんですねー! よかったです!

      この記事から、例のパワポコピペのコードにつなげて記事にしたいと思っているのですがなかなかたどり着けず…。。ゆっくり書いていきます(*`・ω・)ゞ わたしも今週末は娘の運動会がんばってきますー!

  5. hatton より:

    60の手習いでクラスの勉強を始めました(男性)。こちらのサイトを参考にユーザーフォームのコントロールを制御するコードを何とか作ることができるようになりました。とても助かっています。クラスはまだまだ自由に使いこなせていませんが、ボケ防止も兼ねて頑張りたいと思っています。
    ところで、説明の順序でちょっとまごつくことがありました。
    「ExcelVBAのクラスモジュールって何?という人向けの使い方まとめ」 “18行目までのコードで、3台の車を手配してもらって、それぞれのプロパティに荷物を載せました。”辺りから実行結果のあとにこのような説明がされるような書き方になっています。次の”この後20~22行目のDebug.Printを通ると、イミディエイトウィンドウに、このように表示されます。”も同様です。以下、何箇所か同じような説明があります。
    “この後20~22行目のDebug.Printを通ると、イミディエイトウィンドウに、このように表示されます。”と言ったあとにイミディエイトウィンドウの表示があった方が、思考が逆転しなくていいのですが。このように感じるのはひょっとしたら私だけなのかもしれません。もし、お時間があればご検討ください。

    • *you より:

      hattonさん、コメントありがとうございます。幅広い層の方に読んでもらえて、また他の記事もお役に立てているようでたいへん光栄です。

      記事へのご指摘、ありがとうございます。確かに前後していてわかりにくい表現だと思いましたので、一部修正いたしました。この記事がさらなるステップアップへのお手伝いになれたら嬉しいです!

      追記:お名前変更、承知いたしました!

  6. あざらち より:

    ものすごくよく分かりました。
    クラスは喰えない、と諦めて10年以上でしたが、お陰様ですっかり使えるようになりました。
    分かりやすい解説に感謝します!感激して泣きそう。

    • *you より:

      あざらちさん、コメントありがとうございます。

      私もかなり長い時間「クラスは喰えない」と思っておりましたので、同じ気持ちだった方のお役に立てましてほんとうに嬉しいです! こちらこそありがたいお言葉に泣きそうです~~!

  7. Platinum Support より:

    凄い 誠にありがとうございました。

    • *you より:

      Platinum Supportさん、コメントありがとうございます。お役に立てたのであれば嬉しいです!


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

コメントは承認制ですので、反映までしばらくお待ち下さい。(稀にスパムの誤判定にて届かないこともあるようですので、必要な際はお問い合わせからお願い致します。)