2009-06-10

I'll handle any key you want... except that one

Here's one of those that makes a little more sense when you think about it, but it still leaves you scratching your head until you figure out what's going on. And then you think about it some more and realize, no, it doesn't make sense at all.

When you want to capture keypresses in a control on a Windows form, the "best" place to do that is the KeyDown event. It gives you the ability to capture keys, the state of the modifier keys (Control, Alt, Shift), and to mark that keypress as being "handled" so the key does not continue on as actually being typed. It also provides access to capturing unprintable keys (like Escape or Insert).

However, one thing it does not do is let you capture the Tab key. This is because the control doesn't even get a KeyDown event when the Tab key is pressed. (Indeed, Microsoft considers it a bug when it does trigger a KeyDown.)

You can sort of rationalize this behavior when you realize that the Tab key is "special". The form uses that key to determine when to move from one control to the next. So, in that way, it would make sense for the Tab key to not even get to the control. However, you can easily debunk that rationalization with two controls: a DataGridView and a TextBox. A DataGridView, with its StandardTab property set to the default of False, will, when a Tab key is pressed, take it upon itself first to move the active cell through the grid, and only when the last cell is reached then let the form move focus to the next control. Likewise, a TextBox that has both Multiline and AcceptsTab properties set to True will take any Tab keypresses and add a tab character to the input, never passing that keypress to the form.

In any case, it seems that, for whatever reason, the Tab key is handled by the form sometime after these possible control overrides, but before the KeyDown event could get fired.

Solution? Well, it turns out, there are two.

The first one I found is to override the form's ProcessCmdKey method. Start by calling the base ProcessCmdKey method, which returns a boolean that indicates whether the key was handled. Then, check to see if this.ActiveControl is the control for which you want to trap the Tab key, and if the keyData parameter passed in to ProcessCmdKey contains the Tab key ((keyData & Keys.Tab) == Keys.Tab). If so, do whatever it is you wanted to do, and set the boolean return value to true, indicating you've processed the key yourself. At the end of the method, return the boolean. (I found a simple example here.)

The annoying part is, if you were trapping for more than just the Tab key in the control's KeyDown event, you now have your logic split in two different methods (control_KeyDown and form override ProcessCmdKey). Also, if you are doing this for multiple controls (as I was for, coincidentally, a DataGridView and a [single-line] TextBox), you have to copy and combine the logic from the other controls' separate KeyDown events into the ProcessCmdKey method, testing the ActiveControl to make sure you know which control you're in.

The advantage, however, is that you only have to test the cases you're interested in. If, for instance, you want to catch a Tab, but still allow a Shift+Tab to default to the standard form's handling, you just test for an un-shifted Tab and do your work, and let the Shift+Tab condition "fall through".

The second solution is to bind to the controls' PreviewKeyDown event. This one fires in that sweet spot before the form claims it as a "command key". The event gets the parameter PreviewKeyDownEventArgs e. If you want to handle the Tab key in the KeyDown event, just check to see if e.KeyCode == Keys.Tab, and if so, set e.IsInputKey = true.

The disadvantage to this method, however, is that now you are responsible for moving the focus as appropriate — you can't just let the handling "fall through" to the form, because it won't handle it anymore. Depending on how confident you are on the structure of your form, you can either call another control's Focus method or the form's SelectNextControl method (and don't forget to set the Handled property in KeyDown, otherwise the new control might receive the same Tab keypress as well, or it might try to type the Tab as input into the old control — either way, the results might not be pleasant).

Personally, I like the second solution better. Although there is more logic required to do what should be standard Tab handling, it means the logic for my non-standard Tab handling is in one place per control, not split across two different events.

1 comment:

Yakko Warner said...

Addendum: Apparently, you should also be aware that, in a KeyDown event, setting the Handled property of the event args doesn't actually suppress the key; you also want to set the SuppressKeyPress flag.

If you want a low-level explanation, this blog post (and the comments therein) seem to suggest it has to do with a difference in the way Windows Forms queues and/or handles window messages.

The high-level view, though: if you don't SuppressKeyPress, you may end up wondering why the heck your TextBoxes beep at you when you tab out of them. Then you might, say, turn your text box into a multi-line to see what happens, notice it's getting a tab character inserted into its text area, and wonder why Handled isn't handling it; which, if you're lucky, will eventually have to stumbling upon the SuppressKeyPress property, which might send you googling for an answer as to why there's a second property that does what the first one is supposed to, but doesn't. (Note the sample code in the MSDN article that seems to suggest Handled aborts a key press, even though the Remarks suggest it doesn't, although for the life of me I can't figure out why it shouldn't besides they just made it so.)