OpenGL Tutorial
- Lesson One -
Abstract: In this lesson, we will quickly go over some of the most important concepts in Win32 programming, especially the message loop and callback function (the message handler). As you can guess from the abstract, we will use Windows as our development platform. All the examples are tested against Visual C++ 6.0 on Windows 2000, and I believe they will run fine on any other Win32 platforms. 1. A "Hello world!" C program To see how the complexity grows when one goes from console programming to Win32 programming, let's start with one of the simplest programs in the world - the "Hello world!" program in C: #include <stdio.h> int main () { printf("Hello, world!\n"); return 0; } Nothing much to say about this little program, everyone reading this tutorial is assumed already knew C/C++ therefore should be familiar with it. One thing to keep in mind is even though we call it a C program and put it in this seperate section as if it were something radically different from a Win32 program, it actually runs on Win32 as well (as a console application). So to be more specific, when we talk about Win32 program we always mean "Win32 Application" (as oppose to "Win32 Console Application"). We will explain these terminologies more in the next section. 2. A lazy man's "Hello world!" Win32 program The "Hello world!" Win32 program - in its simplest and laziest form - is fairly parallel to the C program we saw above: #include <windows.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) { MessageBox(NULL, "Hello, world!", "Example", 0); return 0; } To try this program out, start a new empty Win32 Application in Visual C++ 6.0, create a C++ source file, paste the code over, and run it. The program will bring up a message box with an "OK" button on it. The text content displayed on the message box - "Hello, world!" - and the title of the message box - "Example" - are two of the four arguments passed to the MessageBox function provided by the Win32 API. C/C++ is a typed language, which means one must enter the correct number and type of arguments in the correct order. Most of the Win32 API functions are declared with a large number of arguments for the sake of generality, many of of these arguments are not used in simple examples. So in this tutorial as well as in many other places you will constantly see trivial values (NULL, 0, etc) passed to functions just to keep the signature correct. To keep our focus sharp, we will skip some of those un-used arguments. You can check out the Win32 API documentation for their meanings if interested. Two major features you will see in almost every Win32 application are present in this simple example: The windows.h file - a master include file (which includes many other header files) for Win32 programs, and the WinMain function - the entry point of Win32 programs analogous to the main function in normal C/C++ programs. It is the WinMain function that differentiates a Win32 application from a Win32 console application. If you replace it by a normal main function, your program will become a console program (if you want to try it out, you should select "Win32 Console Application" when creating the application). The return type of the WinMain function may look a little bit strange if you never seen a Win32 program before, the first one - int - is the return type as you normally see in C/C++ functions, the second one - WINAPI is a calling convention used to tell the compiler that the Pascal rather than C ordering should be used for pushing the arguments onto the stack (it is totally transparent to programmers). The four arguments passed to WinMain are as follows:
3. A "real" window The message-box we use in the previous example is a window pre-defined by the Win32 API, the complexity of creating a window is hidden to the users. To see the real structure of a Win32 program and have a full control on a window, we need to create a user-defined window - a "real" window. Before we jump into the code, it is helpful to have a quick look at the general picture of Win32 platform and Win32 programming. Windows is an event-driven operating system, each time an event occurs, a message will be generated by the operating system. The message contains all the information needed to process the event, including a handle to its destination window. Some of those messages (usually the ones generated by user interacting with a window) will be sent to the message queue of the program that owns that window. The message queue is created by the operating system once a program begins to run. Each running program has a chunk of code called message loop that checks its message queue and dispatches available messages to their destination windows (a program may own multiple windows). Each window a program created has a function called window procedure that handles those messages. In addition to the messages that go through the message queue, the operating system can also send messages directly (namely bypassing the message queue) to the relevant window procedure. Those are usually the system generated messages, for instance when a CreateWindow function is called by the program, a WM_CREATE message will be sent directly to the window procedure of the new window. Win32 programming follows the style of object-oriented programming in which it is necessary to define a class before creating any instance of it. So to create a window, one must specify a class - called window class - first. Windows API provides a structure called WNDCLASS which contains, among other fields, a pointer to the window procedure. To specify a window class, you need to assign values for each of the fields in the WNDCLASS structure. Once a window class is ready, you register it by calling the RegisterClass function. Finally, of course, the window needs to be created and visually displayed on the screen. In summary, the steps of creating a window are the following:
With this general picture in mind, now let's take a look at the code (the comments in the code are in one-to-one correpondence with the steps listed above). This code will create an empty window with white background on the screen. The code is made as minimal as I could. You are welcome to if you think it is possible to simplify it even further. #include <windows.h> // Window procedure - the message handler LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch (iMsg) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, iMsg, wParam, lParam); } return 0; } // WinMain function - the entry point int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) { HWND hwnd; MSG msg; WNDCLASS wndclass; // Specify a window class wndclass.style = 0; 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 = "ExampleClass"; // Register the window class RegisterClass(&wndclass); // Create a window based on the window class hwnd = CreateWindow("ExampleClass", "Example", WS_OVERLAPPEDWINDOW, 0, 0, 200, 200, NULL, NULL, hInstance, NULL); // Display the window on the screen ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); // Run a message loop to take care of the message queue while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } Let's go through the code. As we already known, the entry point of the program is the WinMain function, so let's start with it. Three variables are declared in the the beginning:
We then assign values to each of the field in wndclass. Among the fields, the following are the most important ones to our example:
All other fields are set to trivial values in our example. With all these fields set, the window class is now well-defined, we then register the class by passing a reference of the class - &wndclass - to the RegisterClass function. Once the class is registered, we can use the text name of the class ("ExampleClass" in our example) to create instance windows. Instance windows are created by calling the CreateWindow function. The first argument CreateWindow takes is the text name of the window class. The second argument is the title of the window - a string that will appear on the window manage. The third argument is the style of the window, the value we use - WS_OVERLAPPEDWINDOW - refers to a standard window with a window manager, border and a basic menu (minimize, maximize, close, etc). The four numbers 0, 0, 200, 200 specify the x and y coordinates (of the upper-left corner) and the width and height of the window. The remaining arguments are the handle to the parent window (none in our case), the handle to the menu (none in our case), the handle to the program itself (hInstance as assigned by the operating system and passed to WinMain), and something called the creation data that we don't use here. The CreateWindow function returns a handle to the newly created window. Once a window is created in this way, a block of memory is allocated for the window. But the window is not visible on the screen yet. To display it on the screen, two other functions - ShowWindow and UpdateWindow - are used. These functions take the handle to the window as an input, the ShowWindow function takes a second argument specifies the display mode (normal window, minimized or maximized) which usually takes the iCmdShow parameter passed to the WinMain function (if you use a different value, users will lose the opportunity of using iCmdShow to control the display mode). The UpdateWindow sends a WM_PAINT message directly to the window procedure, forces the window to be painted immediately (without it the WM_PAINT message will be queued, therefore in a dynamic program with lots of messages in the queue, the painting is not guaranteed to be immediate). After the window gets displayed, the program starts a message loop that takes care of its message queue. The GetMessage function in the loop condition retrieves one message at a time from the queue, stores it in msg - a MSG structure passed to it (the first argument). The GetMessage returns zero only when it retrieved a WM_QUIT message (if the queue is empty, GetMessage will wait), so the message loop will keep running until a WM_QUIT message is encountered. Once a message is retrieved, the message loop does some standard keyboard translation using the TranslateMessage function and then dispatches the message to the destination window procedure using the DispatchMessage function. (As we said before, each message - the MSG structure - has a handle to destination window, and each window has a pointer - in the window class - to its window procedure, this is how a message gets dispatched to the appropriate window procedure). The message loop introduced here is the simplest one. There is an alternative message loop you should check out in the Appendix. Please do not skip that appendix because we will be using that message loop when discussing OpenGL. Now let's look at the window procedure. The return type of a window procedure is declared as: LRESULT CALLBACK, where LRESULT is a macro representing a 32-bit value, CALLBACK is a calling convention used for all callback functions. The arguments a window procedure takes are the following:
Compare these arguments with the fields available in the MSG structure, it's not hard to notice that they are exactly the first four fields in the MSG structure. You may wonder why the DispatchMessage function only passes four out of the six fields to the window procedure? Why doesn't it pass &msg - which is what it takes - directly to the window procedure? The reason is because these two fields are used only by the operating system to resolve any conflict over the order of events and to determine where a specific event should be addressed. Also notice that these two fields are all related to the message queue therefore don't always make sense (since not every message goes through the message queue). The way of writing a window procedure is quite simple and standardized: write handlers for each of the message types that needs a manual handling, and pass everything else to the default handler - DefWindowProc - provided by the Win32 API. In the example we have, nothing much needs to be handled, so we pass everything except the WM_DESTROY message to the default handler. For WM_DESTROY we invoke the PostQuiteMessage function that puts a WM_QUIT message to the message queue associated to the program. As mentioned before, WM_QUIT is the message that breaks the message loop (therefore finishes the program). The reason that even in our rather trivial example, WM_DESTROY needs to be handled manually is because the default handler does NOT automatically call the PostQuitMessage function when recieving a WM_DESTROY message, therefore even though the window will still be closed, no WM_QUIT message is generated to stop the message loop and the program will therefore keep running (you can verify this by checking the processes running on your system). There are many things you can do in the window procedure to enhance your application. For instance if you want to give users an option before closing the window, you can handle the WM_CLOSE message - it is this message rather than the WM_DESTROY message that is directly generated by user pressing the close button on the window. (If you don't handle the WM_CLOSE message, the default message handler will close the window and send a WM_DESTROY message - by calling the DestroyWindow function.) By the way, instead of calling the PostQuitMessage function in the WM_DESTROY handler, some people do it in the WM_CLOSE handler, which is slightly more efficient because it bypasses the WM_DESTROY handler. (The reason we didn't do it this way is to demonstrate the message sequence in a closing process.) Alright, now there is only one line of code we haven't explained: the last line - return msg.wParam; - in the WinMain function. It is required by the operating system that when a program exits, the exit value returned to the system must be the wParam parameter of the WM_QUIT message. Since msg is exactly of type WM_QUIT when the message loop exits, so we return msg.wParam to the system. Finally, one thing to remind you is that in a real program, you should write some error checking code so that when something goes wrong, the user will get useful information (MessageBox is a handy function to display some of those information) ... What's next? So far the window we create is empty (no "Hello world!" on it). In the next lesson, we will introduce the Windows' Graphics Device Interface (GDI) and complete our "Hello world!" example. Appendix: An alternative message loop There is another way to run the message loop. Even though not as concise as the one we introduced above and will cause certain problem in our current example (see below for explanation), it is the one we need for OpenGL programming, so we introduce it here. The code is as follows:
// An alternative message loop
bool quit = false;
while(!quit) {
PeekMessage(&msg, hwnd, NULL, NULL, PM_REMOVE);
if (msg.message == WM_QUIT) {
quit = true;
} else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
The logic is quite simple: run a loop, peek the next message in the queue using the PeekMessage function. If the next message is WM_QUIT, set the loop variable quit to true so the message loop will exit. Otherwise, perform the same actions (TranslateMessage and DispatchMessage) as in the previous message loop. The PM_REMOVE argument passed to PeekMessage tells the function to remove the message from the queue after processing (so we will not peek the same message repeatedly). For an ordinary program such as the example we had in this lesson, the new message loop is not a good choice because the PeekMessage function, unlike its GetMessage cousin, will NOT wait when the message queue is empty, which makes the while-loop a busy loop that consumes lots of CPU cycles. In most OpenGL programs such as video games, however, fast rendering is the key and the program is usually not run in parallel with other major programs, therefore we do have a large fraction of the CPU cycles and we don't want to waste time sending WM_PAINT messages to the callback function for the handler to do the rendering each time. In that case, it is a lot more efficient to render graphics directly in the else-branch of the new (busy) message loop. That's why we need this new message loop for OpenGL programming. Glossary Callback function - A callback function is a function that will be called in the midst of the execution of an API function. A typical callback function is the window procedure, which will be called in the midst of the message loop in the WinMain function. MSG structure - The MSG structure is a structure used to store message information. An MSG structure contains the following fields:
References
|