I was recently using a ComboBox control with the DropDownStyle set to Simple, effectively turning into a combined text box and list box.

However, when I wanted an action to occur on double clicking an item in the list I found that the control doesn't actually offer double click support. I suppose I should have just ripped out the combo box at that point and went with dedicated controls but instead I decided to extend ComboBox to support double clicks.

Double click events from a simple mode ComboBox control
Double click events from a simple mode ComboBox control

Hmm, no WM_LBUTTONDBLCLK message?

I had assumed I could simply get the handle of the list component, set the CS_DBLCLKS style, and start receiving WM_LBUTTONDBLCLK messages. Unfortunately I couldn't get this to work. Something to revisit another day perhaps.

Fine, lets fake it with WM_LBUTTONUP instead

So plan A was a bust. Not to worry, I had another idea. In a previous post I described how to use the GetComboBoxInfo Win32 API call to obtain the handles to the integrated controls. We'll use this along with a NativeWindow to watch for WM_LBUTTONUP messages and handle our double clicks that way.

What is NativeWindow?

I haven't described NativeWindow in any previous post, so I'll briefly cover it now. NativeWindow is a managed wrapper around a Win32 window handle, and allows you to easily hook into it's window procedure (WndProc) in order to capture and process messages sent to the window. Very tidy. The most important class members are

  • AssignHandle - attaches the class to a window
  • ReleaseHandle - detaches the handle once you're finished with it
  • WndProc - allows you to process messages, otherwise there's not really much point in using the class!

One final point, in most cases you're probably going to want to subclass NativeWindow as WndProc is protected. And that's what we'll do here, using a new ListBoxNativeWindow class.

Attaching the handle

As I mentioned above, you have to explicitly attached your NativeWindow implementation to a window. For this demonstration control we'll do it when the control handle is created, and when the drop down list style is changed. I'll also add a AllowDoubleClick property to control the new behaviour, so we'll also set it from there.

NativeWindow doesn't implement IDisposable so for best practice you should make sure you manually clean up by calling ReleaseHandle when you are done.

As I've previously covered the COMBOBOXINFO structure and GetComboBoxInfo call I won't go over these again - please refer to my previous post if you need more info.

Assuming we successfully obtain the combo box information, we instantiate a new instance of our ListBoxNativeWindow and attach it to the handle of the list box.

csharp
private ListBoxNativeWindow _listBoxWindow;

private void AttachHandle()
{
  this.ReleaseHandle();

  if (this.IsHandleCreated && this.AllowDoubleClick && this.DropDownStyle == ComboBoxStyle.Simple)
  {
    COMBOBOXINFO info;

    info = new COMBOBOXINFO();
    info.cbSize = Marshal.SizeOf(info);

    if (GetComboBoxInfo(this.Handle, ref info))
    {
      IntPtr hWnd;

      hWnd = info.hwndList;

      _listBoxWindow = new ListBoxNativeWindow(this);
      _listBoxWindow.AssignHandle(hWnd);
    }
  }
}

Our new class is also storing a reference to the owner ComboBox control so that we can raise events as appropriate later on.

As we should clean up behind ourselves, there's a helper method to release any existing handles which we will call when assigning a new handle, or when disposing of the control.

csharp
private void ReleaseHandle()
{
  if (_listBoxWindow != null)
  {
    _listBoxWindow.ReleaseHandle();
    _listBoxWindow = null;
  }
}

Now it's time to watch for some messages.

Intercepting messages

Intercepting messages in a NativeWindow is no different to that of a normal control - just override WndProc and wait for something interesting.

csharp
const int WM_LBUTTONUP = 0x0202;

protected override void WndProc(ref Message m)
{
  if (m.Msg == WM_LBUTTONUP)
  {
    // do stuff!
  }

  base.WndProc(ref m);
}

Double clicks

A double click is a pretty simple thing - it is the second click to occur within a defined interval and with the cursor within the region of the first click. These system values are configurable by the end user so we shouldn't hard code our own values.

The DoubleClickSize and DoubleClickTime properties of the SystemInformation class provide managed access to these system values, and so we can now populate our WndProc template with some real code.

csharp
if (m.Msg == NativeMethods.WM_LBUTTONUP)
{
  long previousMessageTime;
  long currentMessageTime;
  Point currentLocation;

  previousMessageTime = _lastMessageTime;
  currentMessageTime = DateTime.Now.Ticks;
  currentLocation = this.GetPoint(m.LParam);

  if (_lastMessageTime > 0)
  {
    Rectangle doubleClickBounds;
    Size doubleClickSize;

    doubleClickSize = SystemInformation.DoubleClickSize;
    doubleClickBounds = new Rectangle(_lastMousePosition.X - (doubleClickSize.Width / 2), _lastMousePosition.Y - (doubleClickSize.Height / 2), doubleClickSize.Width, doubleClickSize.Height);

    if (previousMessageTime + (SystemInformation.DoubleClickTime * TimeSpan.TicksPerMillisecond) > currentMessageTime && doubleClickBounds.Contains(currentLocation))
    {
      MouseEventArgs e;

      e = new MouseEventArgs(MouseButtons.Left, 2, currentLocation.X, currentLocation.Y, 0);

      _owner.RaiseDoubleClick(e);
    }
  }

  _lastMessageTime = currentMessageTime;
  _lastMousePosition = currentLocation;
}

Although it might look a little complicated at first glance, it should be straight forward.

  • The very first time you click with the left mouse button, we record the current time and the cursor location
  • Each subsequent click then
    • Compares the current cursor position against a rectangle centered on the previous position
    • Compares the previous click time with the current time subtracted from the interval
    • If both the interval since the last click has not elapsed and the cursor is in the same general area, then we have our double click
    • Regards of if an event is to be raised or not, we then update the time and position for the next click

Raising the event

Although I'd like to do the "right thing" and trigger a WM_LBUTTONDBLCLK message, the control doesn't support it and there's not really much point in adding it when it's not going to have any real value. So we'll manually do it.

I start by adding an internal method to our ComboBox control - I tend to avoid internals where possible but I don't really see a need to expose this publicly.

csharp
internal void RaiseDoubleClick(MouseEventArgs e)
{
  this.OnDoubleClick(EventArgs.Empty);

  this.OnMouseDoubleClick(e);
}

Short and to the point, it simply raises the two different events .NET controls have for double clicks.

And back in our WndProc, we construct a new MouseEventArgs object and then call the new method.

csharp
MouseEventArgs e;

e = new MouseEventArgs(MouseButtons.Left, 2, currentLocation.X, currentLocation.Y, 0);

_owner.RaiseDoubleClick(e);

It's worth pointing out the fudge in this - the magic number 2 which represents the number of times the button was clicked. The 0, while still magic, represents a mouse wheel delta which is not appropriate for this event.

And with that code in place, this slightly long winded article has gotten to the point and you now have fully working events.

Really? I can't see them

Oh of course. As the ComboBox control doesn't support the DoubleClick and MouseDoubleClick events, the DoubleClick event has been hidden (but not MouseDoubleClick for some reason). Easy enough to bring it back - just redefine DoubleClick with the new keyword set the EditorBrowsable and Browsable attributes so it will appear in designers.

csharp
[EditorBrowsable(EditorBrowsableState.Always)]
[Browsable(true)]
public new event EventHandler DoubleClick
{
  add { base.DoubleClick += value; }
  remove { base.DoubleClick -= value; }
}

Always a catch

This was yet another blog post that was written in a hurry after writing some code in a hurry. I'm positive there must be a better way using normal window styles and messages rather than the manual approach I've taken.

There's also a flaw in the code - if you triple click (or more) then you'll get two (or more) double click events. I don't know of too many people who spam double clicks so I'm going to ignore this for now. Possibly at some point I'll be bored enough to take another look at this and see where I went wrong with the pure API approach.

Finally, given the hurry with which both of these items were written, it hasn't had any robust testing, and so may be a flawed piece of work.

As always, a demonstration project accompanies this article.

Update History

  • 2014-10-11 - First published
  • 2020-11-21 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments

# Luis Mendieta

It was unintended, but deadly effective and quick. Here the code:

Private Sub cboProdsServs_Click(sender As Object, e As EventArgs) Handles cboProdsServs.Click
    Try
        Dim item As Object
        For Each item In lbSeleccion.Items
            If item = cboProdsServs.Text Then
                Exit Sub
            End If
        Next

        lbSeleccion.Items.Add(cboProdsServs.Text)

    Catch ex As Exception
        Debug.Print(ex.Message)
    End Try

End Sub

My idea was avoid user double-add items in the second combo (lbSeleccion), while first (cboProdsServs) has items to move to cboProdsServs list, but finally worked like double click event! Hope it helps. Note that the host event is cboProdsServs_Click.

Reply