C# Windowsフォームでフォーム間の変数がリセットされる原因と対策|静的フィールド・コンストラクタ・プロパティ・イベントで値を正しく受け渡す

「ボタンを押して新しいフォームを開いたら、前のフォームで設定した値がなぜか 0 に戻っている」――Windowsフォーム(WinForms)では非常に起きやすい落とし穴です。本記事では、原因の正体(“別インスタンス”問題)を図解的に理解し、静的フィールド・コンストラクタ引数・プロパティ注入・イベント/デリゲート・共有サービス(DI)など、規模や保守性に応じて選べる実装まで、実務で迷わない判断軸とコード例を網羅します。

目次

状況の整理:なぜ variableA が 0 に戻るのか

まずは典型的な再現例です。First_Form でフィールドを 1 または 2 に更新し、New_Form を開きます。

// First_Form
public partial class First_Form : Form
{
    public ushort variableA = 0;


private void button1_Click(object sender, EventArgs e)
{
    variableA = 1;
    new New_Form().Show();
}

private void button2_Click(object sender, EventArgs e)
{
    variableA = 2;
    new New_Form().Show();
}


}

// New_Form
public partial class New_Form : Form
{
private void New_Form_Load(object sender, EventArgs e)
{
First_Form myForm = new First_Form();
if (myForm.variableA == 1) { /* … */ } // ここが常に false
}
} 

問題の本質はただ一つ。new First_Form() で完全に新しいインスタンスを生成しているため、variableA は再び 0 に初期化されます。つまり「値が届かない」のではなく、「別のオブジェクトを見ている」だけです。

フォームのインスタンスと状態:ほんの少しだけ内部を理解する

WinForms のフォームは通常「インスタンス(= オブジェクト)ごとに状態を持つ」設計です。フィールドやプロパティはインスタンス単位で保持され、新たに new すればデフォルト値に戻ります。下図のイメージで把握しましょう。

BeforeFirst_Form #A(画面に表示中) … variableA = 1

AfterNew_Form 内で new First_Form()First_Form #B を作成 … variableA = 0

あなたが見たかったのは #A ですが、コードは #B を参照しています。ここを正せば一気に解決に近づきます。

解決アプローチ早見表(用途と規模で選ぶ)

方法概要向く規模メリットデメリット/注意点
1. 静的フィールドpublic static ushort VariableA を使い First_Form.VariableA と参照小規模試作・ツール実装が最小。既存コードを大きく崩さないアプリ全体で共有され副作用が増える。テスト困難
2. コンストラクタ引数New_Form(ushort current) で値を受け取る中規模以上必要な値だけ渡せる。依存が明確生成箇所ごとに引数を渡し忘れない設計が必要
3. プロパティ注入new New_Form { InitialValue = variableA }中規模生成と設定のタイミングを柔軟に制御セット漏れのリスク。初期化順序に注意
4. イベント/デリゲートボタンクリック時に値をイベント引数で通知/コールバックを渡す中規模~分離重視疎結合でテストしやすいイベント購読の解除忘れ・設計がやや複雑
5. 共有サービス(DI)AppState のような状態オブジェクトを注入して共有中~大規模、長期保守構造化された状態管理。拡張・テスト容易導入コスト。DI の理解が必要
6. MVVM/VM共有INotifyPropertyChanged な ViewModel を複数フォームで共有大規模・複雑UIUI とロジック分離。双方向データバインドが容易学習・実装コスト。WinFormsでのバインド知識が必要

まずは誤りの最小再現と是正

誤りパターン(新しいインスタンスを見てしまう)

// NG: New_Form で新しい First_Form を生成してしまう
private void New_Form_Load(object sender, EventArgs e)
{
    First_Form myForm = new First_Form(); // <-- これが原因
    if (myForm.variableA == 1) { /* … */ }
}

最短で直す:静的フィールド(方法1)

// First_Form
public partial class First_Form : Form
{
    public static ushort VariableA = 0;


private void button1_Click(object sender, EventArgs e)
{
    VariableA = 1;
    new New_Form().Show();
}

private void button2_Click(object sender, EventArgs e)
{
    VariableA = 2;
    new New_Form().Show();
}


}

// New_Form
public partial class New_Form : Form
{
private void New_Form_Load(object sender, EventArgs e)
{
if (First_Form.VariableA == 1)
{
// 何か処理
}
}
} 

この方法は「今すぐ動かす」には有効ですが、アプリ全体に広がるグローバル状態になるため拡張・テストの難易度が上がります。中長期の保守を想定するなら次以降の方法がおすすめです。

堅実に直す:値を明示的に渡す

方法2:コンストラクタ引数で渡す(推奨)

// First_Form
public partial class First_Form : Form
{
    public ushort variableA = 0;


private void button1_Click(object sender, EventArgs e)
{
    variableA = 1;
    new New_Form(variableA).Show();
}

private void button2_Click(object sender, EventArgs e)
{
    variableA = 2;
    new New_Form(variableA).Show();
}


}

// New_Form
public partial class New_Form : Form
{
private readonly ushort _initialValue;


public New_Form(ushort initialValue)
{
    InitializeComponent();
    _initialValue = initialValue;
}

private void New_Form_Load(object sender, EventArgs e)
{
    if (_initialValue == 1)
    {
        // 何か処理
    }
}


} 

コンストラクタで依存する値が必須であることを表現でき、「渡し忘れ」をコンパイル時に防げます。最も安全な基本形です。

方法3:プロパティ注入

// New_Form
public partial class New_Form : Form
{
    public ushort InitialValue { get; set; } = 0;


private void New_Form_Load(object sender, EventArgs e)
{
    if (InitialValue == 1)
    {
        // 何か処理
    }
}


}

// First_Form
private void button1_Click(object sender, EventArgs e)
{
variableA = 1;
var f = new New_Form { InitialValue = variableA };
f.Show();
} 

生成後に設定したいケース(フォームのサイズや位置設定と同時に値を渡したい等)で有効。ただし設定忘れのリスクがあるため、初期化メソッドを明示して呼ぶパターン(InitializeBy(value))にするのも一手です。

方法4:イベント/デリゲート(疎結合設計)

呼び出し側が値を持ち、開く先のフォームは「値の供給元」を意識しない設計です。Func<ushort> のような「値の取得方法」を渡すと、テストもしやすくなります。

// New_Form: 値を提供するコールバックを受け取る
public partial class New_Form : Form
{
    private readonly Func<ushort> _valueProvider;


public New_Form(Func<ushort> valueProvider)
{
    InitializeComponent();
    _valueProvider = valueProvider;
}

private void New_Form_Load(object sender, EventArgs e)
{
    var v = _valueProvider();
    if (v == 1) { /* … */ }
}


}

// First_Form
private void button1_Click(object sender, EventArgs e)
{
variableA = 1;
var f = new New_Form(() => variableA);
f.Show();
} 

フォーム間の依存が薄まり、ユニットテストで New_Form に任意の値を供給できます。

よりスケーラブルに:共有サービス(DI)で状態を一元管理

画面数が増えると「どの値をどこから渡すか」が複雑になります。アプリ内で共有する状態クラス(AppState)を DI(依存性注入)で配布すると、見通しが劇的に良くなります。

状態クラスと列挙型

public enum OperationMode : ushort
{
    None = 0,
    ModeA = 1,
    ModeB = 2
}

public class AppState : INotifyPropertyChanged
{
private OperationMode _mode;
public OperationMode Mode
{
get => _mode;
set
{
if (_mode == value) return;
_mode = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Mode)));
}
}


public event PropertyChangedEventHandler? PropertyChanged;


} 

.NET 6 以降の最小 DI セットアップ例

// Program.cs
using Microsoft.Extensions.DependencyInjection;

internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();


    var services = new ServiceCollection()
        .AddSingleton<AppState>()
        .AddTransient<First_Form>()
        .AddTransient<New_Form>();

    using var provider = services.BuildServiceProvider();
    Application.Run(provider.GetRequiredService<First_Form>());
}


} 

フォーム側の受け取り

// First_Form
public partial class First_Form : Form
{
    private readonly AppState _state;
    private readonly Func<New_Form> _newFormFactory;


public First_Form(AppState state, Func<New_Form> newFormFactory)
{
    InitializeComponent();
    _state = state;
    _newFormFactory = newFormFactory;
}

private void button1_Click(object sender, EventArgs e)
{
    _state.Mode = OperationMode.ModeA;
    _newFormFactory().Show();
}

private void button2_Click(object sender, EventArgs e)
{
    _state.Mode = OperationMode.ModeB;
    _newFormFactory().Show();
}


}

// New_Form
public partial class New_Form : Form
{
private readonly AppState _state;


public New_Form(AppState state)
{
    InitializeComponent();
    _state = state;
    // 例:バインドして UI に自動反映
    this.labelMode.DataBindings.Add(
        "Text", _state, nameof(AppState.Mode));
}

private void New_Form_Load(object sender, EventArgs e)
{
    if (_state.Mode == OperationMode.ModeA)
    {
        // 何か処理
    }
}


} 

DI を使うと「新しいフォームを開いても、アプリ全体で共有する AppState は同一インスタンス」のまま保たれます。グローバル静的より安全で、テストもしやすいのが利点です。

マジックナンバーをやめる:ushort から enum

12 は意味が伝わらず、定義漏れも検出しづらい値です。列挙型でドメインを明示しましょう。

public enum OperationMode : ushort { None = 0, ModeA = 1, ModeB = 2 }

// 使用例
if (_initialValue == (ushort)OperationMode.ModeA) { /* … */ }
// または、構造ごと enum に切替えてコンストラクタ引数も enum にする
public New_Form(OperationMode mode) { /* … */ } 

追加パターン:状況に応じて使えるテクニック集

Owner/ShowDialog パターン

ShowDialog() を用いると、モーダルダイアログの戻りと同時にプロパティで値を受け渡しできます。

// First_Form - 値を渡して結果を受け取る
using (var dlg = new New_Form { InitialValue = variableA })
{
    if (dlg.ShowDialog(this) == DialogResult.OK)
    {
        // dlg.Result などで値を回収
    }
}

Application.OpenForms を使う(要注意)

既存のフォームインスタンスを列挙して見つける方法もありますが、フォーム名やライフサイクルに強く依存するため脆い設計になりがちです。推奨はしませんが、既存資産の暫定対応としての例を示します。

var first = Application.OpenForms.OfType&lt;First_Form&gt;().FirstOrDefault();
if (first != null &amp;&amp; first.variableA == 1) { /* … */ }

Tag プロパティの活用(軽量)

Control.Tag は「ちょっとした付帯情報」を運ぶのに便利ですが、型安全性が低く、意味の見えにくい実装になりがちです。短命の値や試作に限定して使いましょう。

避けたいアンチパターンと落とし穴

  • 静的の乱用:手早い反面、並行実行やテストで想定外の副作用を生みます。最終手段として最小限に。
  • イベントの解除忘れ:購読元が長生きだと メモリリーク の原因に。FormClosed 等で -=> 解除や WeakEvent パターンを検討。
  • 別スレッドから UI を触る:非同期処理後にフォームを更新する際は Invoke/BeginInvoke を必ず使用。
  • 初期化順序の問題:プロパティ注入時に Load より前に設定される保証を設計で担保する(初期化メソッドを明示する等)。

テスト容易性と可読性を高める設計チェックリスト

観点確認ポイントOK の目安
依存の明確化フォームが必要とする値はコンストラクタ or 明示的メソッドで受け取る引数無しで生成できないなら「必須依存」
型安全性モード等は enum 化し、ushort を生値で使わないマジックナンバーの排除
共有状態静的ではなくサービス(DI)で注入・共有AppState を 1 箇所で管理
ライフサイクルイベント購読の解除・フォーム破棄の明示FormClosed でクリーンアップ
将来の拡張画面追加時に「値の受け渡し経路」が増えても影響最小コンストラクタ or DI で自然に拡張

実務に効くサンプル:最小限のリファクタで堅実に

既存コードへの影響を抑えつつ堅牢にする「コンストラクタ引数 + enum」版を提示します。

public enum OperationMode : ushort { None = 0, ModeA = 1, ModeB = 2 }

// First_Form
public partial class First_Form : Form
{
private OperationMode _mode = OperationMode.None;


private void button1_Click(object sender, EventArgs e)
{
    _mode = OperationMode.ModeA;
    new New_Form(_mode).Show();
}

private void button2_Click(object sender, EventArgs e)
{
    _mode = OperationMode.ModeB;
    new New_Form(_mode).Show();
}


}

// New_Form
public partial class New_Form : Form
{
private readonly OperationMode _mode;


public New_Form(OperationMode mode)
{
    InitializeComponent();
    _mode = mode;
}

private void New_Form_Load(object sender, EventArgs e)
{
    switch (_mode)
    {
        case OperationMode.ModeA:
            // A の処理
            break;
        case OperationMode.ModeB:
            // B の処理
            break;
    }
}


} 

MVVM 風にまとめる(WinForms でもできる)

WinForms でも INotifyPropertyChanged を使った ViewModel 共有は有効です。フォームが複数あっても、同一の ViewModel をデータバインドすれば表示や挙動を同期できます。

public class MainViewModel : INotifyPropertyChanged
{
    private OperationMode _mode;
    public OperationMode Mode
    {
        get => _mode;
        set
        {
            if (_mode == value) return;
            _mode = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Mode)));
        }
    }
    public event PropertyChangedEventHandler? PropertyChanged;
}

// 共有 ViewModel を注入して使う
public partial class First_Form : Form
{
private readonly MainViewModel _vm;
private readonly Func _newFormFactory;


public First_Form(MainViewModel vm, Func<New_Form> newFormFactory)
{
    InitializeComponent();
    _vm = vm;
    _newFormFactory = newFormFactory;
}

private void button1_Click(object sender, EventArgs e)
{
    _vm.Mode = OperationMode.ModeA;
    _newFormFactory().Show();
}


} 

この方式なら New_Form 側は _vm.Mode にバインドするだけで状態を共有できます。UI と状態を分離できるため、ユニットテストは ViewModel に集中させられます。

よくある質問(FAQ)

Q. 静的フィールドで十分では?
小規模ツールでは有効です。ただし「一度動いた」あとに仕様が増えた瞬間、静的の副作用や初期化順序の問題でデバッグが難しくなります。長く使う可能性があるなら DI かコンストラクタ注入を検討しましょう。

Q. 値を返す方向(New → First)はどうする?
ShowDialog() と戻り値/公開プロパティで回収する、イベントで通知する、共有 ViewModel に書き戻す等が実務的です。

Q. 複数フォームが同時に開く場合の整合性は?
共有サービス(AppState/ViewModel)を単一インスタンスで持ち、変更通知(PropertyChanged)で UI を同期させると保守性が高まります。

まとめ:キーは「同じインスタンス」を参照すること

  • 値が失われるのは 新しいインスタンスを生成しているから。意図したのは「既存インスタンスの状態」。
  • 最短は静的フィールド。ただし副作用が大きいため、基本はコンストラクタ引数プロパティ注入で値を明示的に受け渡す。
  • 画面が増える・長期運用では共有サービス(DI)ViewModel 共有で状態を一元管理。
  • enum 化で可読性と安全性を高め、イベント解除・スレッド境界などの落とし穴を避ける。

「変数が 0 に戻る」を卒業する最短のコツは、“同じものを見続ける”設計に切り替えること。インスタンスの境界を意識した受け渡しに変えるだけで、WinForms の信頼性は大きく向上します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次