【C#】Formの中にFormを表示して良い感じに切り替える

今回めちゃくちゃニッチな技術の紹介をしたいと思います。

その名は!

Formの中にFormを表示する!という技術!

これ使いどころどこなんや??ってレベルで、ニッチなのですが、この技術が平気で使えると1画面の中でいろいろな画面を表示できるので結構おすすめです!

それでは早速!

MainForm に Form 埋め込み3パターン + 切替コード
目次

Formを4つ作成

💡 そもそも別フォームに値を渡す手前のところから固めたいなら FormとFormの間で値の共有・受け渡しをする! の方を先に見ると整理が早いです。値の持ち方の設計はそこで決まります。

MainFormはForm切り替え用のButtonを配置

まずはこんな感じでPanelを適当に二つ配置

Dockを良い感じにいじったら上記のように整った形になる

Dockはプロパティにある。

でForm切り替え用のボタンを配置していく

これもまずは適当に配置して

今回はDockを全てTopにすると上記のように整った

高さを良い感じに合わせて

スタイルを良い感じに整えたらとりあえずMainFormは終わり

MainFormに入れるFormは設定を気を付けて!

ここからはMainFormの中に入れるFormを追加していくが、注意が必要。

とりあえずFormを追加すると下記のような感じになる。

でもこのままだとFormの中に✖ボタンを持ったFormが表示され不格好になるので、プロパティを一つ変える

SubFormはヘッダーと✖ボタンを消す!

FormBorderStyleをNoneにしただけで下記のようになる。

これならいい感じに表示できそう!

こんな感じでサイズが変わってもど真ん中に配置される世にしておく

サイズが変わった時でも配置を良い感じにしたいときはAnchorプロパティを触っておく

これはTopとすると上からの配置は固定、Bottomとすると下からの配置は固定といった感じになる。

その際に注意が必要なのが、下記のようにガイドに沿ってちゃんとコントロールを置いておくこと!

ちゃんと上みたいな感じでガイドが出るのでそれに合わせること!

こんな感じでほかのフォームも作る。

ここまでくれば下準備は完了!

MainFormロード時に下記コードを実行するようにする。

でMainFormのロード時に下記のようなメソッドを実行するようにしておく

namespace FormInForm
{
    public partial class MainForm : Form
    {
        private Form form1;
        private Form form2;
        private Form form3;
    public MainForm()
    {
        InitializeComponent();
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
        form1 = new Form1();
        form1.TopLevel = false;
        form1.Dock = DockStyle.Fill;
        panel2.Controls.Add(form1);

        form2 = new Form2();
        form2.TopLevel = false;
        form2.Dock = DockStyle.Fill;
        panel2.Controls.Add(form2);

        form3 = new Form3();
        form3.TopLevel = false;
        form3.Dock = DockStyle.Fill;
        panel2.Controls.Add(form3);

        form1.Show();
    }
}

}

するとこんな感じでMainFormの中にForm1が表示された状態で立ち上がった

サイズが変わってもこの通り!

ちゃんと真ん中にLabelがよって良い感じ!

ボタンにメソッドを追加していく

        private void button1_Click(object sender, EventArgs e)
        {
            HideAllForm();
            form1.Show();
        }
    private void button2_Click(object sender, EventArgs e)
    {
        HideAllForm();
        form2.Show();
    }

    private void button3_Click(object sender, EventArgs e)
    {
        HideAllForm();
        form3.Show();
    }

    private void HideAllForm()
    {
        form1.Hide();
        form2.Hide();
        form3.Hide();
    }</code></pre></div>

ボタンを押すとこんな感じ!

Form1ボタンを押した

Form2ボタンを押した

Form3ボタンを押した

アクティブなFormのButtonの色を変える

このままだとどのFormがアクティブなのかいまいちわかりずらい。

っていうのも今回はForm1とかForm2といったLabelを貼っているが、実際はただコントロールが配置されているだけだからである。

なので、今アクティブになっているFormのButtonの色を変えることで、わかりやすくしたいと思う。

    public partial class MainForm : Form
    {
        private Form form1;
        private Form form2;
        private Form form3;
    private Color _activeColor = Color.Aquamarine;
    private Color _defaultColor = Color.CadetBlue;

こんな感じでアクティブなボタンの色を設定して

ロード時に表示しているFormのボタンの色が変わるようにしておく

        private void MainForm_Load(object sender, EventArgs e)
        {
            form1 = new Form1();
            form1.TopLevel = false;
            form1.Dock = DockStyle.Fill;
            panel2.Controls.Add(form1);
        form2 = new Form2();
        form2.TopLevel = false;
        form2.Dock = DockStyle.Fill;
        panel2.Controls.Add(form2);

        form3 = new Form3();
        form3.TopLevel = false;
        form3.Dock = DockStyle.Fill;
        panel2.Controls.Add(form3);

        button1.BackColor = _activeColor;
        form1.Show();
    }</code></pre></div>

で、各ボタンを押したときに下記のメソッドを追加

        private void button1_Click(object sender, EventArgs e)
        {
            form2.Hide();
            form3.Hide();
        form1.Show();

        button2.BackColor = _defaultColor;
        button3.BackColor = _defaultColor;

        button1.BackColor = _activeColor;
    }

    private void button2_Click(object sender, EventArgs e)
    {
        form1.Hide();
        form3.Hide();

        form2.Show();

        button1.BackColor = _defaultColor;
        button3.BackColor = _defaultColor;

        button2.BackColor = _activeColor;
    }

    private void button3_Click(object sender, EventArgs e)
    {
        form1.Hide();
        form2.Hide();

        form3.Show();

        button1.BackColor = _defaultColor;
        button2.BackColor = _defaultColor;

        button3.BackColor = _activeColor;
    }</code></pre></div>

こうするといい感じに色が変わった!

リアファクタリング

ただこれだと、SubFormが増えた時にめっちゃ面倒なコード。

というのも

        private void button2_Click(object sender, EventArgs e)
        {
            form1.Hide();
            form3.Hide();
        form2.Show();

        button1.BackColor = _defaultColor;
        button3.BackColor = _defaultColor;

        button2.BackColor = _activeColor;
    }</code></pre></div>

各ボタンで上記の実装をしているので、Formが増えるたびに追加したりして対応しないといけない。

これだと面倒すぎるので、リファクタリングしていく。

まずはこんな感じでFormとボタンのリストを作っていく

    public partial class MainForm : Form
    {
        private Form form1;
        private Form form2;
        private Form form3;
    private IEnumerable&lt;Form&gt; _subForms;
    private IEnumerable&lt;Button&gt; _formButtons;

ロードイベントでnewする

        private void MainForm_Load(object sender, EventArgs e)
        {
            form1 = new Form1();
            form1.TopLevel = false;
            form1.Dock = DockStyle.Fill;
            panel2.Controls.Add(form1);
        form2 = new Form2();
        form2.TopLevel = false;
        form2.Dock = DockStyle.Fill;
        panel2.Controls.Add(form2);

        form3 = new Form3();
        form3.TopLevel = false;
        form3.Dock = DockStyle.Fill;
        panel2.Controls.Add(form3);

        // ここでフィールドのリストを作る
        _subForms = new List&lt;Form&gt;()
        {
            form1,form2,form3
        };

        // ここでフィールドのリストを作る
        _formButtons = new List&lt;Button&gt;()
        {
            button1,button2,button3
        };

        button1.BackColor = _activeColor;
        form1.Show();
    }</code></pre></div>

で下記のメソッドを作る

        private void ShowForm(Form form)
        {
            // subFormの配列をぶん回して引数のFormと一致してたら表示して、
            // 一致してなかったら隠す
            foreach (var f in _subForms)
            {
                if (f == form)
                {
                    f.Show();
                }
                else
                {
                    f.Hide();
                }
            }
        }
    private void ActivateBtn(Button btn)
    {
        // form表示するボタンのリストをぶん回して、
        // 引数のボタンを一致してたらactiveColorにして、
        // 一致してなかったらdefaultColorにする
        foreach (var b in _formButtons)
        {
            b.BackColor = b == btn ? _activeColor : _defaultColor;
        }
    }</code></pre></div>

このコードがあればボタンを押したときのメソッドがめちゃくちゃ短くなる。

        private void button1_Click(object sender, EventArgs e)
        {
            ShowForm(form1);
            ActivateBtn((Button)sender);
        }
    private void button2_Click(object sender, EventArgs e)
    {
        ShowForm(form2);
        ActivateBtn((Button)sender);
    }

    private void button3_Click(object sender, EventArgs e)
    {
        ShowForm(form3);
        ActivateBtn((Button)sender);
    }</code></pre></div>

これで動きは一緒だったらいい感じ!

良い感じ!!

今回のコードはGithubに上げています!

今回のサンプルコード「FormInForm」というタイトルで下記Githubに上げています!

GitHub
GitHub - HayashiyamaHiroshi/blog-source-demo Contribute to HayashiyamaHiroshi/blog-source-demo development by creating an account on GitHub.

是非ローカルで触ってみてください!

💡 補足: 業務系の現場でよくハマるパターン

Form の中に Form を埋め込む MDI 的なパターン、業務画面でメニュー切り替えしたい時の定番なんですが、現場で踏みがちな罠が3つあるんですよ。

① 親 Form のリサイズで子 Form の位置がずれる

親 Form のサイズを変更したとき、Panel に埋め込んでる子 Form が想定外の位置に飛んでいくケース。原因は子 Form の Dock プロパティと Anchor プロパティの組み合わせ。 Dock = Fill + Panel 側で Dock = Fill にしておくと、親リサイズに追従します。最初の設定で1回詰まったら、 Formから別フォームを表示しメインフォームを切り替える のレイアウト設計を見直すと早いです。

② MdiParent と TopLevel の組み合わせを間違えると例外

子 Form を埋め込む時は childForm.TopLevel = false を呼ばないと System.ArgumentException が出ます。MdiContainer モードと Panel 埋め込みモードで設定値が違うので、書き始めの時点で迷いがち。 Form.ShowDialog と Form.Show の違いと使い分け で表示モードの違いを整理してます。

③ Form_Load タイミングのズレで初期化エラー

子 Form の Form_Load 内で親側のコントロールを参照すると、まだ Parent が null のことがある。回避は this.HandleCreated イベントで初期化する形。地味な罠ですが、現場で半日溶かしました。

❓ よくある質問

Q1. MDI ではなく Panel に Form を埋め込みたい

A. Panel 埋め込みの方が業務画面では多いです。 childForm.TopLevel = false; panel1.Controls.Add(childForm); childForm.Show(); の3行で動きます。子 Form のサイズは Dock = Fill で Panel フィットさせるのが定石。値受け渡しは Form と Form の間で値の共有・受け渡し と同じパターンが使えます。

Q2. 子 Form からメイン側のメソッドを呼ぶには?

A. 子 Form のコンストラクタで親 Form の参照を受け取って、フィールドに保持しておくパターンが一番素直です。デリゲートやイベント経由でも実現できますが、ファイル間結合度が増えるので業務画面では参照渡しが多い。 コールバックとデリゲートの違い で通知系の設計はまとめてます。

Q3. 子 Form を切り替えた時に前の状態を保持したい

A. 切り替えるたびに Dispose する設計だと値が消えます。回避は「子 Form を最初に全部 new して Panel に Add しておき、Visible で切り替える」型。ただしメモリ消費は増えるので、頻繁に切り替えない画面は都度 new でも問題ないです。

Q4. 共通ヘッダー・フッターを持たせたい

A. 親 Form 側にヘッダー Panel とフッター Panel を Dock=Top/Bottom で配置し、中央 Panel に子 Form を埋め込む形にすると共通化できます。子 Form は中央 Panel のサイズに合わせて自動リサイズされる。業務画面でメニューバーを共通化するときの定石。

Q5. 子 Form 同士で値を受け渡すには?

A. 親 Form を仲介させるのが一番安全です。子 Form A → 親 Form → 子 Form B、というルートで持ち回る。「親が共通の状態を持つ」設計にすると保守しやすい。 DataAdapter で Fill と Update でやってる「DataTable を親が保持」のパターンと同じ思想です。

📚 関連記事

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次