【VBA】セルの変更履歴を自動記録して差分管理する方法(コピペOK)

VBA
スポンサーリンク
スポンサーリンク
  1. この記事でわかること
  2. どんな場面で使う?
  3. 完成イメージ(Before / After)
  4. 実行前の準備
    1. バックアップを取る
    2. Excelをマクロ有効ブック(.xlsm)で保存する
    3. コードの貼り付け先に注意(シートモジュール)
  5. 手順(コピペ → 実行まで約5分)
    1. VBE(コードを書く画面)を開く
    2. シートモジュールを開く
    3. コードを貼り付ける
  6. コード(基本版)– セル変更時に変更内容をログ記録
    1. 書き換えポイント
  7. コード(応用版)– 変更前の値も記録(Undo対応)
    1. 書き換えポイント
  8. コード(実務版)– 変更履歴シートに自動蓄積+フィルタ機能
    1. 書き換えポイント
  9. よくある落とし穴5選
    1. 1. Worksheet_Change 内でセルを書き換えて無限ループ
    2. 2. EnableEvents = True に戻し忘れてイベントが全停止
    3. 3. 標準モジュールに貼り付けて動かない
    4. 4. 複数セルを貼り付けたらエラーになった
    5. 5. ブックを開き直したら変更前の値が取れない
    6. 6. 変更履歴シートへの書き込み自体がChangeイベントを発火する
    7. VBAのWorksheet_Changeで無限ループになるときの対処法
    8. VBAのイベントが二重発火して止まらないときの対処法
    9. VBAで変更前の値が取得できないときの対処法
  10. FAQ
    1. Q1: 特定の列だけ変更を記録したい
    2. Q2: 変更履歴が溜まりすぎたらどうする?
    3. Q3: 複数シートの変更を1つの履歴シートに記録したい
    4. Q4: イベントが発火しなくなった(EnableEventsが止まっている)
    5. Q5: 変更者(ユーザー名)も記録したい
  11. まとめ
    1. 関連記事
  12. 次にやりたくなること
  13. もっとカスタマイズしたい場合

この記事でわかること

  • VBAでセルの変更を自動検知して記録できる
  • 変更前の値と変更後の値を両方残せる(差分管理)
  • 変更履歴シートに自動蓄積し、フィルタで絞り込める

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

どんな場面で使う?

セルの変更履歴を自動記録するマクロは、以下のような場面で活躍する。

  • 共有Excelで複数人がデータを編集している。誰がいつ何を変えたのか分からず、月末の数値が合わないときに原因調査に時間がかかる
  • 単価表や料率表など、数値の変更が業務に大きな影響を与えるデータを管理している。変更があったことに気づけないと、請求書や見積書に誤った金額が反映される
  • 監査対応やISO対応で、データの変更履歴を残す必要がある。手動で変更ログを書くルールにしていても、忘れる人が必ず出てくる
  • マスタデータの更新を定期的にレビューしたい。オートフィルタで特定の期間やセルの変更だけを抽出すれば、差分確認が簡単にできる
  • 「前の値に戻したい」と言われたときに、変更前の値がすぐ分かるようにしておきたい

手動で変更履歴を管理しようとすると、必ず記録漏れが発生する。VBAに任せれば、セルが変更されるたびに自動で記録されるので漏れがない。

完成イメージ(Before / After)

Before(手動管理):

セル 変更内容 備考
誰かが単価を変えた? いつ変わったか不明

After(自動記録):

日時 セル番地 変更前 変更後 シート名
2026/03/14 10:30:15 C5 1000 1200 単価表
2026/03/14 10:32:08 C8 500 480 単価表
2026/03/14 11:05:22 D3 東京 大阪 単価表

自分も共有Excelで単価表を管理していたとき、誰かが値を書き換えても気づけず、月末の請求額が合わなくて原因調査に半日かかったことがある。VBAで変更履歴を自動記録するようにしてからは、いつ・何が変わったか一目で分かるようになった。トラブル時の原因特定が5分で終わる。同じように共有ブックの変更を追跡したい人に、この記事でセル変更の自動記録を体験してほしい。

セルの変更は手動で追跡しようとすると必ず漏れる。VBAに任せれば全ての変更が自動で残る。

なお、Worksheet_Changeイベントの基本(セルの値が変わったら自動で処理を実行する仕組み)については セルの値が変わったら自動実行(Worksheet_Change) を参照。

実行前の準備

バックアップを取る

変更履歴の記録コードを入れる前に、必ずファイルのコピーを別フォルダに保存する。 コードのミスで無限ループが発生するとExcelがフリーズする可能性がある。

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

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

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

コードの貼り付け先に注意(シートモジュール)

このコードは標準モジュールではなく、シートモジュールに貼り付ける。

  1. VBEを開く(Alt + F11
  2. 左側のプロジェクトエクスプローラーで、変更を記録したいシート(例: Sheet1(単価表))をダブルクリック
  3. 開いたコードウィンドウにコードを貼り付ける

標準モジュール(「挿入」→「標準モジュール」)に貼り付けても動作しない。シートモジュールに貼り付けることで、そのシートのセルが変更されたときにイベントが自動発火する。

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

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

  1. Excelで Alt + F11 を押す

シートモジュールを開く

  1. プロジェクトエクスプローラーで、変更を記録したいシートをダブルクリック

コードを貼り付ける

  1. コードウィンドウに、下のコードをそのままコピペする
  2. VBEを閉じてExcelに戻る
  3. セルの値を変更すると自動で記録が始まる(手動実行は不要)

コード(基本版)– セル変更時に変更内容をログ記録

まずは最小構成。セルが変更されたら、変更日時・セル番地・変更後の値をイミディエイトウィンドウに出力する。


'============================================================
' ■ セル変更をイミディエイトウィンドウに記録(基本版)
'   → セルを変更すると日時・セル番地・変更後の値を出力
'   → 貼り付け先: シートモジュール
'============================================================
Private Sub Worksheet_Change(ByVal Target As Range)

    '--- イベントの一時停止(無限ループ防止)
    Application.EnableEvents = False

    '--- ★書き換えポイント ---
    Dim targetArea As String
    targetArea = "A1:Z100"           '← 記録対象の範囲(この範囲外の変更は無視)
    '--- ★ここまで ---

    '--- 記録対象の範囲外なら何もしない
    If Intersect(Target, Me.Range(targetArea)) Is Nothing Then
        Application.EnableEvents = True
        Exit Sub
    End If

    '--- 複数セル同時変更(貼り付け)に対応
    Dim cell As Range
    For Each cell In Target
        If Not Intersect(cell, Me.Range(targetArea)) Is Nothing Then
            Debug.Print Format(Now, "yyyy/mm/dd hh:nn:ss") & _
                         " | " & cell.Address(False, False) & _
                         " | → " & cell.Value
        End If
    Next cell

    '--- イベントを再開
    Application.EnableEvents = True

End Sub

書き換えポイント

変数 説明 初期値
targetArea 記録対象の範囲 "A1:Z100"

ポイント: イミディエイトウィンドウはVBEの画面内に表示される(Ctrl + G)。本番運用では実務版(変更履歴シートに記録)を使う。

この基本版のコードで特に重要なのは、Application.EnableEvents = FalseTrue のペア。Worksheet_Change イベントの中でセルに値を書き込むと、その書き込み自体が再びChangeイベントを発火させ、無限ループに陥る。EnableEvents = False でイベントを一時停止し、処理が終わったら True に戻すことで無限ループを防いでいる。この仕組みは応用版・実務版でも共通するので、しっかり理解しておこう。

また、Intersect(Target, Me.Range(targetArea)) で記録対象の範囲を限定しているのもポイント。全セルの変更を記録するとログが膨大になるし、ヘッダー行の編集まで記録されてしまう。業務で管理したいデータ範囲だけに絞ることで、実用的なログが取れる。

コード(応用版)– 変更前の値も記録(Undo対応)

基本版では変更後の値しか分からない。応用版では Worksheet_SelectionChange を使って、セルを選択した時点の値(=変更前の値)を保持する。


'============================================================
' ■ 変更前後の値を記録(応用版)
'   → SelectionChangeで変更前の値を保持
'   → Changeで変更前→変更後を出力
'   → 貼り付け先: シートモジュール
'============================================================

'--- 変更前の値を保持する変数(モジュールレベル)
Private beforeValue As Variant
Private beforeAddress As String

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    '--- セルを選択した時点の値を保持(=変更前の値になる)
    If Target.Count = 1 Then
        beforeValue = Target.Value
        beforeAddress = Target.Address(False, False)
    End If
End Sub

Private Sub Worksheet_Change(ByVal Target As Range)

    '--- イベントの一時停止(無限ループ防止)
    Application.EnableEvents = False

    '--- ★書き換えポイント ---
    Dim targetArea As String
    targetArea = "A1:Z100"           '← 記録対象の範囲
    '--- ★ここまで ---

    '--- 記録対象の範囲外なら何もしない
    If Intersect(Target, Me.Range(targetArea)) Is Nothing Then
        Application.EnableEvents = True
        Exit Sub
    End If

    '--- 単一セル変更の場合(変更前の値あり)
    If Target.Count = 1 And Target.Address(False, False) = beforeAddress Then
        Debug.Print Format(Now, "yyyy/mm/dd hh:nn:ss") & _
                     " | " & Target.Address(False, False) & _
                     " | " & beforeValue & " → " & Target.Value
    Else
        '--- 複数セル同時変更(貼り付け)の場合
        Dim cell As Range
        For Each cell In Target
            If Not Intersect(cell, Me.Range(targetArea)) Is Nothing Then
                Debug.Print Format(Now, "yyyy/mm/dd hh:nn:ss") & _
                             " | " & cell.Address(False, False) & _
                             " | (貼り付け) → " & cell.Value
            End If
        Next cell
    End If

    '--- イベントを再開
    Application.EnableEvents = True

End Sub

書き換えポイント

変数 説明 初期値
targetArea 記録対象の範囲 "A1:Z100"

注意: beforeValue はモジュールレベル変数のため、ブックを閉じるとリセットされる。ブックを開き直した後の最初の変更では、変更前の値は空になる。

応用版のポイントは、Worksheet_SelectionChangeWorksheet_Change の2つのイベントを組み合わせていること。VBAの Worksheet_Change イベントでは変更後の値しか取得できない。そこで、セルを選択した瞬間にその時点の値を beforeValue に保存しておき、値が変更されたときに beforeValue(変更前)と Target.Value(変更後)を比較する、というアプローチを取っている。この2段階の仕組みのおかげで、「1000 → 1200」のように変更前後の差分がログに残る。

ただし、複数セルを同時に貼り付けた場合は Target.Count > 1 になるため、変更前の値が取れない。この場合は「(貼り付け)」というラベルを付けて変更後の値のみを記録している。完璧ではないが、実務上はこれで十分なことが多い。

コード(実務版)– 変更履歴シートに自動蓄積+フィルタ機能

実務で使う本命のコード。変更内容を「変更履歴」シートに自動で蓄積する。オートフィルタで特定セルや期間の変更を絞り込める。

変更履歴シートにフィルタを付けたら、特定セルの変更だけを絞り込めるようになった。月末の差異チェックが格段に楽になった。


'============================================================
' ■ 変更履歴シートに自動蓄積(実務版)
'   → 変更日時・セル番地・シート名・変更前・変更後を記録
'   → 「変更履歴」シートに自動追記+オートフィルタ
'   → 貼り付け先: シートモジュール
'============================================================

'--- 変更前の値を保持する変数(モジュールレベル)
Private oldValue As Variant
Private oldAddress As String

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    '--- セルを選択した時点の値を保持
    If Target.Count = 1 Then
        oldValue = Target.Value
        oldAddress = Target.Address(False, False)
    End If
End Sub

Private Sub Worksheet_Change(ByVal Target As Range)

    '--- イベントの一時停止(無限ループ防止)
    Application.EnableEvents = False

    On Error GoTo ErrorHandler

    '--- ★書き換えポイント ---
    Dim targetArea As String
    targetArea = "A1:Z1000"          '← 記録対象の範囲

    Dim logSheetName As String
    logSheetName = "変更履歴"        '← 履歴を記録するシート名
    '--- ★ここまで ---

    '--- 記録対象の範囲外なら何もしない
    If Intersect(Target, Me.Range(targetArea)) Is Nothing Then
        Application.EnableEvents = True
        Exit Sub
    End If

    '--- 変更履歴シートの存在確認(なければ自動作成)
    Dim wsLog As Worksheet
    Dim sheetExists As Boolean
    sheetExists = False

    Dim ws As Worksheet
    For Each ws In ThisWorkbook.Worksheets
        If ws.Name = logSheetName Then
            sheetExists = True
            Set wsLog = ws
            Exit For
        End If
    Next ws

    If Not sheetExists Then
        Set wsLog = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
        wsLog.Name = logSheetName

        '--- ヘッダーを作成
        wsLog.Range("A1").Value = "日時"
        wsLog.Range("B1").Value = "シート名"
        wsLog.Range("C1").Value = "セル番地"
        wsLog.Range("D1").Value = "変更前"
        wsLog.Range("E1").Value = "変更後"

        '--- ヘッダー行を太字に
        wsLog.Range("A1:E1").Font.Bold = True

        '--- 列幅を調整
        wsLog.Columns("A").ColumnWidth = 20
        wsLog.Columns("B").ColumnWidth = 15
        wsLog.Columns("C").ColumnWidth = 10
        wsLog.Columns("D").ColumnWidth = 20
        wsLog.Columns("E").ColumnWidth = 20
    End If

    '--- 履歴の書き込み開始行を取得
    Dim nextRow As Long
    nextRow = wsLog.Cells(wsLog.Rows.Count, "A").End(xlUp).Row + 1

    '--- 変更内容を記録
    If Target.Count = 1 And Target.Address(False, False) = oldAddress Then
        '--- 単一セル変更(変更前の値あり)
        wsLog.Cells(nextRow, 1).Value = Format(Now, "yyyy/mm/dd hh:nn:ss")
        wsLog.Cells(nextRow, 2).Value = Me.Name
        wsLog.Cells(nextRow, 3).Value = Target.Address(False, False)
        wsLog.Cells(nextRow, 4).Value = oldValue
        wsLog.Cells(nextRow, 5).Value = Target.Value

        '--- 変更前の値を更新
        oldValue = Target.Value
    Else
        '--- 複数セル同時変更(貼り付け操作)
        Dim cell As Range
        For Each cell In Target
            If Not Intersect(cell, Me.Range(targetArea)) Is Nothing Then
                wsLog.Cells(nextRow, 1).Value = Format(Now, "yyyy/mm/dd hh:nn:ss")
                wsLog.Cells(nextRow, 2).Value = Me.Name
                wsLog.Cells(nextRow, 3).Value = cell.Address(False, False)
                wsLog.Cells(nextRow, 4).Value = "(複数セル変更)"
                wsLog.Cells(nextRow, 5).Value = cell.Value
                nextRow = nextRow + 1
            End If
        Next cell
    End If

    '--- オートフィルタを設定(まだ設定されていない場合)
    If wsLog.AutoFilterMode = False Then
        wsLog.Range("A1:E1").AutoFilter
    End If

    Application.EnableEvents = True
    Exit Sub

ErrorHandler:
    Application.EnableEvents = True
    MsgBox "変更履歴の記録中にエラーが発生しました: " & Err.Description, vbExclamation

End Sub

書き換えポイント

変数 説明 初期値
targetArea 記録対象の範囲 "A1:Z1000"
logSheetName 履歴を記録するシート名 "変更履歴"

ポイント: エラーハンドラーを入れているため、エラーが発生しても EnableEvents = True に戻る。これがないとエラー後に全てのイベントが停止する。

実務版で注目すべきは、変更履歴シートの自動作成機能。初回実行時に「変更履歴」シートが存在しなければ自動的に作成し、ヘッダー行とオートフィルタを設定する。2回目以降は既存のシートに追記していく。これにより、ユーザーが事前にシートを準備する必要がなく、コードを貼り付けるだけで運用を開始できる。

オートフィルタが自動で設定されるのも地味に便利で、「特定のセルだけの変更履歴を見たい」「今日の変更だけを確認したい」といったフィルタ操作がすぐにできる。フィルタ操作の詳細はオートフィルタでデータを絞り込む方法(記事062)を参照。また、変更履歴の記録と合わせてマクロの実行ログをファイルに記録する方法(記事052)も導入しておくと、マクロの実行状況も含めた包括的なログ管理ができる。

変更内容をセルのコメント(メモ)として残したい場合は セルのコメントを一括操作する方法 も参考になる。

よくある落とし穴5選

1. Worksheet_Change 内でセルを書き換えて無限ループ

自分もこれでExcelをフリーズさせた。Worksheet_Change内でセルに値を書き込むと、その書き込み自体がChangeイベントを再発火させ、無限ループになる。

対策: セルに書き込む前に Application.EnableEvents = False を入れ、処理後に Application.EnableEvents = True で戻す。実務版コードには最初から入っている。

2. EnableEvents = True に戻し忘れてイベントが全停止

原因: エラーが発生して EnableEvents = True の行に到達しないまま処理が中断すると、以降の全シートでイベントが発火しなくなる。

対策: On Error GoTo ErrorHandler でエラーハンドラーを設定し、エラー発生時にも必ず EnableEvents = True に戻す。もし既にイベントが停止している場合は、イミディエイトウィンドウ(Ctrl + G)で Application.EnableEvents = True を実行する。

3. 標準モジュールに貼り付けて動かない

原因: Worksheet_Changeイベントはシートモジュール専用。標準モジュール(「挿入」→「標準モジュール」)に貼り付けても発火しない。

対策: VBEの左側で対象シートをダブルクリックし、開いたコードウィンドウに貼り付ける。

4. 複数セルを貼り付けたらエラーになった

原因: Target.Value は複数セルの場合に配列を返す。単一セル前提のコードだとエラーになる。

対策: Target.Count で判定し、複数セルの場合は For Each で1セルずつ処理する。実務版コードは対応済み。

5. ブックを開き直したら変更前の値が取れない

原因: beforeValue(変更前の値)はモジュールレベル変数で、ブックを閉じるとリセットされる。開き直した後の最初の変更では変更前の値が空になる。

対策: これはVBAの仕様による制限。実務上は「ブックを開いた後の最初の1回だけ変更前が空」になるだけなので、大きな問題にはならない。

6. 変更履歴シートへの書き込み自体がChangeイベントを発火する

原因: 変更履歴シートに記録を書き込むコードが、そのシートの Worksheet_Change を発火させてしまうケースがある。特に変更履歴シートにもChangeイベントのコードが入っている場合に発生する。

対策: 変更履歴シートのシートモジュールにはChangeイベントのコードを入れないこと。もし全シートに同じコードを入れる場合は、If Me.Name = logSheetName Then Exit Sub のようにログシート自体の変更は記録対象外にする。

VBAのWorksheet_Changeで無限ループになるときの対処法

「Worksheet_Change内でセルに値を書き込んだらExcelがフリーズした」という場合、原因はChangeイベント内のセル書き込みが再びChangeイベントを発火させて無限ループに陥っていること。対処法は、セルに書き込む前に Application.EnableEvents = False でイベントを一時停止し、書き込み後に Application.EnableEvents = True で再開すること。このペアは必ずセットで書く。もし既にフリーズした場合は Ctrl + Break で停止し、イミディエイトウィンドウで Application.EnableEvents = True を実行してイベントを復活させる。

VBAのイベントが二重発火して止まらないときの対処法

「セルを1つ変更しただけなのにイベントが何度も発火する」という場合、原因はエラーが途中で発生して EnableEvents = True に到達しないまま処理が中断し、次回の変更時にイベントが不正な状態で動いていること。対処法は、On Error GoTo ErrorHandler でエラーハンドラーを設定し、エラー時にも確実に Application.EnableEvents = True に戻す処理を入れること。実務版コードにはこのガードが入っているので、そのまま使えば安全。自分もエラーハンドラーなしで運用して痛い目を見た。

VBAで変更前の値が取得できないときの対処法

「Worksheet_Changeイベントで変更後の値は取れるが、変更前の値が取れない」という場合、これはVBAの仕様によるもの。Changeイベント単体では変更前の値にアクセスする方法がない。対処法は、Worksheet_SelectionChange イベントでセルを選択した時点の値をモジュールレベル変数に保存しておき、Changeイベント発火時にその保存値を「変更前の値」として使うこと。本記事の応用版・実務版コードにこのパターンが実装されている。

FAQ

Q1: 特定の列だけ変更を記録したい

Intersect で対象列を限定する:


'--- C列(3列目)だけ記録対象にする場合
If Intersect(Target, Me.Columns(3)) Is Nothing Then
    Application.EnableEvents = True
    Exit Sub
End If

targetArea の代わりに Me.Columns(3) を使えば、C列の変更だけが記録される。複数列にしたい場合は Me.Range("C:C,E:E") のように指定する。

Q2: 変更履歴が溜まりすぎたらどうする?

月次で履歴をクリアするマクロを用意しておくと便利:


Sub ClearChangeLog()
    Dim wsLog As Worksheet
    Set wsLog = ThisWorkbook.Worksheets("変更履歴")

    '--- ヘッダー行以外を削除
    If wsLog.Cells(wsLog.Rows.Count, "A").End(xlUp).Row > 1 Then
        wsLog.Range("A2:E" & wsLog.Cells(wsLog.Rows.Count, "A").End(xlUp).Row).ClearContents
        MsgBox "変更履歴をクリアしました。", vbInformation
    End If
End Sub

Q3: 複数シートの変更を1つの履歴シートに記録したい

各シートのシートモジュールに同じコード(実務版)を貼り付ける。Me.Name でシート名が自動的に記録されるため、どのシートの変更か区別できる。

Q4: イベントが発火しなくなった(EnableEventsが止まっている)

イミディエイトウィンドウ(Ctrl + G)で以下を実行する:


Application.EnableEvents = True

これでイベントが復活する。

Q5: 変更者(ユーザー名)も記録したい

Environ("USERNAME") でWindowsのログインユーザー名を取得できる:


wsLog.Cells(nextRow, 6).Value = Environ("USERNAME")

ヘッダーにF列「変更者」を追加し、上記コードを実務版に追加する。

まとめ

  • 基本版: Worksheet_Change でセル変更を検知し、変更後の値を記録する最小構成
  • 応用版: Worksheet_SelectionChange で変更前の値も保持し、変更前→変更後の差分を記録
  • 実務版: 変更履歴シートに日時・セル番地・変更前・変更後を自動蓄積。オートフィルタで絞り込み可能
  • 最重要ポイント: Application.EnableEvents = False で無限ループを防止すること

セルの変更履歴を自動記録する仕組みは、一度入れてしまえばあとは何もしなくていい。手動でログを取るルールは「忘れたら終わり」だが、VBAなら確実に記録される。自分の経験上、この仕組みが最も威力を発揮するのは「トラブルが起きたとき」。月末の数値が合わない、見積金額が変わっている、といった問題の原因を5分で特定できるようになる。共有ブックを使っているチームなら、導入して損はない。

関連記事

次にやりたくなること

もっとカスタマイズしたい場合

「変更履歴の記録対象を特定の列だけに絞りたい」「複数シートの変更を一括で記録したい」「変更者名やIPアドレスも記録したい」など、業務に合わせたカスタマイズが必要な場合は、ココナラで相談できる。

相談時に伝えると話が早い情報:

  • Excel のバージョン / OS
  • 記録対象のシート構成(何シートあるか、記録対象の列はどこか)
  • 変更履歴に記録したい項目(日時・セル番地・変更前後・変更者など)
  • 履歴データの保存期間・クリア方針

コメント

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