今回私がハマったのは、C#のWinFormsでuseWaitCursorを使ったあとに元のカーソルに戻らない!というバグ。
たぶんこれ、WinFormsのバグですよ。
っていうのもね。下記のようなコードを書いたわけです。
try{
this.UseWaitCursor = true;
// 時間のかかるイベント。非同期実装
MessageBox.Show("処理が終わりました。")
}catch(Exception exception){
MessageBox.Show("エラーが発生しました。")
}finally{
this.UseWaitCursor = false;
}
ありがちなコードですね。
処理に時間がかかるから、非同期実装して、その前後にUseWaitCursorを置く。
めちゃくちゃシンプルです。
私の場合、try catch finallyを仕掛けて途中でエラーが出ても戻るようにしています。
なのに!!
なのに!!!
数回に1回待機中からもとに戻らんことがある!!!
っということで、UseWaitCursorから戻らないときに内部的にどのようなステータスを持っているのか見てみました。
Cursorに関係するプロパティ
💡 UseWaitCursor が戻らないバグでハマったら別記事 UseWaitCursor が戻らない解決法 で詳しくまとめてます。
まずはCursorに関するプロパティはどこにあるのか?ということですが、下記の通りです。
- Control.Cursor
- Control.UseWaitCursor
- Application.UseWaitCursor
- Cursor.Current
続いて、この事象が発生した時のすべてのプロパティを見ていきます。
MessageBox.Show(Cursor.Current.ToString());
適当なボタンに上記のプログラムを仕込んで、事象が発生したらボタンを押して、中身を見るようにします。
- Cursor.CurrentはDefaultになってる。

続いてApplication.UseWaitCursorを見てみる。
実行前はこんな感じ。

事象が発生している状態はこんな感じ。

続いて各コントロールのUseWaitCursorのプロパティを見てみる。
MessageBox.Show(GetWaitCursorStatus(this));
これを仕込んで、GetWaitCursorStatusは下記の関数を作った。
private string GetWaitCursorStatus(Control control)
{
var str = $@"{control.Name} : {control.UseWaitCursor.ToString()}";
foreach (Control c in control.Controls)
{
str += Environment.NewLine + GetWaitCursorStatus(c);
}
return str;
}</code></pre></div>
コントロールの中にあるコントロールを再帰的に取得して、中身を見てくだけの関数。
事象が発生する前はこんな感じ。

全部Falseになってる。
事象が発生した時のプロパティはこんな感じ。

うん。
変わりないね。
っで、最後のControl.Cursorを見てみる。
private string GetWaitCursorStatus(Control control)
{
var str = $@"{control.Name} : {control.Cursor.ToString()}";
foreach (Control c in control.Controls)
{
str += Environment.NewLine + GetWaitCursorStatus(c);
}
return str;
}</code></pre></div>
さっきのコードをちょっと修正して実行。

まあ普通って感じですね。
事象発生してるときはこんな感じ。

はい見つけました。
Control.CursorがWaitCursorになっててもとに戻ってない!!!!
ということでバグが発生しているコードに、下記を追加してFix!!
dgv_MainGrid.Cursor = Cursor.Current;
try{
this.UseWaitCursor = true;
// 時間のかかるイベント。非同期実装
MessageBox.Show("処理が終わりました。")
}catch(Exception exception){
MessageBox.Show("エラーが発生しました。")
}finally{
this.UseWaitCursor = false;
dgv_MainGrid.Cursor = Cursor.Current;
}
💡 補足: 業務系の現場でよくハマるパターン
俺もこの WaitCursor、 業務でハマってきたところを3つ並べておきます。
① 例外発生時にカーソルが戻らない
処理開始時に Cursor.Current = Cursors.WaitCursor セットしたが、 try-catch で例外発生 → finally で戻す処理を入れ忘れ → 永久に砂時計のまま。 UseWaitCursor 戻らない問題 で詳細解説。
② Form.UseWaitCursor は Form 全体だけ
this.UseWaitCursor = true は Form 上の全コントロールに適用。 個別コントロール (DataGridView の特定セル等) には別途 Cursor プロパティを設定。 混同して半日溶かす。
③ 非同期処理中にカーソル戻し忘れ
Task.Run で重い処理 → UI スレッドではないので Cursor は直接戻せない。 Invoke 経由で UI スレッドに戻すか、 async/await + IProgress<T> でやる。
❓ よくある質問
Q1. UseWaitCursor と Cursor.Current の違いは?
A. UseWaitCursor は Form/Control プロパティで設定後継続。 Cursor.Current は1ショット (次の WindowsMessage で消える)。 業務系では UseWaitCursor 推奨 (戻し忘れ防止)。
Q2. using で自動戻しはできる?
A. はい。 using (new WaitCursor(this)) 的なヘルパークラスを作ると自動 Dispose で戻る。 try-finally が冗長になる時に便利。 業務系チームではユーティリティ化して使い回す。
Q3. SplashScreen 表示中のカーソル制御は?
A. SplashScreen 表示後の本 Form 起動時に WaitCursor を出すなら、 SplashScreen 側で完了通知 → 本 Form 側で UseWaitCursor=true → 初期化終了で false。
Q4. モーダルダイアログ表示時のカーソルは?
A. ShowDialog() は同期処理なので呼び出し前後で UseWaitCursor 切替するパターン。 詳細は ShowDialog vs Show 参照。
Q5. async/await でのベストプラクティスは?
A. this.UseWaitCursor = true; try { await DoWork(); } finally { this.UseWaitCursor = false; } パターン。 UI スレッド維持のまま戻せる。 WinForms 非同期パターン でも触れてます。
📚 関連記事
- UseWaitCursor が戻らないバグの解決法 — このハマり方の詳細 fix 集
- WinForms 非同期 3パターン — 非同期中のUI制御
- Form と Form の間で値共有 — Form 間のカーソル状態
- Form の中に Form を表示 — 親子 Form の cursor
- DataGridView に DataTable 反映 — DataGridView 上のカーソル






コメント