Adding Double Click support to the ComboBox control
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.
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 windowReleaseHandle
- detaches the handle once you're finished with itWndProc
- 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 implementIDisposable
so for best practice you should make sure you manually clean up by callingReleaseHandle
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.
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.
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.
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.
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.
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.
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.
[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
Related articles you may be interested in
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
ComboBoxDoubleClick.zip
|
Sample project for the Adding Double Click support to the ComboBox control blog post. |
1.0.0.0 | 11/10/2014 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Luis Mendieta
#
It was unintended, but deadly effective and quick. Here the code:
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.