Strange behaviour with mouse hook in .NET and Windows Forms
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.
精彩评论