views:

293

answers:

1

I have a form that is displayed, not by ShowDialog, but by setting it's 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 http://support.microsoft.com/default.aspx?scid=kb;en-us;318804 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 2 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)
    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);
    }
}

}
+1  A: 

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.

Hans Passant
Hi, thanks for that. I'd never heard of IMessageFilter which seems to take care of the hooking for me. I tried to replace my existing code with IMessageFilter but have run across a problem. In order to ignore events that are nothing to do with my dropdown, I used to Hook and UnHook in my dropdown Activate and Deactivate events. However, Deactivate is fired before PreFilterMessage. Using PreFilterMessage, I can determine the control that was clicked and what form it belongs to. How can I determine if that form was created after my dropdown?
Jules
I may have a way to do it. First I take a list of all open forms just before I show my dropdown, In PreFiltureMessage, if the clicked control belongs to a form in the saved collection then I handle the click and close my dropdown. Otherwise, I ignore.
Jules
Ignore my drivel above. I've decided to stick with my hook as opposed to IMessageFilter, and the addition of the mouse-up event filter has solved my problem. Thanks again.
Jules