Replaying input is not the same as reprocessing it


Once upon a time, there was an application that received some input and said, "Okay, this input cancels my temporary state. I want to exit my temporary state, but I also want the input that took me out of the temporary state to go to whatever control would have received the input if I hadn't been in the temporary state in the first place." (For example, you might want the input that dismisses a pop-up window to be acted upon rather than eaten by the pop-up.) The application decided to solve this problem by regenerating the input message via Send­Input, so that it goes back into the input queue. The theory, is that when the message pump pulls the regenerated input out of the queue, the temporary state will not be present, and the message will be routed to the correct window.

I raised concerns that this technique would create problems with input reordering and multiple-processing, but the customer decided to stick with their original design.

Time passed, and I had forgotten about this application.

Some months later, another question came in: "We find that when the system is under load, we sometimes get into a state where dismissing our temporary state results in the mouse button getting 'stuck' down. i.e., the user physically releases the mouse button, but we get spurious WM_LBUTTON­DOWN with no matching WM_LBUTTON­UP."

The customer, it turns out, was the same one I had cautioned earlier about the dangers of replaying input.

When you get input, that is your chance to process the input. If you decide you don't want to deal with the input right now and replay it via Send­Input, you create a few new problems:

First, you've caused everybody else who is looking at input states to see a second copy of your replayed events. If it were a keyboard event you replayed, a keyboard hook (or any code which subclassed your window) would see a key go down twice. If there were any mouse hooks, they would see the button go down twice. This is particularly confusing because the mouse button doesn't autorepeat. How can it go Down two times in a row without an intervening Up?

Second, if there is other input in your queue, you just rearranged input events. For example, suppose the input queue consists of the following events:

WM_LBUTTON­DOWN
WM_LBUTTON­UP

You retrieve the first message (the button-down), resulting in the following input queue:

WM_LBUTTON­DOWN
WM_LBUTTON­UP

For illustrative purposes, I crossed out the message that is no longer in the queue, so you can see where it used to be.

Now you decide to replay that message via Send­Input. This appends the event to your queue, resulting in

WM_LBUTTON­DOWN
WM_LBUTTON­UP
WM_LBUTTON­DOWN

Your message pump runs, it processes the button-up event ("Huh? How did I get an Up without a Down?"), and then it processes the button-down event. There are no further events, so the mouse button is down and gets stuck that way.

You can imagine what other sorts of bad things can happen if an event in the queue is, say, a press or release of the shift key. Oops, the user clicked the Delete button and then hit the shift key afterwards to type a capital letter A, but due to your input reordering, your code saw it as a Shift+Click on the Delete button, and the item was deleted without confirmation.

When you get an input message, that is your chance to process it. If you decide that you want to hand the message off to somebody else, you have to do it during the processing of that message. If you try to process it at some other time, the input states may not be right.

Comments (19)
  1. alegr1 says:

    "I'm too lazy to go get a screwdriver, so I'll just use a hammer for this screw. When I get a screwdriver, I'll just pull the screw out and drive it properly to the same hole."

  2. Just yesterday I had to fix a bug… For some reason, long ago in our software someone decided to create a custom subclassed version of CButton for checkboxes. They handled the action of checking the box by watching for LBUTTONUP. The way other messages were handled was mangled – many passing through, but other being handled and reimplemented for no apparent reason. Of course, there were no comments explaining why. Recently, a customer discovered that when tapping the screen to wake the system up from a blank screen, it was possible to toggle a checkbox without any of the normal warnings screaming "you've changed something" or other visual side effects of checking that box. They changed a few other unrelated settings, then ran the software, and had issues caused because of the setting being checked.

    It turns out that the bad implementation allowed the LBUTTONDOWN to wake up the system, and the up event checked the box. The base class never got a complete click, so our event handler never triggered even though the state changed. Completely removing the subclassing and using a standard CButton fixed the problem – we're still testing to make sure we can't figure out what drove the original developer to implement it in the first place.

  3. Joshua says:

    My solution to the problem was slightly more sane. I ran a second copy of the message loop in the input receiver until the temporary state was cleared (basically one cycle) then processed the input.

  4. Henning Makholm says:

    In an ideal world the function would probably have been called PostInput instead.

  5. Lockwood says:

    Can I make a preemptive comment about autosorting the message queue, since WinGustavsonwhatever hasn't said anything on that line for a while?

  6. DWalker says:

    Yep, painting a wall red and then blue is not the same as painting the wall blue and then red.  The same is true of shift states, mouse clicks, etc….

  7. spork says:

    perhaps a CBT hook would be a better fit for the user's use case.

  8. Jonathan says:

    Re-processing input queues reminds me of something that used to happen with DOS Hebrew support. There were TSRs that hooked the keyboard interrupt (INT9), and changed English keystrokes into their Hebrew key equivalents. If there were 2 different TSRs loaded, this could result in double-translation: User pressed '/', TSR1 translated that into '.', and TSR2 translated that into ץ.

  9. benjamin says:

    I used to see a case where the right mouse button acted as if it was stuck down all the time in XP. I wonder if I was running some program that was doing something similar.

  10. Entegy says:

    Lockwood, if you read his Twitter, he apparently "gave up" on Microsoft.

    [He seems awfully engaged for somebody who gave up. -Raymond]
  11. Joshua says:

    [He seems awfully engaged for somebody who gave up. -Raymond]

    I gave up a decade ago. The company that hired me runs a Windows shop. The UNIX tools have given me such power as no one else in the company had seen before, and few could match.

    But we're developing on Windows so for the moment I have to care. I'm not buying MS stock though. Too high a risk.

  12. BC_Programmer says:

    How did we go from "Be careful messing with your input queue for the purpose of reprocessing" to a lesson in Microsoft Stock purchases?

  13. alegr1 says:

    The UNIX tools have given me such power as no one else in the company had seen before, and few could match.

    Behold! Kneel before me, puny humans!

  14. Drak says:

    Interesting.. This could be the cause of a certain browser 'typing' every character twice into an input box in a dialog window. Except for control keys (backspace, arrows, etc.). Thanks for this insight, Raymond.

  15. ErikF says:

    Replaying events [aka Macro Recorder for everyone who remembers :-)] always seemed a little brittle and/or dangerous even before I knew about the Windows event model. It sort of reminded me of Logo, where you told the turtle to move but if the turtle wasn't where you thought it would be, all sorts of fun would happen (like the turtle running into walls or falling off the table)!

    The only safe method of replaying events that I can think of is in your own application, where you can send yourself a list of what specific actions were queued up.

  16. Key events getting eaten is a frequent bugbear for me – easy to encounter with remote control and virtualisation software. Use a keyboard shortcut to switch away from the remote control tool – the modifier keys' keydown events get relayed to the target machine, the keyup stays on the host because you've switched focus to another application by that point.

    In this case, presumably the best way would be to pretend not to have handled it, so the message gets passed on to the next recipient – if you're trying to record whether the user has clicked inside a particular area yet, but still want clicks to get handled by whatever they clicked on: set the 'user has clicked here' flag, then return "these aren't the wndprocs you're looking for, keep looking"? If it's a pop-up window and you want to pass the click through to the window underneath as if you weren't there, that'll be harder – though it's not something I can see much need for, being a pretty bizarre goal.

  17. JoeWoodbury says:

    The UNIX tools have given me such power as no one else in the company had seen before, and few could match.

    Reminds me of the conversion I've had more than once:

    Him: "These UNIX tools let me do this!"

    Me: "I've never needed to do that and can't conceive of ever needing to do so in the future."

    Crickets chirping.

    Him: "These UNIX tools let me do this!"

  18. Douglas says:

    @j3anders

    It is correct to test button UP for mouse actions, but that's only the last step. The full procedure goes something like this:

    Button Down : If hit, capture the mouse.

    Button Up : If mouse is captured, un-capture it, and if hit, take action.

    You can see this with the standard controls by clicking on something like a checkbox, keep holding and move the mouse away, then release. No check happens. Try again, but release when you return to the checkbox, it checks.

  19. @Douglas,

    That's correct, which is why we were having issues. When I saw that the custom checkbox had a mouse up but not a mouse down handler I knew exactly what was going on.

Comments are closed.

Skip to main content