Welcome to MSDN Blogs Sign in | Join | Help

Implementing higher-order clicks

Another question people ask is "How do I do triple-click or higher?" Once you see the algorithm for double-clicks, extending it to higher order clicks should be fairly natural. The first thing you probably should do is to remove the CS_DBLCLKS style from your class because you want to do multiple-click management manually.

Next, you can simply reimplement the same algorithm that the window manager uses, but take it to a higher order than just two. Let's do that. Start with a clean scratch program and add the following:

int g_cClicks = 0;
RECT g_rcClick;
DWORD g_tmLastClick;

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags)
{
  POINT pt = { x, y };
  DWORD tmClick = GetMessageTime();

  if (!PtInRect(&g_rcClick, pt) ||
      tmClick - g_tmLastClick > GetDoubleClickTime()) {
    g_cClicks = 0;
  }
  g_cClicks++;

  g_tmLastClick = tmClick;
  SetRect(&g_rcClick, x, y, x, y);
  InflateRect(&g_rcClick,
              GetSystemMetrics(SM_CXDOUBLECLK) / 2,
              GetSystemMetrics(SM_CYDOUBLECLK) / 2);

  TCHAR sz[20];
  wnsprintf(sz, 20, TEXT("%d"), g_cClicks);
  SetWindowText(hwnd, sz);
}

void ResetClicks()
{
  g_cClicks = 0;
  SetWindowText(hwnd, TEXT("Scratch"));
}

void OnActivate(HWND hwnd, UINT state, HWND, BOOL)
{
  ResetClicks();
}

void OnRButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags)
{
  ResetClicks();
}

    HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
    HANDLE_MSG(hwnd, WM_ACTIVATE, OnActivate);

[Boundary test for double-click time corrected 10:36am.]

(Our scratch program doesn't use the CS_DBLCLKS style, so we didn't need to remove it - it wasn't there to begin with.)

The basic idea here is simple: When a click occurs, we see if it is in the "double-click zone" and has occurred within the double-click time. If not, then we reset the consecutive click count.

(Note that the SM_CXDOUBLECLK and SM_CYDOUBLECLK values are the width of the entire rectangle, so we cut it in half when inflating so that the rectangle extends halfway in either direction. Yes, this means that a pixel is lost if the double-click width is odd, but Windows has been careful always to set the value to an even number.)

Next, we record the coordinates and time of the current click so the next click can compare against it.

Finally, we react to the click by putting the consecutive click number in the title bar.

There are some subtleties in this code. First, notice that setting g_cClicks to zero forces the next click to be treated as the first click in a series, for regardless of whether it matches the other criteria, all that will happen is that the click count increments to 1.

Next, notice that the way we test whether the clicks occurred within the double click time was done in a manner that is not sensitive to timer tick rollover. If we had written

      tmClick > g_tmLastClick + GetDoubleClickTime()) {

then we would fail to detect multiple clicks properly near the timer tick rollover. (Make sure you understand this.)

Third, notice that we reset the click count when the window gains or loses activation. That way, if the user clicks, then switches away, then switches back, and then clicks again, that is not treated as a double-click. We do the same if the user clicks the right mouse button in between. (You may notice that few programs bother with quite this much subtlety.)

Exercise: Suppose your program isn't interested in anything beyond triple-clicks. How would you change this program in a manner consistent with the way the window manager stops at double-clicks?

Published Monday, October 18, 2004 7:00 AM by oldnewthing
Filed under:

Comments

# re: Implementing higher-order clicks

Monday, October 18, 2004 7:31 AM by Carlos
This is nit-picking, but I don't like the hit-test rectangle because it's not symmetrical around the starting point, and the code is broken when SM_CXDOUBLECLK==1. (Even if this can't happen in practice, it's still scruffy.) You can fix both problems by inflating a 1x1 rectangle rather than an empty rectangle.

# re: Implementing higher-order clicks

Monday, October 18, 2004 8:05 AM by anonymouse
A more important question would be:

"Does Raymond Chen know everything!"

# re: Implementing higher-order clicks

Monday, October 18, 2004 8:32 AM by asdf
Windows doesn't make sure it's an even number (hell, it doesn't even make sure it's a positive number) in the call to SystemParametersInfo.

# re: Implementing higher-order clicks

Monday, October 18, 2004 8:36 AM by Chris Becke
Raymond Chen forgot to make his rect bottom right exclusive? Ha, that sounds like a much needed blog entry. Dealing with bottom right exclusive rects in Win32 :)

# re: Bottom Right Exclusive Rects

Monday, October 18, 2004 8:43 AM by Steve Thresher

# re: Implementing higher-order clicks

Monday, October 18, 2004 8:50 AM by Chris Becke
hmmm. isn't that ironic.

# re: Implementing higher-order clicks

Monday, October 18, 2004 8:55 AM by Matthew Riley
g_rcClick is of indeterminate value the first time through this function and is passed to PtInRect before initialized... might this pose a problem?

# re: Implementing higher-order clicks

Monday, October 18, 2004 8:55 AM by Merle
Why in the name of all that is sacred would you want to use *triple* clicks in an application?

Yes, I understand Word has some magic selection behaviour where the more you click the more text is selected. But I still don't think that's a good idea. I would much rather have that sort of thing in a context menu where you can select. It feels more like a "wow, this is so cool that I can detect this, let's give it some random functionality" thing to me.

Unless you have a trackball (or glidepoint and use the buttons) it's really hard to click multiple times without moving the mouse.

# re: Implementing higher-order clicks

Monday, October 18, 2004 9:10 AM by Raymond Chen
1. Yes, the rectangle is asymmetric, but that's how Windows has worked since 1983 and the goal here is to emulate the behavior not to fix it.

2. asdf: I meant that the Windows control panels enforce the even-ness, not the API.

3. Matthew: One of the subtleties of the code is that it doesn't matter what g_rcClick is initialized to.

# re: Implementing higher-order clicks

Monday, October 18, 2004 9:43 AM by Nicholas Allen
Merle-

Your browser probably supports triple clicking as well. In Netscape it selects the line while in IE it selects the paragraph.

# re: Implementing higher-order clicks

Monday, October 18, 2004 9:45 AM by Merle
Ha! You're right. Even if the click happens to be within the uninitialized g_rcClick, and if by wild happenstance g_tmLastClick is close to what GetMessageTime() returns (another uninitialized variable), it's a NOOP the first time through.

Tricky.

I'd feel safer initializing the RECT, though. ;-)

# re: Implementing higher-order clicks

Monday, October 18, 2004 9:48 AM by Merle
Nicholas: hmm, you're right. Even though Opera 5 pops up a contexty menu on double click, continued clicking selects text much as in Word. Never noticed. (never tried, actually) Three clicks gives you text between punctuation (oddly stopping at the comma), four the paragraph.

But I maintain my initial question: why do you want that?

Especailly since it's not consistent between apps...

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:03 AM by Nicholas Allen
Merle-

Well, it's mostly because you can't just shove everything into a context menu. Usability studies have shown that small, consistent context menus are the best way to go. So rarely used features get pushed to more obscure trigger combinations. At some point they should be dropped entirely but then you'd get nasty letters by the three people that use the feature all the time.

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:13 AM by Smeghead
Apple has it right, Microsoft has it wrong.

This just in, Water is wet, the sky is blue.

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:20 AM by Anon
Shouldn't:

tmClick - g_tmLastClick >= GetDoubleClickTime()) {

be:

tmClick - g_tmLastClick > GetDoubleClickTime()) {

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:36 AM by Raymond Chen
Anon: Yup, good catch. Fortunately this is undetectable in practice since no human being can do things to 1ms precision anyway.

# re: Implementing higher-order clicks

Monday, October 18, 2004 11:04 AM by Henk Devos
Smeghead: Why does Apple get it right and Microsoft wrong?
I just tested on Safari and IE:Mac.
On Safari, 1 click is positioning the cursor, 2 clicks is selecting a word, 3 clicks is selecting a line.
In IE:Mac, this behavior is the same, except that 4 clicks is selecting a paragraph. They are compatible, while offering an extra option that is more useful than selecting a line.
I think Microsoft got it right and Apple got it wrong.

# re: Implementing higher-order clicks

Monday, October 18, 2004 11:56 AM by Smeghead
Not according to the users I have to HELP. ITs always confusion between LEFT or RIGHT and single nad double click. So yes, Apple (or xerox) got it right numbnuts.

# Make sure you understand this

Monday, October 18, 2004 1:06 PM by Alan De Smet
I understand that GetMessageTime's return value occasionally wraps around to 0. But I'm not entirely clear on how your suggestion is better.

The wrong answer is:

tmClick > g_tmLastClick + GetDoubleClickTime()

As far as I can tell, this will actually work in practice. Everyone should get
promoted to LONG. GetMessageTime rolls over based on MAXLONG. So
(g_tmLastClick+GetDoubleClickTime()) should roll over in matching behavior.
That said, relying on roll over and generally overflowing variables is A Bad
Idea, so it's wrong. Am I correct so far?

The right answer is apparently:

tmClick - g_tmLastClick > GetDoubleClickTime()

However, if GetMessageTime rolled over, we've still got problems. tmClick will be small, g_tmLastClick will be very large. (tmClick-g_tmLastClick) will be massively negative and will never trigger. As far as I can tell any click over the rollover boundary will register as double/triple/whatever click.

This leads me to conclude that I need to do something like so (code untested):

LONG clickDelta = tmClick - g_tmLastClick;
if ( tmClick < g_tmLastClick) {
clickDelta += MAXLONG;
}

I suspect I'm overlooking something and would appreciate knowing what.

(Oh, and thanks for the blog, it's fascinating reading.)

# re: Implementing higher-order clicks

Monday, October 18, 2004 1:12 PM by Raymond Chen
Everything gets promoted to DWORD, not LONG. Does that help? (Hint: What if g_tmLastClick = 0xFFFFFFFF - GetDoubleClickTime()?)

# re: Implementing higher-order clicks

Monday, October 18, 2004 2:53 PM by A non anon
Uhm. None of the g_* variables in the sample code are uninitialized, since global variables are zero-initialized.

# nedit

Monday, October 18, 2004 10:00 PM by Johan Thelin
The lovely editor nedit uses multiple clicks in a great way. Single click moves the carret, double click selects a line, tripple click selects a paragraph and, if I'm not missremembering, quadruple(!) click selects the entire document. Quite handy actually :)

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:35 PM by muro
OK, this is nitpicking, but anyway:
shouldn't there be another line at the end?
HANDLE_MSG(hwnd, WM_RBUTTONDOWN, OnRButtonDown);

a pleasure to read this stuff.

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:37 PM by Raymond Chen
Extending to right-clicks is left as an exercise.

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:38 PM by muro
The missing line is actually funny, when you read second paragraph from the end of the article - especially the parenthesis. :-)
but sorry for nitpicking, its lame.

# re: Implementing higher-order clicks

Monday, October 18, 2004 10:40 PM by Raymond Chen
Oh, you're right. I forgot what I wrote. (I wrote it so long ago.)

# re: Implementing higher-order clicks

Tuesday, October 19, 2004 12:06 AM by Cyrus Najmabadi
Ugh, you had to write the code for this now after i spent a good half day getting this working right in some code i was writing. What's worse is that you're the one who told me how to do it, but you weren't willing to take the time to write this up :-P

Oh, and to those of you who are interested this was to add a feature in C# where as you click more and more we start selecting more and more of the C# code you've written based on the parse tree.

So we'll start by selecting the expression, then the statement, the then method, then the class, namespace, etc. Basically each successive click selects one higher level in the parse tree.

it's a feature you grow to love :-)

# re: Implementing higher-order clicks

Tuesday, October 19, 2004 12:30 AM by krisztian pinter
i still not get that DoubleClickTime trick

say
g_tmLastClick == ~4bil
tmClick == 10

now 10 - ~4bil is a large number because of the overflow. it will treated as a double. OK.

but

g_tmLastClick == ~4bil
tmClick == 60000 (click one minute later)

60000 - ~4bil is still a large number, so it will also be treated as double, however it is clearly a single.

can someone explain this to me?

# re: Implementing higher-order clicks

Tuesday, October 19, 2004 2:07 AM by muro
To clarify: consider, the difference calculation is correct:
tmClick - g_tmLastClick really calculates the time difference between the clicks correctly for our purpose, even when an overflow happens.

First, lets show the overflow calculations with 8bits. GetDoubleClickTime() = 0x10:
0x01 - 0xff = 0x01 + ~(0xff) = 0x01 + (0x100 - 0xff) = 0x01 + 0x01 = 0x02 -> double click
0x21 - 0xff = 0x21 + ~(0xff) = 0x21 + (0x100 - 0xff) = 0x21 + 0x01 = 0x22 -> single click


Then the same for 32bit:
GetDoubleClickTime() = 10:
g_tmLastClick == 4bil = e.g.: 2^32 - 10
tmClick == 5:
5 - 4bil = 5 + ~(4bil) = 5 + (2^32 - 10) = 5 + 10 = 15 -> double click


tmClick == 60000 (click one minute later):
60000 - 4bil = 60000 + ~(4bil) = 60000 + (2^32 - 10) = 60000 + 10 = 60010 -> single click

Sometimes it helps to think in lower precision. Makes all the scary big numbers go away :-). And off course - remember that subtraction is addition of complement. Makes it even less scary.

# re: Implementing higher-order clicks

Tuesday, October 19, 2004 2:10 AM by muro
Overlooked in 32 bit part:
GetDoubleClickTime() = 10: would mean, the first 32bit example is also single click (as time difference is 15), but I hope you get the idea.

# re: Implementing higher-order clicks

Tuesday, October 19, 2004 2:50 AM by krisztian pinter
thanks, muro!

in my mind, result of substraction was the same as the distance. in a "modulo world", it is not that easy.

# re: Implementing higher-order clicks

Tuesday, October 19, 2004 8:05 AM by Raymond Chen
You can think of modulo arithmetic as points around a circle. Subtraction gives you the distance along the circumference, which doesn't care where your zero marker is.

Another way of looking at subtraction is to view it as a translation of the circle, which is isometric (distance-preserving).

# re: Implementing higher-order clicks

Thursday, November 11, 2004 3:58 PM by Joshua Schaeffer
Merle, triple clicks in Word are one of the biggest time savers for me. Sometimes I'm in this utmost lazy chair position where even using right-click is a tiresome expense of a few seconds. Although I personally wouldn't mind a "click chart" like so:

1 - (normal)
2 - word
3 - sentence
4 - paragraph
5 - everything

# Using modular arithmetic to avoid timing overflow problems

Tuesday, May 31, 2005 9:03 AM by The Old New Thing
Avoiding timing overflows is easier than you think.

# Clairvoyant Interaction | Chui&#8217;s counterpoint

Sunday, August 24, 2008 12:41 PM by Clairvoyant Interaction | Chui&#8217;s counterpoint
New Comments to this post are disabled
 
Page view tracker