Introduction
An essential feature of any "professional" program is a keyboard customization dialog, where users can assign hotkeys of their choice for all menu commands. It looks like a big job, at least it did to me when I was contemplating adding it to my xplorer². Do I need to have script-ready structure for all the commands? Do I have to add text and description for each one of them? What about translations of the GUI?
Searching in MSDN, CodeProject, and usenet groups for a readymade solution - even paid - turned out fruitless. Searching SourceForge with Krugle (great resource) dug up some promising code snippets, but no complete solution.
As you do, I ended up writing my own keyboard customization class from scratch. The good news is that it makes adding hotkey customization to your WTL application a breeze: just add a header file, a dialog resource, a few lines to the frame creation, and you are done. All the command strings are read from your existing menu structure.
Usability wise, the dialog resembles the hotkey customization dialog of VS6, albeit without command categories. You still get support for more than one accelerator key per command. The class takes care of its own registry persistence, and even allows a reset, where all hotkeys are restored to factory defaults (those defined in the accelerator resource). Finally, the hotkey mnemonics that appear on menu items are automatically updated each time you change the accelerator table.
Background
Command hotkeys are stored in an accelerator table. Most programs have a fixed accelerator table created at design time in a resource editor and loaded when the frame window is created through LoadAccelerators
. In WTL, the handle is stored in a member variable of CFrameWindowImplBase
, called m_hAccel
. The framework does a good job hiding the use of accelerators. The message pump calls TranslateMessage
on each retrieved message, and if it happens to correspond to an accelerator key, the equivalent WM_COMMAND
is dispatched instead of the keyboard event.
In order to offer adjustable hotkeys, an application must comprise:
- A means to modify the accelerator table on the fly. There is an API called
CreateAcceleratorTable
that can achieve that with an array of key/command information (ACCEL
struct). - A way to read key combinations, gratis
CHotKeyCtrl
system control. - Provide user readable hotkey names, through the
GetKeyNameText
API.
Using these ingredients, changing the hotkeys on the fly involves these steps:
- Obtain a copy of the current accelerator table stored in the frame's
m_hAccel
, using CopyAcceleratorTable
. - Use the provided
CKeyAssignDlg
to modify the table to taste. - Destroy the old table and create a new one, setting it to
m_hAccel
for immediate effect. - Update the menu hotkey mnemonics (and toolbar tooltips) to reflect the current keyboard assignments.
That's it! The customized table is stored in a registry key as REG_BINARY
. More details can be found in the source code, which demonstrates a minimal WTL SDI application with full hotkey customization support.
Using the code
The sample code is built using Visual Studio 6, using WTL v7.1 (yes, I am a laggard). I imagine it shouldn't be too hard to build with the latest VS/WTL kit, or even port it to MFC, if you are industrious.
To add keyboard customization to your application, all you need is the main header file keyAssignDlg.h, the dialog IDD_ACCELEDIT_DLG
, and a few error message strings from the resources. To integrate it with your project, you need to add a few member variables and message handlers to your main frame window:
class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler
{
public:
void CustomAccelerators();
BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER(ID_CUSTOMIZE_KEYBOARD, OnCustomizeKeys)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT OnCustomizeKeys(WORD , WORD ,
HWND , BOOL& );
LRESULT OnDestroy(UINT , WPARAM ,
LPARAM , BOOL& );
protected:
CAccelCombo* m_paCustomKeys;
HACCEL m_hAccelDefault;
};
After WTL's Run
handler has successfully created the main frame, call CustomAccelerators
to load the custom keys from the registry and store them in the helper class CAccelCombo
. We backup the default keys stored in the resources in m_hAccelDefault
in case users reset them from the customization dialog. Finally, during destruction, we save the custom accelerator table back to the registry. See the source code for all the juicy details.
Points of interest
The really neat feat of this dialog class is the way it populates the categories and the commands. It reuses the structure from your main menu resource. It automatically imports all the information from the resources, and you don't have to type command names and descriptions separately. Not rocket science, but very convenient.
Each top level menu becomes a category, and all its commands (and subcommands) are flattened into a list. Command descriptions are taken from the texts that normally appear on the status bar as you traverse the menu system. This ensures a pleasant and straightforward user experience.
Finally, note that the Windows hotkey control used is not perfect. It passes all key combinations that include ENTER, TAB, SPACE, DEL, ESC, or BACKSPACE to DefWindowProc
, which makes these keys unavailable for keyboard shortcuts. I tried using WM_GETDLGCODE
to eat all keys, without much success. At least, the key names returned by GetKeyNameText
are internationalization ready.
References
History
- June 2007: Initial release.