Excel VBA 逆引き集 | コールバック

Excel VBA
スポンサーリンク
  1. ねらい:VBAで「コールバック」を使い、処理の途中で任意の動きを差し込めるようにする
    1. 重要ポイントの深掘り
  2. 基本形:関数名を文字列で渡して呼ぶ(最短で動く)
    1. 使いどころと考え方
    2. 例題:大量処理の途中で進捗コールバックを呼ぶ
    3. 重要ポイントの深掘り
  3. 安全形:クラスのメソッドをコールバックにする(型安全・契約固定)
    1. 使いどころと考え方
    2. 例題:進捗とログをクラスに注入して差し替える
    3. 重要ポイントの深掘り
  4. CallByNameで柔軟に呼び分ける(メソッド名を文字列で動的指定)
    1. 使いどころと考え方
    2. 例題:任意のフックを文字列指定で呼び分ける
    3. 重要ポイントの深掘り
  5. イベントをコールバックとして使う(自然なフックポイント)
    1. 使いどころと考え方
    2. 例題:セル変更イベントで検証コールバックを呼ぶ
    3. 重要ポイントの深掘り
  6. 段階実行のコールバック:OnTimeでチャンク処理にフックを挿む
    1. 使いどころと考え方
    2. 例題:チャンクごとに進捗コールバックを呼ぶ
    3. 重要ポイントの深掘り
  7. コールバック設計のルールと落とし穴の回避
    1. 契約を明確にし、引数の意味を固定する
    2. 重い処理をコールバックに押し込まない
    3. 例外と後片付けを徹底する
  8. 導入手順:今日からコールバックを使う最短ルート
    1. 最初は文字列コールバックで進捗を差し込む
    2. 次にクラス契約へ移行する
    3. 柔軟な拡張はCallByNameでフック増設
  9. まとめ:コールバックで「外から差し込む」設計にすると現場適応が速い

ねらい:VBAで「コールバック」を使い、処理の途中で任意の動きを差し込めるようにする

コールバックは「ある処理の途中で、外から渡された関数(やメソッド)を呼び返す」仕組みです。Excel VBAでも、関数名文字列を渡して呼ぶ、クラスのメソッドを渡す、イベントを受け取る、OnTimeで段階実行するなどの方法で実現できます。これにより、進捗表示、ログ記録、検証、エラー通知、UI更新などを“差し替え可能”にでき、同じ本処理を様々な現場要件にフィットさせられます。

重要ポイントの深掘り

初心者は「コールバック=呼び先を外から渡す」と捉えると分かりやすいです。処理の本体は「フックポイント(呼び出し位置)」だけを持ち、具体的な動きは外から渡されたコールバックに委ねます。文字列で手軽に、クラスで安全に、イベントで自然に——目的に応じて使い分けるのがコツです。


基本形:関数名を文字列で渡して呼ぶ(最短で動く)

使いどころと考え方

最短でコールバックを体験するには、関数名(Sub/Function名)を文字列で渡し、Application.Runで呼ぶ方法が簡単です。“名前で呼ぶ”だけなので柔軟ですが、引数の型安全性は弱めです。学習の入口として最適です。

例題:大量処理の途中で進捗コールバックを呼ぶ

Sub ProcessWithCallback(ByVal cbName As String)
    Dim ws As Worksheet: Set ws = Worksheets("Input")
    Dim arr As Variant: arr = ws.Range("A1").CurrentRegion.Value

    Dim rows As Long: rows = UBound(arr, 1)
    Dim i As Long
    For i = 2 To rows
        ' 本処理(ここでは合計の例)
        Dim s As Double: s = s + Val(arr(i, 4))

        ' 1%刻みでコールバック呼び出し
        If i Mod WorksheetFunction.Max(1, rows \ 100) = 0 Then
            Application.Run cbName, i, rows, "進捗"
        End If
    Next

    Worksheets("Summary").Range("B2").Value = s
End Sub

Sub ProgressCallback(ByVal cur As Long, ByVal total As Long, ByVal label As String)
    Application.StatusBar = label & " " & Format(cur / total, "0%") & " (" & cur & "/" & total & ")"
    DoEvents
End Sub

Sub Run_Demo_StringCallback()
    ProcessWithCallback "ProgressCallback"
    Application.StatusBar = False
End Sub
VB

重要ポイントの深掘り

文字列コールバックは「戻り値や引数の数が一致しているか」を呼ぶ側が自分で気をつける必要があります。小さな進捗やログには十分ですが、複雑な契約(引数構造)が増えるほど、次章の「クラスコールバック」に切り替えると保守が楽になります。


安全形:クラスのメソッドをコールバックにする(型安全・契約固定)

使いどころと考え方

Publicメソッドを持つクラスを「コールバック契約」として渡すと、引数・戻り値が型で保証されます。呼び出し側は「契約どおり呼ぶだけ」、受け手は「具体動作を自由に」実装できます。VBAに正式なインターフェース構文はないため、契約用クラスを“ドキュメントとして”固定します。

例題:進捗とログをクラスに注入して差し替える

' IProgressReporter.cls(契約:呼ばれる側の型)
Option Explicit
Public Sub Report(ByVal cur As Long, ByVal total As Long, ByVal label As String)
    Err.Raise 9000, , "Not implemented"
End Sub

' StatusBarReporter.cls(具体:ステータスバー更新)
Option Explicit
Implements IProgressReporter

Private Sub IProgressReporter_Report(ByVal cur As Long, ByVal total As Long, ByVal label As String)
    If total <= 0 Then Exit Sub
    If cur Mod WorksheetFunction.Max(1, total \ 100) = 0 Then
        Application.StatusBar = label & " " & Format(cur / total, "0%")
        DoEvents
    End If
End Sub

' LogSheetReporter.cls(具体:Logシートへ記録)
Option Explicit
Implements IProgressReporter

Private Sub IProgressReporter_Report(ByVal cur As Long, ByVal total As Long, ByVal label As String)
    Dim ws As Worksheet
    On Error Resume Next
    Set ws = ThisWorkbook.Worksheets("Log")
    If ws Is Nothing Then Set ws = ThisWorkbook.Worksheets.Add: ws.Name = "Log"
    On Error GoTo 0
    Dim r As Long: r = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1
    ws.Cells(r, 1).Value = Format(Now, "yyyy-mm-dd HH:NN:SS")
    ws.Cells(r, 2).Value = label
    ws.Cells(r, 3).Value = cur
    ws.Cells(r, 4).Value = total
End Sub

' ModCore.bas(呼ぶ側)
Option Explicit

Public Sub ProcessWithClassCallback(ByVal reporter As IProgressReporter)
    Dim arr As Variant: arr = Worksheets("Input").Range("A1").CurrentRegion.Value
    Dim rows As Long: rows = UBound(arr, 1)

    Dim i As Long, s As Double
    For i = 2 To rows
        s = s + Val(arr(i, 4))
        reporter.Report i, rows, "集計"
    Next

    Worksheets("Summary").Range("B2").Value = s
End Sub

Public Sub Run_Demo_ClassCallback_StatusBar()
    Dim rep As IProgressReporter
    Set rep = New StatusBarReporter
    ProcessWithClassCallback rep
    Application.StatusBar = False
End Sub

Public Sub Run_Demo_ClassCallback_Log()
    Dim rep As IProgressReporter
    Set rep = New LogSheetReporter
    ProcessWithClassCallback rep
End Sub
VB

重要ポイントの深掘り

クラスコールバックの強みは「差し替え容易+壊れにくい」です。契約(メソッド名・引数)が変わらない限り、何種類の具体実装でも同じ呼び出しコードで動きます。進捗・ログ・監査・通知など“目的ごとに”実装を増やし、注入で切り替えると、現場要件への適応が楽です。


CallByNameで柔軟に呼び分ける(メソッド名を文字列で動的指定)

使いどころと考え方

クラスのメソッド名を文字列で渡して、CallByNameで呼ぶ方法です。「一つのオブジェクトに複数のフック(OnStart/OnTick/OnFinish)を生やして、必要なものだけ実装」など、柔軟な拡張に向いています。型安全性は落ちるため、エラー処理を必ず入れます。

例題:任意のフックを文字列指定で呼び分ける

' HookTarget.cls(受け手クラス:任意のフックを用意)
Option Explicit
Public Sub OnStart(ByVal label As String)
    Debug.Print "Start:", label
End Sub
Public Sub OnTick(ByVal cur As Long, ByVal total As Long)
    If total > 0 And cur Mod WorksheetFunction.Max(1, total \ 100) = 0 Then Debug.Print "Tick:", cur & "/" & total
End Sub
Public Sub OnFinish(ByVal count As Long)
    Debug.Print "Finish:", count
End Sub

' ModFlexibleCallback.bas(呼ぶ側)
Option Explicit
Public Sub ProcessWithFlexibleHooks(ByVal target As Object, ByVal onStart As String, ByVal onTick As String, ByVal onFinish As String)
    On Error Resume Next
    CallByName target, onStart, VbMethod, "集計"
    On Error GoTo 0

    Dim arr As Variant: arr = Worksheets("Input").Range("A1").CurrentRegion.Value
    Dim rows As Long: rows = UBound(arr, 1)
    Dim i As Long
    For i = 2 To rows
        On Error Resume Next
        CallByName target, onTick, VbMethod, i, rows
        On Error GoTo 0
    Next

    On Error Resume Next
    CallByName target, onFinish, VbMethod, rows - 1
    On Error GoTo 0
End Sub

Public Sub Run_Demo_CallByName()
    Dim hk As New HookTarget
    ProcessWithFlexibleHooks hk, "OnStart", "OnTick", "OnFinish"
End Sub
VB

重要ポイントの深掘り

CallByNameは「存在すれば呼ぶ」「なければスキップ」の柔軟性が魅力です。一方で、タイポや引数不一致の検出は弱いので、On Errorで包む、実行前に“存在チェック”するなどの安全策を必ず入れます。学習後は、型安全なクラス契約に移行するのが王道です。


イベントをコールバックとして使う(自然なフックポイント)

使いどころと考え方

Worksheet_Change、Workbook_Open、UserFormイベントは“Excel側から呼ばれるコールバック”です。これを「イベント→サービスを呼ぶ」形に整えると、UI操作やデータ変更に対して自然にフックできます。

例題:セル変更イベントで検証コールバックを呼ぶ

' Sheetモジュール(Inputシートのコード)
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
    If Not Intersect(Target, Me.Range("D:D")) Is Nothing Then
        On Error Resume Next
        Dim v As Variant: v = Target.Value
        If Not IsNumeric(v) Or v < 0 Or v > 100 Then
            MsgBox "Scoreは0~100の数値です。", vbExclamation
            Application.EnableEvents = False
            Target.Value = ""
            Application.EnableEvents = True
        End If
        On Error GoTo 0
    End If
End Sub
VB

重要ポイントの深掘り

イベントは「コールバックの起点」ですが、イベント内で重たい処理をしないことが重要です。イベント内では“検証や軽い通知”だけに留め、重たい処理は別の入口(Run_XXXX)で呼ぶように分離すると、暴走や応答低下を防げます。


段階実行のコールバック:OnTimeでチャンク処理にフックを挿む

使いどころと考え方

Application.OnTimeで処理を分割し、各チャンク終了時に“次のチャンクを予約”します。UI応答を保ちつつ、各チャンクでコールバック(進捗・ログ・キャンセル判定)を呼べます。

例題:チャンクごとに進捗コールバックを呼ぶ

' ModChunk.bas
Option Explicit
Private gStart As Long, gLast As Long, gChunk As Long

Public Sub StartChunkProcess(ByVal cbName As String)
    gStart = 2
    gLast = Worksheets("Input").Cells(Worksheets("Input").Rows.Count, "A").End(xlUp).Row
    gChunk = 5000
    Application.Run cbName, 0, gLast, "開始"
    Application.OnTime Now, "'" & ThisWorkbook.Name & "'!RunNextChunk(""" & cbName & """)"
End Sub

Public Sub RunNextChunk(ByVal cbName As String)
    If gStart > gLast Then
        Application.Run cbName, gLast, gLast, "完了"
        Exit Sub
    End If
    Dim ws As Worksheet: Set ws = Worksheets("Input")
    Dim endRow As Long: endRow = WorksheetFunction.Min(gStart + gChunk - 1, gLast)

    Dim arr As Variant
    arr = ws.Range(ws.Cells(gStart, "A"), ws.Cells(endRow, "F")).Value

    ' …チャンク処理…

    Application.Run cbName, endRow, gLast, "進捗"
    gStart = endRow + 1
    Application.OnTime Now + TimeValue("0:00:01"), "'" & ThisWorkbook.Name & "'!RunNextChunk(""" & cbName & """)"
End Sub

Sub ProgressCb(ByVal cur As Long, ByVal total As Long, ByVal label As String)
    Application.StatusBar = label & " " & Format(cur / WorksheetFunction.Max(1, total), "0%")
    DoEvents
End Sub

Sub Run_Demo_OnTime()
    StartChunkProcess "ProgressCb"
End Sub
VB

重要ポイントの深掘り

OnTimeは「疑似非同期」。長処理でもUIが固まりにくく、各チャンクで確実にコールバックを差し込めます。関数名はブック修飾で指定すると、他ブック干渉を防げます。終了時にはStatusBarを戻す“後片付け”も忘れずに。


コールバック設計のルールと落とし穴の回避

契約を明確にし、引数の意味を固定する

コールバックの引数(例:cur、total、label)の意味を“名前で”固定すると、呼ぶ側・受ける側の誤解が減ります。クラス契約にすると、IDE支援でミスが減り、メンテが楽になります。

重い処理をコールバックに押し込まない

コールバックは“補助”役(進捗・ログ・軽い検証)が基本です。重いI/Oや長い計算は本処理側に置き、コールバックは必要最小限にすることで、速度と安定性を保てます。

例外と後片付けを徹底する

コールバック失敗で全体が止まるのは避けたい場面が多いです。文字列・CallByName系はOn Errorで包み、失敗時はスキップする設計に。StatusBarやEnableEventsの復帰は“最後に必ず”戻す枠(AppEnter/AppLeave)で守ると安心です。


導入手順:今日からコールバックを使う最短ルート

最初は文字列コールバックで進捗を差し込む

Application.Runで“進捗”を呼び、体感を掴みます。1〜5%刻みの間引きを入れ、DoEventsの呼びすぎを避けます。

次にクラス契約へ移行する

IProgressReporterのような“契約クラス”を作り、StatusBar/Log/Messageの具体実装を差し替えてみます。型安全で壊れにくく、保守が楽になります。

柔軟な拡張はCallByNameでフック増設

OnStart/OnTick/OnFinishなど、任意フックを増やしたい場面ではCallByNameを併用します。エラー対策を忘れずに。


まとめ:コールバックで「外から差し込む」設計にすると現場適応が速い

VBAでも、文字列、クラス契約、イベント、OnTime、CallByNameを使えば、十分にコールバック設計が可能です。本処理は“フック位置”だけ用意し、具体動作は外から渡す。これで進捗、ログ、検証、通知の差し替えが容易になり、同じ処理を様々な現場要件に合わせられます。

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