Accelerator Table Decoding for OS/2
| Article Info | |
|---|---|
| Author | Alfred Longyear |
| Date | 1992 |
| OS Version | |
| Companion File | |
| Source | ACLINF.ZIP |
[PM has an affinity for accelerator tables. It loves to translate the WM_CHAR message into a WM_COMMAND or WM_SYSCOMMAND or WM_HELP. It looks everywhere for the translation table.]
Since there seems to be little information about the accelerator tables used for OS/2, I am supplying what information that I have collected. Some of this is documented by Microsoft [and almost none by IBM], some is deduced with the aid of monitor programs such as SPY, and some is simply deduced (make that "intelligently guessed" ...)
However, it is not all black. The procedures mentioned here and the tables are documented. If IBM later changes the accelerator processing then they will break their own programs and rules. There are no secret addresses mentioned here. All structures are public. All procedures are public. This is safe PM programming.
Accelerator tables come in two forms.
- You can create them with the aid of the resource compiler and store them in your resources; or
- You can simply build them from the structures supplied by OS/2.
There is no reason that a single application NEED use only one accelerator table. If you have an accelerator table to translate some key combinations to various commands then the most common approach is to give it the ID of your client window and place the accelerator table in the resource file. Then by using the frame control flag, FCF_ACCELTABLE, in creating the standard window, the accelerator table is loaded and used.
However, if you wish to have a different accelerator table for various states of the program then you may have more than one accelerator table in the resource file. The only difficulty is that you have only one chance to call the accelerator table the same as the client window ID and thereby have the system load and manage the accelerator table. The others are left to you to manage.
You can do the management yourself by processing the WM_TRANSLATEACCEL messages or by using WinSetAccelTable to set the default accelerator table for your application.
If you wish to store multiple accelerator tables in the resource file then you can use the WinLoadAccelTable procedure to load them. The result is a handle which may be given to the WinTranslateAccel procedure. WinDestroyAccelTable will remove the accelerator table handle which was loaded by WinLoadAccelTable.
If you wish to create the accelerator tables programmatically, or even statically, then use the WinCreateAccelTable procedure to turn the list of structures into an accelerator table handle. WinDestroyAccelTable again will remove the handle association when you no longer need it.
Accelerator translations
The last accelerator procedure is the WinTranslateAccel. This is the most useful of the lot.
The Microsoft quick help information for version 1 has this for WinTranslateAccel():
Generally, applications do not have to call this function. It is normally called automatically by WinGetMsg and WinPeekMsg when a WM_CHAR message is received, with the window handle of the active window as the first parameter. The standard frame window procedure always passes WM_COMMAND messages to the FID_CLIENT window. Since the message is physically changed by WinTranslateAccel, applications will not receive the WM_CHAR messages that resulted in WM_COMMAND, WM_SYSCOMMAND, or WM_HELP messages.
It takes an accelerator table handle and a message pointer. The result is the translated accelerator based upon the WM_CHAR message. It works as follows:
Lets assume that you have an accelerator table which you have stored in the resource file. It was defined as:
#define MY_UNSHIFTED_FUNCTION 101
#define MY_SHIFTED_FUNCTION 102
ACCELTABLE 523
BEGIN
"a", MY_UNSHIFTED_FUNCTION, ALT
"a", MY_SHIFTED_FUNCTION, SHIFT | ALT
END
When you press a key on the keyboard, the hardware generates an interrupt. The interrupt suspends OS/2. OS/2 reads the keyboard and determines that the key has been pressed. Let's assume that this is the post with the label "A". OS/2 looks in the keyboard translation table for the proper shift state and determines that this is the shift letter "a". (You had the shift key down.)
The shifted letter "a" is placed into the system queue as a WM_CHAR message. (First guess: I am only guessing that this is a WM_CHAR - nowhere is it documented to the general public. However, it must be some event. I will call this the WM_CHAR message for the lack of a better name.)
Sometime later PM looks at the system queue and fetches the WM_CHAR message. It looks at the active window and places it into the message queue for that application. If the application was waiting for a message, then it clears the generates the event for the semaphore at the same time. (OS/2 will then move the thread from BLOCKED to READY and it will be dispatched at the next appropriate time. If you want more information about the dispatcher then there are several good books on OS theory.)
The application sometime later calls WinGetMsg (or it was in WinGetMsg and was blocked on that previous semaphore). WinGetMsg peeks into the application queue and sees a WM_CHAR message for the shift "a".
The next step that WinGetMsg does before it returns is to do any accelerator translations. This is done by sending a WM_TRANSLATEACCEL message to the active window. As part of the message is a pointer to the WM_CHAR message that it will supply should the translation not be successful.
Since most applications do not process the WM_TRANSLATEACCEL message it is given to our old friend the WinDefWindowProc. The WinDefWindowProc knows what to do with the WM_TRANSLATEACCEL message. It passes the buck. It does this by doing a WinSendMessage to the parent. (Next Guess: I believe that it uses the parent. The docs are contradict the help information. However, since there is a parent/child relationship between the frame window and the active window while there is not necessarily a owner relationship, it is a good guess that it follows the parent rather than the owner relationship.)
This message is the WM_TRANSLATEACCEL message that it just received.
Eventually, the WM_TRANSLATEACCEL message will filter to the frame window. The frame window will finally do something with the message.
The message processing is done with the aid of a procedure called WinTranslateAccel() This procedure takes the message pointer given in mp1 and returns TRUE or FALSE depending upon whether or not it found the WM_CHAR data in the accelerator table. If the procedure returns TRUE, the frame window returns TRUE.
[Moral: If you want to override any system accelerator, you need only put it into your application accelerator table with the proper value.
This override applies to all windows for the application. If you have an accelerator entry for the enter key, then it applies to every window (including dialog entry fields - isn't that just great! How do you expect to complete the field without a return key? If you override F1 then you have overridden F1 for all windows, menus, dialogs, etc.)]
If the WinTranslateAccel returns FALSE then the frame window looks in the system translation queue for a match. If the WinTranslateAccel returns TRUE then the frame window returns TRUE.
[Another grey area ....] [A similar effect may be done if the frame window passed it to its parent. The parent of the application frame window is the desktop. It may be that the system translation is done in the desktop window procedure rather than the frame window. I don't know. I can't see a message sent to the desktop but that only means that SPY was not able to capture the event if it occurred.]
Since you have a translator for the shift "a" in your table the WinTranslateAccel found the entry. It changed the message from WM_CHAR to WM_COMMAND and gave the command value 102. It then returned TRUE. Now your shift "a" has been destroyed in favor of a WM_COMMAND message.
The return linkage is then un-rolled and the user gets a WM_COMMAND to be given to the WinDispatchMsg procedure.
If, instead the character had been a "b" (which is not in your table ... you like "b"s) then the WinTranslateAccel would not have found the entry. In this case, WinTranslateAccel does not change the message and returns FALSE.
The frame window sees the FALSE and then uses the system accelerator table to do the translation. There is no translation in the system table entry for a shift "b", so it returns FALSE again.
Ok, true or false the thread then unwinds. The frame window returns to the WinDefWindowProc which returns to the WinDefWindowProc which returns ....
Eventually, you will end up back in WinGetMsg. The result of the translated message is then given to the caller to give to the WinDispatchMsg procedure. This is the untranslatable "b" or the WM_COMMAND of 102 when you used the shift "a".
Moral Advice
It is not a good idea to simply process the WM_TRANSLATEACCEL message to see if it is some key combination and if it is to return FALSE without having changed the message. That is cheating. The false return from the message means that you have processed the message. But you made no changes! If you wish to have your own table then do it by the rules. Use the WinCreateAccel or WinLoadAccel procedures.
Accelerator Tables.
The WinTranslateAccel procedure is fairly simple. It does a top down scan for the first character which matches the appropriate shift combinations.
A common mistake is to have two entries.
Entry one is ALT "a". Entry two is SHIFT ALT "a".
If you place the items in the table in this order:
"a", MY_UNSHIFTED_FUNCTION, ALT "a", MY_SHIFTED_FUNCTION, SHIFT | ALT
Then you will never, never, see the translator event for the SHIFT ALT "a". This is because the system sees the ALT "a" combination and finds a match on this combination. The shift key will become irrelevant in this case.
The proper order is
"a", MY_SHIFTED_FUNCTION, SHIFT | ALT "a", MY_UNSHIFTED_FUNCTION, ALT
Which will cause the event without the shift to fail and find the ALT "a" in the second position. The shift key needs to be pressed to get the first translation.
Rule of thumb: Place the most complicated combinations of key events FIRST. The first item in the table should be THE most complicated. Work down from there to the unshifted, un-control, un-alt, key codes. Those should be at the end of the table.
The translated item may have one of three valid modes (it is stored in two bits). By default the translated item will be a WM_COMMAND. If you wish a WM_SYSCOMMAND then that selection is available also. However, the WM_SYSCOMMAND usually means that the entry was found in the system table and not in your table. But, if you like F4 to be WM_SYSCOMMAND with SC_CLOSE then put in the entry
VK_F4, SC_CLOSE, SYSCOMMAND | VIRTUALKEY
and you too can close your application with a simple F4 keypress.
The only modifier which should be avoided is the HELP modifier. This will generate a WM_HELP message and that is another whole story of cascaded messages.
Questions
Assume that you have windows such as:
Standard window S1
Client of S1 - C1
Standard Window S2 (exactly covers C1 -- a MDI configuration)
Client of S2 - C2
A custom window D1 that covers C2
A dozen windows that are children of D1, including
X1 - a custom window we have created
E1 - a WC_ENTRYFIELD window
B1, B2 - WC_PUSHBUTTON windows
Z1 - a custom window, child of X1
>> Who sends the WM_TRANSLATEACCEL messages--PM or the default window >> procedure?
PM sends the message (or rather you do from your thread calling WinGetMsg)
>> Is that done before or after WM_CHAR is sent to the window >> with the focus?
The message is sent BEFORE the WM_CHAR message. You will only get a WM_CHAR message if there are no accelerators for your entry.
>> Is the message passed up the owner chain? If so, by >> whom?
I believe that it is passed up the PARENT chain. I may be wrong, but it is an educated guess. WinDefWindowProc does the passing. (Again another educated guess.)
>> Or is it sent specifically to each window in the owner chain?
No, it is not "broadcast". The message should eventually find the frame window which will do the standard translations unless you intercept the message.
>> When (if) the accelerator table on S2 is found, who is the message sent >> to? S2, the window with the focus, or ?
WM_CHAR, WM_COMMAND, and WM_SYSCOMMANDS are always sent to the window with the current focus. WM_HELP messages filter though the help hook and start another cascade of messages to eventually display the IPF pannel or be ignored.
The result of the translation is one of the four type of messages. It must be a WM_COMMAND, WM_SYSCOMMAND, WM_HELP if it is found in the table. If it is not found in the table, the message is WM_CHAR. (It probably started as WM_CHAR and was simply left as such)
>> The specific problem I have is that an accelerator table entry >> (Ctrl+F4) attached to S2 is apparently not being processed.
The problem with MDI is that the frame window for the MDI is not really the frame with the accelerator tables. The accelerator tables are loaded against the frame window of the client window created by WinCreateStdWindow().
Therefore, when the first frame window "up" from the active window is not really the owner of the accelerator tables, it will not find the entry in your accelerator tables.
If you have access to a PM toolkit for version 1 of OS/2 (from Microsoft) then you will find a sample program called MDI. This is a Multiple Document Interface. The work done in the MDIDOC.C procedure (near line 510) describes the method which must be performed. You must subclass the frame window to the MDI client.
THE FOLLOWING CODE IS FROM THE SAMPLE MDIDOC.C. IT IS COPYRIGHTED BY MICROSOFT.
ctlData = FCF_TITLEBAR | FCF_MINMAX | FCF_SIZEBORDER |
FCF_VERTSCROLL | FCF_HORZSCROLL;
hwndS2 = WinCreateStdWindow(hwndMDI,
FS_ICON | FS_ACCELTABLE,
(VOID FAR *)&ctlData,
pszClassName, szDocTitle,
WS_VISIBLE,
(HMODULE)0, IDR_MDIDOC,
(HWND FAR *)&hwndC2);
pfnFrameWndProc = WinSubclassWindow(hwndS2,
(PFNWP)DocFrameWndProc);
If you forget to create the MDI frame windows with FS_ACCELTABLE then you will not have accelerators in your application. Even if your outer frame window does have the accelerator table defined.
Then in the DocFrameWndProc, the following is performed:
MRESULT EXPENTRY DocFrameWndProc(HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2)
{
MRESULT mres;
USHORT cFrameCtls;
HWND hwndParent, hwndClient;
register NPDOC npdoc;
RECTL rclClient;
switch (msg) {
case WM_SYSCOMMAND:
if (SHORT1FROMMP(mp2) == CMDSRC_ACCELERATOR)
{
/*
* If the command was sent because of an accelerator
* we need to see if it goes to the document or the main
* frame window.
*/
if ((WinGetKeyState(HWND_DESKTOP, VK_CTRL) & 0x8000))
{
/*
* If the control key is down we'll send it
* to the document's frame since that means
* it's either ctl-esc or one of the document
* window's accelerators.
*/
return (*pfnFrameWndProc)(hwnd, msg, mp1, mp2);
}
else
if (SHORT1FROMMP(mp1) == SC_DOCSYSMENU)
{
/*
* If the window is maximized then we want
* to pull down the system menu on the main
* menu bar.
*/
if ((WinQueryWindowULong(hwnd, QWL_STYLE) & WS_MAXIMIZED)
&&
(SHORT1FROMMP(mp1) == SC_DOCSYSMENU))
{
WinPostMsg(miAabSysMenu.hwndSubMenu, MM_STARTMENUMODE,
MPFROM2SHORT(TRUE, FALSE), 0L);
return ((MRESULT) 0);
}
else
{
WinPostMsg(WinWindowFromID(hwnd, FID_SYSMENU),
MM_STARTMENUMODE, MPFROM2SHORT(TRUE, FALSE), 0L);
}
}
else
{
/*
* Control isn't down so send it the main
* frame window.
*/
return WinSendMsg(hwndMDIFrame, msg, mp1, mp2);
}
}
else
{
/*
* WM_SYSCOMMAND not caused by an accelerator
* so hwnd is the window we want to send the
* message to.
*/
return (*pfnFrameWndProc)(hwnd, msg, mp1, mp2);
}
break;
....
Documentation
>> I'm asking this question generally because I'm not sure where the problem >> is, or whether it is in the code or in my understanding of some detail. If this >> is documented especially clearly anywhere, please tell me where!
A good source of this information is (or was before the divorce) Microsoft Online. That was/is an additional cost service if Microsoft. However, now all that is present in the databases is information relating to Windows. The OS/2 information seems to have been archived.
You might find information in the Microsoft Knowledge Base (GO MSKB) on Compuserve. There is no guarantee that they are there. They were on MSONLINE where I accessed them.
The articles which I found most useful are identified as
Q46125 Accelerator Translation Flow of Control Q39339 Using Accelerator Keys within a dialog box within a DLL Q58076 Processing Accelerator keystokes when a Dialog Box is Up
and something called "ACCEL".
S12433 PM Accelerators Sample Program
That may be in the Microsoft Library (GO MSL). If you don't find it there then you might try Microsoft Developer Relations to see if they can supply you with a copy. [No guarantees.]
Additionally, you can find information documented under the version 1 quick help database, strangely enough at the end of the "Window Procedures Overview". It talks about the WM_TRANSLATEACCEL message.
Note that much of the documentation contridicts what SPY tells you is
- reallly* going on the system. So, take it with a grain of salt.
If you can find out where it is fully documented then TELL ME. I have a curiosity as to the correctness of the guesswork (educated as it is).
New Accelerator tables for dialog procedures in a DLL
>> I need to load an accelerator table for a dialog box. I have a DLL >> that creates a dialog box when it is called. In the dialog box are several >> buttons. I would like to take the accelerator table table defined in the >> DLL's resource file and have it send messages to the dialog procedure. >> How?
(This answer comes from originated from a message from Microsoft. It is basically obvious, given the statements earlier, so I have restated it below.)
This method will work for buttons only. I have no other method for any other control. If you find a way then let us all know.
Use the WinLoadAccelTable() procedure to read the accelerator table from your DLL's resource file.
To obtain the previous accelerator table, send the parent window a message WM_GETACCELTABLE. There are no parameters to this message. The result is the accelerator table of the frame. There is no procedure to directly read this information, but the message will suffice.
Then use WinSetAccelTable to set the new accelerator table into place.
At the end of the dialog procedure you should restore the previous accelerator table using the WinSetAccelTable() procedure.
WinDestroyAccelTable() will destory the accelerator table when you are no longer using it. (After you have restore the previous accelerator table.)
Dialog Mnemonics
Dialogs call WinDefDlgProc(). The processing for mnemonics is done by the control windows itself. They respond to the WM_MATCHMNEMONIC message to match the input character to the mnemonic text. This is a kind of accelerator but not really one. They are mnemonics. Do not get them confused.
The processing of a mnemonic is strange indead. It varies depending upon the type of control being used. A list box may select the item in the list if it matches the first character of an item text. If there is no entry then the list box does something stupid. It passes it along. The result may be pushing a button to delete the item in the list!
The ugly word: Windows 3.x
Windows does not combine the accelerator processing with the WinGetMsg procedure. It is a separate procedure to translate the accelerators. If you don't wish to translate the functions in the manner described then it is up to you to do your own translation and not call the standard translate message procedure.
The messaage loop for windows is something like:
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg); // Accelerators et al translated here
DispatchMessage (&msg); // The standard dispatch routine
}
The accelerators are translated by the TranslateMessage() procedure. If you don't wish accelerators translated then don't call TranslateMessage(). If you want something else done to the message then do it yourself.
PM merged the GetMessage and the TranslateMessage routines.
A. Longyear