1
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?

G10 【外伝】WinForms Formライフサイクル完全ガイド ── ctor/Load/Shown/Closing/Disposeの置き場と分け方

Last updated at Posted at 2026-01-20

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

WinFormsが不安定になる典型は、初期化と後始末がイベントに散って、順序が崩れることにある。
Loadで重い処理を抱えると初回表示が固まり、Closingで止め切れないと終了が詰まり、Disposeで漏れが出ると常駐系が残る。
このページは Form のライフサイクルを ctor → Load → Shown → Closing → Dispose の順で揃え、責務の置き場を言語化する。


0. このページの使い方

困った場面から逆引きする場合、次の順で十分。

  1. 初回表示が固まる: 「Load/Shown」へ進む(同期I/O、.Result/.Wait の混入を点検)
  2. 終了が詰まる: 「Closing」へ進む(停止要求・キャンセル・常駐抑止の有無を点検)
  3. 常駐系が残る: 「Dispose」へ進む(Timer/NotifyIcon/event 解除漏れを点検)
  4. 2回目表示だけ壊れる: 「再表示/トレイ常駐」へ進む(Shownは1回、毎回更新の置き場を点検)

1. 用語

1-1. ctor とは何か

ctor は constructor(コンストラクタ)の略称。
WinFormsでは public MainForm() のように書く部分が ctor になる。

1-2. ここでの「軽い」とは何か

  • UIスレッドを長時間占有しない(初回表示が詰まらない)
  • 外部I/Oを開始しない(ファイル/DB/HTTP 等の待ちが出ない)
  • 失敗時の扱いが明確(例外が握り潰されず、落としどころがある)

2. ライフサイクル全景

イベント名が散ると「どこに何を置くべきか」が崩れる。
まずは全体の順序を頭に置く。


3. 責務の置き場

位置 タイミング 置く責務 置かない責務
ctor new の生成過程 InitializeComponent、依存注入、軽い初期値、イベント配線 外部I/O、重い計算、待ちが出る処理
Load 表示直前 初期表示に必要なUI状態、レイアウト準備、初期値反映 .Result/.Wait、長時間処理、重いI/O
Shown 初回表示直後 重い初期化の開始、非同期起動、例外と縮退の整理 UIスレッドでの長時間同期処理
Closing 閉じる直前 停止要求、キャンセル、未保存確認、終了可否 「Dispose任せ」で停止しない形
Dispose 破棄時 event解除、Timer停止、NotifyIcon/Handle解放、資源解放の保険 主要ロジックの本体、判断の中心

4. ctor は「軽くすべき」

4-1. ctor を重くすると困る理由

  • ここが詰まると「起動しない」に見える(初回表示まで進まない)
  • 例外が出るとフォーム生成自体が失敗する(回復ルートが作りにくい)
  • デザイナ(Visual Studio)が ctor を通すため、重い処理で設計時が壊れる

4-2. ctor に置いてよいもの

  • InitializeComponent()
  • DIの受け取り、フィールド初期化
  • 「軽い」イベント配線(Timer開始などの起動はここに置かない)
  • 画面の静的な見た目(固定値のプロパティ設定)
public partial class MainForm : Form
{
    private readonly ILogger _logger;

    // ★長寿命の停止トークン。Closing で Cancel して、Dispose で解放する。
    private readonly CancellationTokenSource _lifeCts = new();

    public MainForm(ILogger logger)
    {
        InitializeComponent(); // ★コントロール生成はここが責務
        _logger = logger;

        // ★配線はここで完了させる(Start/Stop は別責務)
        this.Load += MainForm_Load;
        this.Shown += MainForm_Shown;
        this.FormClosing += MainForm_FormClosing;

        // ★軽い初期値(見た目・フラグ)
        this.Text = "Main";
    }
}

4-3. ctor と Load の境界が揺れる場面

「コントロール初期化を ctor に置くか Load に置くか」で迷う場面は多い。
判断軸は次の2つに寄せるとブレが減る。

  • 固定値で決まるか: 端末/設定/データで変わらないなら ctor 側が成立しやすい
  • Handle/サイズ/レイアウトに依存するか: 依存するなら Load 側が成立しやすい

例: DataSource の差し替えや動的列生成など、「初回描画前に揃えたい」変更は Load へ寄せる。

4-4. Handle 未作成の罠

ctor では Handle が未作成のことが多い。
この段階で BeginInvokeCreateGraphics、一部のコントロール操作を行うと、Handle が意図せず作られたり、順序が変わることがある。
Handle 依存の処理は Load 以降へ寄せる方が安全寄り。

4-5. .NET Framework 4.8 補足

.NET Framework 4.8 でも考え方は同じ。差が出やすいのは「設計時の落ち方」。
WinFormsデザイナは ctor を通すため、I/Oや環境依存を ctor に置くと設計時が壊れやすい。

設計時を避けたい場合、DesignMode だけに頼らず、用途モードも併用する方が安全寄り。

private static bool IsDesignTime(Control c)
{
    // ★DesignMode は ctor 直後だと false になりやすい
    if (LicenseManager.UsageMode == LicenseUsageMode.Designtime) return true;

    // ★Site が取れる段階なら DesignMode が効く
    if (c.Site?.DesignMode == true) return true;

    return false;
}

5. Load は「初期表示を作る」

Load は「初回描画に必要なUI状態」を揃える場所。
重い処理を置くと初回表示が詰まるため、揃えるだけに寄せる。

5-1. 失敗パターン: Load で同期I/O

現象は「固まる」「起動しない」に見える。

private void MainForm_Load(object? sender, EventArgs e)
{
    // ★ここで待ちが出ると初回表示が詰まる
    var json = File.ReadAllText(_path);
    _settings = JsonSerializer.Deserialize<AppSettings>(json);
}

5-2. 正攻法: Load は軽く、重い処理は Shown で開始

Load は「表示できる状態」を作る。
重い処理は Shown で起動し、例外と縮退の落としどころを作る。

private void MainForm_Load(object? sender, EventArgs e)
{
    // ★初期表示に必要な最低限(プレースホルダ、状態)
    statusLabel.Text = "initializing...";
    progressBar.Visible = true;

    // ★Load では開始しない(開始は Shown へ)
}

6. Shown は「重い初期化の開始点」

Shown は初回表示の後に呼ばれる。
ここで非同期初期化を起動すると、「表示は先に出す」「後で詰まったら縮退する」が取りやすい。

6-1. 典型形: 初期化を起動し、Closing でキャンセルする

private CancellationTokenSource? _initCts;

private async void MainForm_Shown(object? sender, EventArgs e)
{
    // ★イベントは async void が避けにくい。try/catch を必ず置く。
    _initCts = new CancellationTokenSource();

    try
    {
        await InitializeAsync(_initCts.Token); // ★重い処理はここで開始
        statusLabel.Text = "ready";
    }
    catch (OperationCanceledException)
    {
        // ★終了経路。ログは必要に応じて。
        statusLabel.Text = "closing...";
    }
    catch (Exception ex)
    {
        // ★初期化失敗の落としどころ(ログ+縮退)
        _logger.LogError(ex, "Initialize failed");
        statusLabel.Text = "failed (degraded)";
    }
    finally
    {
        progressBar.Visible = false;
    }
}

private void MainForm_FormClosing(object? sender, FormClosingEventArgs e)
{
    // ★Closing は「止める」を担当する(Dispose 任せにしない)
    _initCts?.Cancel();
}

6-2. InitializeAsync の中身(重い処理の置き場)

Shown 側が「起動」、InitializeAsync 側が「実体」と分けるとレビューが通りやすい。
例はファイル読み+UI反映。UI反映は await 後に行う(UIスレッドへ戻る前提)。

private async Task InitializeAsync(CancellationToken ct)
{
    // ★重いI/Oはここへ寄せる(Load/ctor へ置かない)
    // .NET 8: File.ReadAllTextAsync が使える
    // .NET Framework 4.8: StreamReader.ReadToEndAsync や Task.Run で代替する場面が多い
    var json = await Task.Run(() => File.ReadAllText(_path), ct);

    ct.ThrowIfCancellationRequested(); // ★キャンセル点を明示

    _settings = JsonSerializer.Deserialize<AppSettings>(json);

    // ★UI反映は UI スレッドで行う(await 後は戻る設計を前提にする)
    ApplySettingsToControls(_settings);
}

6-3. Shown は 1 回、毎回更新は別イベントへ

Shown は基本的に「初回だけ」に寄る。
Hide→Show のような再表示で毎回更新したい場合、VisibleChangedActivated へ寄せる方が安定する。

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

    if (!this.Visible) return;

    // ★再表示ごとに必要な更新(表示文字、状態同期)
    RefreshUiSnapshot();
}

7. Closing は「止める・止め方を決める」

FormClosing は「止める要求」と「終了可否」の判断点。
ここで止めないと「閉じたのに動く」「終了が詰まる」へ繋がる。

7-1. 常駐(トレイ)で Close を抑止する形

private bool _exitRequested;

private void exitMenuItem_Click(object? sender, EventArgs e)
{
    // ★明示的な終了要求だけ通す
    _exitRequested = true;
    this.Close();
}

private void MainForm_FormClosing(object? sender, FormClosingEventArgs e)
{
    if (!_exitRequested)
    {
        // ★Close を抑止して常駐へ回す
        e.Cancel = true;
        this.Hide();
        return;
    }

    // ★本当の終了: ここで停止要求を出す
    _initCts?.Cancel();
    StopAllBackgroundSources();
}

7-2. Close 前に Hide を入れる場面

終了時に「後処理でUIがちらつく」「フォーカスが荒れる」などが出る場合、
Hide → 後処理 → Close の順へ寄せると落ち着くことがある(常駐系や入力系と同居する場合に効きやすい)。


8. Dispose は「解除と解放の保険」

Dispose は「解除漏れの最後の砦」。
Closing に主要な停止を置き、Dispose では idempotent(何度呼ばれても同じ結果)を狙う。

8-1. 解除漏れが出やすい対象

  • TimerSystem.Windows.Forms.Timer / System.Threading.Timer
  • NotifyIcon
  • 外部イベント購読(ホットキー、外部通知のコールバック)
  • CancellationTokenSource
  • IDisposable なクライアント(例: 長寿命HttpClientを保持する場合)
private System.Windows.Forms.Timer? _uiTimer;
private NotifyIcon? _tray;

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        // ★解除: 参照が残ると常駐系が生き残る
        if (_uiTimer is not null)
        {
            _uiTimer.Stop();
            _uiTimer.Tick -= UiTimer_Tick;
            _uiTimer.Dispose();
            _uiTimer = null;
        }

        if (_tray is not null)
        {
            _tray.Visible = false;
            _tray.Dispose();
            _tray = null;
        }

        _initCts?.Dispose();
        _initCts = null;

        _lifeCts.Cancel();
        _lifeCts.Dispose();
    }

    base.Dispose(disposing);
}

9. 再表示・外部イベントが同居する場合の分割

タイマー、外部通知、ホットキー、トレイ常駐が同居すると、Form が「責務の集積所」になりやすい。
Start/Stop を持つコンポーネントへ寄せ、Closing で停止要求を一本化するのが安定しやすい。

public sealed class BackgroundSources : IDisposable
{
    private readonly System.Windows.Forms.Timer _timer;

    public BackgroundSources()
    {
        _timer = new System.Windows.Forms.Timer { Interval = 1000 };
        _timer.Tick += (_, __) => OnTick?.Invoke();
    }

    public event Action? OnTick;

    public void Start() => _timer.Start();   // ★開始
    public void Stop()  => _timer.Stop();    // ★停止

    public void Dispose()
    {
        _timer.Stop();
        _timer.Dispose();
    }
}

Form 側は「開始は Shown、停止は Closing、解除は Dispose」へ揃える。


10. レビュー観点

「知っていれば防げる」を「機械的に潰せる」へ落とす。
見つけ方は grep・検索ワード前提で整理する。

10-1. ctor

  • File. / SqlConnection / HttpClient / WebRequest など外部I/Oの開始がない
  • Task.Run で重い処理を走らせていない(成功/失敗/キャンセルの扱いが迷子になりやすい)
  • 設計時に壊れる要因がない(環境変数、レジストリ、ネットワーク依存など)
  • Handle 依存APIが ctor に無い(BeginInvokeCreateGraphics、ホットキー登録など)

10-2. Load

  • .Result / .Wait() / Task.WaitAll が無い
  • Thread.Sleep / DoEvents が無い
  • 初期表示に必要なUI状態だけが置かれている(重い初期化は Shown 側)

10-3. Shown

  • async voidtry/catch があり、ログと縮退がある
  • キャンセル手段がある(CancellationTokenSource
  • 初期化失敗時に UI が壊れない(表示上の落としどころがある)

10-4. Closing

  • 停止要求がある(Cancel/Stop)
  • 常駐抑止(トレイ運用)が明示されている(Close の抑止条件がある)
  • 終了処理が詰まる場合の最短切り分けが残っている(ログ・タイムアウト方針)

10-5. Dispose

  • Timer/event/NotifyIcon の解除がある
  • 解除が idempotent になっている(null 化、二重解除で落ちない)

11. 関連トピック

  • 止血と計測(ログ/タイムアウト/計測の型): E04
  • UIスレッドの構造理解(メッセージループ/帰還先): G11
  • 非同期の実戦整理(待ち方/合成/デッドロック回避): G13
  • 規約化(レビュー観点へ落とす): R06

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

1
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
1
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?