【VBA】クラスモジュールで再利用できる部品を作る方法(コピペOK)

VBA
スポンサーリンク
スポンサーリンク
  1. この記事でわかること
    1. どんな場面で使う?
  2. 完成イメージ(Before / After)
  3. クラスモジュールとは(30秒で分かる概要)
  4. 実行前の準備
    1. バックアップを取る
    2. Excelをマクロ有効ブック(.xlsm)で保存する
  5. 手順(コピペ → 実行まで約10分)
    1. Step 1: VBE(コードを書く画面)を開く
    2. Step 2: クラスモジュールを挿入する
    3. Step 3: クラスモジュールにコードを貼り付ける
    4. Step 4: 標準モジュールを挿入して呼び出しコードを貼る
    5. Step 5: マクロを実行する
  6. コード(最小版)– Property Get/Let の基本
    1. クラスモジュール側(clsEmployee)
    2. 標準モジュール側(呼び出しコード)
    3. 書き換えポイント
  7. コード(実務版)– Collection+クラスで社員一覧を処理
    1. クラスモジュール側(clsEmployee)– 実務版
    2. 標準モジュール側(呼び出しコード)– 実務版
    3. シート構成の例
    4. 書き換えポイント
  8. よくある落とし穴5選
    1. 1. Newを付けずにインスタンス化しようとしてエラーになる
    2. 2. Collectionのループ内でNewしないと全データが同じ値になる
    3. 3. クラスモジュール名を変え忘れて「Class1」のまま使っている
    4. 4. Property Let の引数名と Private変数名が同じでコンパイルエラー
    5. 5. End ステートメントで強制終了すると Class_Terminate が実行されない
    6. VBAのクラスモジュールでNewを忘れてエラーが出るときの対処法
    7. VBAのCollectionに入れたクラスが全部同じ値になるときの対処法
  9. FAQ
    1. Q1: クラスモジュールと標準モジュール、どう使い分ける?
    2. Q2: Property Get/Let を使わず、Public変数にしてはダメ?
    3. Q3: Collection と 配列、どちらでクラスのインスタンスを管理すべき?
    4. Q4: クラスモジュールに配列やDictionaryを持たせられる?
    5. Q5: クラスモジュールを別のブックにコピーできる?
  10. まとめ
    1. 関連記事
  11. 次にやりたくなること

この記事でわかること

  • クラスモジュールで「自分だけの部品(オブジェクト)」を作る方法
  • Property Get / Let でプロパティを読み書きする書き方
  • Class_Initialize / Class_Terminate の使いどころ
  • Collectionにクラスのインスタンスを格納して一覧データを処理するパターン

対象: Excel 2016以降 / Microsoft 365、Windows 10/11

どんな場面で使う?

  • 同じ集計処理を複数シートにコピペして使い回しているのを1箇所にまとめたいとき
  • 社員マスタ・商品マスタなど同じ構造のデータを部品化して管理したいとき
  • 仕様変更のたびにコピペした全箇所を修正する地獄から抜け出したいとき
  • CollectionやDictionaryにオブジェクトを格納して一覧処理したいとき

完成イメージ(Before / After)

Before(同じ処理をコピペで5箇所に書いている):


' シート1用
Sub 集計_シート1()
    Dim name As String: name = Sheets("Sheet1").Range("A2").Value
    Dim dept As String: dept = Sheets("Sheet1").Range("B2").Value
    ' ... 同じ処理が続く
End Sub

' シート2用(ほぼコピペ)
Sub 集計_シート2()
    Dim name As String: name = Sheets("Sheet2").Range("A2").Value
    Dim dept As String: dept = Sheets("Sheet2").Range("B2").Value
    ' ... 同じ処理が続く
End Sub

After(クラスモジュールで部品化):


' クラスモジュール「clsEmployee」に部品を集約
Dim emp As New clsEmployee
emp.EmployeeName = Range("A2").Value
emp.Department = Range("B2").Value
MsgBox emp.Summary  '← 「田中太郎(総務部)」と表示

データと処理をクラスに集約。呼び出し側のコードが短くなり、修正も1箇所で済む。

自分も以前、同じ集計処理を5つのシートにコピペして使い回していた。仕様変更のたびに5箇所全部を直すのが正直しんどかった。クラスモジュールで部品化してからは、修正は1箇所だけ。呼び出し側のコードも短くなって、メンテが格段にラクになった。この記事では、最小版→実務版の2段階でクラスモジュールの使い方を解説する。コピペの連鎖で苦しんでいる人が、この記事でクラスモジュールの第一歩を踏み出せればうれしい。

クラスモジュールは「データと処理をセットにした部品」。一度作れば、どこからでも呼び出して使い回せる。

なお、Sub/Functionの呼び出しの基本については マクロから別のマクロを呼び出して処理を分割する方法 を先に読むとスムーズに進められる。

クラスモジュールとは(30秒で分かる概要)

比較 標準モジュール クラスモジュール
役割 処理(Sub/Function)の置き場 データと処理をセットにした「部品」
使い方 Call マクロ名 で呼ぶ Dim obj As New クラス名 で作って使う
量産 同じ処理をコピペで増やすしかない New するたびに独立した部品が作れる
修正 コピペした全箇所を直す クラスの中身を1箇所直すだけ

クラスモジュール = 「設計図」、New で作ったもの = 「実物(インスタンス)」と考えればよい。

実行前の準備

バックアップを取る

実務版コードはD列にデータを書き込む。必ずファイルのコピーを別フォルダに保存してから実行する。

Excelをマクロ有効ブック(.xlsm)で保存する

拡張子が .xlsx のままだとマクロが保存できない。

  1. 「ファイル」→「名前を付けて保存」
  2. ファイルの種類を「Excelマクロ有効ブック (*.xlsm)」に変更
  3. 保存

手順(コピペ → 実行まで約10分)

Step 1: VBE(コードを書く画面)を開く

  1. Excelで Alt + F11 を押す

Step 2: クラスモジュールを挿入する

  1. VBEのメニュー →「挿入」→「クラスモジュール」
  2. 画面左下の「プロパティ」ウィンドウで (オブジェクト名)clsEmployee に変更する
    • プロパティウィンドウが見えない場合は F4 キーで表示
    • 名前は半角英数字で入力。日本語は使わない

Step 3: クラスモジュールにコードを貼り付ける

  1. 右側のコードウィンドウに、下の「クラスモジュール側コード」をそのままコピペする

Step 4: 標準モジュールを挿入して呼び出しコードを貼る

  1. VBEのメニュー →「挿入」→「標準モジュール」
  2. 右側のコードウィンドウに、下の「標準モジュール側コード」をそのままコピペする

Step 5: マクロを実行する

  1. Alt + F8 → マクロ名を選んで「実行」

コード(最小版)– Property Get/Let の基本

クラスモジュールに「社員名」「部署」のプロパティを定義し、標準モジュールから値をセットして読み出す。

クラスモジュール側(clsEmployee)


'============================================================
' ■ クラスモジュール: clsEmployee(最小版)
'   → 社員名と部署を持つ「社員」部品
'   → 貼り付け場所: クラスモジュール(名前を clsEmployee に変更)
'============================================================
Option Explicit

'--- Private変数(外部から直接アクセスさせない)
Private pName As String
Private pDept As String

'--- Class_Initialize: Newした瞬間に自動実行される
'    (文字列型は宣言時に空文字になるため省略可だが、明示しておくと分かりやすい)
Private Sub Class_Initialize()
    pName = ""
    pDept = ""
End Sub

'--- Class_Terminate: Set Nothing またはスコープ終了時に自動実行される
Private Sub Class_Terminate()
    '後片付けが必要な場合はここに書く(ファイルを閉じる等)
    '今回は特に処理なし
End Sub

'--- Property Let: 値を書き込む(emp.EmployeeName = "田中" で呼ばれる)
Public Property Let EmployeeName(ByVal val As String)
    pName = val
End Property

'--- Property Get: 値を読み出す(MsgBox emp.EmployeeName で呼ばれる)
Public Property Get EmployeeName() As String
    EmployeeName = pName
End Property

Public Property Let Department(ByVal val As String)
    pDept = val
End Property

Public Property Get Department() As String
    Department = pDept
End Property

'--- メソッド: 社員のサマリーを返す
Public Function Summary() As String
    Summary = pName & "(" & pDept & ")"
End Function

標準モジュール側(呼び出しコード)


'============================================================
' ■ 標準モジュール: クラスの動作確認(最小版)
'   → clsEmployee を使って社員情報を表示
'   → 貼り付け場所: 標準モジュール
'============================================================
Option Explicit

Sub TestEmployee()

    '--- クラスのインスタンスを作る(New が必須)
    Dim emp As clsEmployee
    Set emp = New clsEmployee

    '--- プロパティに値をセット(Property Let が呼ばれる)
    emp.EmployeeName = "田中太郎"
    emp.Department = "総務部"

    '--- プロパティを読み出す(Property Get が呼ばれる)
    MsgBox emp.Summary, vbInformation
    '→ 「田中太郎(総務部)」と表示される

    '--- 使い終わったら解放(省略可だがクセにしておくとよい)
    Set emp = Nothing

End Sub

書き換えポイント

箇所 説明 初期値
pName / pDept Private変数。プロパティの実体 空文字
EmployeeName / Department プロパティ名。好きな名前に変更可
Summary 関数 戻り値のフォーマット 名前(部署)

ポイント: Property Let の引数名(val)と Private変数名(pName)を別にすること。同じ名前にするとコンパイルエラーになる。

コード(実務版)– Collection+クラスで社員一覧を処理

シートのA列〜C列に「社員名・部署・入社日」が入った一覧表を読み込み、クラスのインスタンスをCollectionに格納。条件に応じてD列に結果を書き込む。

この書き方を覚えてからは、社員マスタ・商品マスタ・取引先マスタを同じパターンでクラス化して使い回している。新しいマスタが増えても「前のクラスをコピーして項目だけ変える」で済むようになった。

※ D列の既存データは上書きされます。実行前にバックアップを取ってください。

クラスモジュール側(clsEmployee)– 実務版


'============================================================
' ■ クラスモジュール: clsEmployee(実務版)
'   → 社員名・部署・入社日を持ち、勤続年数を計算できる部品
'   → 貼り付け場所: クラスモジュール(名前を clsEmployee に変更)
'============================================================
Option Explicit

Private pName As String
Private pDept As String
Private pHireDate As Date
Private pRowIndex As Long      '← 元データの行番号(書き戻し用)

'--- Initialize: 初期値を設定
Private Sub Class_Initialize()
    pName = ""
    pDept = ""
    pHireDate = DateSerial(1900, 1, 1)  '← 変換できない場合のフォールバック値
    pRowIndex = 0
End Sub

'--- Terminate: 必要に応じて後片付け
Private Sub Class_Terminate()
    '今回は特に処理なし
End Sub

'--- Property: EmployeeName
Public Property Let EmployeeName(ByVal val As String)
    pName = val
End Property
Public Property Get EmployeeName() As String
    EmployeeName = pName
End Property

'--- Property: Department
Public Property Let Department(ByVal val As String)
    pDept = val
End Property
Public Property Get Department() As String
    Department = pDept
End Property

'--- Property: HireDate
Public Property Let HireDate(ByVal val As Date)
    pHireDate = val
End Property
Public Property Get HireDate() As Date
    HireDate = pHireDate
End Property

'--- Property: RowIndex(読み取り専用にする場合はLetを省略)
Public Property Let RowIndex(ByVal val As Long)
    pRowIndex = val
End Property
Public Property Get RowIndex() As Long
    RowIndex = pRowIndex
End Property

'--- メソッド: 勤続年数を返す(簡易計算。厳密な勤続年数が必要な場合は月日も考慮してください)
Public Function YearsOfService() As Long
    YearsOfService = DateDiff("yyyy", pHireDate, Date)
End Function

'--- メソッド: サマリー文字列を返す
Public Function Summary() As String
    Summary = pName & "(" & pDept & ") 勤続" & YearsOfService() & "年"
End Function

'--- メソッド: 勤続年数に応じた区分を返す
Public Function ServiceCategory() As String
    Dim years As Long
    years = YearsOfService()

    Select Case years
        Case Is >= 20
            ServiceCategory = "ベテラン"
        Case Is >= 10
            ServiceCategory = "中堅"
        Case Is >= 3
            ServiceCategory = "一般"
        Case Else
            ServiceCategory = "新人"
    End Select
End Function

Select Caseの書き方は Select Caseで複数条件の分岐をスッキリ書く方法 を参照。

標準モジュール側(呼び出しコード)– 実務版


'============================================================
' ■ 標準モジュール: 社員一覧をクラスで処理(実務版)
'   → A列:社員名, B列:部署, C列:入社日 の一覧を読み込み
'   → D列に勤続区分を書き込む
'   → 貼り付け場所: 標準モジュール
'============================================================
Option Explicit

Sub ProcessEmployeeList()

    '--- ★書き換えポイント ---
    Dim startRow As Long
    startRow = 2                '← データ開始行(1行目はヘッダー)

    Dim nameCol As Long
    nameCol = 1                 '← 社員名の列(A列=1)

    Dim deptCol As Long
    deptCol = 2                 '← 部署の列(B列=2)

    Dim hireDateCol As Long
    hireDateCol = 3             '← 入社日の列(C列=3)

    Dim resultCol As Long
    resultCol = 4               '← 結果を書き込む列(D列=4)
    '--- ★ここまで ---

    Dim ws As Worksheet
    Set ws = ActiveSheet

    '--- 最終行を取得
    Dim lastRow As Long
    lastRow = ws.Cells(ws.Rows.Count, nameCol).End(xlUp).Row

    If lastRow < startRow Then
        MsgBox "データがありません。", vbExclamation
        Exit Sub
    End If

    '--- Collectionにクラスのインスタンスを格納
    Dim employees As Collection
    Set employees = New Collection

    Dim r As Long
    Dim emp As clsEmployee

    For r = startRow To lastRow

        '--- ★ループのたびに New する(これを忘れると全部同じデータになる)
        Set emp = New clsEmployee

        emp.EmployeeName = CStr(ws.Cells(r, nameCol).Value)
        emp.Department = CStr(ws.Cells(r, deptCol).Value)
        emp.RowIndex = r

        '--- 入社日の変換(日付形式はOSの地域設定に依存します)
        On Error Resume Next
        emp.HireDate = CDate(ws.Cells(r, hireDateCol).Value)
        If Err.Number <> 0 Then
            emp.HireDate = DateSerial(1900, 1, 1)  '← 変換できない場合のフォールバック値
            Err.Clear
        End If
        On Error GoTo 0

        employees.Add emp

    Next r

    '--- Collectionをループして結果を書き込み
    Dim item As clsEmployee
    Dim processCount As Long
    processCount = 0

    For Each item In employees
        ws.Cells(item.RowIndex, resultCol).Value = item.ServiceCategory()
        processCount = processCount + 1
    Next item

    MsgBox processCount & " 人の勤続区分を書き込みました。", vbInformation

    '--- 後片付け
    Set employees = Nothing

End Sub

エラー処理の詳細は エラー処理(On Error)で止まらないマクロを作る方法 を参照。

シート構成の例

A列(社員名) B列(部署) C列(入社日) D列(勤続区分)← マクロで書き込み
田中太郎 総務部 2005/04/01 ベテラン
佐藤花子 営業部 2018/10/01 一般
鈴木一郎 製造部 2024/04/01 新人
高橋美咲 品質管理部 2012/07/01 中堅

書き換えポイント

変数 説明 初期値
startRow データの開始行 2(1行目はヘッダー)
nameCol / deptCol / hireDateCol 各列の番号 1 / 2 / 3
resultCol 結果を書き込む列 4(D列)
ServiceCategory の閾値 勤続年数の区分基準 20 / 10 / 3

よくある落とし穴5選

1. Newを付けずにインスタンス化しようとしてエラーになる

自分も最初にクラスモジュールを使ったとき、Set emp = clsEmployee と書いて「型が一致しません」エラーで30分ハマった。原因はNew忘れ。

原因: Set emp = New clsEmployeeNew が必要。Newなしだとオブジェクトとして生成されないためエラーになる。また、Dim emp As clsEmployee だけで New を付けない場合、変数は Nothing のままでプロパティにアクセスした瞬間に実行時エラー91(オブジェクト変数が設定されていません)になる。

対策: インスタンスを作るときは必ず Set 変数 = New クラス名 と書く。


' NG: New がない → 型が一致しませんエラー
Dim emp As clsEmployee
Set emp = clsEmployee

' NG: New も Set もない → Nothing のままでエラー91
Dim emp As clsEmployee
emp.EmployeeName = "田中"  '← ここでエラー

' OK: New を付ける
Dim emp As clsEmployee
Set emp = New clsEmployee

2. Collectionのループ内でNewしないと全データが同じ値になる

原因: ループ内で毎回Newしないと、同じインスタンスへの参照がCollectionに追加され続ける。結果、全要素が最後にセットした値になる。

対策: Forループの中で Set emp = New clsEmployee を毎回実行する。


' NG: New がループの外
Set emp = New clsEmployee
For r = 2 To lastRow
    emp.EmployeeName = ws.Cells(r, 1).Value
    employees.Add emp  '← 全部同じインスタンスを指す
Next r

' OK: New がループの中
For r = 2 To lastRow
    Set emp = New clsEmployee  '← 毎回新しいインスタンスを作る
    emp.EmployeeName = ws.Cells(r, 1).Value
    employees.Add emp
Next r

3. クラスモジュール名を変え忘れて「Class1」のまま使っている

原因: クラスモジュールを挿入した直後の名前は Class1。プロパティウィンドウで名前を変更せずにコードを書き始めると、後から修正が面倒になる。

対策: クラスモジュールを挿入したら、最初にプロパティウィンドウ(F4キー)で (オブジェクト名) を意味のある名前(clsEmployee 等)に変更する。名前の先頭に cls を付けると標準モジュールと区別しやすい。

4. Property Let の引数名と Private変数名が同じでコンパイルエラー

原因: Property Let の引数名を Private変数と同じにすると、VBAが区別できずにエラーになる。

対策: Private変数には p プレフィックスを付けて区別する。


' NG: 引数名とPrivate変数名が同じ
Private EmployeeName As String
Public Property Let EmployeeName(ByVal EmployeeName As String)  '← エラー
    EmployeeName = EmployeeName
End Property

' OK: Private変数に p を付けて区別
Private pName As String
Public Property Let EmployeeName(ByVal val As String)
    pName = val
End Property

5. End ステートメントで強制終了すると Class_Terminate が実行されない

原因: VBAの End ステートメント(End Sub ではなく、単独の End)はプログラムを即時終了する。Class_Terminate は呼ばれず、後片付け(ファイルを閉じる、接続を切る等)がスキップされる。

対策: End の代わりに Exit Sub を使う。どうしても異常終了させたい場合は、エラーハンドリング内で Set obj = Nothing を明示的に実行してからEndする。

VBAのクラスモジュールでNewを忘れてエラーが出るときの対処法

「オブジェクト変数が設定されていません(エラー91)」が出る場合、原因は Set 変数 = New クラス名 のNewを書き忘れていることだ。Dimだけでは変数はNothingのまま。必ずSetとNewでインスタンスを生成してからプロパティにアクセスする。

VBAのCollectionに入れたクラスが全部同じ値になるときの対処法

「CollectionにAddしたのに全部最後の値になる」という場合、原因はForループ内で毎回Newしていないこと。ループの外で1回だけNewすると同じインスタンスの参照が追加され続ける。ループ内で毎回 Set emp = New clsEmployee を実行する。

FAQ

Q1: クラスモジュールと標準モジュール、どう使い分ける?

用途 使うべきモジュール
単発の処理(ファイル一覧取得、色分け等) 標準モジュール
同じ構造のデータを複数扱う(社員、商品、取引先等) クラスモジュール
処理を部品化して複数のマクロから呼び出したい クラスモジュール

結論: 「同じ構造のデータを何個も作る」場面が出てきたらクラスモジュールの出番。1回限りの処理は標準モジュールで十分。

Q2: Property Get/Let を使わず、Public変数にしてはダメ?

動くが、おすすめしない。Public変数にすると、外部から自由に値を変更できてしまう。Property Let を使えば、値をセットするときにバリデーション(入力チェック)を入れられる。


Public Property Let Department(ByVal val As String)
    If Len(val) = 0 Then
        pDept = "(未設定)"  '← 空文字なら初期値を入れる
    Else
        pDept = val
    End If
End Property

Q3: Collection と 配列、どちらでクラスのインスタンスを管理すべき?

比較 Collection 配列
要素の追加 .Add で簡単 ReDim Preserve が必要
要素のループ For Each が使える For ループのみ
キーで検索 .Add Item, Key で可能 自分で検索処理を書く必要あり
速度 数千件なら十分 大量データでは配列の方が速い

結論: 数千件までならCollectionで十分。キーで検索したい場合は Dictionaryで重複チェック・集計を高速化する方法 のDictionaryが便利。

Q4: クラスモジュールに配列やDictionaryを持たせられる?

持たせられる。Private変数にDictionaryやCollectionを宣言し、Class_Initialize内でNewすればよい。


Private pSkills As Collection

Private Sub Class_Initialize()
    Set pSkills = New Collection
End Sub

Q5: クラスモジュールを別のブックにコピーできる?

できる。VBEの左側のプロジェクトエクスプローラーで、クラスモジュールを別ブックのプロジェクトにドラッグ&ドロップするか、「ファイルのエクスポート」→「ファイルのインポート」で移動できる。コピー後の動作確認には VBAのデバッグ技術を使いこなす方法 のステップ実行が役立つ。

まとめ

  • クラスモジュール: データと処理をセットにした「部品」。修正が1箇所で済む
  • Property Get / Let: プロパティの読み書き。Private変数+Propertyで値を管理する
  • Class_Initialize / Terminate: インスタンスの作成時・破棄時に自動実行される
  • Collection + クラス: ループ内で毎回Newして、インスタンスをCollectionに格納する
  • New忘れに注意: Set 変数 = New クラス名 を必ず書く

関連記事

次にやりたくなること

コメント

タイトルとURLをコピーしました