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?

Comments (35)
  1. Carlos says:

    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 1×1 rectangle rather than an empty rectangle.

  2. anonymouse says:

    A more important question would be:

    "Does Raymond Chen know everything!"

  3. asdf says:

    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.

  4. Chris Becke says:

    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 :)

  5. Chris Becke says:

    hmmm. isn’t that ironic.

  6. Matthew Riley says:

    g_rcClick is of indeterminate value the first time through this function and is passed to PtInRect before initialized… might this pose a problem?

  7. Raymond Chen says:
    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.

  8. Nicholas Allen says:

    Merle-

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

  9. Merle says:

    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. ;-)

  10. Merle says:

    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…

  11. Nicholas Allen says:

    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.

  12. Smeghead says:

    Apple has it right, Microsoft has it wrong.

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

  13. Anon says:

    Shouldn’t:

    tmClick – g_tmLastClick >= GetDoubleClickTime()) {

    be:

    tmClick – g_tmLastClick > GetDoubleClickTime()) {

  14. Raymond Chen says:

    Anon: Yup, good catch. Fortunately this is undetectable in practice since no human being can do things to 1ms precision anyway.

  15. Henk Devos says:

    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.

  16. Smeghead says:

    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.

  17. Alan De Smet says:

    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.)

  18. Raymond Chen says:

    Everything gets promoted to DWORD, not LONG. Does that help? (Hint: What if g_tmLastClick = 0xFFFFFFFF – GetDoubleClickTime()?)

  19. A non anon says:

    Uhm. None of the g_* variables in the sample code are uninitialized, since global variables are zero-initialized.

  20. Merle says:

    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.

  21. Johan Thelin says:

    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 :)

  22. muro says:

    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.

  23. Raymond Chen says:

    Extending to right-clicks is left as an exercise.

  24. muro says:

    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.

  25. Raymond Chen says:

    Oh, you’re right. I forgot what I wrote. (I wrote it so long ago.)

  26. 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 :-)

  27. 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?

  28. muro says:

    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.

  29. muro says:

    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.

  30. krisztian pinter says:

    thanks, muro!

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

  31. Raymond Chen says:

    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).

  32. 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

  33. Avoiding timing overflows is easier than you think.

Comments are closed.