Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Exploring Secrets of .NET Keystroke Handling : Page 5

If you've ever been frustrated trying to figure out how to intercept or assign specific keystrokes to specific controls, you'll be glad you found this article.


advertisement
Practical Techniques
Now that you've seen the details of how .NET handles keystrokes, and how you can use Keystroke Sandbox to examine what actually happens at runtime, you have a good foundation for using the practical techniques presented in the final portion of this article. These techniques will guide you in wiring up appropriate keystroke-handling mechanisms for specific use cases.

Bind a Key Combination to a Menu Item
 
Figure 11. Defining a Menu Item Shortcut: The figure shows the process of binding the key combination Ctrl+5 to the fixed menu item labeled "Fixed menu with shortcut."
This is the most common approach for managing keystrokes, and Visual Studio makes it simple. With the designer open, select the menu item of interest, and then select ShortcutKeys in the Property pane. Click the drop-down arrow that appears, and then enter the shortcut key along with any desired modifiers. The shortcut you enter appears in the designer view of your form, as shown in Figure 11.

Note that just below the ShortcutKeys property you also have a ShowShortcutKeys property. That value must be set to True for the key combination to appear in the menu. One other useful property (not shown) is ShortcutKeyDisplayString. This optional property lets you override the default display for the key combination you have selected (not used in this example: Ctrl+5 is perfectly adequate). If, however, you wanted to display the keystroke combination Alt plus a period (Alt+.), the default display would show Alt+OemPeriod. Using ShortcutKeyDisplayString you could change this to a more user-friendly Alt+Period or Alt+dot or even Alt+. .

When you bind a key combination in this fashion, a user can simply press Ctrl+5 to invoke the menu item without ever having to open or access the menu at all. This is a quick and easy way to provide a set of keyboard commands to drive your program.

 
Figure 12. Autogenerating Overrides: Visual Studio Intellisense lists available overridable methods based on your initial keystrokes and inserts skeleton code for the method you select.
Bind a Key Combination to a Command without an Associated Menu Item
The first hurdle you are likely to consider when trying to expand beyond the above technique is: How do you use a key combination to initiate a particular command not tied to a menu item? For this you need to delve under the covers just a bit, adding a hook into the .NET framework. Specifically, you need to override one or more of the methods common to every Control listed in Table 2. ProcessCmdKey is the most appropriate for this purpose.

Type override in the code editor followed by a space. When you enter the space, Intellisense pops up with a list of all overridable methods. Select ProcessCommandKey—either by typing its initial characters or by scrolling in the Intellisense list, and then press Tab to auto-generate the complete overridden method (see Figure 12).

The stub method override that Visual Studio inserts simply calls the original base method. To make it do something different, you need to insert code before the single line of code provided. Assume, for this example, that you want Alt+M to perform some action not available on a menu. Here's the altered method:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if ((keyData & Keys.Alt) > 0 && (keyData & Keys.KeyCode) == Keys.M) { DoSomething(); return true; // indicate key has been handled } return base.ProcessCmdKey(ref msg, keyData); }

This overridden method gets invoked for almost any keystroke a user might happen to type. Therefore, the gatekeeper code added in the example above ensures that both the Alt key and the M key are activated before calling the DoSomething() method and returning true. The return value is important: it signals that you have handled the key(s) and that no further propagation is necessary. If the user did not press Alt+M, the method calls the base method, which will return an appropriate Boolean value.

Modify Mouse Actions Based on Modifier Keys
Suppose your Form contains a Button for which you want to take one action in most cases, but a different action when users depress the Shift key while clicking the button. The Control.ModifierKeys is a static (class) property that acts as a convenient handle for you to examine the state of the Control, Shift, and Alt modifier keys at any point in your application. For example, you can check for the Shift key modifier during a mouse click with code similar to the following:

private void loadButton_Click(object sender, EventArgs e) { if (Control.ModifierKeys == Keys.Shift) DoAlternateAction(); else DoPrimaryAction();

}

What if the user happens to press both Control and Shift? Because the previous code fragment checks for a single, specific modifier key, the above code would not invoke DoAlternateAction(). The Control.ModifierKeys property, though, is a bitwise combination of all modifier keys active at any given moment. To look for the Shift key irrespective of any other modifier keys, you would need to write the click handler like this:



private void loadButton_Click(object sender, EventArgs e) { bool shiftPressed = (Control.ModifierKeys & Keys.Shift) > 0; if (shiftPressed) DoAlternateAction(); else DoPrimaryAction();

}

The preceding code works because Keys.Shift (and similarly Keys.Alt and Keys.Control) are single bits. If this were not the case the simple logic shown would not suffice.

Similarly, if you want your alternate action to be invoked when a user presses a specific combination of modifier keys instead of just a single one, you can test multiple keys. For example, this fragment requires both the Control and Shift keys to be depressed before it will call DoAlternateAction(). It does not matter whether the Alt key is pressed:

private void loadButton_Click(object sender, EventArgs e) { bool ctrlPressed = (Control.ModifierKeys & Keys.Control) > 0; bool shiftPressed = (Control.ModifierKeys & Keys.Shift) > 0; if (ctrlPressed && shiftPressed) DoAlternateAction(); else DoPrimaryAction();

}

Bind Multiple Key Combinations to a Single Menu Item
In the immediately preceding sections you learned:

  • How to bind shortcut keys to menu items.
  • How to bind shortcut keys without associated menu items.
  • How to work with multiple modifier keys.
 
Figure 13. Multiple Shortcuts on a Single Menu: (1) Create a separate menu item for each key shortcut. (2) Create a single shortcut and show only one of the possible shortcuts. (3) Create a single shortcut and show all shortcuts.
This section leverages all those techniques to enable you to bind multiple key combinations to a single menu item. Consider the scenario where you want to create a menu item binding Ctrl+F to some action. But you also want Ctrl+Shift+F to do the same action. (This tip applies whether the desired action is the same or not; it is just more poignant in this example to suppose it is the same.)

Approach 1: Create a menu item for each key combination.
Each menu item would indicate a different shortcut key. Both menu items would reference the same Click event handler. See Figure 13, item 1. You saw how to do this using the visual designer in the "Bind a Keystroke to a Menu Item Command" section. But because this approach is something users do not typically expect it would be rather confusing, to say the least!

Approach 2: Use a single menu item invoked on both an advertised key combination and a second, hidden key combination (Figure 13, item 2).
Having unadvertised or hidden key combinations, one could argue, is useful for expert users on occasion, or even for diagnostic purposes. But it is not very user-friendly… or is it? Say, for example that your application happens to attract the user to invoke this sequence often: Shift+G, Shift+T, Ctrl+F, Shift+X. Using the conventional approach, it would be awkward and unfriendly for the user to have to release the Shift key just before the Control+F then reapply it afterwards. By making Control-Shift-F do exactly what Control+F does, the user can continue to hold down the Shift key while invoking it, even though technically it is the "wrong" key combination.

To implement this, consider this problem as two subproblems. The first subproblem is identical to Approach 1 above, discussed in the section "Bind a Keystroke to a Menu Item Command." So set up a menu item that specifies Ctrl+F as its shortcut key. The second subproblem is identical to that discussed in the section "Bind a Keystroke to a Command without an Associated Menu Item." In other words, you would override ProcessCmdKey with this:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if ( (keyData & Keys.Control) > 0 && (keyData & Keys.Shift) > 0 && (keyData & Keys.KeyCode) == Keys.F) { FindElement(); return true; } return base.ProcessCmdKey(ref msg, keyData); }

Approach 3: Use a single menu item that displays multiple shortcut keys (Figure 13, item 3).
If you are adamantly opposed to unadvertised keys, you can display all keystrokes on a single menu item. To do that, create a menu item in the visual designer but do not assign a shortcut key. Instead, find the ShortcutKeyDisplayString property for the menu item and manually set it to the list of key combinations you wish to display—in other words, literally type in Ctrl+F, Ctrl+Shft+F. You need to override ProcessCmdKey much like the previous example, but here the logic is even simpler:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { // Identify Ctrl-F while ignoring the Shift key if ((keyData & Keys.Control) > 0 && (keyData & Keys.KeyCode) == Keys.F) { FindElement(); return true; } return base.ProcessCmdKey(ref msg, keyData); }

Remember, the goal is to invoke the same action for two different key combinations. Thus, you do not need to test the Shift key state in the preceding code. To apply this technique to perform different actions with different modifiers, simply add the logic to test for those, just as described in the section "Modify Mouse Actions Based on Modifier Keys."

Remapping Keystrokes of Child Controls
Generally speaking, when a particular control in your application has focus, and the user presses a key, that control is first to receive the key. If that control doesn't handle it, the keystroke bubbles up the control hierarchy; next, its parent gets the keystroke, and so on, up to the parent form. The Form.KeyPreview property alters this path, letting the form see OnKeyDown, OnKeyPress, and OnKeyUp events before any child controls. Figure 14 shows two different key sequences when you press the A key: the first without KeyPreview and the second with KeyPreview. In both scenarios the inputTextBox control has the focus (as shown in the black header bar). Without KeyPreview, then, the text box receives the A and processes it: it is stored in the buffer and displayed, as you would expect. The text box consumes the keystroke—it never goes to any other control's handler for those three events. Look for the OnKeyDown, OnKeyPress, and OnKeyUp key panels in the first sequence—all other key panels have been grayed out for clarity (a handy feature of Keystroke Sandbox: just clicking a key panel toggles its grayed out state).

However, when you set KeyPreview to true, a different set of actions occurs, diagrammed in the second key sequence. Again, look just at the OnKeyDown, OnKeyPress, and OnKeyUp events. In this sequence, the form sees the key first for each of the three events. This gives your application the vital control to handle keystrokes that override any actions assigned by subordinate controls.

For example, suppose you have an editor control called DuplicatingTextBox, where Ctrl+L pastes the contents of the clipboard into the buffer twice. You want to use this control in your new application for other reasons but you want to deactivate the functionality of Ctrl+L. To do that, turn on KeyPreview, check for that keystroke combination, ensure that the DuplicatingTextBox has the focus and then consume the key. That way, the DuplicatingTextBox will never see it.

 
Figure 14. Sandbox Output: The key A activates different methods based on whether the form's KeyPreview property is enabled or not. Here the focus is on the TextBox.
When programming key-handling overrides you may feel at times that it is not deterministic. There are scenarios in where repeating a key combination results in different key sequences in Keystroke Sandbox. Figure 15 shows one example. Both rows show the same two key sequences as in Figure 14; however, in Figure 14 the focus was on a TextBox while in Figure 15 the focus is on a CheckBox. Naturally, you would expect the key panels for the TextBox to be omitted in the latter figure, but actually a new key panel appears—ProcessMnemonic—that did not appear in Figure 14. And it's invoked by the TextBox! (You would see the same method invoked by the DataGridView if it were not disabled—even though the DataGridView was never in the picture at all.) This happens because when a TextBox has focus and you type an A the TextBox consumes it. But when a CheckBox has the focus it doesn't consume the A; instead, the keystroke bubbles up to the Form level. The Form, in turn, checks whether any of its controls have a mnemonic (thus the ProcessMnemonic panel) that matches the keystroke and, if so, it performs the associated action. One common scenario where this occurs is when you have assigned mnemonics to Button controls, so users can press the buttons with the keyboard rather than with the mouse.

 
Figure 15. Sandbox Output: The key A with and without the form's KeyPreview property enabled. This time, though, the focus is not on the TextBox.
So far, you've been following the sequence of OnKeyXyz handlers. But note that other methods are always invoked first by the subordinate control that has focus, even when KeyPreview is enabled. Look at the first two frames of both key sequences in Figure 14. Regardless of the KeyPreview setting, the application invokes the TextBox handler for ProcessCmdKey before the ProcessCmdKey handler of the top-level form. That is, KeyPreview gives the form an advance look at regular characters; navigational, mnenomics, shortcuts, etc. follow the same route regardless of this setting.

Controlling the Tab Key
When simple controls (Buttons, Labels, CheckBoxes, etc.) have the focus, pressing the Tab key advances the focus to the next control in either a default or an assigned sequence. You may want to alter this behavior. If you are using a RichTextBox as a custom text editor, for example, you want the user to be able to type a Tab character in the RichTextBox rather than advancing the focus. Similarly, in a DataGridView, you might want Tab to move focus from cell to cell within the DataGridView, rather than advance to the next control. Altering the behavior of the Tab key depends on the particular control of interest. A DataGridView is the easiest one to adjust: Just set its StandardTab property to false. Standard tab behavior (true) is to move to the next control, so setting it to false makes the Tab keystroke stay within the DataGridView, moving focus from cell to cell.

For most other controls you need to override ProcessDialogKey to achieve the same effect. The code shown below exposes the ControlTabInsertsTab property, which lets you decide at control instantiation how you wish the Tab key to behave.

Author's Note: If you prefer, you could call this property StandardTab, to match the corresponding property in the DataGridView. I chose the name ControlTabInsertsTab because I have used it only with a customized RichTextBox, where this name makes more sense to me.

The code below provides two additional properties that are more elegant when working with a TextBox or similar control, allowing you to choose (via the ExpandTab property) whether the Tab key inserts a Tab character or the equivalent number of spaces. If the latter, you can decide (via the Spaces property) how many spaces to insert:

// When set to true, Control-Tab stays within control. // When set to false, Tab stays within control. public bool ControlTabInsertsTab { get; set; } // When set to true, inserts spaces; otherwise, a tab character. public bool ExpandTab { get; set; } // Set this to the number of spaces to insert when ExpandTab is true. public string Spaces { get; set; } protected override bool ProcessDialogKey(Keys keyData) { Keys keyCode = (keyData & Keys.KeyCode); // operate only on TAB and CTRL-TAB; skip if other modifiers depressed if (keyCode == Keys.Tab && ((keyData & Keys.Alt) == 0) && ((keyData & Keys.Shift) == 0)) { if (!ControlTabInsertsTab) // invert the sense { if ((keyData & Keys.Control) > 0) { keyData &= ~Keys.Control; } else { keyData |= Keys.Control; } } else { /* already has correct sense */ } // Now internally, Ctrl-Tab always means insert tab if ((keyData & Keys.Control) > 0) { SelectedText = ExpandTab ? Spaces : "\t"; return true; } // ... while unmodified tab continues through with standard behavior } return base.ProcessDialogKey(keyData); }

One related method to be aware of is ProcessTabKey which lets you explicitly select the next control in the current container and give it focus, just as if you had pressed the Tab key.

Close and Cancel a Subform with Escape
There are three situations when you may want users to be able to close a form by pressing Escape—a long-standing convention:

  1. Standard .NET dialogs such as OpenFileDialog and SaveFileDialog: These are the simplest case: They automatically accept Escape as equivalent to pressing the Cancel button on the form.
  2. Custom subforms/dialogs with a Button that cancels the form: This is very straightforward: just set the CancelButton property of the Form to your cancel button. This binds the Escape key to the button's Click handler, thereby performing whatever action you have defined for the button.
  3. Custom subforms without a Cancel button: This requires only a couple of lines of code, putting into practice what you have learned about key handling. Depending on your needs, you could have the Escape key either hide or close the subform:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (keyData == Keys.Escape) { Hide(); // could use Close() instead of Hide() depending on your needs return true; } return base.ProcessCmdKey(ref msg, keyData); }

Closing a Subform with Enter
Analogous to cancelling a subform with the Escape key, you may want to accept or acknowledge settings by just pressing Enter or Return, like standard .NET dialogs do. To do this, set the AcceptButton property to any existing Button on your form. If you do not have a suitable button, use code similar to the preceding example, checking for Keys.Enter.

Acting On Other Single Keystrokes
Besides Enter and Escape, you may want to perform actions on other single keystrokes. You do this by setting mnemonics on the displayed text of any Control that has a UseMnemonic property. (Not all Controls do so you will not find this among the base Control properties.)

To activate a mnemonic you must do two things: include an ampersand in the Text property of the control preceding the mnemonic letter, and set UseMnemonic to true. As an example, in Keystroke Sandbox, the Text property of the Load Grid button, for example, is set to Load &Grid, so pressing G (when focus is set to something that does not consume the keystroke) is equivalent to clicking on the Load Grid button. (Note that pressing Alt+G is more forgiving, as it will work irrespective of where the focus is.)

Working with DataGridViews
Just when you think you have covered it all… it turns out that the DataGridView, due to its complexity, provides quite a few more methods that you may override, giving you a good deal of control over all aspects of its behavior (see Table 5). The table does not include methods that handle raw messages.

Table 5. Common Key-Handling Methods in a DataGridView: The DataGridView gives you many possible keystroke-handling methods to override.
Method Processes…
ProcessAKey The <SPAN CLASS="PF">A</SPAN> key.
ProcessCmdKey A command key.
ProcessDataGridViewKey Keys used for navigating in the DataGridView.
ProcessDeleteKey The DELETE key.
ProcessDialogChar A dialog character.
ProcessDialogKey Keys such as Tab, Escape, Enter, and arrows,used to control dialog boxes.
ProcessDownKey The DOWN ARROW key.
ProcessEndKey The END key.
ProcessEnterKey The ENTER key.
ProcessEscapeKey The ESC key.
ProcessF2Key The F2 key.
ProcessHomeKey The HOME key.
ProcessInsertKey The INSERT key.
ProcessLeftKey The LEFT ARROW key.
ProcessMnemonic A mnemonic character.
ProcessNextKey The PAGE DOWN key.
ProcessPriorKey The PAGE UP key.
ProcessRightKey The RIGHT ARROW key.
ProcessSpaceKey The SPACEBAR.
ProcessTabKey The TAB key.
ProcessUpKey The UP ARROW key.
ProcessZeroKey The 0 key.

Sending Keystrokes Programmatically
Occasionally you may have a need to send keystrokes to your application programmatically, such as when automating GUI control tests. The .NET framework provides the SendKeys.Send method to do just that. You'll find that the linked method documentation provides all the details you need for sending special keys, and includes a small sample as well, so there's no need to repeat that here.

Assigning an Event Handler vs. Overriding an OnKeyXyz method?
If you add a Button to an application you are designing and you want that button to perform some action when you click it, you assign an event handler, or delegate, to the Click event of the Button:

loadButton.Click += new System.EventHandler(loadButton_Click);

You use a similar approach to key events. If, for example, you want to process certain keys for a TextBox, you might use:

customTextBox.KeyUp += new System.EventHandler(customTextBox_KeyUp);

Assigning a handler lets you customize your child TextBox behavior externally (from the application) rather than internally (from within the custom TextBox class itself). It also lets you create different behaviors for different instances of your custom TextBox. To create a custom TextBox that processes keystrokes the same way for every instance, wire up the KeyUp event internally. Do this either by attaching a delegate to the KeyUp event (like that shown above) or by overriding the OnKeyUp method (similar to the many overridden methods you have seen earlier). The whole purpose of the OnKeyXyz methods is that they allow derived classes to handle events without attaching delegates. The MSDN documentation, in fact, states that that is the preferred technique for handling the event in a derived class. See, for example, OnKeyDown. So internally, while you could either assign a delegate to KeyUp or override the OnKeyUp method, prefer the latter.

There are probably other keystroke-handling methods hidden in the MSDN documentation, but the ones you've seen in this article—and the downloadable code for the Keystroke Sandbox application—should cover most situations. If you do run across others, please let me know.



Michael Sorens is a freelance software engineer, spreading the seeds of good design wherever possible, including through his open-source web site, teaching (University of Phoenix plus community colleges), and writing (contributed to two books plus various articles). With BS and MS degrees in computer science and engineering from Case Western Reserve University, he has worked at Fortune 500 firms and at startups, using C#, SQL, XML, XSL, Java, Perl, C, Lisp, PostScript, and others. His favorite project: designing and implementing the world's smallest word processor, where the medium was silicon, the printer "head" was a laser, and the Declaration of Independence could literally fit on the head of a pin. You can discuss this or any other article by Michael Sorens here.
Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap