Introduction
I recently bumped into the problem of drawing accelerators in my owner draw
menus. I will with this brief article present how I solved the problem.
Accelerators and Menus
Accelerators are basically keyboard shortcuts which can be mapped to
different functions in your application. The function an accelerator maps to is
a control ID. The same kind of ID which is assigned to buttons, combo boxes,
list boxes, and also menu items. This way it is possible to make different, but
functionally similar, GUI objects represent the same function.
Since using the menus, and subsequently the mouse, is a bad idea from a
physiological point of view, it's imperative that we give our users the choice
to minimize usage of the mouse. That's why showing the accelerators in menus is
a good thing; it informs the user that there are shortcuts, and that using the
mouse is not always mandatory.
The pinnacle of accelerator usage is to make all key combinations user
definable. That however is out of this article's scope.
Accelerator Tables
The accelerator table is basically a table consisting of three columns as
this structure definition tells us:
typedef struct tagACCEL {
BYTE fVirt;
WORD key;
WORD cmd;
} ACCEL, *LPACCEL;
fVirt
is a bit group containing whether the key combination
includes, Ctrl, Alt or Shift. It also tells whether key
is a
virtual key code or an ascii code. The cmd
member is the ID which
this accelerator is associated with.
Accelerators normally live in your resource section of your binary. In MFC
applications you rarely see them even. MFC, and WTL as well, lets the
accelerator table and main frame window share the same ID. When the main frame
is created, it automatically loads the accelerator table using the same ID it's
got itself. To load the accelerator yourself, you need to use the Win32
function:
HACCEL LoadAccelerators(HINSTANCE hInstance, LPCTSTR lpTableName)
hInstance
is typically the instance handle to your executable,
but it may also be a handle to a DLL which you've loaded dynamically.
lpTableName
is the resource name. Since you won't have any names
available, but integer resource IDs in your typical MFC project, you will need
to use the macro MAKEINTRESOURCE()
. The return value of the
function is a handle to an accelerator. You can't use the handle in a documented
way but to use the functions Win32 provides.
To get to the table itself, you will have copy its contents into a buffer you
provide. The function
int CopyAcceleratorTable(
HACCEL hAccelSrc,
LPACCEL lpAccelDst,
int cAccelEntries
);
will do just that for you. In order to allocate the buffer needed to hold the
entire table, you must first figure out the row count of the table. This can be
done by calling CopyAcceleratorTable(hAccelSrc, 0, 0)
. The function
will then return the row count of the table. Then allocate the buffer and call
the function again, but this time with the address of your buffer and its length
in table entries.
There, we now possess the knowledge on how to extract the accelerator data
from the resource.
Virtual Key Codes, Scan Codes, and Names
Keys are identified using either virtual key codes or scan codes. A scan code
is a low level code which identifies the key in such degree that you can tell
exactly where it is on the keyboard. If you press the insert key on the
numeric pad, the scan code will represent that key, and not the insert key just
above your arrow keys. In contrast, virtual key codes, does not always make this
distinction. Because of this fact, some virtual key codes translates to several
scan codes. This fact becomes a small problem when translating the keys into
text.
The reason we're doing this exercise is that we want to translate virtual key
codes contained in the accelerator table into human readable text. We'd also
like the translations to adher to the keyboard language settings of the user.
After browsing the MSDN documentation, you will find that there is no virtual
key code-to-text function to help you in your quest. There is however a scan
code-to-text function:
int GetKeyNameText(
LONG lParam,
LPTSTR lpString,
int nSize
);
This shifts the original problem into a problem of mapping virtual key codes
to scan codes. As pointed out earlier this is a problem. There is not always an
unambigous translation from virtual key codes into scan codes. If you call the
function
UINT MapVirtualKey(
UINT uCode,
UINT uMapType
);
to map the virtual key code for insert into a scan code, and then use
the scan code with GetKeyNameText()
, you will get the text
"NUM INS". Clearly, this is not what you'd want in your menus. The "NUM" part
would confuse the user, and it would somewhat of a lie since any insert key
would do just fine.
The problem dates back to the day when AT compatible keyboards were
introduced. The older XT compatible keyboards did not have the keys between the
alphanumeric and numeric keyboard (and only 10 function keys, but that's no
problem for us anyway). If you look closely on your keyboard, you will see the
same key setup on the numeric keyboard, which you have on your "extended" part
of the keyboard. To disambiguate scan codes between these two sets, an
extended bit was added to the scan codes.
The extended bit (28, 256d, 100h) will be exploited for those keys
which are on the extended part of the keyboard. If this bit is used with
GetKeyNameText()
, any "NUM"s will be removed from the text, and all
will look just great.
So, to translate an accelerator into a non confusing human readable format,
you'd do something like this:
String s;
if(accel[i].fVirt & FALT)
s += GetKeyNameText(MapVirtualKey(VK_MENU));
if(accel[i].fVirt & FCONTROL) {
if(s) s += "+";
s += GetKeyNameText(MapVirtualKey(VK_CONTROL));
}
if(accel[i].fVirt & FSHIFT) {
if(s) s += "+";
s += GetKeyNameText(MapVirtualKey(VK_SHIFT));
}
if(accel[i].fVirt & FVIRTKEY) {
scancode = MapVirtualKey(accel[i].key);
switch(accel[i].key) {
case VK_INSERT:
case VK_DELETE:
case VK_HOME:
case VK_END:
case VK_NEXT:
case VK_PRIOR:
case VK_LEFT:
case VK_RIGHT:
case VK_UP:
case VK_DOWN:
scancode |= 0x100;
}
s += GetKeyNameText(scancode);
} else {
s += (char)accel[i].key;
}
Source Code
The source code which is attached to this article contains three
functions:
bool GetAcceleratorTexts(HACCEL hAccel, std::map<UINT,
CString>& mapId2AccelText);
bool GetAcceleratorTexts(HINSTANCE hInst, LPCTSTR lpszAccelRes,
std::map<UINT, CString>& mapId2AccelText);
bool GetAcceleratorTexts(HINSTANCE hInst, int nId, std::map<UINT,
CString>& mapId2AccelText);
As you can see it depends on CString
, so you'll need either MFC,
ATL or WTL to use this code. With very little work you can make it work with
std::basic_string<>
as well. All three functions do the same
thing, they just accept different inputs.
The third argument, mapId2AccelText
, will hold the texts when
the function returns successfully. As the name hints, it maps the IDs against
each accelerator text.