开发者

Strange behaviour with mouse hook in .NET and Windows Forms

开发者 https://www.devze.com 2022-12-12 00:46 出处:网络
I have a form that is displayed, not by ShowDialog, but by setting its visible property to true. This is so it behaves like a dropdown.

I have a form that is displayed, not by ShowDialog, but by setting its visible property to true. This is so it behaves like a dropdown.

The form installs a mouse hook, using SetWindowsHookEx(WH_MOUSE, ...).

I detect if the mouse is clicked outside of the dropdown and if so, return 1 in my HookProc method and close the dropdown.

The strange thing is, if I click outside of my drop down on to a textbox, the textbox still receives the mouse click, after my dropdown closes, even though it's been handled by my HookProc method.

It gets stranger... If I click on a label or button, they do not receive the mouse click, as expected, after the drop down closes!

Any idea what's going on?

ETA 2:

You can ignore all my code below because, on further investigation, I've found out that this behaviour is exhibited in at least one framework control that implements a drop down type form.

To replicate, create a form and add a property grid, button, textbox and label. Set the selected object of the property grid to a font.

Run the form and select the font name. A drop down list appears. Now click on the form's textbox. The textbox click event is fired. However, the same does not happen for the button or label.

What's going on?

ETA 1:

Here's some bare-bones code from How to set a Windows hook in Visual C# .NET to demonstrate what's going on. I've used a converter to convert the code back to C#, but hopefully it's OK. I'm not sure but you may need to replace Console.WriteLine with Debug.WriteLine.

Create two forms, Form1 and DropDown.

(1) VB.NET

In Form1, add a Button, Label and TextBox, and the following code.

Imports System.Runtime.InteropServices

Public Class Form1

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Console.WriteLine("Button1_Click")
        Dim dd As New DropDown
        dd.Visible = True
        Do While dd.Visible
            Application.DoEvents()
            MsgWaitForMultipleObjectsEx(0, IntPtr.Zero, 250, &HFF, 4)
        Loop
    End Sub

    <DllImport("user32.dll", CharSet:=CharSet.Auto, ExactSpelling:=True)> _
    Public Shared Function MsgWaitForMultipleObjectsEx(ByVal nCount As Integer, ByVal pHandles As IntPtr, ByVal dwMilliseconds As Integer, ByVal dwWakeMask As Integer, ByVal dwFlags As Integer) As Integer
    End Function

    Private Sub Label1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Label1.Click
        Console.WriteLine("Label1_Click")
    End Sub

    Private Sub TextBox1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.Click
        Console.WriteLine("TextBox1_Click")
    End Sub

End Class

In DropDown, place the following code.

Imports System.Runtime.InteropServices

Public Class DropDown

    Public Delegate Function HookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer

    Private hHook As Integer = 0
    Public Const WH_MOUSE As Integer = 7
    Private MouseHookProcedure As HookProc

    <StructLayout(LayoutKind.Sequential)> _
    Public Class POINT
        Public x As Integer
        Public y As Integer
    End Class

    <StructLayout(LayoutKind.Sequential)> _
    Public Class MouseHookStruct
        Public pt As POINT
        Public hwnd As Integer
        Public wHitTestCode As Integer
        Public dwExtraInfo As Integer
    End Class

    <DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
    Public Shared Function SetWindowsHookEx(ByVal idHook As Integer, ByVal lpfn As HookProc, ByVal hInstance As IntPtr, ByVal threadId As Integer) As Integer
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
    Public Shared Function UnhookWindowsHookEx(ByVal idHook As Integer) As Boolean
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.StdCall)> _
    Public Shared Function CallNextHookEx(ByVal idHook As Integer, ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
    End Function

    Protected Overrides Sub OnDeactivate(ByVal e As System.EventArgs)
        MyBase.OnDeactivate(e)
    开发者_如何学Python    UnhookWindowsHookEx(hHook)
        hHook = 0
    End Sub

    Public Sub New()
        InitializeComponent()
        MouseHookProcedure = New HookProc(AddressOf MouseHookProc)
        hHook = SetWindowsHookEx(WH_MOUSE, MouseHookProcedure, New IntPtr(0), AppDomain.GetCurrentThreadId())
    End Sub

    Public Function MouseHookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
        Dim MyMouseHookStruct As MouseHookStruct = DirectCast(Marshal.PtrToStructure(lParam, GetType(MouseHookStruct)), MouseHookStruct)
        If nCode < 0 Then
            Return CallNextHookEx(hHook, nCode, wParam, lParam)
        Else
            Select Case CInt(wParam)
                Case &H21, &HA1, &HA4, &H204, &H207, &HA7, &H201
                    Me.Visible = False
                    Return 1
            End Select
            Return CallNextHookEx(hHook, nCode, wParam, lParam)
        End If
    End Function

End Class

(2) C#

In Form1 add a Button, Label and TextBox, and the following code:

using System.Runtime.InteropServices;

public class Form1
{
    public Form1()
    {
        InitializeComponent();
        Button1.Click += Button1_Click;
        Label1.Click += Label1_Click;
        TextBox1.Click += TextBox1_Click;
    }

    private void Button1_Click(System.Object sender, System.EventArgs e)
    {
        Console.WriteLine("Button1_Click");
        DropDown dd = new DropDown();
        dd.Visible = true;
        while (dd.Visible) {
            Application.DoEvents();
            MsgWaitForMultipleObjectsEx(0, IntPtr.Zero, 250, 0xff, 4);
        }
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern int MsgWaitForMultipleObjectsEx(int nCount, IntPtr pHandles, int dwMilliseconds, int dwWakeMask, int dwFlags);

    private void Label1_Click(object sender, System.EventArgs e)
    {
        Console.WriteLine("Label1_Click");
    }

    private void TextBox1_Click(object sender, System.EventArgs e)
    {
        Console.WriteLine("TextBox1_Click");
    }
}

In DropDown, place the following code:

using System.Runtime.InteropServices;

public class DropDown
{
    public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    private int hHook = 0;
    public const int WH_MOUSE = 7;
    private HookProc MouseHookProcedure;

    [StructLayout(LayoutKind.Sequential)]
    public class POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    public class MouseHookStruct
    {
        public POINT pt;
        public int hwnd;
        public int wHitTestCode;
        public int dwExtraInfo;
    }

    [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);

    protected override void OnDeactivate(System.EventArgs e)
    {
        base.OnDeactivate(e);
        UnhookWindowsHookEx(hHook);
        hHook = 0;
    }

    public DropDown()
    {
        InitializeComponent();
        MouseHookProcedure = new HookProc(MouseHookProc);
        hHook = SetWindowsHookEx(WH_MOUSE, MouseHookProcedure, new IntPtr(0), AppDomain.GetCurrentThreadId());
    }

    public int MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        MouseHookStruct MyMouseHookStruct = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
        if (nCode < 0) {
            return CallNextHookEx(hHook, nCode, wParam, lParam);
        }
        else {
            switch ((int)wParam) {
                case 0x21:
                case 0xa1:
                case 0xa4:
                case 0x204:
                case 0x207:
                case 0xa7:
                case 0x201:
                    this.Visible = false;
                    return 1;
            }
            return CallNextHookEx(hHook, nCode, wParam, lParam);
        }
    }
}


This problem is caused because you filter the mouse down message but not the mouse up message. You can fix it like this:

  Select Case CInt(wParam)
    Case &HA1, &HA4, &HA7, &H201, &H204, &H207
      Me.Capture = True
    Case &HA2, &hA5, &HA8, &H202, &H205, &H208
      Me.Visible = False
  End Select

Consider implementing IMessageFilter instead.

0

精彩评论

暂无评论...
验证码 换一张
取 消