▷▷▷▷▷▷ Follow My Threads Account ◁◁◁◁◁◁
手机版
OpenGL Tutorial (1)
- by Changhai Lu -
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:
-
hInstance is a handle (pointer) to the current instance of the program. It is
assigned by the operating system to identify and access the program.
-
hPrevInstance is an abandoned argument kept only for
backward compatibility, it is always NULL for Win32 applications.
-
lpCmdLine is a pointer to the command line arguments passed to the program
as a single string (excluding the program name).
-
iCmdShow is a parameter specifies how the
program window is to be shown (as normal window, maximized or minimized). Since all the command line arguments go to
lpCmdLine, you may wonder how could you pass a value to
iCmdShow? Well, there are some tricky ways to do it. For instance,
when you create a shortcut for a program, among the properties you can select for the shortcut, there
is a combo-box named "Run:", in which you can choose among "Normal window", "Minimized"
and "Maximized". When you launch the program from the shortcut
the choices you made for the combo-box will pass to iCmdShow.
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:
- Write a window procedure to handle messages
- Specify a window class
- Register the window class
- Create a window based on the window class
- Display the window on the screen
- Run a message loop to take care of the message queue
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.
#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:
-
hwnd is a handle to the to-be-created window.
-
msg is a structure used to store message information.
-
wndclass is the window class we talked before.
We then assign values to each of the field in wndclass. Among
the fields, the following are the most important ones to our example:
-
lpfnWinProc is a pointer to the window procedure, the value
assigned to it - "WinProc" - matches the name we choose for the window procedure. This
field tells a window where to find its window procedure.
-
hInstance is the handle to the program that owns the window - one of the
argument passed to WinMain by the operating system. We know
that a program has a handle to each of the window
it owns, now that we see a window holds a handle to the program as well. This
means not only that a program can control its windows, a window can have back-reaction to the program
as well (example of such back-reaction: closing the last window of a program
usually kills the program).
-
hIcon
and hCursor specify the icon and cursor used by the window.
-
hbrBackground is a handle to the physical brush for painting
the background of the window, if sets to NULL, no specific
painting will be done, whatever behind the window will fill in the background. In our
example, we used the GetStockObject function to load a white brush, which
paints a white background for the window.
-
lpszClassName is a text name of the window class, used
to identify the window class in the CreateWindow function.
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:
-
hwnd is a handle to the window recieving the message (the destination window).
This argument is needed because mutliple windows may be created based on a single
window class, therefore use a common window procedure, so it is necessary for
the procedure to know which window is recieving the message.
-
iMsg is a number that identifies the message type.
-
wParam and lParam carry
some further information about the message.
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.
>> Next Tutorial
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:
-
hwnd is handle to the window whose window procedure receives the message.
-
message is a number that identifies the message type.
-
wParam specifies additional information about the message.
The exact meaning depends on the value of the message field (i.e. the message type).
-
lParam specifies additional information about the message.
The exact meaning depends on the value of the message field (i.e. the message type).
-
time specifies the time at which the message was placed in the message queue.
-
pt specifies the cursor position, in screen coordinates,
when the message was placed in the message queue.
-
Microsoft Corporation, MSDN Library, comes with Visual Studio 6.0, also available
on the Microsoft website.
-
Charles Petzold, Programming Windows (5th edition), Microsoft Press, 1998.
posted on September 10, 2002 https://www.changhai.org/
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.
September 10, 2002
|