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

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

ただいまの
回答率

90.83%

  • C#

    6017questions

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

  • Visual Studio

    1562questions

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

  • .NET Framework

    402questions

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

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

解決済

回答 4

投稿

  • 評価
  • クリップ 0
  • VIEW 177

aruheno

score 26

お世話になっております。
今回はPanelのMouseLeaveイベントに関する質問です。
Panelは.NetFrameworkのものです。

 やりたいこと

イメージ説明
パネルの中と外にボタンを設置し、button1を押すとパネルが表示され、パネルからマウスが離れるとパネルが消えるようにしたいと思っています。

コードはこんな感じです。

private void panel1_MouseLeave(object sender, EventArgs e)
{
   if (panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position)) == false)
   panel1.Visible = false;
}

 問題点

以下の画像のようにマウスカーソルを動かすと、MouseLeaveイベントが発生しないことがあります。
特に、カーソルを高速で動かすと起きやすいです。
イメージ説明

これらの問題の対処法を探しております。
以上、よろしくお願いいたします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+2

回答依頼があったので回答します。
他の方々が指摘されているように MouseLeave イベントを完全に捕捉することは原理的に不可能になります。これは MouseLeave イベントが Win32 API の TrackMouseEvent によって生成されていることに起因しています。TrackMouseEvent を .NET Framework が呼び出すタイミングは MouseMove イベント発生時になりますが、YAmaGNZ さんが指摘されている通り MouseMove イベントは 1 ピクセル単位に発生するわけではないので、TrackMouseEvent が呼び出されないマウス操作が発生しえます。

この問題を解決する案として一番のおすすめは MouseEnter / MouseLeave イベントとタイマーを組み合わせることです。

    public partial class Form1 : Form
    {
        private bool flag = false;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            panel1.Visible = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            panel1.Visible = true;
            timer1.Start();
        }

        private void panel1_MouseEnter(object sender, EventArgs e)
        {
            Debug.Print("panel1_MouseEnter");
            flag = true;
        }

        private void panel1_MouseLeave(object sender, EventArgs e)
        {
            Debug.Print("panel1_MouseLeave");
            if (!panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position)))
            {
                flag = false;
                panel1.Visible = false;
                timer1.Stop();
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position)))
            {
                if (!flag)
                {
                    flag = true;
                }
            }
            else
            {
                if (flag)
                {
                    flag = false;
                    panel1.Visible = false;
                    timer1.Stop();
                }
            }
        }
    }


上記のような処理でタイマーをデフォルトの 100ms 間隔で処理しても、パネルが非表示になるタイミングとして体感的には違和感はないと思います。気になるようでしたらタイマー間隔を 50ms 程度に縮めていただいて試していただければよいと思います。それぐらいのタイマー間隔でもパネルの数が 100 枚を超えるような状況でなければ、遅延を感じることはないと思います。(MouseLeaveのイベント内容次第ではありますが・・・)

なお、TrackMouseEvent 自体はかなり優秀な API でフォーム外のマウスの動作を補足することができます。通常の MouseMove イベントでこれと同程度の処理を実施する場合、他の回答者のコメント通りフックを使用したりマウスをキャプチャーすることになります。いずれの処理も実装するのがちょっと難しいので、タイマーで処理するのがベターであると私は考えています。

検証用に書いたコードを Github に上げました。
https://github.com/atata0319/teratail130742


takabosoftさんが指摘されている案2に対応してみました。コーディングのコストはかなり上がった印象になりますね。Panelの派生クラスとして共通の部品として実装されているのであれば、ありかなと思います。線分の交差判定には別のサイトのサンプルを使用しています。

可能な限り Win32 API を直接使用するのを避けるためにキャプチャーやフックに頼らない方法で実装してあります。タイマー処理を削除したかったのですが、前述の制限によりフォーム外に移動した際の処理を記述するのが難しいので諦めました。

    public partial class Form1 : Form, IMessageFilter
    {
        private bool flag = false;
        private Point previous;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            panel1.Visible = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ShowPanel();
        }

        private void panel1_MouseEnter(object sender, EventArgs e)
        {
            flag = true;
        }

        private void panel1_MouseLeave(object sender, EventArgs e)
        {
            if (!panel1.ClientRectangle.Contains(panel1.PointToClient(Cursor.Position)))
            {
                HidePanel();
            }
        }

        bool IMessageFilter.PreFilterMessage(ref Message m)
        {
            const int WM_MOUSEMOVE = 0x0200;
            const int WM_NCMOUSEMOVE = 0x00A0;
            if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_NCMOUSEMOVE)
            {
                Point current;
                if (m.Msg == WM_NCMOUSEMOVE)
                {
                    current = panel1.PointToClient(new Point(m.LParam.ToInt32()));
                }
                else
                {
                    var control = Control.FromHandle(m.HWnd);
                    if (control != null)
                        current = panel1.PointToClient(control.PointToScreen(new Point(m.LParam.ToInt32())));
                    else
                        current = panel1.PointToClient(Cursor.Position);
                }
                //Debug.Print("WM_MOUSEMOVE: {0}", current);
                if (panel1.ClientRectangle.Contains(current))
                {
                    flag = true;
                }
                else
                {
                    if (flag)
                    {
                        HidePanel();
                    }
                    else
                    {
                        // 直前の座標と現在の座標を結ぶ線分がパネルの矩形と交差する場合、パネルを閉じる。
                        if (IsIntersected(panel1.ClientRectangle, current, previous))
                        {
                            HidePanel();
                        }
                    }
                }
                previous = current;
            }
            return false;
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            var current = panel1.PointToClient(Cursor.Position);
            // フォーム内から一気にフォーム外に出ていったカーソルはここで処理する。
            if (IsIntersected(panel1.ClientRectangle, current, previous))
            {
                HidePanel();
            }
            previous = current;
        }

        private void ShowPanel()
        {
            panel1.Visible = true;
            timer1.Start();
            Application.AddMessageFilter(this);
            previous = panel1.PointToClient(Cursor.Position);
        }

        private void HidePanel()
        {
            flag = false;
            timer1.Stop();
            Application.RemoveMessageFilter(this);
            panel1.Visible = false;
        }

        private static bool IsIntersected(Rectangle rectangle, Point a, Point b)
        {
            // 矩形と線分の交差は各辺と交差しているかどうかで判定する。
            var lefttop = new Point(rectangle.Left, rectangle.Top); // 左上座標
            var righttop = new Point(rectangle.Right, rectangle.Top); // 右上座標
            var leftbottom = new Point(rectangle.Left, rectangle.Bottom); // 左下座標
            var rightbottom = new Point(rectangle.Right, rectangle.Bottom); // 右下座標
            if (IsIntersected(a, b, lefttop, leftbottom)) // 左辺
                return true;
            if (IsIntersected(a, b, righttop, rightbottom)) // 右辺
                return true;
            if (IsIntersected(a, b, leftbottom, rightbottom)) // 下辺
                return true;
            if (IsIntersected(a, b, lefttop, righttop)) // 上辺
                return true;
            return false;
        }

        private static bool IsIntersected(Point a, Point b, Point c, Point d)
        {
            // https://qiita.com/ykob/items/ab7f30c43a0ed52d16f2
            // 線分(ab)と線分(cd)が交差しているか判定する。
            var ta = (c.X - d.X) * (a.Y - c.Y) + (c.Y - d.Y) * (c.X - a.X);
            var tb = (c.X - d.X) * (b.Y - c.Y) + (c.Y - d.Y) * (c.X - b.X);
            var tc = (a.X - b.X) * (c.Y - a.Y) + (a.Y - b.Y) * (a.X - c.X);
            var td = (a.X - b.X) * (d.Y - a.Y) + (a.Y - b.Y) * (a.X - d.X);
            return tc * td < 0 && ta * tb < 0;
        }
    }


Github には検証用プロジェクトを追加してあります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/06/13 09:28

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

    キャンセル

  • 2018/06/13 13:57

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

    キャンセル

  • 2018/06/13 13:59

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

    キャンセル

  • 2018/06/14 02:24

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

    キャンセル

  • 2018/06/14 09:05

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

    キャンセル

  • 2018/06/14 13:37

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

    キャンセル

  • 2018/06/15 00:39

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

    キャンセル

+2

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

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/06/13 09:43

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

    キャンセル

  • 2018/06/13 14:02

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

    キャンセル

+2

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/06/13 09:34

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

    キャンセル

0

 追記

MouseEnterを使ってもマウスポインタの速度によってはイベントが発生しないというご指摘を頂戴しました。
私のほうでもマウスを高速で動かした場合に発生しないことを確認しましたので、本回答は質問者様の要求を満たしません。


こんにちは。

この場合、MouseEnterイベントを使うほうが適切なようです。

次のサイトに同様の質問があります。ご確認ください。
Stack Overflow  MouseLeave not fired C# WinForms

Stack Overflowの回答をもとに、質問者様が求める機能を実装すると以下のようになりました。

    public partial class Form1 : Form
    {
        private bool _entered;

        public Form1()
        {
            InitializeComponent();
            panel1.Visible = false;
        }

        private void panel1_MouseEnter(object sender, EventArgs e)
        {
            // panel1の領域にマウスポインタが来たことを記録
            _entered = true;
        }

        /// <summary>
        /// マウスポインタの現在地がpanel1の上にあるかどうか確認する
        /// </summary>
        /// <param name="point">マウスポインタのロケーション</param>
        /// <returns>
        /// panel1の上にある場合、true, それ以外はfalse.
        /// </returns>
        private bool HasMousePointer(Point point)
        {
            var p1 = new Point(panel1.Location.X, panel1.Location.Y);
            var p2 = new Point(p1.X + panel1.Size.Width, p1.Y + panel1.Size.Height);
            return
                (point.X >= p1.X && point.X <= p2.X) &&
                (point.Y >= p1.Y && point.Y <= p2.Y);
        }

        private void Form1_MouseMove(object sender, MouseEventArgs e)
        {
            // まだマウスポインタがpanel1に乗ってないので抜けます
            if (!_entered) return;
            // マウスポインタの現在値がpanel1のうえかどうか調べる
            var result = HasMousePointer(e.Location);
            // まだ乗っているので抜けます
            if (result) return;

            // マウスポインタがすでにpanel1の上からいなくなっているので
            // panel1を非表示にし、フラグも戻します。
            _entered = false;
            panel1.Visible = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            panel1.Visible = true;
        }
    }

ご参考になれば幸いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/06/12 21:26

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

    キャンセル

  • 2018/06/12 21:39

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

    キャンセル

  • 2018/06/13 09:38 編集

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

    キャンセル

  • 2018/06/13 10:49

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

    キャンセル

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

  • ただいまの回答率 90.83%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • C#

    6017questions

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

  • Visual Studio

    1562questions

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

  • .NET Framework

    402questions

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