[VBA] クラスを利用するメリットと方法について & 簡単なサンプル(1)
概要
この記事について
VBAにおけるクラスっていまいちマイナーで使いにくい存在だと思っている。
理由はいくつかあって、
- Javaなどと違ってクラスを使わなければ何も書けないという言語ではなく、
書こうと思えばクラスを使わなくても手続き型で全て書けてしまう - クラスの持つ機能が後発の言語に比べて弱いので(後述)、あえて使うメリットが見出しにくい。
- モジュールにPublicとして書けば何もせずとも呼び出せる関数でも、クラスの関数の場合はクラスのインスタンスを生成してからでないと利用できない。
ただ、
クラスを利用したほうがずっと安全で、保守性も高く、バグも少なくコーディングできる場面ももちろん存在するため、
この記事においてクラスを利用するメリットと方法についてを記したい。
また、クラスを利用した簡単なサンプルも合わせて記載したい。
説明のために作成したExcelファイルとソースコードはこちらでダウンロードできます。
クラスを利用する方法
クラスとは?
クラスとは、
利用するデータ(変数・定数)と、そのクラスが持つ処理内容(関数)をひとつの箱(クラスモジュール)にまとめたもの。
といっても、無作為に適当な変数や関数を一緒にまとめれば良いというものではなく、
そのクラスが指す一つの存在を定め、その存在に属するものとしての変数や関数でなければならない。
クラスの例
例えば、
ヒトという存在をクラスと定めれば、
ヒトは目、口、胴体などのデータ(変数)と、
走る、食べる、喋るなどの処理内容(関数)を持つ。
MSOffice風に言えば、
ExcelのRangeオブジェクトは
Addressプロパティ(参照範囲)やRowプロパティ(行の数値)、Valueプロパティ(セルの値)などの変数を持ち、
Selectメソッド(範囲にカーソルを合わせる)や、
AutoFitメソッド(範囲の行幅、列幅を自動調整する)などの関数を持つ、
典型的なクラスのオブジェクトである。
また、ある一定の機能を持つモノという抽象的な概念もクラスとしてまとめ得る。
例えば、ExcelのRangeオブジェクトを取込み、
表のように、罫線を引き、
ヘッダーの強調を行い、
小計列の合計をセルに書き込むという一連の処理を担当するモノが挙げられる。
これを、TableCreaterと命名しよう。
存在 | 変数・定数 | 関数 |
---|---|---|
ヒト | 目 口 胴体 |
走る 食べる 喋る |
Excel Rangeオブジェクト |
Address Row Value |
Select AutoFit |
TableCreater | 対象のRangeオブジェクト 小計列の列番号 ヘッダーセルの色 |
罫線を引く ヘッダーの強調のためのスタイル変更を行う 小計から合計を計算し出力する |
クラスを利用するメリット
安全なコーディングができる
クラスに宣言する変数、定数は
基本的にはPrivateの適用範囲(スコープ)で宣言し、
クラス内部の関数から用いるため、
不用意な呼出しで値が影響を受けることを防ぐことができる(そもそも呼出しをするとコンパイルエラーになる)
また、外部から変数の値などを参照したい場合や変数の値を設定したい場合は、
それ専用の関数として、
Porperty Getステートメント、Porperty Let(変数の型がオブジェクトの場合はSet)ステートメントを用いた関数を実装し、
それを通して外部とやりとりを行うようにすればよい。
これを
カプセル化
という。
例えば、次のようなパーセント格納変数とPorperty Let関数は、
パーセンテージとして0~100の値のみ許容し、それ以外の場合はNullを代入する。
このように、値の取得や設定の際になんらかのフィルタやチェックを行い、安全性を高めることができる。
1Private percentVal As Variant
2
3'**************************
4'*Setter
5'**************************
6Public Property Let percent(ByVal v As Long)
7 If v < 0 Or 100 < v Then percentVal = Null: Exit Property
8 percentVal = v
9End Property
10
保守性の高いコーディングができる
クラスの関数が、引数や内部で宣言した変数や定数などのみを極力使用するように
設計しておけば、
他のクラスやモジュールの変数・関数を利用したり利用されたりする頻度が減り、
互いのコードの結合性を低くすることができる。
(これを
疎結合
という)。
そうすれば、関数の処理変更や新しい関数の追加などでの影響範囲の大部分を
クラス内に留めることができるため、
思わぬ影響で想定外の動作をするリスクを低減することができる。
また、あるクラスにはそのクラスの指す対象に関係したコードしか書かれないようにしておけば、コードの見通しが大きく改善し、誰が見ても分かりやすいコードが書けるようになる。
決まりきった処理を必ず実行するようにでき、コーディングのミスが減る
VBAのクラスには、クラスのオブジェクト生成・破棄時に必ず実行される関数がある。
生成時は初期化処理であるClass_Initialize()、
破棄時は終了処理であるClass_Terminate()が実行される。
クラス内部でOutlookのMailアイテムやAccessのRecordSetなどを扱う場合、
外部の関数でこのクラスのオブジェクトを生成した際や破棄した際に必ず行うべき決まりきった処理をClass_InitializeやClass_Terminateに記載しておけば、
コードを書く際にその処理を忘れたり間違った処理を書いたりすることを防ぐことができる。
例えば、
- OutlookのMailアイテムを扱うクラスの場合は、
クラスのオブジェクトの生成時にOutlookアプリケーションも同時に生成したいし、
クラスのオブジェクトの破棄時に新規作成あるいは編集したMailアイテムをDisplayメソッドにより見える状態にしたい。
もし可視化を忘れてしまうと、バックグラウンドで生成されたメールにユーザはおそらく気づかない。 - AccessのRecordSetを扱うクラスの場合は、
クラスのオブジェクトの生成時にAccessデータベースとのコネクションを設定する処理を実行したい。
また、クラスのオブジェクトの破棄時にRecordSetをCloseし、それ以上の接続が必要なければデータベースとの接続もCloseしても良いかもしれない(「そのデータで何をしたいか」に依存するけれども、一例として)。
もしCloseを忘れてしまうと、ずっとそのデータベースにつなぎ続けることになり、不本意な結果を生むかもしれない。
※VBAのクラスの持つ機能は、後発の言語に比べて弱い※
ただし、VBAのクラスにも弱点がある。
VBAのクラス機能には残念ながら後発の言語に比べて癖があり、機能面でも後塵を拝する面が多少ある。
- Class_Initializeは引数を持つことができない。
そのため、生成時に同時にクラス変数に値を代入したりすることができない。 - クラスの継承という概念が無い(継承については→→→ [1] [2])
- staticな変数や関数を持つことができない。
すなわち、クラスに記述した変数や関数は、すべて、一度クラスをオブジェクト化しないと利用できない。
これらのような不便さがあるのが弱点だと思う。
Microsoftは、より機能の高いPythonやJavascriptをもって、
Officeの搭載言語をVBから入れ替えるみたいな話も小耳に挟んだので
なんとも言えないですが。。。
ともあれ、VBAはオブジェクト指向を理解する入り口としては優れたツールかと思っています。
VBAにおけるクラスの利用方法の一例
作成環境
Windows10
MSOffice 2016
i. VBEにおいてクラスモジュールを作成する
上記のTableCreaterの作成を例にしたいと思う。
VBE(VBAの開発画面)において
挿入タブからクラスモジュールを選択し、
作成する。
1Option Explicit
2
3'**************************
4'*TableCreater
5'**************************
6
ii. 変数・定数を宣言する
変数として、
対象のRangeオブジェクト
小計列の列番号
ヘッダーセルの色
を宣言する。
同時にProperty Let関数とProperty Set関数を記述する。
1Option Explicit
2
3'**************************
4'*TableCreater
5'**************************
6
7
8'定数
9Private Const HEADER_COLOR = 15917529 'ヘッダーセルの色
10
11'変数
12Private myRange As Range '範囲
13Private myColumnSubTotal As Long '小計列番号
14
15
16'******************************************************************************************
17'*getter/setter
18'******************************************************************************************
19
20
21Public Property Set Range(ByVal pRng As Range)
22 Set myRange = pRng
23End Property
24
25
26Public Property Let ColumnSubTotal(ByVal num As Long)
27 '範囲変数が設定されていない状態で呼出は禁止
28 If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
29 '引数が範囲オブジェクトの列番号より外の列の番号であればエラー
30 If num < myRange.EntireColumn(1).Column Or myRange.EntireColumn(myRange.EntireColumn.Count).Column < num Then Err.Raise 1001, , "列番号の指定が不正です。"
31 '値を設定
32 myColumnSubTotal = num
33End Property
34
35
iii. Class_Initialize、Class_Terminateを記述する
通常はここで初期化処理・終了処理のClass_Initialize、Class_Terminateを記述する。
ただ、TableCreaterクラスでは特にやることはない。
1Option Explicit
2
3'**************************
4'*TableCreater
5'**************************
6
7
8'~~略~~
9
10'******************************************************************************************
11'*関数名 :Class_Initialize
12'*機能 :
13'*引数 :
14'******************************************************************************************
15Private Sub Class_Initialize()
16
17 '定数
18 Const FUNC_NAME As String = "Class_Initialize"
19
20 '変数
21
22 On Error GoTo ErrorHandler
23
24 '本クラスでは特にやることはない。
25
26ExitHandler:
27
28 Exit Sub
29
30ErrorHandler:
31
32 MsgBox "エラーが発生したため、マクロを終了します。" & _
33 vbLf & _
34 "関数名:" & FUNC_NAME & _
35 vbLf & _
36 "エラー番号:" & Err.Number & vbNewLine & _
37 Err.Description, vbCritical, "TableCreater"
38
39 GoTo ExitHandler
40
41End Sub
iv. 関数を記述する
クラスが持つべきそれぞれの処理内容を一つずつの関数として記述する。
TableCreaterの持つべきクラスとしては以下のようになる
- 罫線を引く
- ヘッダーの強調のためのスタイル変更を行う
- 小計から合計を計算し出力する
1Option Explicit
2
3'**************************
4'*TableCreater
5'**************************
6
7
8'~~略~~
9
10
11'******************************************************************************************
12'*関数名 :drawLines
13'*機能 :罫線を引く
14'*引数 :
15'*戻り値 :True > 正常終了、False > 異常終了
16'******************************************************************************************
17Public Function drawLines() As Boolean
18
19 '定数
20 Const FUNC_NAME As String = "drawLines"
21
22 '変数
23
24 On Error GoTo ErrorHandler
25
26 drawLines = False
27
28 '範囲変数が設定されていない状態で呼出は禁止
29 If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
30
31 '罫線を引く
32 myRange.Borders.LineStyle = xlContinuous
33
34 drawLines = True
35
36ExitHandler:
37
38 Exit Function
39
40ErrorHandler:
41
42 MsgBox "エラーが発生したため、マクロを終了します。" & _
43 vbLf & _
44 "関数名:" & FUNC_NAME & _
45 vbLf & _
46 "エラー番号:" & Err.Number & vbNewLine & _
47 Err.Description, vbCritical, "TableCreater"
48
49 GoTo ExitHandler
50
51End Function
52
53
54'******************************************************************************************
55'*関数名 :setStyleForHeader
56'*機能 :ヘッダーの強調のためのスタイル変更を行う
57' ヘッダーは与えられた範囲の一行目のセルとする
58'*引数 :
59'*戻り値 :True > 正常終了、False > 異常終了
60'******************************************************************************************
61Public Function setStyleForHeader() As Boolean
62
63 '定数
64 Const FUNC_NAME As String = "setStyleForHeader"
65
66 '変数
67
68 On Error GoTo ErrorHandler
69
70 setStyleForHeader = False
71
72 '範囲変数が設定されていない状態で呼出は禁止
73 If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
74
75 'スタイル変更
76 With myRange.Rows(1)
77 '範囲の一行目のセルの色を変更
78 .Interior.color = HEADER_COLOR
79 '範囲の一行目を太字にする
80 .Font.Bold = True
81 '範囲の一行目を中央揃えにする
82 .HorizontalAlignment = xlCenter
83 End With
84
85
86 setStyleForHeader = True
87
88
89ExitHandler:
90
91 Exit Function
92
93ErrorHandler:
94
95 MsgBox "エラーが発生したため、マクロを終了します。" & _
96 vbLf & _
97 "関数名:" & FUNC_NAME & _
98 vbLf & _
99 "エラー番号:" & Err.Number & vbNewLine & _
100 Err.Description, vbCritical, "TableCreater"
101
102 GoTo ExitHandler
103
104End Function
105
106
107
108
109'******************************************************************************************
110'*関数名 :calcTotalFromSubTotal
111'*機能 :小計列から合計を計算し出力する
112'*引数 :
113'*戻り値 :True > 正常終了、False > 異常終了
114'******************************************************************************************
115Public Function calcTotalFromSubTotal() As Boolean
116
117 '定数
118 Const FUNC_NAME As String = "calcTotalFromSubTotal"
119
120 '変数
121 Dim sumVal As Long
122 Dim cell As Range
123 Dim subTotalOrder As Long
124
125 On Error GoTo ErrorHandler
126
127 calcTotalFromSubTotal = False
128
129 '範囲変数が設定されていない状態で呼出は禁止
130 If myRange Is Nothing Then Err.Raise 1000, , "範囲が設定されていません。"
131 '小計列が設定されていない状態で呼出は禁止
132 If myColumnSubTotal = 0 Then Err.Raise 1002, , "小計列が設定されていません。"
133
134 '小計列が範囲の列の何番目であるかを算出
135 subTotalOrder = myColumnSubTotal - myRange(1).Column + 1
136
137 '合計値計算(ヘッダー列は含まない)
138 For Each cell In myRange.Columns(subTotalOrder).Cells.Offset(1).Resize(myRange.Columns(subTotalOrder).Cells.Offset(1).Cells.Count - 1)
139 '数値の場合のみ加算
140 If IsNumeric(cell.Value) Then sumVal = sumVal + cell.Value
141 Next cell
142 If sumVal = 0 Then GoTo TruePoint
143
144 '合計値を小計列の最下端セルの下のセルに記入
145 With myRange.Columns(subTotalOrder).Rows(myRange.Columns(subTotalOrder).Cells.Count).Offset(1)
146 .Value = sumVal
147 '合計ラベルセル
148 With .Offset(, -1)
149 'ラベルを記入
150 .Value = "合計"
151 '合計ラベルセルと合計セルに罫線を引く
152 .Resize(, .Columns.Count + 1).Borders.LineStyle = xlContinuous
153 End With
154
155 End With
156
157TruePoint:
158
159 calcTotalFromSubTotal = True
160
161ExitHandler:
162
163 Exit Function
164
165ErrorHandler:
166
167 MsgBox "エラーが発生したため、マクロを終了します。" & _
168 vbLf & _
169 "関数名:" & FUNC_NAME & _
170 vbLf & _
171 "エラー番号:" & Err.Number & vbNewLine & _
172 Err.Description, vbCritical, "TableCreater"
173
174 GoTo ExitHandler
175
176End Function
177
ここまででクラスのコーディングは完了。
データ、動作を併せ持つ「テーブルを作成するモノ」の雛形であるクラスができた。
v. 外部の関数からクラスを利用する。
このように2種類のテンプレの表データが記載されている「原本」シートが
ある。
それぞれのテンプレデータをもとに、
新しいシートに表を作成するプログラムを作成した。
この関数はTableCreaterを用いてなるべくシンプルに記述している。
1'******************************************************************************************
2'*関数名 :TestTemplateA
3'*機能 :原本のテンプレAについて、TableCreaterを用いて表を作成する
4' 作成場所:新規シート
5'*引数 :
6'******************************************************************************************
7Public Sub TestTemplateA()
8
9 '定数
10 Const FUNC_NAME As String = "TestTemplateA"
11
12 '変数
13 Dim ws As Worksheet
14 Dim objTableCreater As tableCreater
15
16 On Error GoTo ErrorHandler
17
18 With ThisWorkbook
19 '新規シート作成
20 Set ws = .Worksheets.Add(, .Worksheets(.Worksheets.Count))
21 ws.Name = "テンプレA_" & Format(Now, "yyyymmddhhnnss")
22
23 '原本よりテンプレをコピー
24 ws.Range(ws.Cells(2, 2), ws.Cells(9, 4)).Value = .Worksheets("原本").Range(.Worksheets("原本").Cells(2, 2), .Worksheets("原本").Cells(9, 4)).Value
25
26 'TableCreaterをオブジェクト化
27 Set objTableCreater = New tableCreater
28 '範囲と小計列を設定
29 Set objTableCreater.Range = ws.Range(ws.Cells(2, 2), ws.Cells(9, 4))
30 objTableCreater.ColumnSubTotal = 4
31
32 '罫線を引く 異常終了時はExitHandler(終了処理)に移行
33 If Not objTableCreater.drawLines Then GoTo ExitHandler
34
35 'ヘッダーの強調のためのスタイル変更を行う 異常終了時はExitHandler(終了処理)に移行
36 If Not objTableCreater.setStyleForHeader Then GoTo ExitHandler
37
38 '小計から合計を計算 異常終了時はExitHandler(終了処理)に移行
39 If Not objTableCreater.calcTotalFromSubTotal Then GoTo ExitHandler
40
41 '列幅の調整
42 ws.Range(ws.Cells(2, 2), ws.Cells(9, 4)).EntireColumn.AutoFit
43
44 End With
45
46
47ExitHandler:
48
49 '変数を解放
50 Set objTableCreater = Nothing
51 Set ws = Nothing
52
53 Exit Sub
54
55ErrorHandler:
56
57 MsgBox "エラーが発生したため、マクロを終了します。" & _
58 vbLf & _
59 "関数名:" & FUNC_NAME & _
60 vbLf & _
61 "エラー番号:" & Err.Number & vbNewLine & _
62 Err.Description, vbCritical, "TableCreater"
63
64 GoTo ExitHandler
65
66End Sub
これを実行すると、このようになる。
vi. クラスの利用により得られたもの
TableCreaterを用いることで、
ある程度は見通しの良いコードになっているかと思う。
objTableCreater.で呼び出されるテキストを含むセンテンスは、
テーブル作成関連の意味を持つとわかる。
また、TableCreaterの関数は引数を必要とせず、
クラス内部の変数の利用で完結しているため、
コードを見たときにどんな動作をしているかが把握しやすいと思う。
終わりに
TableCreaterサンプルとソースコードについて
こちらのリンクをご参照ください。
記事の続き
TableCreaterの例だと、カプセル化の恩恵や初期化処理・終了処理を必ず実行できることの恩恵が十分には表現できなかったように思える。
続きの記事を作成しました。
[VBA] クラスを利用するメリットと方法について & 簡単なサンプル(2)
関連記事
- [Excel VBA]ポリモーフィズムを用いて、IF文を使わずラジオボタンごとの処理分岐を行う
- [Access VBA] フォームのコントロールに対してWithEventsでイベントリスナークラスを作成する際に気をつけなければならないこと
- [Excel VBA] 個人的に作業がはかどった自作Excelショートカット
- [Excel VBA]選択フォルダ配下のエクセルブックの全シートでA1にカーソル移動させるツールを作成した
- [Excel VBA] 見積書作成ツールを作成した