
お世話になっております。
今回はPanelのMouseLeaveイベントに関する質問です。
Panelは.NetFrameworkのものです。
やりたいこと
パネルの中と外にボタンを設置し、button1を押すとパネルが表示され、パネルからマウスが離れるとパネルが消えるようにしたいと思っています。
コードはこんな感じです。
C#
1private void panel1_MouseLeave(object sender, EventArgs e) 2{ 3 if (panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position)) == false) 4 panel1.Visible = false; 5}
問題点
以下の画像のようにマウスカーソルを動かすと、MouseLeaveイベントが発生しないことがあります。
特に、カーソルを高速で動かすと起きやすいです。
これらの問題の対処法を探しております。
以上、よろしくお願いいたします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答4件
0
ベストアンサー
回答依頼があったので回答します。
他の方々が指摘されているように MouseLeave イベントを完全に捕捉することは原理的に不可能になります。これは MouseLeave イベントが Win32 API の TrackMouseEvent によって生成されていることに起因しています。TrackMouseEvent を .NET Framework が呼び出すタイミングは MouseMove イベント発生時になりますが、YAmaGNZ さんが指摘されている通り MouseMove イベントは 1 ピクセル単位に発生するわけではないので、TrackMouseEvent が呼び出されないマウス操作が発生しえます。
この問題を解決する案として一番のおすすめは MouseEnter / MouseLeave イベントとタイマーを組み合わせることです。
C#
1 public partial class Form1 : Form 2 { 3 private bool flag = false; 4 5 public Form1() 6 { 7 InitializeComponent(); 8 } 9 10 private void Form1_Load(object sender, EventArgs e) 11 { 12 panel1.Visible = false; 13 } 14 15 private void button1_Click(object sender, EventArgs e) 16 { 17 panel1.Visible = true; 18 timer1.Start(); 19 } 20 21 private void panel1_MouseEnter(object sender, EventArgs e) 22 { 23 Debug.Print("panel1_MouseEnter"); 24 flag = true; 25 } 26 27 private void panel1_MouseLeave(object sender, EventArgs e) 28 { 29 Debug.Print("panel1_MouseLeave"); 30 if (!panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position))) 31 { 32 flag = false; 33 panel1.Visible = false; 34 timer1.Stop(); 35 } 36 } 37 38 private void timer1_Tick(object sender, EventArgs e) 39 { 40 if (panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position))) 41 { 42 if (!flag) 43 { 44 flag = true; 45 } 46 } 47 else 48 { 49 if (flag) 50 { 51 flag = false; 52 panel1.Visible = false; 53 timer1.Stop(); 54 } 55 } 56 } 57 }
上記のような処理でタイマーをデフォルトの 100ms 間隔で処理しても、パネルが非表示になるタイミングとして体感的には違和感はないと思います。気になるようでしたらタイマー間隔を 50ms 程度に縮めていただいて試していただければよいと思います。それぐらいのタイマー間隔でもパネルの数が 100 枚を超えるような状況でなければ、遅延を感じることはないと思います。(MouseLeaveのイベント内容次第ではありますが・・・)
なお、TrackMouseEvent 自体はかなり優秀な API でフォーム外のマウスの動作を補足することができます。通常の MouseMove イベントでこれと同程度の処理を実施する場合、他の回答者のコメント通りフックを使用したりマウスをキャプチャーすることになります。いずれの処理も実装するのがちょっと難しいので、タイマーで処理するのがベターであると私は考えています。
検証用に書いたコードを Github に上げました。
https://github.com/atata0319/teratail130742
takabosoftさんが指摘されている案2に対応してみました。コーディングのコストはかなり上がった印象になりますね。Panelの派生クラスとして共通の部品として実装されているのであれば、ありかなと思います。線分の交差判定には別のサイトのサンプルを使用しています。
可能な限り Win32 API を直接使用するのを避けるためにキャプチャーやフックに頼らない方法で実装してあります。タイマー処理を削除したかったのですが、前述の制限によりフォーム外に移動した際の処理を記述するのが難しいので諦めました。
C#
1 public partial class Form1 : Form, IMessageFilter 2 { 3 private bool flag = false; 4 private Point previous; 5 6 public Form1() 7 { 8 InitializeComponent(); 9 } 10 11 private void Form1_Load(object sender, EventArgs e) 12 { 13 panel1.Visible = false; 14 } 15 16 private void button1_Click(object sender, EventArgs e) 17 { 18 ShowPanel(); 19 } 20 21 private void panel1_MouseEnter(object sender, EventArgs e) 22 { 23 flag = true; 24 } 25 26 private void panel1_MouseLeave(object sender, EventArgs e) 27 { 28 if (!panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position))) 29 { 30 HidePanel(); 31 } 32 } 33 34 bool IMessageFilter.PreFilterMessage(ref Message m) 35 { 36 const int WM_MOUSEMOVE = 0x0200; 37 const int WM_NCMOUSEMOVE = 0x00A0; 38 if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_NCMOUSEMOVE) 39 { 40 Point current; 41 if (m.Msg == WM_NCMOUSEMOVE) 42 { 43 current = panel1.PointToClient(new Point(m.LParam.ToInt32())); 44 } 45 else 46 { 47 var control = Control.FromHandle(m.HWnd); 48 if (control != null) 49 current = panel1.PointToClient(control.PointToScreen(new Point(m.LParam.ToInt32()))); 50 else 51 current = panel1.PointToClient(Cursor.Position); 52 } 53 //Debug.Print("WM_MOUSEMOVE: {0}", current); 54 if (panel1.ClientRectangle.Contains(current)) 55 { 56 flag = true; 57 } 58 else 59 { 60 if (flag) 61 { 62 HidePanel(); 63 } 64 else 65 { 66 // 直前の座標と現在の座標を結ぶ線分がパネルの矩形と交差する場合、パネルを閉じる。 67 if (IsIntersected(panel1.ClientRectangle, current, previous)) 68 { 69 HidePanel(); 70 } 71 } 72 } 73 previous = current; 74 } 75 return false; 76 } 77 78 private void timer1_Tick(object sender, EventArgs e) 79 { 80 var current = panel1.PointToClient(Cursor.Position); 81 // フォーム内から一気にフォーム外に出ていったカーソルはここで処理する。 82 if (IsIntersected(panel1.ClientRectangle, current, previous)) 83 { 84 HidePanel(); 85 } 86 previous = current; 87 } 88 89 private void ShowPanel() 90 { 91 panel1.Visible = true; 92 timer1.Start(); 93 Application.AddMessageFilter(this); 94 previous = panel1.PointToClient(Cursor.Position); 95 } 96 97 private void HidePanel() 98 { 99 flag = false; 100 timer1.Stop(); 101 Application.RemoveMessageFilter(this); 102 panel1.Visible = false; 103 } 104 105 private static bool IsIntersected(Rectangle rectangle, Point a, Point b) 106 { 107 // 矩形と線分の交差は各辺と交差しているかどうかで判定する。 108 var lefttop = new Point(rectangle.Left, rectangle.Top); // 左上座標 109 var righttop = new Point(rectangle.Right, rectangle.Top); // 右上座標 110 var leftbottom = new Point(rectangle.Left, rectangle.Bottom); // 左下座標 111 var rightbottom = new Point(rectangle.Right, rectangle.Bottom); // 右下座標 112 if (IsIntersected(a, b, lefttop, leftbottom)) // 左辺 113 return true; 114 if (IsIntersected(a, b, righttop, rightbottom)) // 右辺 115 return true; 116 if (IsIntersected(a, b, leftbottom, rightbottom)) // 下辺 117 return true; 118 if (IsIntersected(a, b, lefttop, righttop)) // 上辺 119 return true; 120 return false; 121 } 122 123 private static bool IsIntersected(Point a, Point b, Point c, Point d) 124 { 125 // https://qiita.com/ykob/items/ab7f30c43a0ed52d16f2 126 // 線分(ab)と線分(cd)が交差しているか判定する。 127 var ta = (c.X - d.X) * (a.Y - c.Y) + (c.Y - d.Y) * (c.X - a.X); 128 var tb = (c.X - d.X) * (b.Y - c.Y) + (c.Y - d.Y) * (c.X - b.X); 129 var tc = (a.X - b.X) * (c.Y - a.Y) + (a.Y - b.Y) * (a.X - c.X); 130 var td = (a.X - b.X) * (d.Y - a.Y) + (a.Y - b.Y) * (a.X - d.X); 131 return tc * td < 0 && ta * tb < 0; 132 } 133 }
Github には検証用プロジェクトを追加してあります。
投稿2018/06/12 18:03
編集2018/06/14 15:35総合スコア881
0
MouseMoveイベントでログを出力すれば分かりますが、マウスのイベントは1ピクセル毎に発生するものではありません。
ですので、Panelの左側の外→Panelの右側の外と飛ぶ可能性があります。
その為、現在の実装を残しつつ、takabosoftさんの案2のようにFormのMouseMoveイベントにて前回のマウスカーソル座標から今回のマウスカーソル座標の直線がPanelと交差(Panelの2辺と交差)しているか判定し、交差しているようであれば、Panelを消すという形になるかと思います。
また、FormのMouseMoveイベントですら捉えられない可能性もあります。
この対応も考えるのであれば、グローバルフックにてForm外のマウスイベントを取得し、マウスカーソル移動の直線がPanelと交差しているかチェックする必要があります。
投稿2018/06/12 15:13
総合スコア10544
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。


0
おそらくですが、MouseLeaveイベントはマウスカーソルがPanel内に一度でも入らないと発生しないと思います。
ある時点でマウスカーソルがパネルより左にあり、高速で右側に動かして、次の時点でカーソルがパネルより右の位置に来た場合がそれに該当すると思います。
解決方法としては二通りが考えられます。
●案1
ボタンを押してパネルを表示した時点でマウスカーソルをパネル内に強制的に移動させます。
結果的にMouseLeaveが発生すると思います。
実現方法も簡単ですが、マウスカーソルがワープするので違和感があるかもしれません。
●案2
マウスカーソル位置を(フォームのMouseMoveなどで)監視し、「ある時点とその一つ前のカーソル座標を結ぶ直線」と「パネルの矩形」が重なっているかを判定し、重なっていたらパネルを非表示にさせます。
こちらは実装がやや面倒になります。
重なり判定はおそらくGraphics系のパス関数を使えば実現できます。
ただ、ここまでコストを掛けて実現する必要があるかどうかは一考の余地があります。
投稿2018/06/12 08:24
総合スコア8356
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

退会済みユーザー
2018/06/13 00:43

0
追記
MouseEnterを使ってもマウスポインタの速度によってはイベントが発生しないというご指摘を頂戴しました。
私のほうでもマウスを高速で動かした場合に発生しないことを確認しましたので、本回答は質問者様の要求を満たしません。
こんにちは。
この場合、MouseEnter
イベントを使うほうが適切なようです。
次のサイトに同様の質問があります。ご確認ください。
Stack Overflow MouseLeave not fired C# WinForms
Stack Overflowの回答をもとに、質問者様が求める機能を実装すると以下のようになりました。
csharp
1 public partial class Form1 : Form 2 { 3 private bool _entered; 4 5 public Form1() 6 { 7 InitializeComponent(); 8 panel1.Visible = false; 9 } 10 11 private void panel1_MouseEnter(object sender, EventArgs e) 12 { 13 // panel1の領域にマウスポインタが来たことを記録 14 _entered = true; 15 } 16 17 /// <summary> 18 /// マウスポインタの現在地がpanel1の上にあるかどうか確認する 19 /// </summary> 20 /// <param name="point">マウスポインタのロケーション</param> 21 /// <returns> 22 /// panel1の上にある場合、true, それ以外はfalse. 23 /// </returns> 24 private bool HasMousePointer(Point point) 25 { 26 var p1 = new Point(panel1.Location.X, panel1.Location.Y); 27 var p2 = new Point(p1.X + panel1.Size.Width, p1.Y + panel1.Size.Height); 28 return 29 (point.X >= p1.X && point.X <= p2.X) && 30 (point.Y >= p1.Y && point.Y <= p2.Y); 31 } 32 33 private void Form1_MouseMove(object sender, MouseEventArgs e) 34 { 35 // まだマウスポインタがpanel1に乗ってないので抜けます 36 if (!_entered) return; 37 // マウスポインタの現在値がpanel1のうえかどうか調べる 38 var result = HasMousePointer(e.Location); 39 // まだ乗っているので抜けます 40 if (result) return; 41 42 // マウスポインタがすでにpanel1の上からいなくなっているので 43 // panel1を非表示にし、フラグも戻します。 44 _entered = false; 45 panel1.Visible = false; 46 } 47 48 private void button1_Click(object sender, EventArgs e) 49 { 50 panel1.Visible = true; 51 } 52 }
ご参考になれば幸いです。
投稿2018/06/12 12:03
編集2018/06/12 12:43総合スコア212
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/06/12 12:26

退会済みユーザー
2018/06/13 00:38 編集

あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2018/06/13 00:28
2018/06/13 04:57
2018/06/13 04:59
2018/06/13 17:24
2018/06/14 00:05
退会済みユーザー
2018/06/14 04:37
2018/06/14 15:39