

お世話になります。

C#.Net4.5で、Windowsフォームアプリを作っているのですが、フォームにマウスが入った(Enter)、フォームから  
マウスが出た(Leave)を拾って処理をさせようとしています。

[![画像の説明をここに入力](https://i.stack.imgur.com/wZ1ON.jpg)](https://i.stack.imgur.com/wZ1ON.jpg)

ただし、上記図のように赤い部分のような別のコントロールが乗っている場合、この赤い部分では  
Form側のマウスイベントは発生しません。

赤いコントロールにOnMouseEnterやonMouseLeaveなどをさせてもみたのですが、その場合、  
画像の赤い部分からグレーの部分に移動しただけでもマウスが離れたと処理してしまい、  
うまくいきません。逆もまた然りです。

フォームの上にどんなコントロールが乗っていようとも、フォームのMouseEnterとMouseLeaveが  
発生するようにするには、どうしたらよいでしょうか。

よろしくお願いいたします。


CC BY-SA 4.0
2020-03-04T02:50:13.317
63549
2020-03-05T04:13:35.120
2020-03-04T03:16:06.557
9374
9374
question
c# winforms
C# コントロールが重なっているフォームでマウスのEnterとLeaveを正しく拾いたい


WPF だったら たたぱた さんの希望するように コントロールが重なっていても `MouseEnter` `MouseLeave` の
イベントが発生します。

WindowsForm の場合は すべてのコントロールに イベントを紐づけして、対処するしかなさそうですね。  
それは そういう設計思想なので・・ としか言いようがありません。

下記のように すべてのコントロールにイベントを紐づけるパターンが考えられます。

`on_MouseEnter` 関数の中で どの コントロールから `MouseEnter` されたのかは `sender`
の情報で取得できるので、それに合わせて、いろいろなコードを組むしかないと思います。

私が良く使うのは `Control` の `Tag` に は
何でも入れられるので、送信元の詳細な情報を入れておいてイベントの中で取り出して使う。なんてやってます。

検討してみてください。

[code]

            public Form1()
            {
                InitializeComponent();
    
                addMouseEvent(this);
            }
    
            private void addMouseEvent(Control c)
            {
                c.MouseEnter += on_MouseEnter;
                c.MouseLeave += on_MouseLeave;
                foreach(Control cc in c.Controls)
                {
                    addMouseEvent(cc);
                }
            }
    
            private void on_MouseEnter(object sender, EventArgs e)
            {
                Control c = sender as Control;
                Point cur_pos = System.Windows.Forms.Cursor.Position;
                this.textBox1.AppendText($"on_MouseLeave { c?.Name } {cur_pos}");
            }
    
            private void on_MouseLeave(object sender, EventArgs e)
            {
                Control c = sender as Control;
                Point cur_pos = System.Windows.Forms.Cursor.Position;
                this.textBox1.AppendText($"on_MouseLeave { c?.Name } {cur_pos}");
    
            }
    
[/code]


CC BY-SA 4.0
2020-03-04T12:51:55.817
63563
2020-03-04T12:53:50.843
2020-03-04T12:53:50.843
3060
18851
63549
answer


WinFormsでも、以下の組み合わせで疑似的にEnter/Leaveのタイミングを検出することが出来ます。

  * [SetWindowsHookEx](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw)の[LowLevelMouseProc(WH_MOUSE_LL)](https://docs.microsoft.com/ja-jp/previous-versions/windows/desktop/legacy/ms644986\(v=vs.85\))登録でマウスイベント処理を行う  
参考記事：[C#でMouseWheelのフック](https://tocsworld.wordpress.com/2014/04/25/c%E3%81%A7mousewheel%E3%81%AE%E3%83%95%E3%83%83%E3%82%AF/)

  * Timerコントロール等の何らかのタイマー処理登録でマウスポインタ位置を定期的に取得する  
参考記事：[マウス・カーソルの位置を取得・設定するには？](https://www.atmarkit.co.jp/fdotnet/dotnettips/376cursorpos/cursorpos.html)

取得したマウスポインタ位置を元に[Rectangle.Contains メソッド](https://docs.microsoft.com/ja-
jp/dotnet/api/system.drawing.rectangle.contains?view=netframework-4.8)で範囲チェックを行う。  
参考記事：[スクリーン座標←→クライアント座標の変換を行うには？](https://www.atmarkit.co.jp/fdotnet/dotnettips/377screentoclient/screentoclient.html)  
[ある座標や領域がコントロールの領域内に含まれるているかを確認するには？](https://www.atmarkit.co.jp/fdotnet/dotnettips/382ctrlcontain/ctrlcontain.html)  
使用するデータは以下のいずれか。

  * [Form.DesktopBounds プロパティ](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.form.desktopbounds?view=netframework-4.8) でタイトルバーやボーダーも含めたウィンドウの外枠側
  * [Control.ClientRectangle プロパティ](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.clientrectangle?view=netframework-4.8)/[Control.DisplayRectangle プロパティ](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.displayrectangle?view=netframework-4.8)で通常のコントロール配置や描画等に使う内側の領域

以前の状態がウィンドウの内か外であったかを記憶しておき、変化があったらEnter/Leaveのタイミング発生と判断する。

LowLevelMouseProcを使ったプログラムソースはこちら(タイマーの例は割愛)

[code]

    namespace WindowsFormsApp1
    {
        using System;
        using System.Windows.Forms;
        using System.Runtime.InteropServices;
        public partial class Form1 : Form
        {
            public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
            HookProc MouseHookLLProcedure;
            static int hLLHook = 0;
            static bool bPrevMouseIn = false;
            public const int HC_ACTION = 0;
            public const int WH_MOUSE_LL = 14;
            
            [StructLayout(LayoutKind.Sequential)]
            public class POINT
            {
                public int x;
                public int y;
            }
            [StructLayout(LayoutKind.Sequential)]
            public class MouseLLHookStruct
            {
                public POINT pt;
                public int mouseData;
                public int flags;
                public int time;
                public int dwExtraInfo;
            }
            
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
            public static extern IntPtr GetModuleHandle(string lpModuleName);
            [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
            public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
            [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
            public static extern bool UnhookWindowsHookEx(int idHook);
            [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
            public static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);
            
            public Form1()
            {
                InitializeComponent();
                bPrevMouseIn = false;
            }
            private void Form1_Activated(object sender, EventArgs e)
            {
                if (hLLHook == 0)
                {
                    MouseHookLLProcedure = new HookProc(Form1.MouseLLHookProc);
                    hLLHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookLLProcedure, GetModuleHandle(null), 0);
                }
            }
            private static int MouseLLHookProc(int nCode, IntPtr wParam, IntPtr lParam)
            {
                if (nCode == HC_ACTION)
                {
                    MouseLLHookStruct MyMouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
                    Form1 tempForm = Form.ActiveForm as Form1;
                    if (tempForm != null)
                    {
                        bool fInMouse = false;
                        // 内側領域の場合
                        fInMouse = tempForm.RectangleToScreen(tempForm.ClientRectangle).Contains(MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y);
                        // 外側領域の場合
                        //fInMouse = tempForm.DesktopBounds.Contains(MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y);
                        if (fInMouse != bPrevMouseIn)
                        {  // EnterまたはLeaveの発生
                            tempForm.textBox1.AppendText("X= " + MyMouseHookStruct.pt.x.ToString("d4") + 
                                "  Y= " + MyMouseHookStruct.pt.y.ToString("d4") + (fInMouse ? "  Enter" : "  Leave") + "\r\n");
                            bPrevMouseIn = fInMouse; // 今回情報の記憶
                        }
                    }
                }
                return CallNextHookEx(hLLHook, nCode, wParam, lParam);
            }
            private void Form1_FormClosed(object sender, FormClosedEventArgs e)
            {
                if (hLLHook != 0)
                {
                    bool ret = UnhookWindowsHookEx(hLLHook);
                    hLLHook = 0;
                }
            }
        }
    }
    
[/code]

フォームのソースはこちら

[code]

    namespace WindowsFormsApp1
    {
        partial class Form1
        {
            private System.ComponentModel.IContainer components = null;
            protected override void Dispose(bool disposing)
            {
                if (disposing && (components != null))
                {
                    components.Dispose();
                }
                base.Dispose(disposing);
            }
            #region Windows フォーム デザイナーで生成されたコード
            private void InitializeComponent()
            {
                this.button1 = new System.Windows.Forms.Button();
                this.button2 = new System.Windows.Forms.Button();
                this.button3 = new System.Windows.Forms.Button();
                this.textBox1 = new System.Windows.Forms.TextBox();
                this.SuspendLayout();
                // button1
                this.button1.Location = new System.Drawing.Point(-1, -1);
                this.button1.Name = "button1";
                this.button1.Size = new System.Drawing.Size(128, 96);
                this.button1.TabIndex = 0;
                this.button1.Text = "button1";
                this.button1.UseVisualStyleBackColor = true;
                // button2
                this.button2.Location = new System.Drawing.Point(146, -1);
                this.button2.Name = "button2";
                this.button2.Size = new System.Drawing.Size(128, 96);
                this.button2.TabIndex = 1;
                this.button2.Text = "button2";
                this.button2.UseVisualStyleBackColor = true;
                // button3
                this.button3.Location = new System.Drawing.Point(294, -1);
                this.button3.Name = "button3";
                this.button3.Size = new System.Drawing.Size(128, 96);
                this.button3.TabIndex = 2;
                this.button3.Text = "button3";
                this.button3.UseVisualStyleBackColor = true;
                // textBox1
                this.textBox1.Location = new System.Drawing.Point(0, 192);
                this.textBox1.Multiline = true;
                this.textBox1.Name = "textBox1";
                this.textBox1.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
                this.textBox1.Size = new System.Drawing.Size(421, 206);
                this.textBox1.TabIndex = 3;
                // Form1
                this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(421, 397);
                this.Controls.Add(this.textBox1);
                this.Controls.Add(this.button3);
                this.Controls.Add(this.button2);
                this.Controls.Add(this.button1);
                this.Name = "Form1";
                this.Text = "Form1";
                this.Activated += new System.EventHandler(this.Form1_Activated);
                this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.Form1_FormClosed);
                this.ResumeLayout(false);
                this.PerformLayout();
            }
            #endregion
            private System.Windows.Forms.Button button1;
            private System.Windows.Forms.Button button2;
            private System.Windows.Forms.Button button3;
            private System.Windows.Forms.TextBox textBox1;
        }
    }
    
[/code]

表示の例がこちら  
[![サンプル画像](https://i.stack.imgur.com/8sAXE.png)](https://i.stack.imgur.com/8sAXE.png)

* * *

追記：

@Uncle-Kei さんの指摘はもっともだと思いますが、それはまた別の課題ということで、それに関連する記事を探してみました。

Windowsが行っているであろう処理を二重に行うことになるでしょうが、[WindowFromPoint
function](https://docs.microsoft.com/ja-jp/windows/win32/api/winuser/nf-
winuser-windowfrompoint) を応用することで自分のウィンドウが最上位かどうかの判断が出来るのではないでしょうか？

> 指定されたポイントを含むウィンドウへのハンドルを取得します。
>
>
> 戻り値は、ポイントを含むウィンドウへのハンドルです。指定されたポイントにウィンドウが存在しない場合、戻り値はNULLです。ポイントが静的テキストコントロール上にある場合、戻り値は静的テキストコントロール下のウィンドウへのハンドルです。
>
>
> WindowFromPointの関数は、ポイントがウィンドウ内であっても、非表示または無効になっウィンドウへのハンドルを取得しません。アプリケーションは、制限のない検索のためにChildWindowFromPoint関数を使用する必要があります。

以下この辺の記事で情報を取得する処理を扱っており、それが自分のプロセスのものであると判断できれば対応する処理を行うとか出来そうです。  
VB/Delphi/C++等でそのまま使うのは出来ませんが参考にはなるでしょう。  
[指定したウィンドウの情報を取得](http://tokovalue.jp/GetWindowWord_U.htm)  
[ホーム >プログラム >Delphi 6 ローテクTips
>マウスカーソル下のウィンドウを取得](http://drang.s4.xrea.com/program/tips/window/window_get.html)  
[Windows APIメモ
WindowFromPoint](http://drang.s4.xrea.com/program/tips/memo/api_windowfrompoint.html)  
[第１０２章 ウィンドウタイトルを変更する](http://www.kumei.ne.jp/c_lang/sdk2/sdk_102.htm)

他にはこんな記事も  
[How to get the z-order in
windows?](https://stackoverflow.com/q/825595/9014308)  
[Given a hwnd, determine if that window is not hidden by other windows
(Z-Ordering)](https://stackoverflow.com/q/34025833/9014308)

いずれにしても、元々の処理と併せてさらに重い処理になるでしょうけど。


CC BY-SA 4.0
2020-03-04T16:41:25.830
63571
2020-03-05T04:13:35.120
2020-06-17T08:14:45.997
-1
26370
63549
answer


回答ではありませんが、やや気になるので発言します。  
もちろんマウスメッセージをフックしたり、キャプチャーすれば実装可能な処理ではありますが、そういった設計をすべきではないというお話です。

可能であれば「隠れている親ウィンドウはマウスの侵入と離脱を検知して何をしようとしているのか」を説明すると、別の簡単な解決方法が提示されるかもしれません。

さて、その様な設計や実装をしない方が良い理由はWindowsはそのように設計されていないからです。

・Windowsは見えないウィンドウにマウスメッセージを送付しない様に設計されています(一般的な他のOSも同じです)。

というわけです。  
Windowsではマウスカーソル直下の画面上の重なり順位(Zオーダー)が最も表面にあるウィンドウのみにマウスメッセージが送付されます。  
これは、「マウス操作を処理する権限があるのは見えるウィンドウだけである」という方針を示唆します。  
つまり、その他のウィンドウは何もするなということですね。  
無理やりやるとバグに悩んだり、他のアプリケーションの迷惑になったりする可能性が高いと予測できます。

従って、子ウィンドウのマウスの状態を監視する必要がある場合には、子にその状態を報告させるのが通常の実装となります。  
例えば「赤コントロールからオーナー様に報告です。マウスが侵入しました」という感じです。

・以下は蛇足です。

Windowsの初めの頃にはWM_MOUSEMOVEはありましたが、WM_MOUSEHOVER、WM_MOUSELEAVEなどは有りませんでした。  
NT4.0(or
2000)の頃から利用できましたが、後のOSの変更でアプリ側からTrackMouseEvent()を実行してメッセージの発行を要請する手続きになりました。  
それまでは一般的にはマウスキャプチャーを使ってマウスの侵入と離脱を検知していたわけです。  
ちなみに、侵入を意味するWM_MOUSEENTERの様なメッセージはいまだにWindowsAPIには存在しません。  
アプリ上の内部的な状態から疑似的に作っているわけですね。

これらの状態が比較的重要視されないのは、それが「ユーザーアクションでない可能性のあるマウスメッセージ」であるからではないかとの個人的な考えを持っています。


CC BY-SA 4.0
2020-03-05T02:07:23.903
63579
2020-03-05T02:26:00.897
2020-03-05T02:26:00.897
3793
3793
63549
answer