Windows Programming Master Class - Stage 1
Frenzy
It seems that all if not most of us have decided to take a plunge into Windows programming. You may or may not like Windows programming at first but stick with it, much of what you will learn is portable knowledge since most GUIs operate on much the same principles. Anyway, this is not an article on the pro's and con's of windows so let's cut the babble and get on with the chase.
Windows has in the region of 1000+ API calls and countless number of flags, undocumented functions etc. The main thing is, don't even try to remember all these functions, it's enough to just to know they exist so you only spend a small amount of time going through the Win32 help to find the syntax. Also, Windows programs generally look worse than they are due to the Hungarian notation that was adopted by many Windows programmers in the honor of a famous Microsoft programmer. I personally hate this syntax, so where possible, I'll not use it. :)
The one thing you should realise is this is not a tutorial about making demos in Windows. Although, if there is request to continue this series I will cover more and more stuff which will relate to demo coding. This is a tutorial for you to learn how to do Windows programming. If you ever create any custom made tools for your demos then the quickest way to do that is in Windows. You'll soon wish you learned this stuff years ago. :)
Semi-OOP, Message-Driven Architecture and the Window Procedure
Where on earth would you get such a disgusting looking title as that you may ask? Obviously in a Windows tutorial. :)
Windows is a semi-object orientated system. You'll see what I mean when I talk about window classes. The thing to realise is that everything you see on a Windows application is most likely a window. A scroll bar is just several windows, a toolbar, a button, etc, etc. All these things are windows. Your application will have a hierarchy of windows with a main window and several child windows hanging off it. Each of these windows will have its own attributes to determine the look and behaviour of the window.
When ever something happens to a window the Windows system sends a message to the program the window belongs too. For example, say someone adjusts the size of the window. Windows will handle the resizing for you but your application will receive this re-size message and it will be up to you to handle it. If you had a program which displayed 'hello' in the center of a window. When the user resizes the window you would expect that the message stays in the center of the window still. At the end of the day, this is known as your event-driven programming paradigm. Most of the time your Windows application will sit there looking pretty until a user clicks his mouse and your window responds.
Where do you get the messages from you may ask?
Simple. Within your program you specify a window procedure for each and every window. This window procedure is called when an event occurs to that window. Windows passes to this procedure what event happened and some extra information depending on the event. For example, in a resize event you would expect that Windows not only sends you the ID of the resize event but also the new width and height of the window. Bill Gates ain't stupid you know, that's exactly what it does. :))
The Structure of a Windows Program
I really must present a Hello World program so you can have a windows shell to work from. All your Windows programs will follow this so here it is and I'll explain it in detail after:
//------------------------- cut here -----------------------------
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// Execution starts here :)
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char AppName[] = "TestProg";
HWND hwnd;
MSG msg;
WNDCLASSEX wc;
// setup our windows class for the main window
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = AppName;
wc.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
RegisterClassEx(&wc);
hwnd = CreateWindow(AppName, // window class name
"Test Program", // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters
ShowWindow(hwnd, iCmdShow); // put up window
UpdateWindow(hwnd); // paint it!
// Message Handling Loop!
while(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
// Window Procedure for the main window
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
switch(iMsg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, iMsg, wParam, lParam);
}
// -------------------- end ------------------------------
The WinMain Function
Yes, you all know that main() is the C function where execution starts when your program is run. Well, not in Windows! It's called WinMain. This is probably a pretty dumb thing to do (hello DLR :). Portability issues... Blah. Well, who are we to ask why? :)
The parameters passed to this function are:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow);
The hInstance is a handle (a handle is just some internal value that is used to index into some table so the system can keep track of things) to your application. If you ran several copies of your program then each one running would be a different instance. Think of it as a process ID.
The hPrevInstance is a handle to the previous instance of the application, it's actually not used anymore. Backwards compatability! Ignore it. :)
szCmdLine is a pointer to the command line. The sz prefix means 'zero terminated string' and the PSTR is pointer to string. I.e., it's really just a typedef for char *. :)
iCmdShow is how your window program is initially displayed. I'm sure you are aware that some Windows programs can be made to load up and minimize straight away. The value in here would just determine how the window is displayed.
Window Classes
Did I just say, for each and every window you specify a window procedure? Geeze, that's a lot of windows and a lot of procedures. Imagine how many windows are used in the Microsoft Visual C/C++ IDE. :) Well, this is where your semi-oo comes into play. Your window is an object. Some windows, however, have the same basic attributes but differ only in certain ways. So, you have your basic CLASS of window and windows of similar functionality are based on this class. This makes sense I hope and it will do very shortly anyway. :)
Let's say we have a menu and for every option in this menu we have a button that corresponds to loading up a different application. You click on button 1 and up pops QUAKE, click on button 2 and wey hey, MIRC comes into action... You can obviously see that the buttons do the exact same job. They get clicked on, they depress, they trigger your app to load an application. So, if this is the case, you wouldnt wan't a different window procedure for every button in your menu. You would want them to share the same window procedure. It's neat, tidy and it's pretty darn obvious. So, let's introduce the window class.
typedef struct _WNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;
Here we are. This is a simple data structure you fill in to tell Windows about your window class. Once you have filled it in you simply call the function:
RegisterClassEx(WNDCLASSEX *myclass);
Now, once you have done this you can create as many windows as you like using this class and all the windows will have the common attributes specified in this data structure.
In the program above I filled in the fields like this:
WNDCLASSEX wc;
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = AppName;
wc.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
The 'cbSize' field is the number of bytes of the WNDCLASSEX structure. The cb prefix means 'count in bytes'.
The 'style' field is a bitmask representing the basic styles windows of this class will have.
CS_HREDRAW - Redraws the entire window if a movement or size adjustment changes the width of the client area.
CS_VREDRAW - Redraws the entire window if a movement or size adjustment changes the height of the client area.
These are just flags, they are optional. Look in the Windows help for more information on the different flags for class styles available. I will of course explain any ones I present. :)
Ahh, a term was introduced just you may have missed. Client Area! That's the area inside the window that you can draw on. I.e, the area contained by the window border and title bar in this case.
The 'lpfnWndProc' is the function pointer to your window procedure for all windows created using this class. Now, the window procedure prototype looks like this:
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);
It always looks like this, I'll talk about the parameters of it in a moment.
The 'cbClsExtra' and 'cbWndExtra' are generally set to zero. Ignore them for the time being.
The 'hInstance' flag is the instance of your application. Imagine if you ran your application several times. Each version of it is an instance. This is passed to your WinMain function.
The 'hIcon' is a handle to an icon. This is the icon you will see when you look on your harddrive at the exe. The LoadIcon() is a function to load an icon and looks like this:
HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);
This returns a handle to an icon. Notice that in the sample program above I did this:
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
The hInstance value was set to NULL. The reason for that is that will tell Windows to use a pre-defined icon (called IDI_APPLICATION). If I had set it to hInstance passed to WinMain I could then pick an icon I drew in the resource editor.
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
The 'hCursor' field is a handle to a mouse cursor. The same rules apply, I'm using the predefined arrow cursor.
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
The 'hbrBackground' specifies what BRUSH to use as the background for the window. When Windows wants to clear the window it will use the brush specified in the window class for that window. It just so happens I'm using a predefined brush which is just white. Meaning, when my window is cleared it's filled white. :) I'm going to leave the GetStockObject function for another day. The 'lpszMenuName' is a pointer to a zero terminated string. It's usually a resource string telling what drop down menu will be placed in the window. You would create these menus in your resource editior and then place the menu name in this value. Then any window created with this class will contain that drop down menu. If you put NULL there you won't get a menu obviously. :)
The 'lpszClassName' field is again a zero terminated string specifing the name of the class you wish to call this. In most Windows programs you have at least one window. This is usually your main window and it's often called the name of your program. You can call it what you like.
The 'hIconSm' field is the same as the 'hIcon' field except that this icon is displayed in the top left of your window and when you minimize it. It can point to the same icon as hIcon and it will automatically be scaled.
Phew, that's a lot of information. Just to specify the class. However, the code is very small. It won't take long before you remember all these fields as you will be creating lots of window classes. :)
Windows
Okay, we still dont have a window on the screen. :) We have created a basic class that we can use to create our main window for our application. We have specified the window procedure for it, so let's create the actual window.
hwnd = CreateWindow(AppName, // window class name
"Test Program", // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters
Boom! This function will return us a handle to the window we have created. Keep this value, anything you do to your window will be through the handle.
I'll quickly break this down, it doesn't really take as much understanding as the class structure since it speaks for itself.
First up, notice I'm specifing the window's class name. It will then go off and use all the parameters I set in that for that class. I can create as many windows as I like using the same class name. You can also see I specify a title. The "Test Program" will be displayed in the window title. The next field is the window style. As well as inheriting the CS_HREDRAW and CS_VREDRAW style it will also have the WS_OVERLAPPEDWINDOW style. This information is all in the Windows help but it will just create a standard looking window. The next few fields of the windows coordinates on the screen. The initial x and y position are for the top left hand corner of the window and the next two values are the width and height of the window in pixels. The flag CW_USEDEFAULT will tell Windows to use a default window position and size.
The window parent is a handle to a window which will become a parent of the child window. If you set this to NULL then any window created has no parent and has the same level hierarchy as the main application window. The window menu handle is the same as the one specified in the class window. You don't have to specify the menu in the class if you don't want to, you can specify it directly here. This field has two uses though. If the window is a child window, i.e, has a parent window, then this field is a numeric id of the child. Let's say you have a parent window, then you have just one child, to recognise this child you simply set the menu field to (HMENU)1, you can put any value here. Notice, that it expects a type of HMENU so you must cast it to HMENU. I'll go into more details about this when we cover child windows. :)
The next field is again the program's instance. See how used this hInstance value is. It's a good idea to store hInstance as a global so you can access it anywhere you like.
Finally, the last field. The creation paramaters. Ignore this value for the time being.
Displaying the Window
You have now created the window in an internal windows structure. It's still not on the screen however. You need to use the function:
ShowWindow(hwnd, iCmdShow);
Notice, I used the iCmdShow value. Since this is our main window you should use this value in the show window function. When you come to make child windows you can use the SW_SHOWNORMAL flag instead. This will just show the window like you would expect. There are other flags which you should look up in the Windows help.
Ahh it's up... Great. But we have still one other function left before we get to that while loop.
UpdateWindow(hwnd);
This will cause Windows to send your window procedure a WM_PAINT message. This tells you that you need to draw your stuff onto the client area of the window. If you didn't do this then Windows would display the window but the client area would be empty and only when you got a WM_PAINT message would your function draw the contents.
Messages and Message Loops
Finally, we get to a loop. This is very simple but might not look like it at first. Let's review the code once more:
while(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
First up, GetMessage() is called. This will go off and retrieve a message from the message queue. Now, I have already talked about the message-driven architecture. Well, every Windows program has its own queue where Windows places messages. You pull them out one by one and process them as you want. The parameter you need to worry about is the &msg one.
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
This is the structure of the msg. There really isn't much to say about this. Just notice the first 4 parameters of this structure. It's the parameters passed to your window procedure. Things are coming together nicely, aren't they? ;)
The other two are not really not hard to guess. The 'time' tells you when the message was posted.
The 'pt' field tells you where the cursor was on the screen when the message was posted. The POINT structure looks like this:
typedef struct tagPOINT {
LONG x, y;
} POINT;
One more point, this is a blocking function call. This means that the function goes off and returns TRUE if it finds a message other than WM_QUIT, if it finds WM_QUIT it returns FALSE and in any other situations it sits and waits and does not return until it finds something.
Next up, TranslateMessage(&msg). This is odd, it passes the message back to Windows for some keyboard translation. I won't cover that now however. :)
Finally, DispatchMessage(&msg). This passes the message back to Windows which then sends it to your window procedure. :)
The Work Horse - The Window Procedure
Almost done. The bit that does all the work. The actual window procedure. Let's look at its prototype once more:
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);
hwnd is the handle of the window that triggered the procedure. Note, that many windows can use the same class and the class specifies the window procedure. So, you have to know what window caused the message. :)
The iMsg is just a value telling you the message ID. There are many messages and they are of the form WM_?????.
The wParam and lParam are curious names. They are actually just typedef'd DWORD values. Nothing special at all. They carry information that comes along with the messages.
In my amazing window procedure I do absolutely nothing. ;)
(I am sometimes told how good I am at doing nothing. ;)
switch(iMsg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
All it does is wait for a WM_DESTROY message. This happens when you close the window. When I see this I use the function:
PostQuitMessage(0);
Remember how I said the GetMessage() function in the message loop returns FALSE when it sees WM_QUIT. That's what this does. It places WM_QUIT in the queue and the message loop will see that and exit. Everything fits nicely now, doesn't it? :)
Now, remember earlier I talked about re-sizing the window. Well it just so happens the message ID for that is WM_SIZE. Let's add code to the window procedure to record the change in size.
.....
.....
static int Width, Height;
switch(iMsg) {
case WM_SIZE:
Width = LOWORD(lParam);
Height = HIWORD(lParam);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
.....
.....
It just so happens that the high word of the lParam when a WM_SIZE message occurs tells you the height and the low word tells you the width. The wParam tells you the size type that happened. Such as minimize, maximize etc. So, now you can see how Windows sends data that is applicable to each message. Through two DWORDS called wParam and lParam. It's called WPARAM and LPARAM instead of DWORD for portability issues. It's 32bit now but in the future it would be 64bit or above etc.
Notice I used static when declaring the varibles. Don't get caught out. If you want to use them you must keep them safe. Soon as you finish your window procedure you'll loose them unless you make them global.
One last little point on the window procedure. Any messages you don't wish to trap should pass to a default window procedure supplied by Windows.
DefWindowProc(hwnd, iMsg, wParam, lParam);
Now you know everything there is in creating a simple Windows program.
Window Exposure and Painting
So now you have your little window. Let's imagine you have drawn something on it. What exactly happens when your window is covered by another application's window or if anything causes the surface of your window to change in some way?
The answer is simple, the window which the change affects gets sent a WM_PAINT message. This happens if any part or all of the window become invalid. That means that it needs drawing. Consider a popup window on top of your application which is then moved. A lot of your window has been wiped out by this window. It can happen when you minimize your window and then maximize it, etc, etc. As you can see, you must redraw the window or the area that was destroyed. Windows won't in normal circumstances do it for you, so it sends your window procedure a WM_PAINT message. Any window that needs repainting will receive one. This is one of the most important and used messages. The WM_PAINT message does not contain any parameters that are stored in the wParam and lParam varibles. To get information on the area of the window that needs re-drawing you use the following function:
HDC BeginPaint(HWND hwnd, LPPAINTSTRUCT lpPaint);
Before I explain this in detail, I'll present a simple window procedure to replace the one given in the example shell that will draw a message on the window:
// new window procedure:
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
switch(iMsg) {
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, "hello", 5);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, iMsg, wParam, lParam);
}
That's it. :)
Okay, this will write the string "hello" at position (10,10) on the window. The value 5 is the length of the string to display (5 chars in hello :). Now let's explain the other parts.
The BeginPaint() function prepares the window for painting and fills up a structure which contains various peices of information. The structure looks like this:
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;
The main bit in here is the rcPaint field. This is the rectangular area of the window that needs repainting. You don't have to just repaint this area, you could repaint the entire window. But perhaps it's quicker just to update this region. Not in all circumstances but anyway, that's what it is. The RECT struct looks like this:
typedef struct _RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
The values are coordinates relative to the top left hand corner of the window you're dealing with.
Anyway, you can use this information or just ignore it. The most important thing though is the value returned by the function and stored as the 'hdc' field. I'll discuss that in a moment, first, you can see that this 'hdc' value is used in the TextOut() function and at the end of the painting we use the EndPaint() function. That must always be done. With every BeginPaint() you must follow your painting code with EndPaint(). Now, let's look at this 'hdc' thing.
Device Contexts
For every physical output device and its device driver you have what's known as a device context. This contains lots of peices of information about a device and its attributes. It's really not too tricky at all to understand. The thing is that if you wish to draw on a window's client area you must get a device context to that window. Most of the graphics functions for drawing require this device context. The return value of BeginPaint() contains the DC for the window you specified. You can also use the function:
hdc = GetDC(hwnd);
If you get your DC like this once you have finished with the hdc you should free it up and give it back to the system using:
ReleaseDC(hwnd, hdc);
Remember, device contexts are a resource. It takes system resources to create them so it's always a good idea to free them up when you don't wish to use them anymore.
Multiple Windows, Same Window Procedure
We've established already the fact we want windows with similar properties to share the same window procedure. However, sometimes we want to associate a unique peice of data to a window. Then, when inside the window procedure we can retrieve this data easily from the window handle (hwnd).
It turns out we have a nice little function at our disposal called:
LONG SetWindowLong(HWND hwnd, int nIndex, LONG lNewLong);
This allows you to set certain bit of data about a window after it's been created with CreateWindow(). The nIndex parameter is the index of the value we wish to change, the lNewLong is the new peice of data we wish to set and the function returns the original peice of data that was present.
The index GWL_USERDATA is an area within the window structure that is available for user use. This is a 32bit value and in these 32bits of space you can store anything you like.
So, if we wished to place the value 1 in this field we would do:
SetWindowLong(hwnd, GWL_USERDATA, 1);
Now, inside the window procedure at the top we can do:
LONG unique_data = GetWindowLong(hwnd, GWL_USERDATA);
So, we can clearly use this to identify the different windows triggering the same window procedure.
Closing Words
That completes the first stage. A lot of information has been covered and that hasn't even scratched the surface. We still have to cover methods of input (mouse, keyboard), some graphics for creating a nice linear frame buffer, timers, child windows, menus, dialogs, resources, DLL's, DDE, etc etc. However, I've just run out of column space and time. I had to get the basics out of the way first before I could get into some elite stuff. Who knows maybe we will end up coding a nice 100% Assembler Win32 application. =)
I seriously recommend you check out the win32 tutorial by SOL_HSA (greets) at this URL:
http://www.compart.fi/~solar/win32.html
If you're interested in continuing this master class then you best email me or Hugi because stage two relies on YOU!
Greets to #ukscene, #coders, #3dcoders. :)
Thanks for reading,
- frenzy