「クラス(クラスモジュール)」についての概説です。
VBAでは「クラス」は必須ではありません。
「Excelでお仕事!」のサイトを開設してから長いのですが、実は「クラス」を取り上げて説明するということをして来ませんでした。
このページはサイト開設20年目にしてやっと作成することにしたものです。
ほとんどVB系の言語にしか携わってこなかったのですが、VB.NETでは「クラス」は相当に扱います。
VB.NETは「クラス」の取扱いが簡単で、コードを保存するファイルレベルで見ると、「標準モジュール」と「クラスモジュール」を同じファイル上で並べて保持させることもできます。
また、共通機能を保持する「クラス」はプロジェクトに内含せず、共通モジュールの置き場を作ってリンク参照できます。
さらに「クラス」の初期化を記述する「New」メソッドに独自に引数を追加できるなど、「クラス」を利用する側の記述も簡素化できるため、ソースコードの転用利用時にもメリットが生まれます。
一方、VBAでは「標準モジュール」も「クラスモジュール」もワークブック内に持ち込まないと機能しないため、ソースコードの転用利用ではVB.NETには劣りますが、
大きなプログラムになる場合には必要な機能になるはずです。
「構造化」や「オブジェクト指向」などをきちんと基礎から学んでいるわけではないので、「クラス」の理論的な解説はできませんが、当サイトにはすでに多くの「クラス」を使ったサンプルがあるので、
具体的な利用例を通しての用途・利点などを説明していきます。
なお、「クラス」はVBAをある程度利用(作成)されている方でないと理解できません。
VBAをこれから始めるような方は「こんなものがあるんだ」程度にサラッと読み飛ばす程度で構いません。
- 「クラス」って何?
-
「クラス(Class)」は直訳では「種類」「部類」「部門」「集合」だそうです。
VBAでは「クラスモジュール」に記述したもののことですが、これを一般に「クラス」と呼んでいます。
「マクロの記録」からVBAを始めた方が多いと思いますから、プロシージャは「標準モジュール」に作成するのが普通であり、大きな機能でなければそれで済みます。
ですが、作成する機能が複雑になって来ると「機能の部品化」の必要性が発生するものです。
大きな機能になると、この「機能の部品化」を行なわずに1つのプロシージャに全てプログラミングすることが困難になってくることはご理解いただけると思います。
一方、1つのプロシージャに書き切れないから部品化するのではなく、同じ記述が何回も発生する場合にその「同じ記述」の部分を「共通部品」として切り出そうという考え方もあると思います。
「機能の部品化」の考え方については、「機能分割と他プロシージャの呼び出し」や「仕様要件からコードの組み立てを考える。」でも説明しています。
このリンク先では「機能の部品化」での「本体」と「部品」の分ける視点や、仕様書的な図式化の方法についても触れています。
但し、「機能の部品化」でプロシージャを分割するという作業でも、これだけでは「クラス」は登場してきません。
「標準モジュール」は「マクロの記録」から自動的に作成されますが、「クラスモジュール」はVisualBasicEditor側で任意に追加しないと作成されません。(「挿入」メニューで選択します)
実は「ThisWorkbook」やワークシート(「Sheet1」等)などもコードの利用上は「クラスモジュール」であり、
自動的に作成されるのですが、VisualBasicEditorでの種類は任意に追加した「クラスモジュール」とは区別されていて、プログラミング上で任意に初期化するものではありません。
これらは「クラス」として意識してしなくても既定の「イベント」の記述場所としての役割があり、VBAに少し踏み込んだ方なら利用されていると思います。
このページでの説明は「ThisWorkbook」やワークシート(「Sheet1」等)などについては頭から外してお読み下さい。
- VBAでの「クラス」の必要性
-
「標準モジュール」にも「クラスモジュール」にもプロシージャを書き込んで呼び出すことができます。
単に「機能の部品化」として考えても「標準モジュール」だけでも良いはずで、必要に応じて機能(群)ごとに複数の「標準モジュール」を用いて、
モジュール自体に名前を付けることも可能です。
ここからは比較的大きな(複雑な)プログラムを頻繁に作成、あるいは修正する方向けの説明になります。
「クラス」でないといけないということが発生することはほとんどないはずですが、
機能を作成する側(プログラムを作る側)からすると、「クラス」にしておいた方が後々便利だということがあります。
このことは逆に見ると、作成したプログラムの利用者側では「クラス」を使用したかどうかでの違いはないということでもあります。
つまりは、開発側での「利点」ということになり、「クラス」を使用することで開発側がいずれ楽ができるというでもあります。
VBAで「クラス」を使う上での一番の利点は「カプセル化」です。(それしかないのかも知れません)
「カプセル化」というのは必要な機能以外は外側から見えないようにするということで、
具体的には「クラス」の内部だけでやりとりをする変数やプロシージャについて「クラス」への公開可否を制限することができます。
実際にはVisualBasicEditor上では「クラスモジュール」のコードは見えますが、スコープ(Public、Friend、Private)を調整することで外部に公開するかを制限できます。
見える、見えないだけなら「標準モジュール」でもスコープである程度制御できますが、「クラスモジュール」にはFriendスコープがあるのでプロジェクトの内外も明確になり、
「クラス」はPublicスコープでも初期化が必要なので、プロジェクトの外部からは「クラスモジュール」上のコードが参照できるわけではありません。
また、初期化から破棄までのライフサイクルが割当てできるため、このサイクルの間に他から割り込みが入るようなことも避けられる上、
例えばワークシートごとに同じ「クラス」をそれぞれで初期化して非同期に利用することもできます。
「標準モジュール」で同じことをやると、モジュールレベルにある変数は喰い合いとなって「後勝ち」になってしまいますが、「クラス」ではそれぞれのインスタンスの独立が保たれ、
モジュールレベルにある変数もそれぞれ独立して別個の値を保持できます。
いろいろな機能のプログラムを作成するのであれば「転用性」についても意識すると「カプセル化」が生かせます。
「クラスモジュール」には機能(群)を表現する名前を付けておき、他の機能プログラムの作成時に転用利用を可能にできます。
動作確認が済んでいる「クラス」であれば、その機能については転用先での同じ動作確認をスキップできる場合もあり、開発作業全般の効率化にも繋がります。
- VBAでの「クラス」の弱点
-
根本は「配布の問題」で説明していることですが、共通機能の「クラス」を作成したとしても、
後から「クラス」側に修正が入った場合は利用する全てのワークブックについてそれぞれの「クラス」を入れ替える必要があります。
この点は共通機能を「標準モジュール」で作成しても全く同じです。ですから「クラス」でなければ解決するわけでもありません。
VB.NETであれば共通機能のモジュールは各プロジェクトフォルダにコピーせずに、外部からリンク参照ができますが、
VBAではプロジェクトに従属するモジュール等はすべてワークブックの中に配置しなければなりません。
それぞれのワークブック内に「コピー」されてしまうため、共通の「クラス」といっても「内容が同じであるはず」というだけです。
もちろん、VB.NETであっても、再ビルドした上で実行ファイル(*.exe)の再配布が必要です。
- 当サイトでの「クラス」の利用例①
-
当サイトで提示しているサンプルで「クラス」を利用しているものの中から代表的なものを紹介します。
特に「ダウンロード」メニューの「組み込み用ExcelVBAモジュール集」にあるものはほとんどが「クラス」での提供で、
その「クラス」を利用するサンプルマクロを持つ「標準モジュール」とセットでワークブックに収容した状態で提供するようにしています。
ダウンロードしていただいて利用できるように汎用的な機能を持つもので、利用方法のバリエーションにも対応できるようにある程度は考えた「クラス」ですから、
「クラス」だけに着目した場合は複雑な記述になるものが多いかも知れません。
記述上の「クラス」を理解するためのサンプルとしては「パスワード生成クラス」が良いでしょう。
機能詳細は該当ページで見ていただくとして、ランダムに生成されたパスワードがシート上に100件も表示されるもので、ダウンロードしてからすぐに動かして試せるものです。
当該ページでは、その「クラス」の機能と制限事項、さらには公開メソッド(「プロシージャ」と記述されている)、プロパティの説明が先頭にあり、
ソースコードが提示されています。
一応、「クラス」の利用サンプルですが、「クラスモジュール」の作り方を説明しているわけではなく、汎用機能の作成済み「クラス」の利用方法の説明です。
ソースコードはモジュール単位で提示しており、
・「クラス」の呼び出しサンプルの「標準モジュール」⇒「Module1」
・当該機能の「クラスモジュール」⇒「clsMakePassword」
の2つです。
「標準モジュール(Module1)」
プロシージャは「パスワード生成テスト(CreatePasswordsTEST)」「パスワード消去テスト(ErasePasswordsTEST)」の2つで、
Excel側のメニューから起動できます。(引数・戻り値なし)
「クラス」を操作する記述は全てこのモジュールに記述されています。
「クラス」を利用するのは「パスワード生成テスト(CreatePasswordsTEST)」だけで、
プロシージャの先頭のオブジェクト変数宣言で「Dim objMakePassword As New clsMakePassword」として初期化も行なっています。
プロシージャの記述では、まずシート上に既にパスワードが生成済みでないか確認の上、一旦以前のパスワードを消去し、新規に100件のパスワードを生成するものです。
パスワード生成テストでは、1件ごとに「パスワード生成クラス」の「MakePassword」メソッドを呼び出しています。
「MakePassword」メソッドにはオプションとして、生成パスワードの文字種別の桁数の通知が受け取れるので、これらもシート上に展開させています。
「クラスモジュール(clsMakePassword)」
「クラスモジュール(clsMakePassword)」内の記述構造は以下の通りです。
記述項目 |
内容 |
モジュール定数 |
パスワード使用文字種(英数字のみ)、文字種別開始・終了位置、記号文字種初期値
|
モジュール変数(設定項目) |
パスワード使用文字種(英数字+記号文字種)、同・桁数
|
モジュール変数(プロパティ項目) |
各プロパティ設定値(当該ページ前方に解説あり)
|
クラス初期化プロシージャ(非公開) |
各プロパティ設定値にデフォルト値をセット(プロパティから指定されない場合の設定値となる)、乱数機能の初期化(Randomize)
|
パスワード生成(MakePassword、公開メソッド) |
1件の乱数を用いたパスワードの生成を行ない生成したパスワードを返す、また、オプションとして生成したパスワードの各文字種ごとの使用文字数を引数経由で通知する
|
サブプロシージャ(非公開) |
「パスワード生成プロシージャ(公開メソッド)」から部品として呼び出される2種のサブプロシージャを作成している
|
プロパティ(公開) |
記号文字種、パスワード桁数、英小文字使用、英大文字使用、数字使用、記号使用、同一文字生成上限件数、記号生成上限件数というパスワード生成に必要な設定項目
|
「クラス初期化プロシージャ(Class_Initialize)」は「クラス」を生成(Newキーワード)する時に呼び出される既定名のプロシージャです。
「クラス」終了時に特定の処理が必要な場合は「Class_Terminate」プロシージャを追加して記述します。
「パスワード生成クラス」の場合は「クラス」が並行して複数生成されることはありません。
「乱数初期化(Randomizeステートメント)」はExcel全体で一意であり、複数生成させると後から生成されるタイミングで
「乱数初期化(Randomizeステートメント)」も再度行なわれます。
「クラス」の初期化段階で各パラメータがプロパティで設定されているので、以降、「パスワード生成プロシージャ」を実行するごとに設定されたパラメータに従って新しいパスワードが生成されます。
このサンプルでは単一シートに100件のパスワードを一括生成するのみですが、
例えば複数シートにシートごとに異なる条件でパスワードを一括生成するようにプログラミングさせるような場合は、現ザンプルのプログラムの上位にシートごとのループを追加して、
そのシートごとのループ中の先頭で「クラス」の生成を行なうことで役割を果たします。
- 当サイトでの「クラス」の利用例②
-
もうひとつ、種類の違う「クラス」の利用例を挙げておきます。
見ていただくページは「カレンダー入力用フォーム」になります。
ワークシート上の日付セルや、ユーザーフォーム上の日付入力用コントロールからダブルクリックやF4キーなどで呼び出す小さなカレンダー機能です。
このカレンダー機能自身がユーザーフォームで作られており、このカレンダー機能の呼び出し部分はクラスではなく「標準モジュール」に置いた共通プロシージャとしています。
ここで紹介する「クラス」の利用例は「イベントクラス」というものです。
カレンダー機能での要件としてはカレンダーフォーム上の所望する日付ラベルをクリックした時にその日付を呼び元のセルや日付入力用コントロールに通知(表示させる)するというものです。
このサンプルには3つの「クラス」があり、その内の1つは他のページでも出てくる「カレンダー及び日付処理(祝日含む)関連関数クラス(clsAboutCalendar2)」です。
これは今回紹介する「イベントクラス」ではないので説明は割愛します。
残りの2つが「イベントクラス」です。特に「clsUF_Cal5Label1」が同じ機能で値が異なる「イベントクラス」です。
カレンダーフォーム上には7曜日×6週と昨日・今日・明日で合計45個の日付ラベルがありますから、普通に作成すると中身は同様で単純ですが45個のクリックイベントのプロシージャ記述が必要になります。
このカレンダーフォームの日付ラベルではクリックイベントの他、MouseMoveイベントも利用しており、年月日曜日祝日のステータス表示を行なっていますから、これも45個記述することになり、
これらだけで数百行のソースコードを占有することになります。
個数が固定されているので45個のプロシージャ記述でも問題ないのですが、誤記が発生することもあり、テストも45×2個のプロシージャで確認することになります。
何かで記述変更が発生した場合は、45個のプロシージャ全てについて同じ記述変更を行なうことになります。
このサンプルでの「クラス」の利点は「記述のまとめ上げ」ということになってしまいますが、動的にコントロールの増減を行なうような仕組みでは「クラス」は必須になります。
「clsUF_Cal5Label1」クラスのコード紹介は当該ページの最後の方になりますが、「イベントクラス」の特徴はコントロールの参照を置く宣言の「WithEvents」キーワードです。
「clsUF_Cal5Label1」クラス自体はその前の「カレンダーフォーム(UF_Calendar5)」のモジュールレベルに配列で宣言しており、
「フォーム初期化(UserForm_Initialize)」で45個分をループ処理で初期化させています。
初期化処理上で該当コントロールである日付ラベルとテーブルインデックスを引数で渡すため、「クラス」側の初期化処理は「Class_Initialize」ではなく独自プロシージャとしています。
このサンプルだと「カプセル化」の意味が解ると思います。
「clsUF_Cal5Label1」クラスはソースコードとしては1つしかありませんが、45個のインスタンスが作成され、それぞれが「クリック」等のイベントを発生します。
「クリック」された日付ラベルに対応した日付やインデックスはクラス側から取り出せるようになっています。
また、カレンダーフォームでのキー操作イベントでの青色反転表示のカーソル位置の移動についても、各日付ラベルの表現は「クラス」に持たせているプロパティで制御されるようにしており、
この「クラス」の役割は「クリック」等のイベントの処理だけではありません。
45個分をテーブルに置いているのも、最後の3つは昨日・今日・明日で別扱いですが、42個は7曜日×6週分であり、
フォームの初期化段階で日付の昇順に登録されます。
カレンダーフォームでのキー操作では、左右の矢印キーであればインデックスに「1」の増減、上下の矢印キーであればインデックスに「7」の増減で移動先のラベルが導き出されます。
ここでは移動元インデックスのラベルの反転色を戻して、移動先インデックスのラベルの反転色をセットするという作業をカレンダーフォーム側の記述で行なっていますが、
実際に反転色などの操作を行なう先はテーブルに配置してある「クラス」のプロパティです。