Excel VBA 逆引き集 | クラスモジュール活用

Excel VBA
スポンサーリンク

ねらい:クラスモジュールで「型」を作り、VBAを一段強くする

VBAは手軽ですが、値やRangeが行き交うだけだと複雑化しがち。クラスモジュールは「自分専用の型(モノ)を作る」仕組みです。社員、注文、ルール、タイマーなどを”モノ”として扱うと、責務が明確になり、テストや再利用が楽になります。初心者でも貼って動かせるコードで、重要点を深掘りして解説します。

  • 狙い: データ+処理をひとまとまりにして、見通しと再利用性を高める
  • 要点: プロパティ(状態)+メソッド(振る舞い)+初期化/後始末で”モノ化”
  • 重要ポイント(深掘り):
    • カプセル化: 内部の実装を隠し、外からはプロパティ・メソッドだけで扱う
    • コンストラクタ的初期化: Class_InitializeClass_Terminate を使い、作成/破棄時の準備・片付けを自動化
    • イベント: WithEvents でExcelのイベントを「クラスに閉じ込めて」扱うと、スッキリ

基本:社員クラスで「データ+処理」をひとまとめに

クラスモジュール(名前: CEmployee)

' CEmployee.cls
Option Explicit

Private pEmpNo As String
Private pName As String
Private pDept As String
Private pHireDate As Date

' 生成/破棄イベント(必要に応じて)
Private Sub Class_Initialize()
    pEmpNo = ""
    pName = ""
    pDept = ""
    pHireDate = 0
End Sub

Private Sub Class_Terminate()
    ' 後始末が必要ならここへ(ファイルクローズなど)
End Sub

' --- プロパティ(外から読み書きできる状態) ---
Public Property Get EmpNo() As String
    EmpNo = pEmpNo
End Property

Public Property Let EmpNo(ByVal v As String)
    If Len(v) <> 6 Or v Like "*[!0-9]*" Then Err.Raise 1001, , "社員番号は6桁の数字"
    pEmpNo = v
End Property

Public Property Get Name() As String
    Name = pName
End Property

Public Property Let Name(ByVal v As String)
    pName = Trim$(v)
End Property

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

Public Property Let Dept(ByVal v As String)
    pDept = v
End Property

Public Property Get HireDate() As Date
    HireDate = pHireDate
End Property

Public Property Let HireDate(ByVal v As Date)
    If v <= DateSerial(1900, 1, 1) Then Err.Raise 1002, , "入社日が不正"
    pHireDate = v
End Property

' --- メソッド(振る舞い) ---
Public Function SeniorityYears() As Long
    If pHireDate = 0 Then SeniorityYears = 0 Else SeniorityYears = DateDiff("yyyy", pHireDate, Date)
End Function

Public Function ToArray() As Variant
    Dim a(1 To 1, 1 To 4) As Variant
    a(1, 1) = pEmpNo
    a(1, 2) = pName
    a(1, 3) = pDept
    a(1, 4) = pHireDate
    ToArray = a
End Function
VB

呼び出し例(標準モジュール)

Sub Example_EmployeeClass()
    Dim e As New CEmployee
    e.EmpNo = "000123"
    e.Name = "山田太郎"
    e.Dept = "営業"
    e.HireDate = DateSerial(2018, 4, 1)
    
    Debug.Print e.Name & "(勤続 " & e.SeniorityYears & " 年)"
End Sub
VB
  • 重要深掘り:
    • 検証をプロパティで担う: 不正値は入れさせない。データ品質をクラスで保証。
    • メソッドは「そのモノの仕事」だけ: 勤続年数など、社員が知っていることを社員クラスに。

コレクション管理:複数件を「リスト」として扱う

クラスモジュール(名前: CEmployeeList)

' CEmployeeList.cls
Option Explicit

Private items As Collection

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

Public Sub Add(ByVal emp As CEmployee)
    items.Add emp, emp.EmpNo ' キーに社員番号
End Sub

Public Function Count() As Long
    Count = items.Count
End Function

Public Function Find(ByVal empNo As String) As CEmployee
    On Error Resume Next
    Set Find = items(empNo)
    On Error GoTo 0
End Function

Public Sub Remove(ByVal empNo As String)
    On Error Resume Next
    items.Remove empNo
    On Error GoTo 0
End Sub

Public Function To2DArray() As Variant
    If items.Count = 0 Then Exit Function
    Dim r As Long: r = items.Count
    Dim a() As Variant: ReDim a(1 To r + 1, 1 To 4)
    ' ヘッダー
    a(1, 1) = "社員番号": a(1, 2) = "氏名": a(1, 3) = "部署": a(1, 4) = "入社日"
    ' 中身
    Dim i As Long
    For i = 1 To items.Count
        Dim e As CEmployee: Set e = items(i)
        a(i + 1, 1) = e.EmpNo
        a(i + 1, 2) = e.Name
        a(i + 1, 3) = e.Dept
        a(i + 1, 4) = e.HireDate
    Next
    To2DArray = a
End Function
VB

使い方(標準モジュール)

Sub Example_EmployeeList()
    Dim list As New CEmployeeList
    Dim e1 As New CEmployee, e2 As New CEmployee
    e1.EmpNo = "000101": e1.Name = "佐藤": e1.Dept = "総務": e1.HireDate = DateSerial(2020, 1, 10)
    e2.EmpNo = "000102": e2.Name = "鈴木": e2.Dept = "開発": e2.HireDate = DateSerial(2019, 7, 1)
    list.Add e1: list.Add e2
    
    Dim arr As Variant: arr = list.To2DArray
    Worksheets("Output").Range("A1").Resize(UBound(arr, 1), UBound(arr, 2)).Value = arr
End Sub
VB
  • 重要深掘り:
    • 「1件」と「複数件」を分ける: CEmployee(1件)+CEmployeeList(複数)。型が明確で迷わない。
    • To2DArrayで一括書き出し: Rangeへの書き戻しが高速・簡潔。

データ読み込みの責務分離:リーダークラス

UIやロジックに読み方を混ぜないために「リーダー」を用意。

クラスモジュール(名前: CEmployeeReader)

' CEmployeeReader.cls
Option Explicit

Public Function ReadFromSheet(ByVal ws As Worksheet) As CEmployeeList
    Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
    Dim list As New CEmployeeList
    Dim r As Long
    For r = 2 To lastRow
        Dim e As New CEmployee
        e.EmpNo = CStr(ws.Cells(r, "A").Value)
        e.Name = CStr(ws.Cells(r, "B").Value)
        e.Dept = CStr(ws.Cells(r, "C").Value)
        If IsDate(ws.Cells(r, "D").Value) Then e.HireDate = CDate(ws.Cells(r, "D").Value)
        list.Add e
    Next
    Set ReadFromSheet = list
End Function
VB

使い方(標準モジュール)

Sub Example_ReadEmployees()
    Dim rd As New CEmployeeReader
    Dim list As CEmployeeList
    Set list = rd.ReadFromSheet(Worksheets("Input"))
    
    Debug.Print "件数:", list.Count
    Dim e As CEmployee: Set e = list.Find("000101")
    If Not e Is Nothing Then Debug.Print e.Name
End Sub
VB
  • 重要深掘り:
    • 読み方が変わってもReaderだけ修正: マッピングや列順変更に強い。
    • ロジックは「クラスを前提」に書ける: シート知らずのきれいなコードに。

Excelイベントの”閉じ込め”:WithEventsで安全運用

シート変更やブックイベントをクラスに閉じ込めると、散らばらずに管理できます。

クラスモジュール(名前: CSheetWatcher)

' CSheetWatcher.cls
Option Explicit

Public WithEvents TargetSheet As Worksheet

Private Sub TargetSheet_Change(ByVal Target As Range)
    If Not Intersect(Target, TargetSheet.Range("A2:A1000")) Is Nothing Then
        ' A列が変わったらB列に「長さ」を書く例
        Dim c As Range
        For Each c In Intersect(Target, TargetSheet.Range("A2:A1000"))
            TargetSheet.Cells(c.Row, "B").Value = Len(CStr(c.Value))
        Next
    End If
End Sub
VB

初期化(標準モジュール)

Dim watcher As CSheetWatcher

Sub InitWatcher()
    Set watcher = New CSheetWatcher
    Set watcher.TargetSheet = Worksheets("Input")
    MsgBox "監視を開始しました。"
End Sub
VB
  • 重要深掘り:
    • イベントをクラスに閉じ込める: ロジックが散らばるのを防ぎ、切り替えや停止も容易。
    • WithEventsは参照が必須: グローバル(モジュールレベル)に保持し、Set で対象を割り当てる。

ビジネスルールの独立:ルールクラスで切り替え容易

判定ルールはコード直書きせず、クラス化すると差し替えが簡単。

クラスモジュール(名前: CScoreRule)

' CScoreRule.cls
Option Explicit

Private pThreshold As Double

Public Property Let Threshold(ByVal v As Double)
    pThreshold = v
End Property

Public Function IsPass(ByVal score As Double) As Boolean
    IsPass = (score >= pThreshold)
End Function
VB

利用(標準モジュール)

Sub Example_Rule()
    Dim rule As New CScoreRule
    rule.Threshold = 70
    Debug.Print rule.IsPass(75) ' True
    rule.Threshold = 80
    Debug.Print rule.IsPass(75) ' False
End Sub
VB
  • 重要深掘り:
    • ルールは”差し替え”が本質: 将来「重み付き」「係数」などに変更しても、ルールクラスだけ差し替え。
    • テスト容易: ルール単体の挙動を検証できる。

ファクトリ(生成役)で初期化の複雑さを隠す

クラスモジュール(名前: CEmployeeFactory)

' CEmployeeFactory.cls
Option Explicit

Public Function FromRow(ByVal ws As Worksheet, ByVal r As Long) As CEmployee
    Dim e As New CEmployee
    e.EmpNo = CStr(ws.Cells(r, "A").Value)
    e.Name = CStr(ws.Cells(r, "B").Value)
    e.Dept = CStr(ws.Cells(r, "C").Value)
    If IsDate(ws.Cells(r, "D").Value) Then e.HireDate = CDate(ws.Cells(r, "D").Value)
    Set FromRow = e
End Function
VB
  • 重要深掘り:
    • 「どう作るか」を一本化: 生成手順を集中管理して、散らばる初期化を撲滅。
    • 呼び出し側はシンプル: Set e = factory.FromRow(ws, r) の一行で生成。

例題で練習(貼って試せる)

  • 例1(基本クラス): CEmployeeを作成→Example_EmployeeClassでプロパティとメソッドを体験。
  • 例2(リスト): CEmployeeListで複数管理→Outputに一括書き出し。
  • 例3(リーダー): CEmployeeReaderでInputから読み込み→Findで検索。
  • 例4(イベント): CSheetWatcherを初期化→Input!A列変更でB列へ自動反映。
  • 例5(ルール): CScoreRuleのしきい値変更→合否判定が切り替わる。

実務の落とし穴と対策(ここが肝)

  • 落とし穴1:クラスを作っても”責務”が曖昧
    • 対策: 1クラス1責務。データモデル、リスト、読み書き、ルール、イベントの役割を分ける。
  • 落とし穴2:Range操作をクラスに混ぜる
    • 対策: 読み書きはReader/Writer/Factoryへ。モデルクラスはセルを知らない。
  • 落とし穴3:初期化/後片付け漏れ
    • 対策: Class_Initialize/Class_Terminate で必ず準備・解放を管理(ファイル・接続など)。
  • 落とし穴4:WithEventsの参照切れ
    • 対策: モジュールレベルで保持し、対象割り当てを明示。Set Nothingで停止を管理。
  • 落とし穴5:使い回しができない設計
    • 対策: 入出力を値/配列ベースに。業務ロジックは純粋関数的に。

スターター手順(最短導入)

  1. データモデルをクラス化(CEmployeeのように状態+検証+簡単な振る舞い)。
  2. リストクラスで複数管理(検索・配列変換・件数)。
  3. Reader/Factoryで読み込みを分離(列マッピングをここに集約)。
  4. WithEventsクラスでイベントを閉じ込める(監視・トリガー)。
  5. ビジネスルールをクラス化(しきい値・判定などを差し替え可能に)。

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