あんみんどうふです。
C#のフォームアプリケーションで描画処理等を非同期ループで回している時にフォームが閉じられると、内容によっては既に閉じられたフォームをいじろうとしてエラーが発生してしまいます。
今回はその対策について備忘録として残します。非同期処理むずいので・・・。
結論
結論から言うと以下の手順で正しく閉じられます。
- フォームを閉じた時、「フォームを閉じたフラグ」をTrueにして一旦閉じる処理をキャンセル
- 「フォームを閉じたフラグ」で非同期ループを抜け、「ループを抜けたフラグ」をTrueにする
- 再度閉じる処理を呼び出し、「ループを抜けたフラグ」をチェックしてTrueならフォームを閉じる(2の時点でTrueになっているので正常に閉じられる)
他にもあるんだろうけどこれが一番単純で簡単だと思います・・・。
実際にC#のコードを用いて解説していきます。
開発環境
- C#.NET
- .NET Core 3.1
- Windowsフォームアプリケーション
- Visual Studio 2019
解説
まず「フォームを閉じたフラグ」と「ループを抜けたフラグ」の2つを変数として用意します。
同期・非同期処理間で共有するのでどちらからもアクセスできる所に作る必要があります。
1 2 3 4 5 | /// <summary>フォームを閉じたフラグ</summary> private bool closeFlag = false; /// <summary>ループを抜けたフラグ</summary> private bool breakFlag = false; |
また、非同期処理からフォームを閉じるにはInvokeを通さないとダメなので、それ専用のdelegateと「フォームを閉じる関数」も用意します。
1 2 3 4 5 6 7 8 9 10 | /// <summary>フォームを閉じる用delegate</summary> private delegate void FormClose(); /// <summary> /// [Invoke用] フォームを閉じる /// </summary> private void InvokeFormClose() { this.Close(); } |
次にフォームを閉じた時のイベント関数(FormClosing)を用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /// <summary> /// 閉じる処理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { // 非同期のループ中にそのまま閉じるとエラーが起きるので // 閉じたフラグ(closeFlag)をtrueにして一旦closeをキャンセルする // ループ停止フラグ(breakFlag)がtrueの時のみcloseさせる closeFlag = true; if (!breakFlag) e.Cancel = true; } |
このFormClosingイベントは2回発火されます。
初回はループを止める為に発火し、非同期処理側でループが止まったことを確認したら再度発火します。この”初回“とは、閉じるボタンがクリックされた時やAlt+F4された時を指します。2回目は後述の非同期処理にて。
ループ停止フラグは非同期処理側でループが止まるとTrueになります。
ループが止まっていない間に閉じられるであろう初回のみ、e.Cancel = trueで閉じる処理をキャンセルしています。
最後に非同期処理です。ループ内では何かしら処理が行われていると思っておいてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private Task Run() { return Task.Run(() => { while (!closeFlag) { // 何かの処理 } breakFlag = true; // ループを抜けたら正常に閉じる Invoke(new FormClose(InvokeFormClose)); }); } |
フォーム閉じたフラグ(closeFlag)がFalseの間、つまりフォームが閉じられるまでの間ループし続けます。
先程のFormClosingイベントが発火されてcloseFlagがTrueになることでループから抜け出します。
ループを抜けたらループ抜けたフラグ(breakFlag)をTrueにし、閉じる関数を実行することで再度フォームを閉じます。これにより2回目のFormClosingイベントが発火されます。
2回目の発火時には既にbreakFlagがTrueなので、閉じる処理がキャンセルされることなくフォームが閉じられます。ループからも抜け出しているので例外も発生せず正常に終了できます。
ちなみに非同期処理内はメインフォームとは別物扱いなので直接フォームやコントロールをいじることはできません。だからInvokeを通す必要があるんですね~。
とりあえずはこんな感じで実装できると思います。以上。