On the Windows operating system, most user-interfacable objects are known as «windows». Each window is associated with a particular class, and once the class is registered with the system, windows of that class can be created.
To register a windows class, you need to fill out the data fields in a WNDCLASS structure, and you need to pass this structure to the system. First, however, you need to provide your class with a name, so that Windows (the system) can identify it. It is customary to define the window class name as a global variable:
LPTSTR szClassName = TEXT("My Class");
You can name it anything you want to name it, this is just an example.
After you have the class name, you can start filling out the WNDCLASS structure. WNDCLASS is defined as such:
typedef struct { UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; } WNDCLASS, *PWNDCLASS;
For more information on this structure, see this Microsoft Developer’s Network article.
Notice the last data field is a pointer to a string named «lpszClassName»? This is where you point to your class name that you’ve just defined. The field named «hInstance» is where you supply the instance handle for your program. We will break the rest of the fields up into a few different categories.
There are a number of different data types in the WNDCLASS structure that begin with the letter «h». As we remember from our discussion of Hungarian notation, if a variable starts with an «h», the variable itself holds a HANDLE object.
- HICON hIcon
- This is a handle to the icon that your program will use, as located in the top left, and in the taskbar. We will discuss icons more later. However, in our example below, we will use a default value for this item.
- HCURSOR hCursor
- This is a handle to the standard mouse pointer that your window will use. In our example, we will use a default value for this also.
- HBRUSH hbrBackground
- This is a handle to a brush (a brush is essentially a color) for the background of your window. Here is a list of the default colors supplied by Windows (these colors will change depending on what ‘theme’ is active on your computer):
COLOR_ACTIVEBORDER COLOR_ACTIVECAPTION COLOR_APPWORKSPACE COLOR_BACKGROUND COLOR_BTNFACE COLOR_BTNSHADOW COLOR_BTNTEXT COLOR_CAPTIONTEXT COLOR_GRAYTEXT COLOR_HIGHLIGHT COLOR_HIGHLIGHTTEXT COLOR_INACTIVEBORDER COLOR_INACTIVECAPTION COLOR_MENU COLOR_MENUTEXT COLOR_SCROLLBAR COLOR_WINDOW COLOR_WINDOWFRAME COLOR_WINDOWTEXT
Because of a software issue, a value of 1 must be added to any of these values to make them a valid brush.
Another value that is worth mentioning in here is the «lpszMenuName» variable. lpszMenuName points to a string that holds the name of the program menu bar. If your program does not have a menu, you may set this to NULL.
There are 2 «extra» data members in the WNDCLASS structure that allow the programmer to specify how much additional space (in bytes) to allocate to the class (cbClsExtra) and to allocate to each specific window instance (cbWndExtra). In case you are wondering, the prefix «cb» stands for «count of bytes».
int cbClsExtra; int cbWndExtra;
If you don’t know how to use these members, or if you don’t want to use them, you may leave both of these as 0. We will discuss these members in more detail later.
There are 2 fields in the WNDCLASS that deal specifically with how the window will operate. The first is the «style» field, which is essentially a set of bitflags that will determine some actions that the system can take on the class. These flags can be bit-wise OR’d (using the | operator) to combine more than one into the style field. The MSDN WNDCLASS documentation has more information.
The next (and arguably most important) member of the WNDCLASS is the lpfnWndProc member. This member points to a WNDPROC function that will control the window, and will handle all of the window’s messages.
After the fields of the WNDCLASS structure have been initialized, you need to register your class with the system. This can be done by passing a pointer to the WNDCLASS structure to the RegisterClass function. If the RegisterClass function returns a zero value, the registration has failed, and your system has failed to register a new window class.
Windows are generally created using the «CreateWindow» function, although there are a few other functions that are useful as well. Once a WNDCLASS has been registered, you can tell the system to make a window from that class by passing the class name (remember that global string we defined?) to the CreateWindow function.
HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam );
(See this MSDN article for more information.)
The first parameter, «lpClassName» is the string associated with our window class. The «lpWindowName» parameter is the title that will be displayed in the titlebar of our window (if the window has a titlebar).
«dwStyle» is a field that contains a number of bit-wise OR’d flags, that will control window creation.
The «x» and «y» parameters specify the coordinates of the upper-left corner of your window, on the screen. If x and y are both zero, the window will appear in the upper-left corner of your screen. «nWidth» and «nHeight» specify the width and height of your window, in pixels, respectively.
There are 3 HANDLE values that need to be passed to CreateWindow: hWndParent, hMenu, and hInstance. hwndParent is a handle to the parent window. If your window doesn’t have a parent, or if you don’t want your windows to be related to each other, you can set this to NULL. hMenu is a handle to a menu, and hInstance is a handle to your programs instance value.
To pass a value to the new window, you may pass a generic, LPVOID pointer (a 32-bit value) in the lpParam value of CreateWindow. Generally, it is a better idea to pass parameters via this method than to make all your variables global. If you have more than 1 parameter to pass to the new window, you should put all of your values into a struct, and pass a pointer to that struct to the window. We will discuss this in more detail later.
Finally, we are going to display a simple example of this process. This program will display a simple window on the screen, but the window won’t do anything. This program is a bare-bones program, and it encompasses most of the framework necessary to make any Windows program do anything. Beyond this, it is easy to add more functionality to a program.
#include <windows.h> LPSTR szClassName = "MyClass"; HINSTANCE hInstance; LRESULT CALLBACK MyWndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInstance, LPSTR szCmdLine, int iCmdShow) { WNDCLASS wnd; MSG msg; HWND hwnd; hInstance = hInst; wnd.style = CS_HREDRAW | CS_VREDRAW; //we will explain this later wnd.lpfnWndProc = MyWndProc; wnd.cbClsExtra = 0; wnd.cbWndExtra = 0; wnd.hInstance = hInstance; wnd.hIcon = LoadIcon(NULL, IDI_APPLICATION); //default icon wnd.hCursor = LoadCursor(NULL, IDC_ARROW); //default arrow mouse cursor wnd.hbrBackground = (HBRUSH)(COLOR_BACKGROUND+1); wnd.lpszMenuName = NULL; //no menu wnd.lpszClassName = szClassName; if(!RegisterClass(&wnd)) //register the WNDCLASS { MessageBox(NULL, "This Program Requires Windows NT", "Error", MB_OK); return 0; } hwnd = CreateWindow(szClassName, "Window Title", WS_OVERLAPPEDWINDOW, //basic window style CW_USEDEFAULT, CW_USEDEFAULT, //set starting point to default value CW_USEDEFAULT, CW_USEDEFAULT, //set all the dimensions to default value NULL, //no parent window NULL, //no menu hInstance, NULL); //no parameters to pass ShowWindow(hwnd, iCmdShow); //display the window on the screen UpdateWindow(hwnd); //make sure the window is updated correctly while(GetMessage(&msg, NULL, 0, 0)) //message loop { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); }
The Win32 API gains more functionality with each generation, although Microsoft faithfully maintains the API to be almost completely backwards-compatible with older versions of windows. To add more functionally, therefore, Microsoft needed to add new functions and new structures, to make use of new features. An extended version of the WNDCLASS structure is known as the «WNDCLASSEX» structure, which has more fields, and allows for more options. To register a WNDCLASSEX structure, you must use the RegisterClassEx function instead.
Also, there is a version of the CreateWindow function with extended functionality: CreateWindowEx. To learn more about these extensions, you can do a search on MSDN.
Dialog Boxes are special types of windows that get created and managed differently from other windows. To create a dialog box, we will use the CreateDialog, DialogBox, or DialogBoxParam functions. We will discuss these all later. It is possible to create a dialog box by defining a WNDCLASS and calling CreateWindow, but Windows already has all the definitions stored internally, and provides a number of easy tools to work with. For the full discussion, see: Dialog Boxes.
There are a number of window classes that are already defined and stored in the Windows system. These classes include things like buttons and edit boxes, that would take far too much work to define manually. Here is a list of some of the pre-made window types:
- BUTTON
- A BUTTON window can encompass everything from a push button to a check box and a radio button. The «title» of a button window is the text that is displayed on a button.
- SCROLLBAR
- SCROLLBAR windows are slider controls that are frequently used on the edge of a larger window to control scrolling. SCROLLBAR types can also be used as slider controls.
- MDICLIENT
- This client type enables Multiple Document Interface (MDI) applications. We will discuss MDI applications in a later chapter.
- STATIC
- STATIC windows are simple text displays. STATIC windows rarely accept user input. However, a STATIC window can be modified to look like a hyperlink, if necessary.
- LISTBOX, COMBOBOX
- LISTBOX windows are drop-down list boxes, that can be populated with a number of different choices that the user can select. A COMBOBOX window is like a LISTBOX, but it can contain complex items.
- EDIT, RichEdit
- EDIT windows allow text input with a cursor. Basic EDIT windows also allow for copy+paste operations, although you need to supply the code to handle those options yourself. RichEdit controls allow for text editing and formatting. Consider an EDIT control being in Notepad.exe, and a RichEdit control being in WordPad.exe.
There are a number of different menus that can be included in a window or a dialog box. One of the most common (and most important) is the drop-down menu bar that is displayed across the top of a window of a dialog box. Also, many programs offer menus that appear when the mouse is right-clicked on the window. The bar across the top of the window is known as the «Menu Bar», and we will discuss that first. For some information about creating a menu in a resource script, see The Resource Script Reference Page, in the appendix to this book.
The easiest and most straightforward method to create a menu is in a resource script. Let’s say that we want to make a menu with some common headings in it: «File», «Edit», «View», and «Help». These are common menu items that most programs have, and that most users are familiar with.
We create an item in our resource script to define these menu items. We will denote our resource through a numerical identifier, «IDM_MY_MENU»:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" POPUP "Edit" POPUP "View" POPUP "Help" END
The keyword POPUP denotes a menu that opens when you click on it. However, let’s say that we don’t want the «Help» menu item to pop up, but instead we want to click on the word «Help», and immediately open the help window. We can change it as such:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" POPUP "Edit" POPUP "View" MENUITEM "Help" END
The MENUITEM designator shows that when we click on «Help», another menu won’t open, and a command will be sent to the program.
Now, we don’t want to have empty menus, so we will fill in some common commands in the «File» and «Edit» menus, using the same MENUITEM keyword as we used above:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" BEGIN MENUITEM "Open" MENUITEM "Save" MENUITEM "Close" END POPUP "Edit" BEGIN MENUITEM "Cut" MENUITEM "Copy" MENUITEM "Paste" END POPUP "View" MENUITEM "Help" END
Now, in the «View» category, we want to have yet another popup menu, that says «Toolbars». When we put the mouse on the «Toolbars» command, a submenu will open to the right, with all our selections on it:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" BEGIN MENUITEM "Open" MENUITEM "Save" MENUITEM "Close" END POPUP "Edit" BEGIN MENUITEM "Cut" MENUITEM "Copy" MENUITEM "Paste" END POPUP "View" BEGIN POPUP "Toolbars" BEGIN MENUITEM "Standard" MENUITEM "Custom" END END MENUITEM "Help" END
This is reasonably easy, to start with, except that now we need to provide a method for interfacing our menu with our program. To do this, we must assign every MENUITEM with a command identifier, that we can define in a headerfile. It is customary to name these command resources with an «IDC_» prefix, followed by a short text saying what it is. For instance, for the «File > Open» command, we will use an id called «IDC_FILE_OPEN». We will define all these ID tags in a resource header script later. Here is our menu with all the ID’s in place:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" BEGIN MENUITEM "Open", IDC_FILE_OPEN MENUITEM "Save", IDC_FILE_SAVE MENUITEM "Close", IDC_FILE_CLOSE END POPUP "Edit" BEGIN MENUITEM "Cut", IDC_EDIT_CUT MENUITEM "Copy", IDC_EDIT_COPY MENUITEM "Paste", IDC_EDIT_PASTE END POPUP "View" BEGIN POPUP "Toolbars" BEGIN MENUITEM "Standard", IDC_VIEW_STANDARD MENUITEM "Custom", IDC_VIEW_CUSTOM END END MENUITEM "Help", IDC_HELP END
When we click on one of these entries in our window, the message loop will receive a WM_COMMAND message, with the identifier in the WPARAM parameter.
We will define all our identifiers in a header file to be numerical values in an arbitrary range that does not overlap with the command identifiers of our other input sources (accelerator tables, push-buttons, etc):
//resource.h #define IDC_FILE_OPEN 200 #define IDC_FILE_SAVE 201 #define IDC_FILE_CLOSE 202 #define IDC_EDIT_COPY 203 #define IDC_EDIT_CUT 204 #define IDC_EDIT_PASTE 205 #define IDC_VIEW_STANDARD 206 #define IDC_VIEW_CUSTOM 207 #define IDC_HELP 208
And we will then include this resource header both into our main program code file, and our resource script. When we want to load a menu into our program, we need to create a handle to a menu, or an HMENU. HMENU data items are identical in size and shape to other handle types, except they are used specifically for pointing to menus.
When we start our program, usually in the WinMain function, we will obtain a handle to this menu using an HMENU data item, with the LoadMenu function:
HMENU hmenu; hmenu = LoadMenu(hInst, MAKEINTRESOURCE(IDM_MY_MENU));
We will discuss how to use this handle to make the menu appear in another section, below.
To associate a menu with a window class, we need to include the name of the menu into the WNDCLASS structure. Remember the WNDCLASS structure:
typedef struct { UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; } WNDCLASS, *PWNDCLASS;
It has a data field called «lpszMenuName». This is where we will include the ID of our menu:
WNDCLASS wnd; wnd.lpszMenuName = MAKEINTRESOURCE(IDM_MY_MENU);
Remember, we need to use the MAKEINTRESOURCE keyword to convert the numerical identifier (IDM_MY_MENU) into an appropriate string pointer.
Next, after we have associated the menu with the window class, we need to obtain our handle to the menu:
HMENU hmenu; hmenu = LoadMenu(hInst, MAKEINTRESOURCE(IDM_MY_MENU));
And once we have the HMENU handle to the menu, we can supply it to our CreateWindow function, so that the menu is created when the window is created:
HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hmenu, HINSTANCE hInstance, LPVOID lpParam );
We pass our HMENU handle to the hMenu parameter of the CreateWindow function call. Here is a simple example:
HWND hwnd; hwnd = CreateWindow(szClassName, "Menu Test Window!", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, hmenu, hInstance, 0);
As a quick refresher, notice that we are using default values for all the position and size attributes. We are defining the new window to be a WS_OVERLAPPEDWINDOW, which is a common, ordinary window type. Also the title bar of the window will say «Menu Test Window!». We also need to pass in the HINSTANCE parameter as well, which is the second-to-last parameter.
- User Interface Controls
Writing a Windows GUI Application with Windows API was avoided by many programmers due to its complexity, but is it really something to be afraid of?
In the WinAPI you essentially need the WinMain(…), CALLBACK(…) and WndProc(…) functions to create and present a GUI. You will also need a Window Class (WNDCLASS) and the CreateWindow(…) function. Content is provided in the form of Text, Cotrol Elements, Menu and MessageBoxes.
All in all it’s not that bad, mostly some typing if you know what you need. In order for you to gain this knowledge, I have summarized the essential elements along with examples in this article.
Creating a WinAPI Window in Modern C
The following article shows the creation of a basic WinAPI application with a GUI that resembles the most common applications used in the MS Windows environment.
Basics
In another article I showed how to create a very basic WinAPI GUI Application and we will reuse most of this code here. It is a lot of code to make very little happen, but it is the basis of our application and shows a MS Windows window with a text.
We will need two functions, WinMain(…) as a replacement for main() and WndProc(…) as a function that processes the MS Windows event loop. In order to use the types and functions of the WinAPI we include windows.h.
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int cmdShow)
{
... /* code here */
}
Creating and Initializing the Window with the WinAPI
Again, the details are shown in another Article about GUI in C, so we will cover it briefly here. Instead of our beloved main(…) function we now have to use WinMain(…) together with some parameters that are necessary for Windows.
Next we create a windowclass and register it. Then we create the window itself (with a title) and show it to the screen. An EventLoop that collects operating system messages is also started.
...
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int cmdShow)
{
static TCHAR appName[] = TEXT("Hello WindowsAPI");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = appName;
if(!RegisterClass(&wndclass)) {
MessageBox(NULL, TEXT("Could not register Window Class!"), appName, MB_ICONERROR);
}
hwnd = CreateWindow(appName, TEXT("The Hello Windows Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, cmdShow);
UpdateWindow(hwnd);
while(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
...
Processing MS Windows Events with the WinAPI
The CALLBACK(…) function is called when the event loop fires events from the operating system. For each event you can react with whatever is necessary and/or appropriate.
The WM_DESTROY event announces the upcoming shutdown of the application, WM_PAINT is fired whenever a repaint of the window is necessary (e.g. because you moved it). WM_CREATE is fired on creation of the window and we will do something here later.
...
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_CREATE:
/* Add Initialization Code */
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
DrawText(hdc, TEXT("This is your first Windows Window!"),-1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
Our Hello World Window
If you compile what we have coded so far, you should get a window with a title, a white background and a black text in the center of the screen.
Compiler Notice: You have to link gdi32 to link the Windows functionality into your code, otherwise you will get a linker error. In gcc you can do it with the -lgdi32 option (e.g. gcc main.c -o winapi -lgdi32)
Most windows have a menu bar at the top where the user can find some actions that he is able to perform with the application. Let’s create a menu bar for our example application.
On the top we add the declaration for an add_menu(…) function which takes the window reference as a parameter.
In the function itself we create four menu items, two of them are main menu items and two are sub menu items. Last we add the created menu to the main window with help of the hWnd Reference.
The & character underlines the following letter and enables access via the ALT Key+Letter (e.g. ALT+F for &File). Make sure that there are no duplicates (like &File and &Format) in the same menu level.
#include <windows.h>
...
void add_menu_bar(HWND);
int WINAPI WinMain(HINSTANCE hInstance...
...
void add_menu_bar(HWND hwnd)
{
HMENU hFileMenu = CreateMenu();
HMENU hHelpMenu = CreateMenu();
HMENU hQuitItem = CreateMenu();
HMENU hAboutItem = CreateMenu();
AppendMenu(hQuitItem, MF_STRING, ITEM_QUIT, "&Quit");
AppendMenuW(hFileMenu, MF_POPUP, (UINT_PTR)hQuitItem, L"&File");
AppendMenu(hAboutItem, MF_STRING, ITEM_ABOUT, "&About");
AppendMenuW(hFileMenu, MF_POPUP, (UINT_PTR)hAboutItem, L"&Help");
SetMenu(hwnd, hFileMenu);
}
Now we have to call the add_menu() function in WndProc(…), namely in the WM_CREATE case so the menu is created when the GUI is created.
...
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_CREATE:
add_menu_bar(hwnd);
return 0;
case WM_PAINT:
...
...
}
...
After compiling and running the application we now have a menu bar with two items which itself contain one item each.
Closing the Application on Menu Item Click
Right now our Menu doesn’t do anything. We want to make the Quit item close the application. When such an item is clicked, WndProc will receive a WM_COMMAND event which we can handle appropriatly.
We extend he switch(message) block and check if ITEM_QUIT was clicked. If yes we will send the PostQuitMessage() in order to close the application.
Now the application should close if you click on File->Quit.
...
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
...
switch (message)
{
case WM_CREATE:
...
case WM_PAINT:
...
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ITEM_QUIT:
PostQuitMessage(0);
break;
default:
/* not handled*/
return 0;
}
break;
case WM_DESTROY:
...
default:
...
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
...
Control Elements
A blank window with some text is mostly not what we want so lets add some control elements. First we will add an EditBox where the user can type some text. We will also add a Buttons that the user can click but will do nothing for now.
The call to the creation again happens in the WndProc(..) function where we already created the menu. We declare add_controls(…) and call it in the WM_CREATE case. We also need two new IDs for our control elements.
We also declare the EditBox Element globally so that we can use it later when we implement Message Boxes.
#include <windows.h>
#define ITEM_QUIT 1
#define ITEM_ABOUT 2
#define EDIT_BOX_ID 3
#define SEND_BUTTON_ID 4
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void add_menu_bar(HWND);
void add_controls(HWND);
HWND hEditBox;
...
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
....
switch (message)
{
case WM_CREATE:
add_menu_bar(hwnd);
add_controls(hwnd);
return 0;
case WM_PAINT:
...
The function itself is rather simple; we create an EditBox and a Button with the help of the CreateWindowL(…) function where the first parameter indicates the type of control that we want. The Button also gets a Text which serves as caption. The four numbers are x and y position as well as width and height of the control.
WS_CHILD makes the Element a child of the main window and WS_VISIBLE makes it visible. We also need an ID for each control in order to identify it later (e.g. for button click events, etc.).
void add_controls(HWND hwnd)
{
hEditBox = CreateWindowW(L"Edit", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER, 30, 50, 150, 25, hwnd, (HMENU)EDIT_BOX_ID, NULL, NULL);
HWND hButtonSend = CreateWindowW(L"Button", L"Send Text", WS_CHILD | WS_VISIBLE, 30, 80, 80, 30, hwnd, (HMENU)SEND_BUTTON_ID, NULL, NULL);
}
When we compile and run the application now we get the following result:
Message Boxes
A MessageBox is also a common control element in a GUI. It is an easy way to show important information to the user or to get an answer for a request.
We will implement a simple MessageBox that will show up when we click on our About menu item in the Help section of the Menu Bar. We created this a few paragraphs ago.
In the WM_COMMAND section of the WndProc(…) function we will add another case for the About item. Instead of closing the application we now call a Windows function called MessageBox(…) which will show us a MessageBox with a Title, Text and Button.
I will only show you the part of WndProc(..) that has changed
...
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
...
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ITEM_QUIT:
PostQuitMessage(0);
break;
case ITEM_ABOUT:
MessageBox(NULL, TEXT("Thank you for reading this far."), TEXT("A Message Box!"), 0);
break;
default:
/* not handled*/
return 0;
}
break;
...
}
If we now navigate to the menu item Help->About and click the MessageBox will appear.
Remember the Button we added in the Control Elements paragraph? We now give it something to do. When the user clicks the button we will show the text that he entered in the EditBox inside a MessageBox.
We have to visit the WndProc(…) function one last time. In the WM_COMMAND section we check if the event comes from our button (SEND_BUTTON_ID) and then check if the event was a button click (BN_CLICKED). Then we create a buffer that will hold our text and copy the contents of the EditBox into this buffer. Finally we show a MessageBox with a title and our buffer as text.
Now you see why the EditBox was declared globally – we need to access it in two different functions.
...
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
...
switch (message)
{
case WM_CREATE:
....
case WM_PAINT:
...
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ITEM_QUIT:
PostQuitMessage(0);
break;
case ITEM_ABOUT:
MessageBox(NULL, TEXT("Thank you for reading this far."), TEXT("A Message Box!"), 0);
break;
case SEND_BUTTON_ID:
if(HIWORD(wParam) == BN_CLICKED)
{
TCHAR buffer[1024];
GetWindowText(hEditBox, buffer, 1024);
MessageBox(NULL, buffer, TEXT("Sending Text:"), 0);
}
break;
default:
/* not handled*/
return 0;
}
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
...
}
Finally when we click on the button the entered text will appear inside a MessageBox.
Summary
This was a brief introduction to GUI Programming with the WinAPI in C. From here you can write your own programs and begin to add more control elements and functionality.
If you want to try out an alternative GUI that you can program in C you can e.g. read about SDL (Simple DirectMedia Layer) in this article. In contrast to WinAPI, SDL is portable.
Example
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <tchar.h>
const TCHAR CLSNAME[] = TEXT("helloworldWClass");
LRESULT CALLBACK winproc(HWND hwnd, UINT wm, WPARAM wp, LPARAM lp);
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PTSTR cmdline,
int cmdshow)
{
WNDCLASSEX wc = { };
MSG msg;
HWND hwnd;
wc.cbSize = sizeof (wc);
wc.style = 0;
wc.lpfnWndProc = winproc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInst;
wc.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor (NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = CLSNAME;
wc.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wc)) {
MessageBox(NULL, TEXT("Could not register window class"),
NULL, MB_ICONERROR);
return 0;
}
hwnd = CreateWindowEx(WS_EX_LEFT,
CLSNAME,
NULL,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInst,
NULL);
if (!hwnd) {
MessageBox(NULL, TEXT("Could not create window"), NULL, MB_ICONERROR);
return 0;
}
ShowWindow(hwnd, cmdshow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK winproc(HWND hwnd, UINT wm, WPARAM wp, LPARAM lp)
{
return DefWindowProc(hwnd, wm, wp, lp);
}
The first thing one sees are the two macro definitions, UNICODE
and _UNICODE
. These macros cause our program to understand wide character strings (wchar_t[n]
), not plain narrow strings(char[n]
). As a result, all string literals must be wrapped in a TEXT(
macro. The generic character type for Win32 strings is TCHAR
, whose definition depends on whether or not UNICODE
is defined. A new header is included: <tchar.h>
contains the declaration of TCHAR
.
A window consists of what is known as a window class. This describes information about a window that is to be shared between instances of it, like the icon, the cursor, and others. A window class is identified by a window class name, which is given in the CLSNAME
global variable in this example. The first act of WinMain
is to fill in the window class structure, WNDCLASSEX wc
. The members are:
- cbSize: The size, in bytes, of the structure
- style: The window class styles. This is 0 for now.
- lpfnWndProc: This is one of the more important fields. It stores the address of the window procedure. The window procedure is a function that handles events for all windows that are instances of this window class.
- cbClsExtra: The number of extra bytes to allocate for the window class. For most situations, this member is 0.
- cbWndExtra: The number of extra bytes to allocate for each individual window. Do not confuse this with
cbClsExtra
, which is common to all instances. This is often 0. - hInstance: The instance handle. Just assign the
hInst
argument inWinMain
to this field. - hIcon: The icon handle for the window class.
LoadIcon(NULL, IDI_APPLICATION)
loads the default application icon. - hCursor: The cursor handle for the window class.
LoadCursor(NULL, IDC_ARROW)
loads the default cursor. - hbrBackground: A handle to the background brush.
GetStockObject (WHITE_BRUSH)
gives a handle to a white brush. The return value must be cast becauseGetStockObject
returns a generic object. - lpszMenuName: The resource name of the menu bar to use. If no menu bar is needed, this field can be NULL.
- lpszClassName: The class name that identifies this window class structure. In this example, the
CLSNAME
global variable stores the window class name. - hIconSm: A handle to the small class icon.
After this structure is initialized, the RegisterClassEx
function is called. This causes the window class to be registered with Windows, making it known to the application. It returns 0 on failure.
Now that the window class has been registered, we can display the window using CreateWindowEx
. The arguments are:
- stylesex: The extended window styles. The default value is WS_EX_LEFT.
- clsname: The class name
- cap: The window title, or caption. In this case, it is the caption that is displayed in a window’s title bar.
- styles: The window styles. If you want to create a top-level (parent) window like this one, the flag to pass in is WS_OVERLAPPEDWINDOW.
- x: The x-coordinate of the upper-left corner of the window.
- y: The y-coordinate of the upper-left corner of the window
- cx: The width of the window
- cy: The height of the window
- hwndParent: The handle to the parent window. Since this window is in itself a parent window, this argument is NULL.
- hMenuOrID: If the window being created is a parent window, then this argument is a handle to the window menu. Do not confuse this with the class menu, which is
WNDCLASSEX::lpszClassName
. The class menu is common to all instances of windows with the same class name. This argument, however, is specific for just this instance. If the window being created is a child window, then this is the ID of the child window. In this case, we are creating a parent window with no menu, so NULL is passed. - hInst: The handle to the instance of the application.
- etc: The extra information that is passed to the window’s window procedure. If no extra information is to be transmitted, pass NULL.
If x
or y
or cx
or cy
is CW_USEDEFAULT
, then that argument’s value will be determined by Windows. That is what is done in this example.
CreateWindowEx
returns the handle to the newly created window. If window creation failed, it returned NULL
.
We then show the window by calling ShowWindow
. The first argument for this function is the handle to the window. The second argument is the show style, which indicates how the window is to be displayed. Most applications just pass the cmdshow
argument passed in WinMain
. After the window is shown, it must be updated by a call to UpdateWindow
. It causes an update message to be sent to the window. We will learn what this means in another tutorial.
Now comes the heart of the application: The message pump. It pumps messages sent to this application by the operating system, and dispatches the messages to the window procedure. The GetMessage
call returns non-zero until the application receieves a messages that causes it to quit, in which case it returns 0. The only argument that concerns us is the pointer to an MSG
structure that will be filled in with information about the message. The other arguments are all 0.
Inside the message loop, TranslateMessage
translates virtual-key messages into character messages. The meaning of this, again, is unimportant to us. It takes a pointer to an MSG
structure. The call directly following it, DispatchMessage
, dispatches the message pointed to by its argument to the window’s window procedure. The last thing WinMain
must do is return a status code. The wParam
member of the MSG
structure contains this return value, so it is returned.
But that’s just for the WinMain
function. The other function is winproc
, the window procedure. It will handle messages for the window that are sent to it by Windows. The signature for winproc
is:
- hwnd: A handle to the window whose messages are being processed.
- wm: The window message identifier
- wp: One of the message information arguments. This depends on the
wm
argument - lp: One of the message information arguments. This depends on the
wm
argument. This argument is usually used to transmit pointers or handles
In this simple program, we do not handle any messages ourselves. But that doesn’t mean Windows doesn’t either. This is why one must call DefWindowProc
, which contains default window handling code. This function must be called at the end of every window procedure.
What is a handle?
A handle is a data type that represents a unique object. They are pointers, but to secret data structures maintained by the operating system. The details of these structures need not concern us. All a user needs to do is simply create/retreive a handle using an API call, and pass it around to other API calls taking that type of handle. The only type of handle we used was the HWND
returned by CreateWindowEx
.
Constants
In this example, we encounter a handful of constants, which are in all-caps and begin with a 2 or 3 letter prefix. (The Windows types are also in all-caps)
- IDI_APPLICATION: The resource name containing the default application icon. This is used with either
LoadIcon
orLoadImage
(LoadIcon in this example). - IDC_ARROW: The resource name countaining the default application cursor. This is used with either
LoadIcon
orLoadImage
(LoadIcon in this example). - WHITE_BRUSH: The name of a stock object. This stock object is the white brush.
- MB_ICONERROR: A flag used with
MessageBox
to display an error icon. - WS_EX_LEFT: The default extended window style. This causes the window to have left-aligned properties.
- WS_OVERLAPPEDWINDOW: A window style indicating that the window should be a parent window with a title bar, size box, and others elements typical of top-level windows.
- CW_USEDEFAULT: Used with
CreateWindowEx
‘sx
,y
,cx
, orcy
arguments. Causes Windows to choose a valid value for the argument for whichCW_USEDEFAULT
was passed.
Windows Types
When programming for Windows, you will have to get used to the Win32 types, which are aliases for builtin types. These types are in all caps. The alias types used in this program are:
- TCHAR: The generic character type. If
UNICODE
is defined, this is awchar_t
. Otheriwse, it is achar
. - UINT: An unsigned integer. Used to represent the message identifier in window procedures, and other purposes.
- WPARAM: In Win16, this was a WORD argument (hence the
W
prefix). With the introduction of Win32, however, this is now aUINT_PTR
. This illustrates the point of these Windows aliases; they are there to protect programs from change. - LPARAM: This is a
LONG
argument (LONG_PTR
in Win64). - PTSTR: The
P
means pointer. TheT
means generic character, and theSTR
means string. Thus, this is a pointer to aTCHAR
string. Other string types include:- LPTSTR: Same as
PTSTR
- LPCTSTR: Means
const TCHAR *
- PCTSTR: Same as
LPCTSTR
- LPWSTR: Wide string (
wchar_t *
) - LPCWSTR: Means
const wchar_t *
- PWSTR: Same as
LPWSTR
- and much more
As you can see, the Win32 types can be a hassle to understand, especially with so many synonymous types, which is an artifact of Win16.
- LPTSTR: Same as
- LRESULT: This type is used to represent the return value of window procedures. It is usually a LONG (hence the
L
).
(СоздатьОкно)
Функция CreateWindow создает перекрывающее, выскакивающее или дочернее окно. Она определяет класс, заголовок, стиль окна и (необязательно) исходное положение и размер окна. Функция также определяет окно родителя или владельца, если таковые вообще имеются, и меню окна.
Синтаксис:
HWND CreateWindow ( LPCTSTR lpClassName, // указатель на зарегистрированное имя класса LPCTSTR lpWindowName, // указатель на имя окна DWORD dwStyle, // стиль окна int x, // горизонтальная позиция окна int y, // вертикальная позиция окна int nWidth, // ширина окна int nHeight, // высота окна HWND hWndParent, // дескриптор родительского или окна владельца HMENU hMenu, // дескриптор меню или идентификатор дочернего окна HANDLE hInstance, // дескриптор экземпляра приложения LPVOID lpParam // указатель на данные создания окна );
Параметры
lpClassName
Указывает на строку с нулевым символом в конце или — на целочисленный атом. Если этот параметр — атом, он должен быть общим атомом, созданным предыдущим обращением к функции GlobalAddAtom. Атом — 16-разрядное значение меньшее, чем 0xC000, должно быть в младшем слове lpClassName; старшее слово должно быть нулевое. Если lpClassName — строка, она определяет имя класса окна. Имя класса может быть любое имя, зарегистрированное функцией RegisterClass или любым из предопределенных имен класса элементов управления. Полный список, см. в следующем разделе Замечания.
lpWindowName
Указывает на строку с нулевым символом на конце, которая определяет имя окна.
dwStyle
Определяет стиль создаваемого окна. Этот параметр может быть комбинацией стилей окна и стилей панелей управления, перечисленных в ниже следующем разделе Замечания.
x
Определяет начальную горизонтальную позицию окна. Для перекрывающего или выскакивающего окна, параметр x — начальная x-координата левого верхнего угла окна, в экранных координатах. Для дочернего окна x — x-координата левого верхнего угла окна относительно левого верхнего угла рабочей области родительского окна. Если этот параметр установлен как CW_USEDEFAULT, Windows выбирает заданную по умолчанию позицию для левого верхнего угла окна и игнорирует параметр y. Параметр CW_USEDEFAULT допустим только для перекрывающих окон; если он определен для выскакивающего или дочернего окна, x и y параметры устанавливаются в нуль.
y
Определяет начальную вертикальную позицию окна. Для перекрывающего или выскакивающего окна, параметр y — начальная y-координата левого верхнего угла окна, в экранных координатах. Для дочернего окна, y — начальная y-координата левого верхнего угла дочернего окна относительно левого верхнего угла рабочей области родительского окна. Для окна со списком, y — начальная y-координата левого верхнего угла рабочей области окна со списком относительно левого верхнего угла рабочей области родительского окна. Если перекрывающее окно создано с установленным битом стиля WS_VISIBLE, а x параметр установлен как CW_USEDEFAULT, Windows игнорирует параметр y.
nWidth
Определяет ширину окна в единицах измерения для устройства. Для перекрывающих окон, nWidth является, или шириной окна в экранных координатах, или параметром CW_USEDEFAULT. Если nWidth — CW_USEDEFAULT, Windows выбирает заданную по умолчанию ширину и высоту для окна; заданная по умолчанию ширина простирается от начальной x-координаты до правого края экрана, а заданная по умолчанию высота простирается от начальной y-координаты до верхней части области пиктограмм. Значение CW_USEDEFAULT допустимо только для перекрывающих окон; если CW_USEDEFAULT определено для выскакивающего или дочернего окна, nWidth и nHeight устанавливаются в нуль.
nHeight
Определяет высоту окна в единицах измерения устройства. Для перекрывающих окон, nHeight — высота окна, в экранных координатах. Если параметр nWidth установлен как CW_USEDEFAULT, Windows игнорирует nHeight.
hWndParent
Идентифицирует окно родителя или владельца создаваемого окна. Правильный дескриптор окна должен быть выдан при создании дочернего окна или окна собственника. Дочернее окно ограничено рабочей областью его родительского окна. Находящееся в собственности окно — перекрывающее окно, разрушается тогда, когда разрушено или скрыто окно его владельца, когда его владелец свернут; оно всегда отображается на верхней части окна своего владельца. Несмотря на то, что этот параметр должен определять допустимый дескриптор, если параметр dwStyle включает в себя стиль WS_CHILD, он необязателен, если в dwStyle включает в себя стиль WS_POPUP.
hMenu
Идентифицирует меню или определяет идентификатор дочернего окна в зависимости от стиля окна. Для перекрывающего или выскакивающего окна, hMenu идентифицирует меню, которое нужно использовать с окном; если должно использоваться меню класса, он может быть значением ПУСТО (NULL). Для дочернего окна, hMenu определяет идентификатор дочернего окна, целочисленное значение, используемое элементом управления диалогового окна, чтобы сообщать своему родителю о событиях. Прикладная программа определяет идентификатор дочернего окна; он должен быть уникальным для всех дочерних окон того же самого родительского окна.
hInstance
Идентифицирует экземпляр модуля, который связан с окном.
lpParam
Указывает на значение, переданное окну через структуру CREATESTRUCT, вызванную при помощи параметра lParam сообщения WM_CREATE. Если прикладная программа вызывает CreateWindow, чтобы создать пользовательское окно интерфейса многодокументной среды(MDI), lpParam должен указывать на структуру CLIENTCREATESTRUCT.
Возвращаемые значения
Если функция завершается успешно, возвращаемое значение — дескриптор созданного окна. Если функция потерпела неудачу возвращаемое значение — ПУСТО (NULL). Чтобы получить расширенную информацию об ошибке вызовите функцию GetLastError.
Замечания
Перед возвратом значения, CreateWindow посылает сообщение WM_CREATE оконной процедуре.
Для перекрывающих, выскакивающих и дочерних окон CreateWindow посылает окну сообщения WM_CREATE, WM_GETMINMAXINFO и WM_NCCREATE. Параметр lParam сообщения WM_CREATE содержит указатель на структуру CREATESTRUCT. Если определен стиль WS_VISIBLE, CreateWindow посылает окну все сообщения, требующиеся для того, чтобы активизировать и показать окно.
Если стиль окна определяет строку заголовка, заголовок окна, указанный в lpWindowName отображается в строке заголовка. Когда CreateWindow используется, чтобы создать элементы управления, типа кнопок, переключателей и статических элементов управления, lpWindowName используют, чтобы определить текст на элементе управления.
Если при компоновке вашей прикладной программы Вы определяете версию Windows — 4.x, ее окна не могут иметь кнопки в строке заголовка, если они к тому же не имеют меню окна. Этого не требуется для прикладных программ, которым при компоновке Вы определили версию Windows — 3.x.
Следующие предопределенные классы элементов управления могут быть определены в параметре lpClassName:
BUTTON
(КНОПКА) Обозначает маленькое прямоугольное дочернее окно, которое представляет собой кнопку и пользователь может щелкать по ней мышью, чтобы включить или отключить ее. Кнопки управления могут использоваться самостоятельно или в группах, и они могут или быть подписаны или появляться без текста. Кнопки управления обычно изменяют свой вид, когда пользователь щелкает мышью по ним.
COMBOBOX
(КОМБИНИРОВАННОЕ ОКНО) Обозначает элемент управления, состоящий из окна со списком и поля выбора, похожего на элемент редактирования текста. При использовании этого стиля, прикладная программа должна отображать все время или окно со списком или включать раскрывающийся список. В зависимости от стиля комбинированного окна, пользователь сможет или не сможет редактировать содержание поля выбора. Если окно со списком видимо, вводимые символы внутри поля выбора подсвечивают сначала ввод в окно со списком, который соответствует печатаемым символам. Наоборот, выбор элемента в окне со списком отображает выбранный текст в поле выбора.
EDIT
(РЕДАКТИРУЕМОЕ ОКНО) Обозначает прямоугольное дочернее окно, внутри которого пользователь может напечатать с клавиатуры текст. Пользователь выбирает элемент управления и дает ему фокус клавиатуры, щелкая мышью по нему или перемещаясь в него, путем нажатия клавиши ТАБУЛЯЦИИ (TAB). Пользователь может напечатать текст, когда элемент управления редактируемого окна отображает мигающую каретку (caret); используйте мышь, чтобы перемещать курсор, выбирать символы, которые будут заменены, или установите курсор для вставки символов; или используйте КЛАВИШУ ВОЗВРАТА НА ПОЗИЦИЮ (BACKSPACE), чтобы удалять символы. Элементы управления редактируемого окна используют шрифт системы с переменным шагом и показывают на экране символы из символьного набора ANSI. Сообщение WM_SETFONT может также быть послано элементу управления окна редактирования, чтобы изменить заданный по умолчанию шрифт. Элементы управления окна редактирования увеличиваются в соответствии с табуляцией символов стольким количеством пробелов, сколько их требуются, чтобы переместить каретку в следующую позицию табуляции. Позиции табуляции принимаются такими, чтобы быть в каждом восьмом знакоместе.
LISTBOX
(ОКНО СО СПИСКОМ) Обозначает список строк символов. Этот элемент управления определяется всякий раз, когда прикладная программа должна представить список наименований, типа имен файлов, из которых пользователь может выбирать. Пользователь может выбрать строку, щелкая мышью по ней. Выбранная строка выделяется, а уведомительное сообщение передается в родительское окно. Чтобы листать списки, которые являются слишком длинными для элемента управления окна, используются вертикальная или горизонтальная линейки прокрутки окна со списком. Окно со списком автоматически скрывает или показывает линейку прокрутки, по мере необходимости.
MDICLIENT
Обозначает рабочее окно МНОГОДОКУМЕНТАЛЬНОГО ИНТЕРФЕЙСА (MDI). Это окно принимает сообщения, которые управляют дочерними окнами прикладной программы МНОГОДОКУМЕНТАЛЬНОГО ИНТЕРФЕЙСА. Рекомендованный стиль содержит — WS_CLIPCHILDREN и WS_CHILD. Чтобы создать рабочее окно MDI, которое позволяет пользователю при появлении листать дочерние окна MDI, определите стили WS_HSCROLL и WS_VSCROLL.
SCROLLBAR
(ЛИНЕЙКА ПРОКРУТКИ) Обозначает прямоугольник, который содержит бегунок и имеет стрелки направленные в оба конца. Линейка прокрутки посылает уведомительное сообщение своему родительскому окну всякий раз, когда пользователь щелкает мышью по элементу управления. В случае необходимости, родительское окно ответственно за модификацию позиции бегунка. Элементы управления линейкой прокрутки имеют тот же самый вид и пользуются функциями, что и линейки прокрутки, используемые в обычных окнах. Однако, в отличие от линеек прокрутки окна, линейки прокрутки элемента управления могут быть установлены для использования где-нибудь в окне, при прокрутке вводимой информации необходимой для окна. Класс линейки прокрутки включает также и элементы управления размером окна. Окно с изменяемыми размерами — это маленький прямоугольник, который пользователь может растягивать, чтобы изменить размер окна.
STATIC
(СТАТИЧЕСКИЙ ЭЛЕМЕНТ) Обозначает простое текстовое поле, окно или прямоугольник, используемый для надписей, окно или другие отдельные элементы управления. Статические элементы управления не берут никакой вводимой информации и не обеспечивают никакой выводимой информации.
Ниже перечисленные стили окон могут быть определены в параметре dwStyle:
- WS_BORDER — Создает окно, которое имеет тонкую линию рамки.
- WS_CAPTION — Создает окно, которое имеет строку заголовка (включает в себя стиль WS_BORDER).
- WS_CHILD — Создает дочернее окно. Этот стиль не может использоваться со стилем WS_POPUP.
- WS_CHILDWINDOW — То же самое, что и стиль WS_CHILD.
- WS_CLIPCHILDREN — Исключает область, занятую дочерними окнами, когда прорисовка происходит внутри родительского окна. Этот стиль используется при создании родительского окна.
- WS_CLIPSIBLINGS — Закрепляет дочерние окна относительно друг друга, то есть когда отдельное дочернее окно принимает сообщение WM_PAINT, стиль WS_CLIPSIBLINGS закрепляет все другие перекрывающие дочерние окна вне области дочернего окна, которое нужно модифицировать. Если стиль WS_CLIPSIBLINGS не определен, а дочерние окна перекрываются, то, возможно, что при прорисовке внутри рабочей области дочернего окна, будет выводиться внутренняя рабочая область соседнего дочернего окна.
- WS_DISABLED — Создает окно, которое первоначально заблокировано. Заблокированное окно не может принимать вводимую информацию от пользователя.
- WS_DLGFRAME — Создает окно, которое имеет стиль рамки, обычно используемый с диалоговыми окнами. Окно с этим стилем не может иметь строку заголовка.
- WS_GROUP — Определяет первый элемент управления в группе элементов управления. Группа состоит из этого первого элемента управления и всех определенных элементов управления после него, до следующего элемента управления со стилем WS_GROUP. Первый элемент управления в каждой группе обычно имеет стиль WS_TABSTOP, чтобы пользователь мог перемещаться из группы в группу. Пользователь может впоследствии передавать фокус клавиатуры от одной группы элементов управления в следующую группу элементов управления, используя клавиши со стрелками.
- WS_HSCROLL — Создает окно, которое имеет горизонтальную линейку прокрутки.
- WS_ICONIC — Создает окно, которое первоначально свернуто. Тот же самый стиль, что и WS_MINIMIZE.
- WS_MAXIMIZE — Создает окно, которое первоначально развернуто.
- WS_MAXIMIZEBOX — Создает окно, которое имеет кнопку Развернуть (Maximize). Не может быть объединен со стилем WS_EX_CONTEXTHELP. К тому же должен быть определен стиль WS_SYSMENU.
- WS_MINIMIZE — Создает окно, которое первоначально свернуто. Тот же самый стиль, что и WS_ICONIC.
- WS_MINIMIZEBOX — Создает окно, которое имеет кнопку Свернуть (Minimize). Не может быть объединен со стилем WS_EX_CONTEXTHELP. К тому же должен быть определен стиль WS_SYSMENU.
- WS_OVERLAPPED — Создает перекрывающее окно. Перекрывающее окно имеет строку заголовка и рамку. Тот же самый стиль, что и WS_TILED.
- WS_OVERLAPPEDWINDOW — Создает перекрывающее окно со стилями WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX и WS_MAXIMIZEBOX. Тот же самый, что и стиль WS_TILEDWINDOW.
- WS_POPUP — Создает выскакивающее окно. Этот стиль не может использоваться со стилем WS_CHILD.
- WS_POPUPWINDOW — Создает выскакивающее окно со стилями WS_BORDER, WS_POPUP и WS_SYSMENU. Стили WS_CAPTION и WS_POPUPWINDOW должны быть объединены, чтобы сделать меню окна (window) видимым.
- WS_SIZEBOX — Создает окно, которое имеет установку размеров рамки окна. Тот же самое, что и стиль WS_THICKFRAME.
- WS_SYSMENU — Создает окно, которое имеет меню окна (window-menu) в его строке заголовка. К тому же должен быть определен стиль WS_CAPTION.
- WS_TABSTOP — Определяет элемент управления, который может принимать фокус клавиатуры, когда пользователь нажимает клавишу ТАБУЛЯЦИИ (TAB). Нажатие на клавиши табуляции передает фокус клавиатуры на следующий элемент управления со стилем WS_TABSTOP.
- WS_THICKFRAME — Создает окно, которое имеет установку размеров рамки окна. То же самое, что и стиль WS_SIZEBOX.
- WS_TILED — Создает перекрывающее окно. Перекрывающее окно имеет строку заголовка и рамку. То же самое, что и стиль WS_ OVERLAPPED.
- WS_TILEDWINDOW — Создает перекрывающее окно со стилями WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX и WS_MAXIMIZEBOX. То же самое, что и стиль WS_ OVERLAPPEDWINDOW.
- WS_VISIBLE — Создает окно, которое является первоначально видимым.
- WS_VSCROLL — Создает окно, которое имеет вертикальную линейку прокрутки.
Ниже перечисленные стили кнопок (в классе BUTTON), которые могут быть определены в параметре dwStyle:
- BS_3STATE — Создает кнопку, которая является такой же, как окошко для флажка, за исключением того, что поле окна может стать недоступным так же, как это делается при установке флажка («галочки») проверки (checked) или при отмене его. Используйте недоступное состояние, чтобы показать, что состояние окошка для флажка не определено.
- BS_AUTO3STATE — Создает кнопку, которая является таким же переключателем с тремя состояниями, за исключением того, что поле окна изменяет свое состояние, когда пользователь выбирает его. Состояние циклически проходит фазы установки флажка проверки, недоступности и отмены установки.
- BS_AUTOCHECKBOX — Создает кнопку, которая также является окошком для флажка, за исключением того, что состояние установки флажка проверки автоматически переключается между установленным и не установленным параметром, каждый раз, когда пользователь выбирает эту кнопку.
- BS_AUTORADIOBUTTON — Создает кнопку, которая то же, что и «радио» кнопка, за исключением того, что, когда пользователь выбирает её, Windows автоматически устанавливает состояние кнопки в режим контроля флажка, отметив ее «галочкой» и автоматически устанавливает проверку состояния для всех других кнопок в той же самой группе без проверки флажка.
- BS_CHECKBOX — Создает маленькое, пустое окошко для флажка с текстом. По умолчанию, текст отображается справа от окошка. Чтобы отображать текст слева от окошка, объедините этот флажок со стилем BS_LEFTTEXT (или с эквивалентным стилем BS_RIGHTBUTTON).
- BS_DEFPUSHBUTTON — Создает командную кнопку, которая ведет себя подобно кнопке стиля BS_PUSHBUTTON и к тому же имеет жирную черную рамку. Если кнопка находится в диалоговом окне, пользователь может выбрать кнопку, нажав клавишу ENTER, даже тогда, когда кнопка не имеет фокуса ввода. Этот стиль полезен для предоставления пользователю возможности быстро выбрать наиболее подходящую (заданную по умолчанию) опцию.
- BS_GROUPBOX — Создает прямоугольник, в котором могут быть сгруппированы другие элементы управления. Любой текст, связанный с этим стилем отображается в верхнем левом угле прямоугольника.
- BS_LEFTTEXT — Помещает текст слева от «радио» кнопки или окошечка-переключателя, когда объединен со стилем переключателя или «радио» кнопкой. Тот же самое, что и стиль BS_RIGHTBUTTON.
- BS_OWNERDRAW — Создает кнопку представляемую владельцем. Окно владельца принимает сообщение WM_MEASUREITEM, когда кнопка создана и сообщение WM_DRAWITEM, когда внешний вид кнопки изменился. Не объединяйте стиль BS_OWNERDRAW с любыми другими стилями кнопки.
- BS_PUSHBUTTON — Создает командную кнопку, которая отправляет сообщение WM_COMMAND окну владельца, когда пользователь выбирает эту кнопку.
- BS_RADIOBUTTON — Создает маленький кружок с текстом. По умолчанию, текст отображается справа от кружка. Чтобы отображать текст слева от кружка, объедините этот флажок со стилем BS_LEFTTEXT (или его эквивалентом — стилем BS_RIGHTBUTTON). Используйте «радио» кнопки для групп связанного, но взаимоисключающего выбора.
- BS_USERBUTTON — Устаревшая, но предусматривающая совместимость с 16-разрядными версиями Windows. Базирующиеся на Win32 прикладные программы должны использовать BS_OWNERDRAW взамен этого параметра.
- BS_BITMAP — Определяет, что кнопка отображает точечный рисунок.
- BS_BOTTOM — Помещает текст внизу прямоугольника кнопки.
- BS_CENTER — Выравнивает текст горизонтально по центру в прямоугольнике кнопки.
- BS_ICON — Определяет, что кнопка отображается как значок.
- BS_LEFT — Выравнивает слева текст в прямоугольнике кнопки. Однако, если кнопка — окошечко-переключатель или «радио» кнопка, которые не имеет стиля BS_RIGHTBUTTON, текст остается выровненным справа от переключателя или «радио» кнопки.
- BS_MULTILINE — Переносит по словам текст кнопки в дополнительные строки, если текстовая строка слишком длинна, чтобы поместиться в одной строке в прямоугольнике кнопки.
- BS_NOTIFY — Дает возможность кнопке послать уведомительные сообщения BN_DBLCLK, BN_KILLFOCUS и BN_SETFOCUS в её родительское окно. Обратите внимание, что кнопки посылают уведомительное сообщение BN_CLICKED независимо от того, имеет ли она этот стиль.
- BS_PUSHLIKE — Создает кнопку (типа переключателя, переключателя с тремя состояниями или «радио» кнопки) имеющую вид и действующую подобно командной кнопке. Выпуклый вид кнопки, когда она не нажата или не выбрана и притопленый, когда она нажата или выбрана.
- BS_RIGHT — Выровненный справа текст в прямоугольнике кнопки. Однако, если кнопка — окошко для флажка или «радио» кнопка, которая не имеет стиля BS_RIGHTBUTTON, текст выровнен по правому краю справа от окошка для флажка или «радио» кнопки.
- BS_RIGHTBUTTON — Устанавливает кружок «радио» кнопки или квадрат окошка для флажка справа от прямоугольника кнопки. Тот же самый стиль, что и BS_LEFTTEXT.
- BS_TEXT — Определяет, что кнопка отображает текст.
- BS_TOP — Размешает текст вверху прямоугольника кнопки.
- BS_VCENTER — Размещает текст в середине (вертикально) прямоугольника кнопки.
Ниже перечислены стили комбинированного окна (в классе COMBOBOX), которые могут быть определены в параметре dwStyle:
- CBS_AUTOHSCROLL — Автоматически прокручивает текст в поле редактирования текста вправо, когда пользователь вводит с клавиатуры символ в конце строки. Если этот стиль не установлен, принимается только текст, который помещается внутри прямоугольной границы поля.
- CBS_DISABLENOSCROLL — В окне со списком показывает вертикальную линейку прокрутки заблокированной, когда поле окна содержит не достаточно элементов для прокрутки. Без этого стиля, линейка прокрутки скрыта, если окно со списком содержит не достаточно элементов.
- CBS_DROPDOWN — Подобен CBS_SIMPLE, за исключением того, что окно со списком не отображается, пока пользователь не выберет значок рядом с полем редактирования текста.
- CBS_DROPDOWNLIST — Подобен CBS_DROPDOWN, за исключением того, что поле редактирования текста заменено статическим текстовым элементом, который отображает текущий выбор в окне со списком.
- CBS_HASSTRINGS — Определяет, что представляемое владельцем комбинированное окно содержит элементы, состоящие из строк. Комбинированное окно поддерживает память и адрес для строк, так что прикладная программа может использовать сообщение CB_GETLBTEXT, чтобы восстановить текст для отдельного элемента.
- CBS_LOWERCASE — Преобразовывает в нижний регистр любые символы верхнего регистра, введенные в поле редактирования текста комбинированного окна.
- CBS_NOINTEGRALHEIGHT — Определяет, что размер комбинированного окна — это точный размер, определенный прикладной программой, когда она создала комбинированное окно. Обычно, Windows устанавливает размеры комбинированного окна так, чтобы оно не отображало элементы частично.
- CBS_OEMCONVERT — Преобразует текст, введенный в поле редактирования текста комбинированного окна. Текст преобразуется из набора символов Windows в набор символов OEM, а затем обратно в набор Windows. Это гарантирует соответствующее символьное преобразование, когда прикладная программа вызывает функцию CharToOem, чтобы преобразовать строку Windows в комбинированном окне в символы OEM. Этот стиль наиболее полезен для комбинированных окон, которые содержат имена файлов и применяются только в комбинированных окнах, созданных со стилем CBS_SIMPLE или CBS_DROPDOWN.
- CBS_OWNERDRAWFIXED — Определяет, что владелец окна со списком ответственен за прорисовку его содержания и что элементы в окне со списком все равной высоты. Окно владельца принимает сообщение WM_MEASUREITEM, когда комбинированное окно создано, а сообщение WM_DRAWITEM, когда внешний вид комбинированного окна изменился.
- CBS_OWNERDRAWVARIABLE — Определяет, что владелец окна со списком ответственен за прорисовку его содержания и что элементы в окне со списком являются переменными по высоте. Окно владельца принимает сообщение WM_MEASUREITEM для каждого элемента комбинированного окна, когда Вы создаете комбинированное окно; окно владельца принимает сообщение WM_DRAWITEM тогда, когда изменился внешний вид комбинированного окна.
- CBS_SIMPLE — Всегда отображать окно со списком. Текущий выбор в окне со списком отображается в поле редактирования текста.
- CBS_SORT — Автоматически сортирует строки, введенные в окно со списком.
- CBS_UPPERCASE — Преобразовывает любые символы нижнего регистра в символы верхнего регистра, введенные в поле редактирования текста комбинированного окна.
Ниже перечисленные стили поля редактирования текста (в классе EDIT) могут быть определены в параметре dwStyle:
- ES_AUTOHSCROLL — Автоматически прокручивает текст вправо на 10 символов, когда пользователь напечатает символ в конце строчки. Когда пользователь нажимает клавишу ENTER, управление прокручивает весь текст обратно, чтобы установить нуль.
- ES_AUTOVSCROLL — Автоматически перемещает текст вверх на одну страницу, когда пользователь нажимает клавишу ENTER на последней строчке.
- ES_CENTER — Выравнивает по центру текст в многостроковом поле редактирования текста.
- ES_LEFT — Выравнивание текста слева.
- ES_LOWERCASE — Преобразовывает все символы в нижний регистр, поскольку они печатаются внутри поля редактирования текста.
- ES_MULTILINE — Обозначает многостроковое окно редактирования текста. Значение по умолчанию — одностроковое окно редактирования текста. Когда многостроковое поле редактирования находится в диалоговом окне, заданная по умолчанию ответная реакция на нажим клавиши ENTER должна активизировать кнопку по умолчанию. Чтобы использовать клавишу ENTER для перевода строки, стиль используйте ES_WANTRETURN. Когда многостроковое окно редактирования не в диалоговом окне и определен стиль ES_AUTOVSCROLL, поле редактирования показывает столько строчек, сколько это возможно и прокручивает вертикально, когда пользователь нажимает клавишу ENTER. Если Вы не определяете ES_AUTOVSCROLL, окно редактирования показывает столько строчек, сколько это возможно и подает звуковой сигнал, если пользователь нажимает клавишу ENTER, но больше ни строчки не может отобразиться в окне. Если Вы определяете стиль ES_AUTOHSCROLL, многостроковое окно редактирования автоматически горизонтально прокручивается, когда каретка проходит за правый край элемента управления. Чтобы запустить новую строку, пользователь должен нажать клавишу ENTER. Если Вы не определяете ES_AUTOHSCROLL, элемент управления, когда это необходимо, автоматически переносит без разрыва слова в начало следующей строки. Новая строка образуется и тогда, если пользователь нажимает клавишу ENTER. Размер окна определяет позицию перехода слова на новую строку. Если размер окна изменяется, изменяется позиция перехода на новую строку, а текст восстанавливается. Многостроковое окно редактирования текста может иметь линейки прокрутки . Окно редактирования с линейками прокрутки обрабатывают свои собственные сообщения от линейки прокрутки. Обратите внимание, что окно редактирования без линеек прокрутки, прокручивают текст, как описано в предыдущих параграфах и обрабатывают любые сообщений прокрутки, посланные родительским окном.
- ES_NOHIDESEL — Отрицает заданное по умолчанию поведение для поля редактирования текста. Заданное по умолчанию поведение скрывает выбор, когда элемент управления теряет фокус ввода и инвертирует выбор, когда панель управления принимает фокус ввода. Если Вы определяете ES_NOHIDESEL, выбранный текст инвертируется, даже если панель управления не имеет фокуса.
- ES_NUMBER — Позволяет ввести в поле редактирования только цифры.
- ES_OEMCONVERT — Преобразует текст, введенный в окно редактирования. Текст преобразуется из набора символов Windows — в набор символов OEM, а затем обратно — в набор Windows. Это гарантирует соответствующее символьное преобразование, когда из прикладной программы вызывается функция CharToOem, чтобы преобразовать строку Windows в окне редактирования в символы OEM. Этот стиль наиболее полезен для окон редактирования текста, которые содержат имена файлов.
- ES_PASSWORD — Отображает звездочку (*) вместо каждого символа, введенного с клавиатуры в окно редактирования. Вы можете использовать сообщение EM_SETPASSWORDCHAR, чтобы заменить ею символ, который отображается.
- ES_READONLY — Не допускает пользователя к вводу или редактированию текста в окне редактирования.
- ES_RIGHT — Выравнивает по правому краю текст в многострочном окне редактирования.
- ES_UPPERCASE — Преобразует все символы в символы верхнего регистра, когда они вводятся в окно редактирования.
- ES_WANTRETURN — Определяет, чтобы служебный код возврата каретки был вставлен тогда, когда пользователь нажимает клавишу ENTER при вводе текста в многострочное поле редактирования текста в диалоговом окне. Если Вы не определяете этот стиль, нажимая клавишу ENTER, вы получите тот же самый эффект, словно нажали заданную по умолчанию командную кнопку диалогового окна. Этот стиль не имеет никакого влияния в однострочном окне редактирования.
Время на прочтение12 мин
Количество просмотров149K
Disclaimer
Казалось бы, что WinAPI уходит в прошлое. Давно уже существует огромное количество кросс-платформенных фреймфорков, Windows не только на десктопах, да и сами Microsoft в свой магазин не жалуют приложения, которые используют этого монстра. Помимо этого статей о том, как создать окошки на WinAPI, не только здесь, но и по всему интернету, исчисляется тысячами по уровню от дошколят и выше. Весь этот процесс разобран уже даже не по атомам, а по субатомным частицам. Что может быть проще и понятнее? А тут я еще…
Но не все так просто, как кажется.
Почему о WinAPI сейчас?
В один прекрасный момент, изучая потроха одной из игр в весьма неплохом эмуляторе NES, я подумал: Вроде неплохой такой эмуль, а в отладчике нет такой простой вещи, как навигация по кнопкам клавиатуры, которая есть в любом нормальном отладчике.
Здесь я не зря дал ссылку на репозиторий, т.к. видно, что ребята столкнулись с проблемой, о которой речь пойдет ниже, но так и не решили ее.
О чем это я? А вот об этом кусочке кода:
case WM_KEYDOWN:
MessageBox(hwndDlg,"Die!","I'm dead!",MB_YESNO|MB_ICONINFORMATION);
break;
Таким образом, авторы хотели добавить поддержку клавиатуры, но суровая реальность недр архитектуры диалоговых окон в Windows жестко пресекла такую самодеятельность. Те, кто пользовался эмулятором и отладчиком в нем, хоть раз видели это сообщение?
В чем же проблема?
Ответ такой: так делать нельзя!
И, возвращаясь, к изначальному вопросу о WinAPI: очень много популярных, и не очень, проектов продолжают его использовать и в настоящее время, т.к. лучше, чем на чистом API многие вещи не сделать (тут можно бесконечно приводить аналогии вроде сравнения высокоуровневых языков и ассемблера, но сейчас не об этом). Да и мало ли почему? Просто используют и все тут.
О проблеме
Диалоговые окна упрощают работу с GUI, одновременно лишая нас возможности сделать что-то самостоятельно. Например, сообщения WM_KEYDOWN/WM_KEYUP, приходящие в оконную процедуру, «съедаются» в недрах DefDlgProc, беря на себя такие вещи, как: Навигация по Tab, обработка клавиш Esc, Enter, и т.д. Кроме того, диалоги не нужно создавать вручную: проще, ведь, набросать кнопок, списков, в редакторе ресурсов, вызвать в WinMain CreateDialog/DialogBox и все готово.
Обойти такие мелкие неприятности просто. Есть, как минимум, два вполне легальных способа:
- Создать свой собственный класс через RegisterClassEx и в процедуре обработки класса схватывать WM_KEYDOWN, перенаправлять в процедуру обработки самого диалога. Да-да! Можно создавать диалоги со своим собственным классом, и встроенный в VS редактор даже позволяет задавать имя класса для диалога. Вот только кто об этом знает и этим пользуется?
Минус очевиден: Нужно регистрировать еще один класс, иметь на 1 CALLBACK процедуру больше, суть которой будет всего-навсего в трансляции пары сообщений. Кроме того, мы не будем знать куда их транслировать, и придется городить костыли. - Использовать встроенный механизм акселераторов. И нам даже не придется менять код диалоговой процедуры! Ну, разве что, добавить одну строчку в switch/case, но об этом ниже.
Tutorials?
Не побоюсь сказать, что все туториалы по созданию окон через WinAPI начинаются с такого незамысловатого кода, обозначая его, как «цикл обработки сообщений» (опущу детали по подготовке класса окна и прочую обвязку):
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Здесь действительно все просто:
- GetMessage() выхватывает очередное сообщение из очереди, и ключевой момент: блокирует поток, если в очереди пусто.
- TranslateMessage() из WM_KEYDOWN/WM_KEYUP формирует сообщения WM_CHAR/WM_SYSCHAR (они нужны, если кто-то хочет сделать свой редактор текста).
- DispatchMessage() отправляет сообщение в оконную процедуру (если таковая существует).
Начнем с того, что этот код использовать опасно, и вот почему. Обратите внимание на сноску:
Because the return value can be nonzero, zero, or -1, avoid code like this:
while (GetMessage( lpMsg, hWnd, 0, 0)) ...
И ниже приводится пример правильного цикла.
Стоит сказать, что в шаблонах VS для Win32 приложений, написан именно такой неправильный цикл. И это очень печально. Ведь мало кто будет вникать в то, что сделали сами авторы, ведь это априори правильно. И неправильный код множится вместе с багами, которые очень сложно отловить.
После этого фрагмента кода, как правило, следует рассказ про акселераторы, и добавляется пара новых строчек (учитывая замечание в MSDN, предлагаю сразу писать правильный цикл):
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);
}
}
Этот вариант я видел чаще всего. И он (та-дам) снова неправильный!
Сперва о том, что изменилось (потом о проблемах этого кода):
В первой строчке из ресурсов загружается таблица клавиш, при нажатии на которые, будет формироваться сообщение WM_COMMAND с соответствующим id команды.
Собственно TranslateAccelerator этим и занимается: если видит WM_KEYDOWN и код клавиши, которые есть в этом списке, то (опять же ключевой момент) будет формировать сообщение WM_COMMAND (MAKEWPARAM(id, 1)) и отправлять в соответствующую для дескриптора окна, указанного в первом аргументе, процедуру обработки.
Из последней фразы, думаю, стало понятно, в чем проблема предыдущего кода.
Поясню: GetMessage выхватывает сообщения для ВСЕХ объектов типа «окно» (в число которых входят и дочерние: кнопки, списки и прочее), а TranslateAccelerator будет отправлять сформированную WM_COMMAND куда? Правильно: обратно в кнопку/список и т.д. Но мы обрабатываем WM_COMMAND в своей процедуре, а значит нам интересно ее получать в ней же.
Ясно, что TranslateAccelerator надо вызывать для нашего созданного окна:
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);
}
}
И вроде все хорошо и замечательно теперь: мы разобрали все детально и все должно работать идеально.
И снова нет. Это будет работать правильно, пока у нас ровно одно окно — наше. Как только появится немодальное новое окно (диалог), все клавиши, которые будут в нем нажаты оттранслируются в WM_COMMAND и отправляться куда? И опять же правильно: в наше главное окно.
На этом этапе предлагаю не городить костылей по решению этой тупиковой ситуации, а предлагаю рассмотреть вещи, которые уже реже (или почти не встречаются) в туториалах.
IsDialogMessage
По названию этой функции можно подумать, что она зачем-то определяет: относится данное сообщение диалогу или нет. Но, во-первых, зачем нам это знать? А во-вторых, что с этой информацией нам делать дальше?
На самом деле, делает она чуть больше, чем следует из названия. А именно:
- Осуществляет навигацию по дочерним контролам кнопками Tab/Shift+Tab/вверх/вниз/вправо/влево. Плюс еще кое-что, но этого нам достаточно
- По нажатии на ESC формирует WM_COMMAND( IDCANCEL )
- По нажатии на Enter формирует WM_COMMAND( IDOK ) или нажатие на текущую кнопку по умолчанию
- Переключает кнопки по умолчанию (рамочка у таких кнопок чуть ярче остальных)
- Ну и еще разные штуки, которые облегчают пользователю работу с диалогом
Что она нам дает? Во-первых, нам не надо думать о навигации внутри окна. Нам и так все сделают. Кстати, навигацию по Tab можно сделать, добавив стиль WS_EX_CONTROLPARENT нашему основному окну, но это топорно и не так функционально.
Во-вторых, она нам облегчит жизнь по всем остальным пунктам, перечисленным в списке (и даже немного больше).
Вообще, она используется где-то в недрах Windows для обеспечения работы модальных диалоговых окон, а программистам дана, чтобы вызывать ее для немодальных диалогов. Однако мы ее можем использовать где угодно:
Although the IsDialogMessage function is intended for modeless dialog boxes, you can use it with any window that contains controls, enabling the windows to provide the same keyboard selection as is used in a dialog box.
Т.е. теперь, если мы оформим цикл так:
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);
}
}
}
То наше окошко будет иметь навигацию, как в родном диалоге Windows. Но теперь мы получили два недостатка:
- Этот код также будет хорошо работать только с одним (немодальным) окном;
- Получив все достоинства диалоговой навигации, мы лишаемся прелестей в виде сообщений WM_KEYDOWN/WM_KEYUP (только для самого окна, а не для дочерних контролов);
И вот на этом этапе вообще все туториалы заканчиваются и начинаются вопросы: How to handle keyboard events in a winapi standard dialog?
Это первая ссылка в гугле, но поверьте: тысячи их. Про предлагаемые решений (лучшее из которых — это создать свой класс диалогов, о чем я писал выше, до subclassing и RegisterHotKey. Где-то я даже видел «лучший» из способов: использовать Windows Hooks).
Пора поговорить о том, чего нет в туториалах и ответах.
Как правило (как правило! Если кому-то захочется большего, то можно регистрировать свой класс для диалогов и работать так. И, если же, кому-то это интересно, я могу дополнить этим статью) WM_KEYDOWN хотят тогда, когда хотят обработать нажатие на клавишу, которая выполнит функцию в независимости от выбранного контрола в окне — т.е. некая общая функция для всего данного конкретного диалога. А раз так, то почему бы не воспользоваться богатыми возможностями, которые нам сама WinAPI и предлагает: TranslateAccelerator.
Везде используют ровно одну таблицу акселераторов, и только для главного окна. Ну действительно: цикл GetMessage-loop один, значит и таблица одна. Куда еще их девать?
На самом деле, циклы GetMessage-loop могут быть вложенными. Давайте еще раз посмотрим описание PostQuitMessage:
The PostQuitMessage function posts a WM_QUIT message to the thread’s message queue and returns immediately; the function simply indicates to the system that the thread is requesting to quit at some time in the future.
И GetMessage:
If the function retrieves the WM_QUIT message, the return value is zero.
Таким образом, выход из GetMessage-loop осуществится, если мы вызовем PostQuitMessage в процедуре окна. Что это значит?
Мы можем для каждого немодального окна в нашей программе создавать свой собственный подобный цикл. В данном случае DialogBoxParam нам не подходит, т.к. оно крутит свой собственный цикл и повлиять мы на него не можем. Однако если создадим диалог через CreateDialogBoxParam или окно через CreateWindow, то можно закрутить еще один цикл. При этом в каждом таком окне и диалоге мы должны вызывать 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);
}
}
}
// ....
LRESULT CALLBACK WndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch( umsg )
{
case WM_MYMESSAGE:
{
HWND hDlg = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_MYDIALOG), hwnd, MyDialogBoxProc);
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR_FOR_MY_DIALOG));
BOOL bRet = 0, fSavedEnabledState = IsWindowEnabled(hwnd);
EnableWindow(hwnd, FALSE); // disable parent window, as dialog window is modal
while (bRet = GetMessage(&msg, nullptr, 0, 0))
{
if ( -1 == bRet ) break;
if ( !TranslateAccelerator(hDlg, hAccel, &msg) )
{
if ( !IsDialogMessage(hDlg, &msg) )
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
EnableWindow(hwnd, fSavedEnabledState); // enable parent window. Dialog was closed
break;
}
}
}
INT_PTR CALLBACK MyDlgProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch(umsg)
{
case WM_CLOSE:
{
// EndDialog( hwnd, 0 ); -- DONT DO THAT!
// EndDialog is valid ONLY for Modal Dialogs, created with DialogBox(Param)
DestroyWindow( hwnd );
break;
}
case WM_DESTROY:
{
PostQuitMessage( 0 );
break;
}
// ....
}
return 0;
}
Обратите внимание: теперь для каждого нового окна в нашей программе мы можем добавить в обработку собственную таблицу акселераторов. WM_QUIT будет выхватывать GetMessage из цикла для диалога, а внешний цикл его даже не увидит. Почему так происходит?
Дело в том, что внешний цикл «встал» на вызове DispatchMessage, который вызвал нашу процедуру, которая крутит свой собственный внутренний цикл GetMessage с таким же DispatchMessage. Классический вложенный вызов (в данном случае DispatchMessage). Посему внешний цикл не получит WM_QUIT и не завершится на этом этапе. Все будет работать стройно.
Но и тут есть свои недостатки:
Каждый такой цикл будет обрабатывать сообщения только для «своего» окна. Про другие-то мы здесь не знаем. А значит, если где-то объявится еще один цикл, то все остальные окна не будут получать нужной обработки своих сообщений парой TranslateAccelerator/IsDialogMessage.
Что ж, пора учесть все эти замечание и написать наконец правильную обработку всех сообщений от всех окон нашей программы. Хочу заметить, что ниже рассматривается случай для одного потока. Т.к. каждый поток имеет свою очередь сообщений, то для каждого потока придется создавать свои структуры. Делается это весьма тривиальными изменениями в коде.
Делаем красиво
Т.к. правильная постановка задачи является половиной решения, то сперва надо эту самую задачу правильно же и поставить.
Во-первых, было бы логично, что только активное окно принимает сообщения. Т.е. для неактивного окна мы не будем транслировать акселераторы и передавать сообщения в IsDialogMessage.
Во-вторых, если для окна не задана таблица акселераторов, то транслировать нечего, будем просто отдавать сообщение в IsDialogMessage.
Создадим простой std::map, который будет мапить дескриптор окна в дескриптор таблицы акселераторов. Вот так:
std::map<HWND,HACCEL> l_mAccelTable;
И по мере создания окон будем в него добавлять новые окна с дескриптором на свою любимую таблицу (или нуль, если такая обработка не требуется).
Вот так:
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 ) );
}
Ну и после закрытия окна удалять. Вот так:
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 );
}
}
Теперь, как создаем новый диалог/окно, вызываем AddAccelerators( hNewDialog, IDR_MY_ACCEL_TABLE ). Как закрываем: DelAccel( hNewDialog ).
Список окон с нужными дескрипторами у нас есть. Немного модифицируем наш основной цикл обработки сообщений:
// ...
HWND hMainWnd = CreateWindow(...);
AddAccelerators(hMainWnd, IDR_ACCELERATOR);
BOOL bRet = 0;
while (bRet = GetMessage(&msg, nullptr, 0, 0))
{
if ( -1 == bRet ) break;
if ( !HandleAccelArray( GetActiveWindow(), msg ) )
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// ...
Значительно лучше! Что же там в HandleAccelArray и зачем там GetActiveWindow()?
Немного теории:
Есть две функции, возвращающих дескриптор активного окна GetForegroundWindow и GetActiveWindow. Отличие первой от второй вполне доходчиво описано в описании второй:
The return value is the handle to the active window attached to the calling thread’s message queue. Otherwise, the return value is NULL.
Если первая будет возвращать дескриптор любого окна в системе, то последняя только того, которое использует очередь сообщений нашего потока. Т.к. нас интересуют окна только нашего потока (а значит те, которые будут попадать в нашу очередь сообщений), то и возьмем последнюю.
Так вот HandleAccelArray, руководствуясь переданным ей дескриптором на активное окно, ищет это самое окно в нашей мапе, и если оно там есть, отдает это сообщение на трансляцию в TranslateAccelerator, а затем (если первый не увидел нужного) в IsDialogMessage. Если и последняя не обработала сообщение, то возвращаем FALSE, чтобы пройти по стандартной процедуре TranslateMessage/DispatchMessage.
Выглядит так:
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 ) )
{
// message not for TranslateAccelerator. Try it with IsDialogMessage
if ( !IsDialogMessage( hWnd, &msg ) )
{
// so, do default stuff
return FALSE;
}
}
// ok, message translated. Say to message-loop, to get next message
return TRUE;
}
BOOL HandleAccelArray( HWND hActive, MSG & msg )
{
if ( !hActive )
return FALSE; // no active window. Nothing to do
std::map<HWND, HACCEL>::const_iterator mh = l_mAccelTable.find( hActive );
if ( mh != l_mAccelTable.end() )
{
// Got it! Try to translate this message for the active window
return HandleAccelWindow( mh, msg );
}
return FALSE;
}
Теперь каждое дочернее окно вправе добавить себе любимую таблицу акселераторов и спокойно ловить и обрабатывать WM_COMMAND с нужным кодом.
А что там еще об одной строчке в коде обработчика WM_COMMAND?
Описание в TranslateAccelerator гласит:
To differentiate the message that this function sends from messages sent by menus or controls, the high-order word of the wParam parameter of the WM_COMMAND or WM_SYSCOMMAND message contains the value 1.
Обычно код обработки WM_COMMAND выглядит так:
switch( HIWORD( wParam ) )
{
case BN_CLICKED: // command from buttons/menus
{
switch( LOWORD( wParam ) )
{
case IDC_BUTTON1: DoButton1Stuff(); break;
case IDC_BUTTON2: DoButton2Stuff(); break;
// ...
}
break;
}
}
Теперь можно написать так:
switch( HIWORD( wParam ) )
{
case 1: // accelerator
case BN_CLICKED: // command from buttons/menus
{
switch( LOWORD( wParam ) )
{
case IDC_BUTTON1: DoButton1Stuff(); break;
case IDC_BUTTON2: DoButton2Stuff(); break;
// ...
}
break;
}
}
И теперь, возвращаясь к тому же fceux, добавив всего одну строчку в код обработки команд от кнопок, мы получим желаемое: управлять дебагером с клавиатуры. Достаточно добавить небольшую обертку вокруг главного цикла сообщений и новую таблицу акселераторов с нужными соответствиями VK_KEY => IDC_DEBUGGER_BUTTON.
P.S.: Мало кто знает, но можно создавать свою собственную таблицу акселераторов, а теперь и применять ее прямо налету.
P.P.S.: Т.к. DialogBox/DialogBoxParam крутит собственный цикл, то от при вызове диалога через них акселераторы работать не будут и наш цикл (или циклы) будет «простаивать».
P.P.P.S.: После вызова HandleAccelWindow мап l_mAccelTable может измениться, т.к. TranslateAccelerator или IsDialogMessage вызывают DispatchMessage, а там может встретиться AddAccelerators или DelAccel в наших обработчиках! Поэтому лучше его после этой функции не трогать.
Пощупать код можно здесь. За основу был взят код, генерируемый из стандартного шаблона MS VS 2017.