DisclaimerIt would seem that WinAPI is a thing of the past. It has long been a huge number of cross-platform frameworks, Windows not only on desktops, but Microsoft themselves don’t like applications that use this monster in their store. In addition to this, articles on how to create windows on WinAPI, not only here, but also all over the Internet, amount to thousands of pre-schoolers and above. This whole process has already been disassembled, not even by atoms, but by subatomic particles. What could be simpler and clearer? And then I still ...
But not everything is as simple as it seems.
Why about WinAPI now?
At one point, studying the offal of one of the games in a very good
NES emulator , I thought: It seems to be a good emulsion, and in the debugger there is no such simple thing as navigating the keyboard buttons, which is in any normal debugger.
')
Here I knowingly gave a link to the repository, because it is clear that the guys are faced with a problem, which will be discussed below, but did not solve it.
What am I talking about? But
about this piece of code:
case WM_KEYDOWN: MessageBox(hwndDlg,"Die!","I'm dead!",MB_YESNO|MB_ICONINFORMATION); break;
Thus, the authors wanted to add support for the keyboard, but the harsh reality of the depths of the dialog box architecture in Windows harshly curbed such initiative. Those who used the emulator and debugger in it, at least once saw this message?
What is the problem?
The answer is: you can not do that!
And, returning to the original question about WinAPI: a lot of popular, and not so much, projects continue to use it even now, because It’s better than many things to do on a pure API (here you can endlessly give analogies like comparing high-level languages ​​and assembler, but this is not about that now). And why is it so little? Just use and that's it.
About the problem
Dialog boxes make it easier to work with the GUI, at the same time making it impossible for us to do something on our own. For example, the WM_KEYDOWN / WM_KEYUP messages coming into the window procedure are “eaten up” in the depths of DefDlgProc, taking on such things as: Tab navigation, Esc key processing, Enter key, etc. In addition, the dialogues do not need to be created manually: it’s easier, in fact, to jot down buttons, lists, in the resource editor, call CreateDialog / DialogBox in WinMain and everything is ready.
Getting around such minor annoyances is simple. There are at least two completely legal ways:
- Create your own class via RegisterClassEx and grab WM_KEYDOWN in the class handling procedure, redirect to the dialog handling procedure. Yes Yes! You can create dialogs with your own class, and the built-in VS editor even allows you to set the class name for the dialog. But who knows about it and uses it?
The minus is obvious: You need to register another class, have more procedure on 1 CALLBACK, the essence of which will be only in the translation of a pair of messages. In addition, we will not know where to broadcast them, and will have to make crutches. - Use the built-in accelerator mechanism. And we do not even have to change the code of the dialogue procedure! Well, except to add one line to switch / case, but more on that below.
Tutorials?
I’m not afraid to say that
all tutorials on creating windows through WinAPI start with such a straightforward code, denoting it as the "message processing cycle" (I’ll leave out the details of preparing the window class and other binding):
while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
Here everything is really simple:
- GetMessage () snatches the next message from the queue, and the key point : it blocks the stream if the queue is empty.
- TranslateMessage () from WM_KEYDOWN / WM_KEYUP generates WM_CHAR / WM_SYSCHAR messages (they are needed if someone wants to make their own text editor).
- DispatchMessage () sends a message to the window procedure (if one exists).
Let's start with the fact that this code is dangerous to use, and
here's why . Note the footnote:
Because the return value can be nonzero, zero, or -1, avoid code like this:
while (GetMessage( lpMsg, hWnd, 0, 0)) ...
And below is an example of the correct cycle.
It should be said that in the VS templates for Win32 applications, it was written just such an incorrect cycle. And it is very sad. After all, few people will delve into what the authors themselves have done, because this is a priori correct. And the wrong code is multiplied along with bugs that are very difficult to catch.After this code snippet, as a rule, there is a story about accelerators, and a couple of new lines are added (taking into account the remark in MSDN, I suggest writing the right cycle right away):
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR)); BOOL bRet = 0; while ( bRet = GetMessage(&msg, nullptr, 0, 0) ) { if ( -1 == bRet ) break; if ( !TranslateAccelerator(msg.hwnd, hAccel, &msg) ) { TranslateMessage(&msg); DispatchMessage(&msg); } }
This option I have seen most often. And he (
ta-dam ) is wrong again!
First about what has changed (then about the problems of this code):
The first line of resources loads a table of keys, when clicked, a WM_COMMAND message will be generated with the corresponding command id.
Actually, TranslateAccelerator does this: if it sees the WM_KEYDOWN and the key code that is in this list, then (again, the key moment) will generate the WM_COMMAND message (MAKEWPARAM (id, 1)) and send it to the appropriate window for the handle specified in the first argument processing procedure.
From the last phrase, I think, it became clear what the problem of the previous code is.
Let me explain: GetMessage snatches messages for ALL objects of the type “window” (which also includes children: buttons, lists, etc.), and TranslateAccelerator will send the generated WM_COMMAND to where? Correct: back to button / list, etc. But we process WM_COMMAND in our procedure, which means we are interested in getting it in it.
It is clear that TranslateAccelerator should be called for our newly created window:
HWND hMainWnd = CreateWindow(...); HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR)); BOOL bRet = 0; while (bRet = GetMessage(&msg, nullptr, 0, 0)) { if ( -1 == bRet ) break; if ( !TranslateAccelerator(hMainWnd, hAccel, &msg) ) { TranslateMessage(&msg); DispatchMessage(&msg); } }
And everything seems to be good and wonderful now: we have disassembled everything in detail and everything should work perfectly.
And no again. :-) This will work correctly as long as we have exactly one window - ours. As soon as a modeless new window (dialog) appears, all the keys that will be pressed in it are translated into WM_COMMAND and sent to? And again, right: in our main window.
At this stage, I propose not to crush the crutches to address this impasse, but I propose to consider things that are less common (or almost never occur) in tutorials.
IsDialogMessage
By the name of this function, you might think that it for some reason determines whether this message is related to dialogue or not. But, first, why do we need to know this? And secondly, what should we do next with this information?
In fact, it makes a little more than the name implies. Namely:
- Navigates through the child controls with Tab / Shift + Tab / up / down / right / left. Plus something else, but this is enough for us
- By clicking on the ESC forms WM_COMMAND (IDCANCEL)
- Clicking on Enter generates WM_COMMAND (IDOK) or pressing the current default button.
- Toggle buttons by default (the frame for such buttons is slightly brighter than the others)
- Well, and still different things that make it easier for the user to work with the dialogue
What does she give us? First, we don’t have to think about navigating inside the window. And so we will do everything.
By the way, Tab navigation can be done by adding the WS_EX_CONTROLPARENT style to our main window, but this is clumsy and not so functional .
Secondly, it will make life easier for us on all the other points listed in the list (and even a little more).
In general, it is used somewhere in the bowels of Windows to make
modal dialogs work, and given to programmers to invoke it for non-modal dialogs. However, we can use
it anywhere :
Although it is a mode of dialog boxes, it allows you to use the dialog box.
Those. Now, if we issue a loop like this:
HWND hMainWnd = CreateWindow(...); HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR)); BOOL bRet = 0; while (bRet = GetMessage(&msg, nullptr, 0, 0)) { if ( -1 == bRet ) break; if ( !TranslateAccelerator(hMainWnd, hAccel, &msg) ) { if ( !IsDialogMessage(hMainWnd, &msg) ) { TranslateMessage(&msg); DispatchMessage(&msg); } } }
Then our window will have navigation, as in the native Windows dialog. But now we got two flaws:
- This code will also work well with only one (non-modal) window ;
- Having received all the advantages of interactive navigation, we lose the charms in the form of WM_KEYDOWN / WM_KEYUP messages (only for the window itself, and not for the child controls);
And at this stage in general, all the tutorials end and the questions begin:
How to handle the keyboard events in a winapi standard dialog?
This is the first link in Google, but believe me: thousands of them. About the proposed solutions (the best of which is to create your own class of dialogs, as I wrote above, before
subclassing and RegisterHotKey. Somewhere I even saw the “best” of ways: using Windows Hooks).
It's time to talk about what is not in the tutorials and answers.
As a rule (as a rule! If someone wants more, you can register your class for dialogs and work like that. And if someone is interested, I can add this article) WM_KEYDOWN want when they want to process a click on a key that performs the function regardless of the selected control in the window - i.e. a kind of common function for all this particular dialogue. And if so, then why not take advantage of the rich features that WinAPI itself offers us:
TranslateAccelerator .
Everywhere they use
exactly one accelerator table, and only for the main window. Well, really: the GetMessage-loop is one, which means the table is one. Where else to put them?
In fact, the GetMessage-loop can be
nested . Let's look at the
PostQuitMessage description
again :
The PostQuitMessage function posts a message and a message queue; The function simply indicates that it is a thread.
And GetMessage:
If the function retrieves the WM_QUIT message, the return value is zero.
Thus, the exit from the GetMessage-loop will be realized if we call PostQuitMessage in the window procedure. What does it mean?
We can create our own similar cycle for
each non-modal window in our program. In this case, DialogBoxParam does not suit us, because it turns its own cycle and we cannot influence it. However, if we create a dialog through CreateDialogBoxParam or a window through CreateWindow, then we can spin another cycle. At the same time, in
each such window and dialog we should call PostQuitMessage:
HWND hMainWnd = CreateWindow(...); HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR)); BOOL bRet = 0; while (bRet = GetMessage(&msg, nullptr, 0, 0)) { if ( -1 == bRet ) break; if ( !TranslateAccelerator(hMainWnd, hAccel, &msg) ) { if ( !IsDialogMessage(hMainWnd, &msg) ) { TranslateMessage(&msg); DispatchMessage(&msg); } } }
Please note: now for each new window in our program we can add our
own accelerator table to the processing. WM_QUIT will grab GetMessage from the loop for the dialog, and the outer loop will not even see it. Why it happens?
The fact is that the outer loop "got up" on the call to DispatchMessage, which called our procedure, which rotates its own
inner loop GetMessage with the same DispatchMessage. Classic nested call (in this case, DispatchMessage). Therefore, the outer loop will not receive WM_QUIT and will not end at this stage. Everything will work slim.
But there are some drawbacks here:
Each such cycle will process messages
only for its own window . About others, we do not know here. So, if another cycle appears somewhere, then all the other windows will not receive the necessary processing of their messages by the TranslateAccelerator / IsDialogMessage pair.
Well, it's time to take into account all these comments and finally write the correct processing of all messages from all windows of our program. I want to note that the case for one stream is considered below. Since each thread has its own message queue, then for each thread you have to create your own structures. This is done by quite trivial changes in the code.
Make beautiful
Since the correct formulation of the problem is half the solution, then you first need to correct this very task itself.
First, it would be logical that only the
active window receives messages. Those. for an inactive window, we will not broadcast accelerators and transmit messages to IsDialogMessage.
Secondly, if the accelerator table is not set for the window, then there is nothing to broadcast, we will simply give the message to IsDialogMessage.
Let's create a simple std :: map, which will map the window handle to the accelerator table handle. Like this:
std::map<HWND,HACCEL> l_mAccelTable;
And as windows are created, we will add new windows to it with a handle to our favorite table (or zero if such processing is not required).
Like this:
BOOL AddAccelerators(HWND hWnd, HACCEL hAccel) { if ( IsWindow( hWnd ) ) { l_mAccelTable[ hWnd ] = hAccel; return TRUE; } return FALSE; } BOOL AddAccelerators(HWND hWnd, LPCTSTR accel) { return AddAccelerators( hWnd, LoadAccelerators( hInstance, accel ) ); } BOOL AddAccelerators(HWND hWnd, int accel) { return AddAccelerators( hWnd, MAKEINTRESOURCE( accel ) ); } BOOL AddAccelerators(HWND hWnd) { return AddAccelerators( hWnd, HACCEL( NULL ) ); }
Well, after closing the window to delete. Like this:
void DelAccel(HWND hWnd) { std::map<HWND, HACCEL>::iterator me = l_mAccelTable.find( hWnd ); if ( me != l_mAccelTable.end() ) { if ( me->second ) { DestroyAcceleratorTable( me->second ); } l_mAccelTable.erase( me ); } }
Now, how to create a new dialog / window, call AddAccelerators (hNewDialog, IDR_MY_ACCEL_TABLE). How to close: DelAccel (hNewDialog).
We have a list of windows with the necessary descriptors. We modify our main message loop a bit:
Much better! What is there in HandleAccelArray and why is there GetActiveWindow ()?
A bit of theory:
There are two functions that return the active window
handle GetForegroundWindow and
GetActiveWindow . The difference of the first from the second is quite intelligibly described in the description of the second:
It is the queuing of the message queue. Otherwise, the return value is NULL.
If the first one returns the handle of any window in the system, then the last one is the one
that uses the message queue of our stream . Since we are only interested in the windows of our stream (which means those that will get into our message queue), then we take the last one.
So, HandleAccelArray, guided by the handle transferred to it on the active window, searches for this window in our map, and if it is there, it sends this message to the translation into TranslateAccelerator, and then (if the first one did not see the right one) in IsDialogMessage. If the last one did not process the message, then return FALSE to follow the standard TranslateMessage / DispatchMessage procedure.
Looks like that:
BOOL HandleAccelWindow(std::map<HWND,HACCEL>::const_iterator mh, MSG & msg) { const HWND & hWnd = mh->first; const HACCEL & hAccel = mh->second; if ( !TranslateAccelerator( hWnd, hAccel, &msg ) ) {
Now each child window has the right to add itself a favorite accelerator table and calmly catch and process WM_COMMAND with the necessary code.
And what about another line in the WM_COMMAND handler code?
The description in TranslateAccelerator reads as follows:
The wise parameter of the WM_COMMAND or the WM_SYSCOMMAND message contains the value of 1.
Usually the WM_COMMAND processing code looks like this:
switch( HIWORD( wParam ) ) { case BN_CLICKED:
Now you can write this:
switch( HIWORD( wParam ) ) { case 1:
And now, returning to the same fceux, adding
just one line to the command processing code from the buttons, we get what we want: control the debager from the keyboard. Just add a small wrapper around the main message loop and a new accelerator table with the necessary matches VK_KEY => IDC_DEBUGGER_BUTTON.
PS: Few people know, but you can create your own accelerator table , and now apply it directly on the fly.PPS: Because DialogBox / DialogBoxParam turns its own cycle, then when you call a dialogue through them, accelerators will not work and our cycle (or cycles) will be "idle".PPPS: After calling HandleAccelWindow, the l_mAccelTable map may change, because TranslateAccelerator or IsDialogMessage call DispatchMessage, and there can be AddAccelerators or DelAccel in our handlers! Therefore, it is better not to touch it after this function.Feel the code
here . The basis was taken from the code generated from the standard template MS VS 2017.