質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

Q&A

解決済

4回答

4663閲覧

MouseLeaveのイベントを確実に取得したい

退会済みユーザー

退会済みユーザー

総合スコア0

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

0グッド

0クリップ

投稿2018/06/12 04:57

お世話になっております。
今回は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ページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答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
atata0319

総合スコア881

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/06/13 00:28

タイマー処理のおかげで違和感なくパネルを消すことができました。 ありがとうございます。助かりました。
takabosoft

2018/06/13 04:57

検証用のコードでもマウス速度によってはパネルが消えませんが、良いのでしょうか?(^_^;) パネルの幅を狭めれば再現しやすいです。
takabosoft

2018/06/13 04:59

パネルの中のコントロールにマウスカーソルが載った場合にMouseLeaveが発生しない点についてはTimer案では改善しているのですね。
atata0319

2018/06/13 17:24

指摘の問題に対処してみました。コーディングコストがかなり上がった印象ですので、個別のフォームに実装していくのはちょっと避けたい感じになりますね。
takabosoft

2018/06/14 00:05

ありがとうございます。そうですね、コストは高めですね・・・。timerでleave判定する程度に留めておくことを私もおすすめします。
退会済みユーザー

退会済みユーザー

2018/06/14 04:37

ボタン付近とパネルに判定を置いて、判定外でタイマーで設定した時間が過ぎるとパネルを消すようにしました。 常にタイマーを動かしていますが、重さ的には問題ない感じでした。
atata0319

2018/06/14 15:39

タイマー処理も判定だけであれば100程度のコントロール数でも平気です。100コントロールの Visible プロパティを触るとかなり重くなりますが、おそらくそのような状況ではないので大丈夫だと思います。
guest

0

MouseMoveイベントでログを出力すれば分かりますが、マウスのイベントは1ピクセル毎に発生するものではありません。
ですので、Panelの左側の外→Panelの右側の外と飛ぶ可能性があります。

その為、現在の実装を残しつつ、takabosoftさんの案2のようにFormのMouseMoveイベントにて前回のマウスカーソル座標から今回のマウスカーソル座標の直線がPanelと交差(Panelの2辺と交差)しているか判定し、交差しているようであれば、Panelを消すという形になるかと思います。

また、FormのMouseMoveイベントですら捉えられない可能性もあります。
この対応も考えるのであれば、グローバルフックにてForm外のマウスイベントを取得し、マウスカーソル移動の直線がPanelと交差しているかチェックする必要があります。

投稿2018/06/12 15:13

YAmaGNZ

総合スコア10242

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/06/13 00:34

マウスのイベント側の問題だったんですね。勉強になりました。
guest

0

おそらくですが、MouseLeaveイベントはマウスカーソルがPanel内に一度でも入らないと発生しないと思います。
ある時点でマウスカーソルがパネルより左にあり、高速で右側に動かして、次の時点でカーソルがパネルより右の位置に来た場合がそれに該当すると思います。

解決方法としては二通りが考えられます。

●案1
ボタンを押してパネルを表示した時点でマウスカーソルをパネル内に強制的に移動させます。
結果的にMouseLeaveが発生すると思います。
実現方法も簡単ですが、マウスカーソルがワープするので違和感があるかもしれません。

●案2
マウスカーソル位置を(フォームのMouseMoveなどで)監視し、「ある時点とその一つ前のカーソル座標を結ぶ直線」と「パネルの矩形」が重なっているかを判定し、重なっていたらパネルを非表示にさせます。
こちらは実装がやや面倒になります。

重なり判定はおそらくGraphics系のパス関数を使えば実現できます。
ただ、ここまでコストを掛けて実現する必要があるかどうかは一考の余地があります。

投稿2018/06/12 08:24

takabosoft

総合スコア8356

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2018/06/13 00:43

案2は考え付かなかったですね。 今回はタイマーで行きますが、タイマー管理でしづらいところがあったら、 この方法を使わせていただきます。
takabosoft

2018/06/13 05:02

Timerの方の検証コードも見させていただきました、対策としては十分だと思います。
guest

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
g_uo

総合スコア212

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

YAmaGNZ

2018/06/12 12:26

StackOverflowの質問はMouseEnterにて表示を行っているので 今回の場合とは微妙に異なるのではないかと思います。 マウスの速度によってはMouseEnterすら発生しない可能性がありますので 提示されたコードでもパネルが消えないことがあります。
g_uo

2018/06/12 12:39

コメントありがとうございます。 確かに質問者様の意図とは異なりますね。私のはやとちりでした。 また、こちらでもMouseEnterが発生しないことを確認しました。 大変失礼いたしました。回答を修正してその旨記載しておきます。 恐れ入りますが、本回答をマイナスしていただけますか。
退会済みユーザー

退会済みユーザー

2018/06/13 00:38 編集

MouseLeave、Enterに限らず、Mouse~系のイベントは全部取れないことがあるみたいですね。 それがわかっただけでも収穫です。 差し引き±0ってことで、ここはひとつお願いします。
g_uo

2018/06/13 01:49

aruhenoさん コメントありがとうございます。お心遣い感謝いたします。 私も勉強になりました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問