0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#でファイル変更を監視したいときの基本|FileSystemWatcher、共有フォルダ、サブフォルダ、WinForms【工房W01】

0
Last updated at Posted at 2026-03-16

連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index

業務アプリでは、フォルダへファイルが置かれたことをきっかけに処理を動かしたい場面があります。
取り込みフォルダへの新規配置、連携ファイルの更新、削除や名前変更の反映などです。

こうした場面では、一定間隔で差分を見る方法もありますが、変化をその場で受けたいなら FileSystemWatcher が最初の候補になります。
FileSystemWatcher は、指定ディレクトリで起きた作成・更新・削除・リネームをイベントで受け取るための .NET の機能です。

今回は .NET Framework 4.8.1 の WinForms サンプルをベースに進めます。
ただし、この話の基本は .NET 8 でもほぼ同じです。
名前空間も使い方も大きくは変わらないので、既存の WinForms 業務アプリへ入れるときにも、新しめの .NET で組むときにも、そのまま持っていきやすくなります。

  • 対象: C# / .NET でファイル監視を入れたいとき
  • 前提: 今回のサンプルは .NET Framework 4.8.1 ベース
  • 補足: 基本の考え方とコードの形は .NET 8 でもほぼ同じ
  • 完成サンプル: GitHubの WinForms サンプル一式はこちら

先に画面を見る

まずは完成サンプルの画面を見ます。
監視先の入力、フィルタ指定、サブフォルダ監視切替、イベント一覧、エラー表示まで入れた画面です。
このあとに出てくる説明と、画面上のどの機能かを対応づけやすくなります。

W01_App_Image.png

GitHub のサンプルでは次を確認できます。

  • 監視開始 / 監視終了
  • 監視先入力、参照、ドラッグアンドドロップ
  • フィルタ指定
  • サブフォルダ監視切替
  • 更新イベントの集約切替
  • イベント一覧表示
  • エラー表示

このページでは、次の順で見ていきます。

  • まずどの方法を選ぶか
  • FileSystemWatcher を使う場面
  • 最小コードでの動かし方
  • WinForms での置き方
  • 共有フォルダやサブフォルダで使うときの注意点
  • Changed の多重通知や受信直後の読込失敗への向き合い方

公式:

まずどの方法を見るか

やりたいこと まず見る方法
変化を受けた時点で処理を動かしたい FileSystemWatcher
後で履歴を確認したい イベント ビューアーのログや監査ログ
一定間隔で状態差分を見たい 定期的なディレクトリ走査と差分比較

今回の話は、変化を受けた時点で処理へつなぐ用途に絞っています。

ここで言う「定期的なディレクトリ走査と差分比較」は、一定間隔でフォルダ内のファイル一覧を取り、前回結果と見比べて追加・更新・削除を自前で判断するやり方です。
実装の流れは思い浮かべやすい一方で、監視頻度、比較用データの保持、取りこぼしや重複判定まで自分で考える必要があります。

ディレクトリの変化をイベントで受けたい用途なら、まず FileSystemWatcher を見る方が早く進みます。

FileSystemWatcher を使う場面

FileSystemWatcher は、ディレクトリの変化をその場で受けたいときに使います。

たとえば次のような場面です。

  • 取り込みフォルダへ CSV が置かれたら処理を始めたい
  • 連携ファイルが更新されたら再読込したい
  • 削除や名前変更を画面へ反映したい
  • サブフォルダを含めて配下の変化を見たい

ここで大事なのは、ファイル1件を登録して追うのではなく、ディレクトリを起点に変化を受けることです。
そのため、監視対象はまずディレクトリで考えます。

後から確認するログではなく、まず FileSystemWatcher を見る理由

ファイル変更を追う方法は FileSystemWatcher だけではありません。
イベント ビューアーのログや監査ログから後で確認する考え方もあります。

ただ、今回やりたいのは履歴確認ではなく、変化を受けた時点で処理へつなぐことです。
ファイルが来たら取り込みを始める、更新されたら再読込する、削除やリネームを反映する。
この流れなら、まず FileSystemWatcher を見る方が自然です。

一方で、FileSystemWatcher で分かるのは基本的に「何が起きたか」と「どのパスで起きたか」です。
「だれが更新したのか」「だれが削除したのか」を後から追いたい場合は、監査設定や Security ログの見方を別に整理した方が進めやすくなります。
この続きは、4663 / 4660 を使って更新者候補を追う 工房W02 で扱います。
今回はそこではなく、アプリでリアルタイム監視を組む話に絞ります。

最初に知っておくと楽なこと

FileSystemWatcher は、変化が起きたことを受ける部品です。
「1回だけ来る」「受信直後に安全に読める」「絶対に取りこぼさない」までを約束する部品ではありません。

この前提を持っておくと、Changed の多重通知や受信直後の読込失敗が理解しやすくなります。

まずはこれだけで動きを見られる

最初は最低限で十分です。
ここでは、監視対象の設定、イベント登録、監視開始までをまとめて置きます。

今回のサンプルは .NET Framework 4.8.1 ベースですが、このコードの形は .NET 8 でもほぼそのままです。
まずは API の並びとイベントの出方をつかむ方を優先します。

using System;
using System.IO;

class Program
{
    private static FileSystemWatcher watcher;

    static void Main()
    {
        watcher = new FileSystemWatcher(@"C:\監視対象")
        {
            // 最初は広めでよいが、本番では絞った方が扱いやすい
            Filter = "*.*",

            // 配下フォルダも監視対象
            IncludeSubdirectories = true,

            // 監視する変化
            NotifyFilter =
                NotifyFilters.FileName |
                NotifyFilters.DirectoryName |
                NotifyFilters.LastWrite |
                NotifyFilters.Size
        };

        watcher.Created += OnCreated;
        watcher.Changed += OnChanged;
        watcher.Deleted += OnDeleted;
        watcher.Renamed += OnRenamed;
        watcher.Error += OnError;

        // 監視開始
        watcher.EnableRaisingEvents = true;

        Console.WriteLine("監視中。Enter で終了");
        Console.ReadLine();

        watcher.EnableRaisingEvents = false;
        watcher.Dispose();
    }

    private static void OnCreated(object sender, FileSystemEventArgs e)
    {
        Console.WriteLine($"作成     : {e.FullPath}");
    }

    private static void OnChanged(object sender, FileSystemEventArgs e)
    {
        Console.WriteLine($"更新     : {e.FullPath}");
    }

    private static void OnDeleted(object sender, FileSystemEventArgs e)
    {
        Console.WriteLine($"削除     : {e.FullPath}");
    }

    private static void OnRenamed(object sender, RenamedEventArgs e)
    {
        Console.WriteLine($"リネーム : {e.OldFullPath} -> {e.FullPath}");
    }

    private static void OnError(object sender, ErrorEventArgs e)
    {
        Exception ex = e.GetException();
        Console.WriteLine($"監視エラー: {ex?.Message}");
    }
}

最初は FullPath を出すだけで十分です。
ここで実際のイベントの出方を見てから、本処理へつなぐ方が進めやすくなります。

この最小コードで確認したいこと

最小コードを動かしたら、まず次を確認します。

  • 新規作成で Created が来るか
  • 保存で Changed が何回出るか
  • リネームで Renamed がどう出るか
  • サブフォルダ配下も拾えているか
  • Error が出ていないか

ここでイベントの飛び方を見ないまま本処理へ直結すると、あとで切り分けが苦しくなります。
最初の段階では、「業務処理を動かす」より「イベントの癖を確認する」方が先です。

GitHub のサンプルで見られるもの

本文の最小サンプルは、まずイベントの出方を見るための最小構成です。
GitHub の WinForms サンプルでは、次も確認できます。

  • 監視先の入力、参照、ドラッグアンドドロップ
  • フィルタ指定
  • サブフォルダ監視切替
  • 更新イベントの集約切替
  • 一覧表示
  • エラー表示

最初は本文の最小コードで動きを見て、その後で GitHub サンプルを見ると流れを追いやすくなります。

WinForms ではどこで開始し、どこで終了するか

WinForms では、フォーム表示時に監視を始め、フォーム終了時に監視を止める形が分かりやすくなります。
まずはこの置き方で十分です。

ここも .NET Framework 4.8.1 の WinForms サンプルとしてそのまま使える形です。
.NET 8 の WinForms でも、イベントを受けて UI スレッドへ戻す流れは大きく変わりません。

using System;
using System.IO;
using System.Windows.Forms;

public partial class MainForm : Form
{
    private FileSystemWatcher watcher;

    public MainForm()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        watcher = new FileSystemWatcher(@"C:\監視対象")
        {
            Filter = "*.csv",
            IncludeSubdirectories = true,
            NotifyFilter =
                NotifyFilters.FileName |
                NotifyFilters.LastWrite |
                NotifyFilters.Size
        };

        watcher.Created += Watcher_Created;
        watcher.Changed += Watcher_Changed;
        watcher.Deleted += Watcher_Deleted;
        watcher.Renamed += Watcher_Renamed;
        watcher.Error += Watcher_Error;

        watcher.EnableRaisingEvents = true;
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        if (watcher != null)
        {
            watcher.EnableRaisingEvents = false;
            watcher.Dispose();
            watcher = null;
        }

        base.OnFormClosing(e);
    }

    private void Watcher_Created(object sender, FileSystemEventArgs e)
    {
        BeginInvoke((MethodInvoker)(() =>
        {
            // 画面更新は UI スレッド側で実施
            listBox1.Items.Add($"作成     : {e.FullPath}");
        }));
    }

    private void Watcher_Changed(object sender, FileSystemEventArgs e)
    {
        BeginInvoke((MethodInvoker)(() =>
        {
            listBox1.Items.Add($"更新     : {e.FullPath}");
        }));
    }

    private void Watcher_Deleted(object sender, FileSystemEventArgs e)
    {
        BeginInvoke((MethodInvoker)(() =>
        {
            listBox1.Items.Add($"削除     : {e.FullPath}");
        }));
    }

    private void Watcher_Renamed(object sender, RenamedEventArgs e)
    {
        BeginInvoke((MethodInvoker)(() =>
        {
            listBox1.Items.Add($"リネーム : {e.OldFullPath} -> {e.FullPath}");
        }));
    }

    private void Watcher_Error(object sender, ErrorEventArgs e)
    {
        Exception ex = e.GetException();

        BeginInvoke((MethodInvoker)(() =>
        {
            listBox1.Items.Add($"監視エラー: {ex?.Message}");
        }));
    }
}

WinForms で気をつけたいのは、イベント受信側から画面部品へそのまま触らない方が扱いやすいことです。
そのため、画面更新を行うなら BeginInvoke などで UI スレッド側へ戻しておく方が進めやすくなります。

WinForms で最初から分けて考えたいもの

WinForms では、次の2つを最初から分けて考えると見通しが良くなります。

  • 監視イベントを受ける部分
  • 画面へ反映する部分

この2つを1つのイベント内でまとめて書き始めると、あとで本処理が重くなったときに整理しづらくなります。
最初は BeginInvoke で画面へ流すだけでも十分ですが、実務では「受信」と「画面反映」を分けて考える方が扱いやすくなります。

サブフォルダまで見たいとき

サブフォルダも監視したいときは、IncludeSubdirectoriestrue にします。

watcher.IncludeSubdirectories = true;

日付フォルダや部門フォルダが配下に増える運用では、この設定が必要です。

ただし、範囲を広げるほど通知も増えます。
便利ですが、不要な階層まで広げるとイベント量が増えて扱いづらくなります。

公式:

共有フォルダでも使えるか

使えます。
ネットワーク ドライブや共有フォルダでも監視対象にできます。
ただし、ローカル監視より慎重に見た方がよい場面があります。

気をつけたい点は次です。

  • 実行ユーザーで共有へ届くこと
  • 共有先の配下にもアクセスできること
  • 通知遅延や一時切断の影響を受けやすいこと
  • 変更集中時に取りこぼしが出やすいこと
  • Error を見ていないと乱れに気づきにくいこと

つまり、「共有フォルダでは使えない」ではありません。
ただ、ローカル監視と同じ感覚で過信しない方がよいという話です。

Windows サービスやタスク スケジューラへ広げると、急に動きが変わることがあります。
多くは FileSystemWatcher 自体より、動かしているユーザーが変わって共有へ届かなくなることが原因です。

そのため、実装前に次を確認した方が早く進みます。

  • 監視対象ディレクトリが実在するか
  • 実行ユーザーで到達できるか
  • 読み取りまで要るなら読めるか
  • サブフォルダも使うなら配下まで届くか

Changed が1回とは限らない理由

ここは最初に知っておくと楽になります。

ファイル保存 = Changed 1回 とは限りません。
保存するアプリごとに、実際の更新手順が違うためです。

たとえば次のような違いがあります。

  • 直接上書きする
  • 一時ファイルへ書いてから置き換える
  • サイズ更新と更新時刻更新が別に動く
  • リネーム時にも Changed が絡むことがある

このため、1回保存したつもりでも Changed が複数回来ることがあります。
これは珍しい動きではありません。

ここで「1回しか来ない前提」で本処理へ直結すると、二重処理になりやすくなります。
最初はまずイベントの出方を見て、その後で業務側の扱いを決める方が安全です。

受けた直後にファイルを開くと失敗する理由

これも現場でかなり当たりやすくなります。

イベントが来た時点では、相手の書き込みがまだ終わっていないことがあります。
大きなファイルのコピー中や、別プロセスが書き込み中の場面が典型です。

そのため、次の流れが起きます。

  • イベントは来る
  • すぐ開こうとする
  • まだ使用中で失敗する

これは不具合ではなく、イベントが「変化が起きた」ことを知らせているためです。
「もう安全に読める」ことまでは保証していません。

このため、必要なら次を入れます。

  • 少し待ってから開く
  • 数回だけ再試行する
  • サイズや更新時刻が落ち着いたことを見て続ける

監視イベントと本処理の間にこの1段があるだけで、かなり扱いやすくなります。

最小の再試行例

受信直後に読めない場面を試すなら、まずはこの程度の小さい再試行で十分です。
いきなり大きな制御を書くより、まずは「使用中で失敗する」現象を吸収できる形から入る方が進めやすくなります。

この考え方も .NET Framework 4.8.1.NET 8 で大きくは変わりません。
ファイルがまだ開けない時間帯をどう吸収するか、という見方は共通です。

using System;
using System.IO;
using System.Threading;

private static string ReadAllTextWithRetry(string path)
{
    const int maxRetryCount = 5;
    const int waitMilliseconds = 300;

    for (int i = 0; i < maxRetryCount; i++)
    {
        try
        {
            using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (StreamReader reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }
        catch (IOException)
        {
            if (i == maxRetryCount - 1)
            {
                throw;
            }

            // 書き込み完了待ち
            Thread.Sleep(waitMilliseconds);
        }
    }

    throw new IOException("ファイル読込に失敗");
}

この例で見たいのは、再試行ロジックの細かさではありません。
イベント受信直後に本体処理へ入れない方がよい場面があることです。
なお、共有方法や運用次第では FileShare の考え方も変わります。ここでは最小の考え方だけを置いています。

通知が多すぎるときに見る場所

通知が多いと、ログも増えるし、本処理も乱れやすくなります。
まず見る場所は2つです。

拡張子で絞る

CSV だけでよいなら、全部見る必要はありません。

watcher.Filter = "*.csv";

変化の種類で絞る

必要な変化だけを見るようにします。

watcher.NotifyFilter =
    NotifyFilters.LastWrite |
    NotifyFilters.Size;

これだけでも不要な通知がかなり減ることがあります。

公式:

InternalBufferSize を大きくする前に見ること

変更が集中すると、内部バッファがあふれることがあります。
このとき InternalBufferSize を大きくしたくなりますが、先に見たいのは別です。

  • Filter が広すぎないか
  • NotifyFilter が多すぎないか
  • IncludeSubdirectories が必要以上に広くないか
  • 共有フォルダで更新が集中していないか

根本がイベント量の多さなら、バッファだけ増やしてもまた苦しくなります。
そのため、順番としては「通知を減らす」方が先です。

公式:

Error を軽く見ない方がよい理由

Error は最後に付ける飾りではありません。
監視異常を受け取る場所なので、少なくともログへ出す形は最初から入れておいた方がよくなります。

特に次の場面では、Error を見ていないと監視の乱れに気づきにくくなります。

  • 変更が集中したとき
  • 共有フォルダで一時的に不安定になったとき
  • 監視対象の量が多すぎるとき

最初のサンプル段階でも Error をつないでおく価値は十分あります。

最初はどう試すと分かりやすいか

最初から共有フォルダやサービス実行まで広げると、どこで詰まっているのか見えにくくなります。
最初は次の順で進めると分かりやすくなります。

  1. ローカルの監視対象ディレクトリを1つ決める
  2. Created / Changed / Deleted / Renamed / Error をつなぐ
  3. FullPath を出してイベントの飛び方を見る
  4. 必要なら FilterNotifyFilter を絞る
  5. 受信直後の読み込みは少し待つか再試行を入れる
  6. その後で共有フォルダやサービス実行へ広げる

どういう設計だと苦しくなりやすいか

現場で詰まりやすいのは次の形です。

  • 受信イベントの中でいきなり重い本処理を始める
  • Changed は1回しか来ない前提で書く
  • 共有フォルダでもローカルと同じつもりで扱う
  • Error を見ない
  • 監視範囲を広く取ったまま様子を見ない

逆に、最初に次を分けて考えるとかなり進めやすくなります。

  • 監視する
  • 受け取る
  • 必要なら少し待つ
  • 本処理へ流す
  • 画面へ出す

この区切りで見ておくと、あとで重くなったときも切り分けしやすくなります。

まとめ

ファイル変更監視が必要になったとき、まず見る名前は FileSystemWatcher です。
今回のサンプルは .NET Framework 4.8.1 ベースですが、基本の考え方とコードの形は .NET 8 でも大きく変わりません。

まず押さえたいのは次です。

  • 変化をその場で受ける用途なら FileSystemWatcher が起点になる
  • Changed は複数回来ることがある
  • 受信直後にファイルを開くと失敗しやすい
  • 共有フォルダでは条件次第で不安定に見える
  • 通知を広く取りすぎると扱いづらくなる
  • Error を見ていないと乱れに気づきにくい

そのため、最初の進め方としては次が分かりやすくなります。

  • まず最小で動かす
  • イベントの出方を見る
  • 通知範囲を必要最小へ絞る
  • 読み込みは受信直後に直結しない
  • その後で共有フォルダや本処理へ広げる

変化は拾えたが、だれが触ったかまでは見えない。
そこで次は、Security ログの 4663 / 4660 を使って更新者候補を追う 工房W02 へ進むと流れがつながります。

動かしながら見たい場合は、GitHub の WinForms サンプルも合わせてどうぞ。
GitHubの WinForms サンプル一式はこちら

参考

連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?