Building Keyboard Accelerators into .NET Windows Forms Applications

by Kenn Scribner of Wintellect

Accelerator tables aren’t the sexiest thing to code in Microsoft .NET. But ask any professional user interface designer and you’ll find that keyboard acceleration is usually considered a critical application feature. Rare is the user that has not placed data on the clipboard using Ctrl+C and pasted it into another application using Ctrl+V. Users expect such functionality in GUI applications, and .NET Windows Forms applications are no exception.

Most GUI programming frameworks simplify the task of incorporating keyboard accelerators in your software. MFC, for example, lets you edit an accelerator table that maps key combinations to command IDs. At compile time, the table gets compiled into a resource that MFC loads at run-time by calling the LoadAccelTable API function and enacts by calling TranslateAccelerator inside the message loop.

The .NET Framework is an exception to the rule. While Windows Forms make it easy to provide keyboard accelerators for menu commands, they offer no obvious way to provide accelerators for commands not featured in menus. How, then, does a developer writing a Windows Forms application provide keyboard accelerators for commonly used features that aren’t exposed through menu commands?

A convenient solution comes in the form of the virtual Control.ProcessCmdKey method, which is inherited by the Form class and called by the .NET Framework during message processing. ProcessCmdKey can be overridden in a derived Form class and used to add “command keys”—another term for keyboard accelerators—to an application. It even works if a child control has the input focus, because calls to it bubble up through the window hierarchy.

Here’s a five-step recipe for using ProcessCmdKey to build custom keyboard accelerators into a Windows Forms application. It assumes that you want to include accelerators for four commands that don’t appear in the application’s menus: Home, Save, Print, and Logout.

1) Define an enumeration that represents the accelerator commands:

private enum Accelerators
   { Unspecified = 0, Home, Save, Print, Logout };

2) Create a hash table to contain the enumerated values:

Hashtable _accelHash() =  new Hashtable();

3) Create a class to represent accelerator keys:

public class AcceleratorKey
{
    private Keys key_ = Keys.None;
    public AcceleratorKey()
    {
    }

    public AcceleratorKey(Keys key)
    {
        key_ = key;
    }

    public Keys Key
    {
        get { return key_; }
        set { key_ = value; }
    }

    public override Int32 GetHashCode()
    {
        return (Int32)key_;
    }

    public override bool Equals(Object obj)
    {
        // It is unlikely that two hashcodes would
        // be equal... :)
        if ( obj.GetHashCode() == (Int32)key_ ) return true;

        return false;
    }
}

The primary reason for defining your own AcceleratorKey class is to control the hash value returned by GetHashCode. The Hashtable uses the hash code to compute a hash table index, so you want every item added to the Hashtable to hash to a unique value.

4) Load the hash table, using the Keys enumeration already defined in System.Windows.Forms to identify keys and key combinations:

_accelHash.Add(new AcceleratorKey(Keys.Alt|Keys.H),
                                  Accelerators.Home);
_accelHash.Add(new AcceleratorKey(Keys.Alt|Keys.S),
                                  Accelerators.Save);
_accelHash.Add(new AcceleratorKey(Keys.Alt|Keys.P),
                                  Accelerators.Print);
_accelHash.Add(new AcceleratorKey(Keys.Alt|Keys.X),
                                  Accelerators.Logout);

5) Override ProcessCmdKey in the main form and use a switch-case construct to map accelerators to methods:

protected override bool ProcessCmdKey( ref Message msg,
                                       Keys keyData )
{
    // Check this key...
    bool bHandled = false;

    // Look up value
    Accelerators accel = Accelerators.Unspecified;
    if ( _accelHash.ContainsKey(AcceleratorKey(keyData)) )
    {
        accel = (Accelerators)_accelHash[key];

        switch ( accel )
        {
            case Accelerators.Home:
                DisplayHome();
                bHandled = true;
                break;

            case Accelerators.Save:
                Save();
                bHandled = true;
                break;

            case Accelerators.Print:
                Print();
                bHandled = true;
                break;

            case Accelerators.Logout:
                LogOut();
                bHandled = true;
                break;

            case Accelerators.Unspecified:
            default:
                break;

        } // switch
    } // if

    return bHandled;
}

There are any number of ways this could be implemented, but I find this approach both straightforward and consistent with other aspects of the Windows Forms programming model.

For those interested in implementing F1 help, note you don’t need to provide a keyboard accelerator for the F1 key. To process help requests, set the form’s HelpButton property to true and provide a handler for the HelpRequested event, which event fires whenever F1 is pressed. Don’t forget to set the Handled argument to true if you do in fact handle the help request!

About the Author…

Kenn Scribner finds himself consulting and helping other developers understand the complexities of Windows and COM programming. Kenn considers himself fortunate to pursue his hobby as a career and enjoys sharing his experiences with other developers. To that end, Kenn has written four books: MFC Programming with Visual C++ 6 Unleashed, Teach Yourself ATL in 21 Days, Understanding SOAP, and his newest, Applied SOAP: Implementing .NET Web Services. He has contributed to several other books, including Mike Blasczak’s Professional MFC with Visual C++ 6. Kenn has also contributed numerous XML and SOAP articles to Visual C++ Developer, which you can find (along with many products and code samples) at his personal Web site, EnduraSoft.

# # #

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read