Preamble
Learning C is hard enough without the torment that is figuring out how to write code for Windows. The average human is directed towards Visual Basic first, just to get a feel for writing under the Windows environment. Having picked up a little Windows coding experience you're maybe ready to look into migrating to C.
Notice that I'm talking about C here. Yes I know everyone's talking about C++ blah blah, well there's a couple of good reasons for avoiding C++ for now. Firstly we're just writing a simple program, that is a string of commands, we're not 'developing an architecture' or 'modelling real world processes'. We're just going to tell the computer to do some things in a particular order. Secondly C is a hell of a lot easier to read and understand, and learning is all about comprehension.
This tutorial is for the programmer who wants to know how to create basic user interface elements using nothing but code, that is the absolute minimum wizardry. I found there to be a real lack of comprehensive (and comprehensible) tutorials on this subject when I started learning. Hopefully, the following will plug the gap.
You will need...
...but I recommend...
- Visual C++ or better yet, Visual Studio, version 5 or later (I quite like version 6). I'm going to assume for this tutorial that this is what you're using.
I'm also going to assume that you've had a play about with VC and you've made a few basic console applications etc. The following is how to make a Window app so you should create it as a 'Win32 Application' and not a 'Win32 Console Application'.
You may also have had a play with MFC. MFC is of course a sort of C++ wrapping around the Windows API. We're going to be talking directly to the API here though, so forget about MFC for now. It's very much in the spirit of C/C++ to know what's going on, and hiding behind MFC is not useful at this stage.
This tutorial seems to contain lots and lots of code, but don't be put off. Much of it is fairly repetetive (though not without good reason!). The last full listing is the complete program, those before it are the preliminary stages. Except where otherwise specified, the code listed is a file called 'main.c' (though obviously you can call it what you like).
Overview
- Basics
- WinMain()
- The Message Loop
- Make a Window: Window Classes
- Make a Window: The Window Procedure
- Made a Window
- Tidy up
- A Button
- Icons
- Play with things
Basics
First of all we need to create an empty Win32 project. Call it something like "WindowTest". Then create a new 'C++ Source File' called "main.c". Open it up and stick the following in there:
<Code Sample 1>
// WindowTest
// ==========
//
// A quick exploration of the windowing API
// main.c
// ------
//
// It's all in here
// INCLUDES
// --------
#include <windows.h>
// GLOBALS
// -------
HINSTANCE ghInstance;
// PROTOTYPES
// ----------
// CODE
// ----
///////////////
// WinMain() //
///////////////
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
ghInstance = hInstance;
return 0;
}
</Code Sample 1>
This program should compile and run. If you have problems at this stage then it probably has something to do with your compilation tools, your development environment or your include paths.
So what did we achieve just now? Well, firstly we've started with a few comments telling us about the project and about this file, and we've peppered the file with little section headings to keep related things grouped together and so we can see at a glance what's where (good habits to get into).
We've included windows.h which is important because it connects us with the Windows API and defines all the Windows data types and stuff.
Our first global variable is ghInstance. It is an Instance Handle. Every running process has a unique one of these so it can be identified.
As I'm sure you know (or can figure out) WinMain is the entry point for a Windows program. The parameters include hInstance which is passed by Windows, hPrevInstance - a handle to any previous instance of the program (which is always 0 - the feature apparently wasn't implemented) and nShowCmd which indicates whether the initial window of the app should appear minimised for example. lpCmdLine is a pointer to a char array containing the command line. Win32 applications do not need a 'main()' function (and it would be confusing to add one).
Obviously the first thing we've done programmatically is to preserve the instance handle into ghInstance, which, having been declared outside the WinMain function has global scope. (hInstance obviously only has local function scope - it only means something inside WinMain). We'll need ghInstance later. We return a value of 0 to show everything's gone well.
The Message Loop is one of the most important parts of an event-driven Windows application because it gives you access to the Windows event system. In Visual Basic, you may well get used to the idea of the program being 'idle', or between events. When an event fires, any code associated with that event is executed. When the program runs out of code for that event, the system just waits for the next event. Of course this is something of an illusion. In reality Windows programs don't just sit back and wait for Windows to kick them into action.
Back in the Good Old Days, when writing a program with a GUI meant clearing the screen and drawing a whole bunch of lines, the programmer could be pretty sure that once their code started running, it would keep running, with a relative monopoly of the processor, until the code exited gracefully or went bang and fell over. Back then, programs that interfaced with a user would frequently feature a core loop, which (for example) tested for keystrokes, performed some maths, and perhaps updated the screen. Now imagine, if you will, that one were writing a program that supported some kind of interface to another computer, a null modem cable perhaps, or maybe even a coaxial network cable. Now the core loop might also have to check the buffer on the network card or serial port for incoming data. This data could arrive at any time, and the program would have to periodically check to see if something had arrived.
The model above bears more than a passing resemblance to the Windows Message Loop system, except that the 'network buffer' above refers to the message buffer for the application, and instead of messages coming from another computer, they come from other processes, for Windows is a multi-tasking environment. Processor time is shared between process threads, so that your application's thread will be frequently suspended, and code in another process' thread will be run. Any of these other processes may ask Windows to add a message to your application's message buffer, including Windows itself, and your app can check this buffer, so it can respond to the messages.
So let's add the Message Loop to our applet:
<Code Sample 2>
// WindowTest
// ==========
//
// A quick exploration of the windowing API
// main.c
// ------
//
// It's all in here
// INCLUDES
// --------
#include <windows.h>
// GLOBALS
// -------
HINSTANCE ghInstance;
// PROTOTYPES
// ----------
// CODE
// ----
///////////////
// WinMain() //
///////////////
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
{
MSG Msg;
ghInstance = hInstance;
while(GetMessage(&Msg, NULL, 0,0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
return 0;
}
</Code Sample 2>
This code too should compile fine, but don't run it just yet.
As you can see there have been only a couple of small changes, namely the addition of the small looping block, and its supporting variable Msg. Msg is of type MSG (helpful huh?), which is a struct containing a bunch of details pertaining to Windows Messages, such as the HWND (Handle to the Window) to which the message applies, the time the message was posted, a couple of parameters and, of course, the actual number of the message itself. Message numbers are simply UINTs (unsigned integers) which are usually tested against constants defined in the Windows include files, such as WM_CREATE (sent when a window is being created) or BN_DBLCLK (sent when a user double clicks a button).
There are three functions being called in the loop. The first, and least scary of the three is GetMessage. GetMessage does exactly what you think it might, it Gets a Message from the Message Queue of our app. The first parameter is the address of the MSG structure variable into which to stick the information for the next message on the queue. Messages are added to the 'bottom' of the queue and retrieved from the 'top' so to speak, meaning that the first message to go on, is the first message to come off. The other parameters are not especially important. They allow you to filter the remaining messages on the queue and retrieve particular ones, eg for particular Windows (a Windows application can have more than one window, and you may wish to retrieve messages for each seperately). GetMessage removes the message it receives from the queue, so that next time, the next message in the queue will be returned. Now remember when I implied that the message loop keeps gurgling away, round and round until a message pops up? Technically that was a Lie. If there is nothing in the queue then GetMessage waits until there is before returning. This saves processor time for threads that need it. However, there is also the function PeekMessage, which does not remove the message (though it does return it in Msg) from the queue, nor does it wait if the queue is empty. Finally, GetMessage always returns a non-zero value UNLESS the message is WM_QUIT, in which case it returns zero. This, obviously will cause the while loop to end, and so the program will exit. (The loop will also exit if GetMessage returns -1, indicating an error has occurred.)
TranslateMessage has to do with keyboard messages. Specifically, when a keyboard event occurs (keypresses, keyup, keydown and so on) a message is stuck on the queues of all threads that might need to know about it. But the keys are referenced by hardware scan code, and not by ANSI code, so if you're going to be using keyboard messages you need to call TranslateMessage. TranslateMessage does not change the Message in Msg, instead it works out the relevant character code and generates a new message, eg a WM_CHAR which is added to the queue and picked up by GetMessage in due course.
DispatchMessage will be explained in more detail in the next section, but can temporarily be thought of as a function that reacts to the message picked up by GetMessage.
So now our app is reading from its Windows Message Queue. It will exit if a WM_QUIT message is encountered, however at this stage there is no way to generate such a message, so don't run it! (If you did run it, then you'll need to end the process using Windows Task Manager. Right click on the taskbar, select Task Manager, go to Processes, find the name of your prog, probably "WindowTest.exe", select it and click End Process.)
Ok, we're about to actually see something in a moment, but first we have a couple more concepts to cover, namely Window Classes and Window Procedures. The latter has to do with the Message Loop above, so let's leave that for a moment, just to let the other stuff sink in a bit.
Window Classes are NOT related to C++ classes, or Java classes, or any other OOP classes, so don't panic. In fact Window Classes are really pretty simple. When you want to make a window you call the CreateWindowEx function. CreateWindowEx basicly takes a whole pile of parameters and does the necessary black magic to make a window. Creating a window requires a LOT of parameters - things like the window title, size, position, border styles, background, icons, mousepointer blah blah BLAH! On and on. The fact is, it takes so much information to create a window, that they decided to break things up a bit. Somewhere deep inside the guts of Windows is an array of structures containing some basic information about some of the Windows that can be created. These structures are called Window Classes, and can be thought of as a kind of template.
Many of the Windows that are created are rather similar, so the most likely related info is stored in the fields of the Window Class structure, and the stuff most likely to be different for each actual window is passed directly. So in order to create a new, customized window we need to make a new Window Class. We do this like so:
<Code Snippet 1>
WNDCLASSEX wcx;
// Define the Window Class
wcx.cbSize = sizeof(wcx);
wcx.style = CS_HREDRAW | CS_VREDRAW;
wcx.lpfnWndProc = WindowTestWProc;
wcx.cbClsExtra = 0;
wcx.cbWndExtra = 0;
wcx.hInstance = ghInstance;
wcx.hIcon = NULL;
wcx.hIconSm = NULL;
wcx.hCursor = (HCURSOR) LoadImage(0, (char *)OCR_NORMAL,
IMAGE_CURSOR, 0, 0, LR_SHARED);
wcx.hbrBackground = CreateSolidBrush(GetSysColor(COLOR_BTNFACE));
wcx.lpszMenuName = NULL;
wcx.lpszClassName = "WindowTestWClass";
// Register Window Class
RegisterClassEx(&wcx);
</Code Snippet 1>
WNDCLASSEX is a struct that matches the structure of the Window Classes registered in Windows.
Let's run through what those params mean. cbSize is basically the size of the struct, so that Windows knows how many bytes to throw around when necessary. A quick sizeof tends to do the trick nicely. The style field holds a bunch of binary flags for various settings. The two specified defaults are good enough for our purposes. They force Windows to redraw the window if it moves or changes size. Notice how they are ORed together.
lpfnWndProc is a function pointer to our Window Procedure which will be explained in a moment.
cbClsExtra and cbWndExtra have to do with allocating extra memory for things like dialog box resources. 0 is the default.
hInstance speaks for itself. But in case it isn't speaking loud enough, this associates the Window Class with our application.
hIcon and hIconSm refer to the icons used to represent the application in the taskbar. We're leaving them blank to keep things simple for now.
hCursor is a handle to a mouse pointer icon, and we're using the Windows API function LoadImage with some standard parameters to specify the default mouse pointer.
hbrBackground defines how to paint the background of the window. We're telling Windows to just paint it solidly using the COLOR_BTNFACE colour. CreateSolidBrush is another API function built in to Windows.
lpszMenuName is left empty because we don't have a menu. lpszClassName is a unique string used later to identify our Window Class.
The function RegisterClassEx registers our Window Class. At this point we no longer need the wcx variable, because the information is now stored within Windows. From this point we identify it using the lpszClassName value we gave it ("WindowTestWClass"). We'll need our Window Class in a moment but for now let's move on.
Make a Window: The Window Procedure
Remember DispatchMessage above? Ok, I said before that it can be considered 'a function that reacts to the message picked up by GetMessage'. As far as the Message Loop is concerned, you can almost treat DispatchMessage like this. DispatchMessage causes Windows to (a) find the Window for which a message is destined and then (b) call that window's 'Window Procedure'. The Window Procedure is a function written by you but called by Windows. You never directly call any of your own Window Procedures. Sounds wierd? Don't worry it'll make sense soon.
Normal functions in everyday C are written by you, and generally called by your code exclusively. Under Windows however, you have the possibility of having other programs call your functions (if you want). The easiest example is WinMain. When you run a program, Windows calls that program's WinMain function. DLLs are libraries of functions that can be called from other programs. You can export functions from a normal executable just as in a DLL if you want to. There are many times when using Windows API functions that you will pass a function pointer as a parameter to a function, so that some part of Windows can call the function. These functions are called 'Callback functions'. A Callback function is written by you to meet with a bunch of criteria defined by Windows, including the function parameters, its return type and values, and the 'calling convention'. (The calling convention has to do with how the function is compiled into machine code, eg how it expects to receive its parameters. You need not worry about the fundamental details of this, because you can specify to your compiler how the function is to be compiled using identifiers like _stdcall or _cdecl which are added to the beginning of the function definition. The windows.h file defines a number of standard prefixes such as WINAPI and CALLBACK which include the necessary calling convention declarations. WinMain, for example, is defined using WINAPI.)
You can name a Callback function anything you like, since it's a function pointer that is passed to whatever Windows API function makes use of it. Back in the section "Window Classes" above, we gave our Window Class' lpfnWndProc parameter (which is a function pointer) the address of our Window Procedure function (as yet unwritten) called "WindowTestWProc". Here's what that function looks like:
<Code Snippet 2>
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
</Code Snippet 2>
LRESULT is the return type (basically just a synonym for a long integer), CALLBACK is, as described above, the calling convention, and the function has the four parameters of the types and positions as required for Windows API functions to be able to call it without breaking.
As you can see we're switching based on the value of uMsg. Whereas previously we were dealing with a MSG structure for messages (in the Message Loop), now we're working with a straight UINT (unsigned integer). This contains a numerical value indicating the message that needs handling. WM_DESTROY is just one such message, which is triggered if the user clicks the Close button in the top right of the window (with the 'X' graphic) once the window has been removed from the screen (Windows does this automatically - if you need to you can intercept this earlier by reacting to other messages). We are then calling the PostQuitMessage API function, which posts a WM_QUIT message onto the Message Queue, which will then be picked up by our Message Loop (see earlier) and cause the program to end.
Note that the message that the Window Procedure deals with is probably no longer on our App's Message Queue. It has been removed by GetMessage into the Msg struct, then passed via DispatchMessage back into Windows, which has in turn passed the unsigned int part of the message to the Window Procedure, where decisions on what to do about it are made.
The default action is to call the function DefWindowProc. DefWindowProc is (duuhh) the Default Window Procedure and does all the default actions in response to standard Windows messages. Depending on the message, the Window Procedure is expected to indicate whether or not it has handled the message or give some kind of success code by way of a return value. For the WM_DESTROY message, a return of 0 indicates it has been handled. DefWindowProc always returns the most sensible default success code, so it is wise just to return this value unchanged, as in the code above.
So let's put all this together:
<Code Sample 3>
// WindowTest
// ==========
//
// A quick exploration of the windowing API
// main.c
// ------
//
// It's all in here
// DEFINES
// -------
#define OEMRESOURCE
// INCLUDES
// --------
#include <windows.h>
// GLOBALS
// -------
HINSTANCE ghInstance;
HWND ghWndTestWindow;
// PROTOTYPES
// ----------
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
// CODE
// ----
///////////////
// WinMain() //
///////////////
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
{
MSG Msg;
WNDCLASSEX wcx;
ghInstance = hInstance;
// Define the Window Class
wcx.cbSize = sizeof(wcx);
wcx.style = CS_HREDRAW | CS_VREDRAW;
wcx.lpfnWndProc = WindowTestWProc;
wcx.cbClsExtra = 0;
wcx.cbWndExtra = 0;
wcx.hInstance = ghInstance;
wcx.hIcon = NULL;
wcx.hIconSm = NULL;
wcx.hCursor = (HCURSOR) LoadImage(0, (char *)OCR_NORMAL,
IMAGE_CURSOR, 0, 0, LR_SHARED);
wcx.hbrBackground = CreateSolidBrush(GetSysColor(COLOR_BTNFACE));
wcx.lpszMenuName = NULL;
wcx.lpszClassName = "WindowTestWClass";
// Register Window Class
RegisterClassEx(&wcx);
ghWndTestWindow = CreateWindowEx(0, "WindowTestWClass", "Test Window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
320, 200,
NULL, NULL, ghInstance, NULL);
ShowWindow(ghWndTestWindow, SW_SHOWNORMAL);
while(GetMessage(&Msg, NULL, 0,0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
return 0;
}
///////////////////////
// WindowTestWProc() //
///////////////////////
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
</Code Sample 3>
This program should compile, and run, so go try it out. If all goes well you should be presented with a small blank window. It will be possible to move, size, minimise and maximise this window, and, of course, close it.
Made a Window
Right so hopefully that worked, and now we'd better tidy up a few loose ends before we continue. First up, there's a few additions to the code which I haven't properly explained yet:
Near the beginning, in a new section called "DEFINITIONS" I've defined 'OEMRESOURCE' but given it no value. This is a preprocessor definition. When the program is being compiled, the file winuser.h is parsed, and if OEMRESOURCE has not been defined then a block of definitions are not compiled into the code. These definitions include OCR_NORMAL, which is used later and identifies the normal mouse pointer icon, which is needed for the Window Class.
Straight after the Window Class has been registered, two more functions are called, firstly CreateWindowEx and then ShowWindow. The purposes of these functions should be pretty clear. In order, the parameters for CreateWindowEx are: the extended window style flags (for things like scroll bars, or various border styles), the Window Class name we used earlier, a title for the window (which appears in the Title Bar), basic window style flags (here we're using the constant WS_OVERLAPPEDWINDOW which actually groups together a standard set of flags to give the properties of a 'normal' window. It is equivalent to the combination: WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX). Next come two parameters for the starting position (x then y, in pixels, counting from the top left corner of the primary display) for the window, then two more for the width and height. Notice that we have used the constant CW_USEDEFAULT for the position params, which lets Windows choose where to create the window. You can also use CW_USEDEFAULT for the width and height properties if you want (try it out and see how it looks!). Finally, we have a HWND (Window Handle) for the parent of our window (we'll see why this is important later, for the time being, this window doesn't have one, hence NULL), a HMENU which is used to either identify a (gasp) menu or a child window (see later - neither are relevent here so NULL again), a HINSTANCE to the application instance and then a pointer to extra window creation data (yet again irrelevant, so NULL).
The CreateWindowEx function returns a HWND (Window Handle) to our new window, which we pass to ShowWindow, along with the constant SW_SHOWNORMAL which just shows the window at normal size (ie not minimised or maximised).
Right, before we go any further, let's (1) tidy the code up a bit and (2) play with things a little.
Tidy up
Right now, the code inside WinMain is a rather confusing mixture of initialisation and execution code. So we'll make two new functions "RegisterWindowClasses" and "BuildUserInterface", the former being called by the latter and the latter being called at the start of WinMain.
RegisterWindowClasses contains all the assignments to our WNDCLASSEX structure, which also moves that local variable (wcx) out of WinMain which is good because it's only used once anyway.
BuildUserInterface contains the CreateWindowEx and ShowWindow functions.
I'm also adding a function "UnregisterWindowClasses" which calls the UnregisterClass API function. This does exactly what you think it does.
I've also added a check in RegisterWindowClasses to see whether the window class has already been registered before. It's a good idea to build in contingency code like this to keep on top of the program as it starts to get complicated. It keeps you thinking "what could possibly happen here?".
Finally I've moved the Window Class name string into a preprocessor definition near the top, and the same for the Window title. Here's the listing:
<Code Sample 4>
// WindowTest
// ==========
//
// A quick exploration of the windowing API
// main.c
// ------
//
// It's all in here
// DEFINES
// -------
// Lets us use OCR_NORMAL
#define OEMRESOURCE
// Some strings for the app
#define STRWCLASSNAME "WindowTestWClass"
#define STRWINDOWTITLE "Test Window"
// INCLUDES
// --------
#include <windows.h>
// GLOBALS
// -------
HINSTANCE ghInstance;
HWND ghWndTestWindow;
// PROTOTYPES
// ----------
int RegisterWindowClasses(void);
int BuildUserInterface(void);
int UnregisterWindowClasses(void);
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam);
// CODE
// ----
///////////////
// WinMain() //
///////////////
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
{
MSG Msg;
ghInstance = hInstance;
if (BuildUserInterface())
{
UnregisterWindowClasses();
return 1;
}
// Message loop
while(GetMessage(&Msg, NULL, 0,0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
// Tidy up
UnregisterWindowClasses();
return 0;
}
///////////////////////
// WindowTestWProc() //
///////////////////////
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
//////////////////////////
// BuildUserInterface() //
//////////////////////////
int BuildUserInterface(void)
{
// Register the window classes
if (!RegisterWindowClasses())
return 1; // abort if there's a problem
// Create Window
ghWndTestWindow = CreateWindowEx(0, STRWCLASSNAME, STRWINDOWTITLE,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
320, 200,
NULL, NULL, ghInstance, NULL);
if (!ghWndTestWindow)
return 2; // again, abort if it goes wrong
// Show the window
ShowWindow(ghWndTestWindow, SW_SHOWNORMAL);
return 0;
}
/////////////////////////////
// RegisterWindowClasses() //
/////////////////////////////
int RegisterWindowClasses(void)
{
WNDCLASSEX wcx;
// Check to see if we've already registered the windowclass
if (GetClassInfoEx(ghInstance, STRWCLASSNAME, &wcx))
return -1; // return a non-zero since a 0 indicates failure
// Define the Window Class
wcx.cbSize = sizeof(wcx);
wcx.style = CS_HREDRAW | CS_VREDRAW;
wcx.lpfnWndProc = WindowTestWProc;
wcx.cbClsExtra = 0;
wcx.cbWndExtra = 0;
wcx.hInstance = ghInstance;
wcx.hIcon = NULL;
wcx.hIconSm = NULL;
wcx.hCursor = (HCURSOR) LoadImage(0, (char *)OCR_NORMAL,
IMAGE_CURSOR, 0, 0,
LR_SHARED);
wcx.hbrBackground = CreateSolidBrush(GetSysColor(COLOR_BTNFACE));
wcx.lpszMenuName = NULL;
wcx.lpszClassName = STRWCLASSNAME;
// Register Window Class and return
return RegisterClassEx(&wcx);
}
///////////////////////////////
// UnregisterWindowClasses() //
///////////////////////////////
int UnregisterWindowClasses(void)
{
// Tidy up by unregistering window class
return UnregisterClass(STRWCLASSNAME, ghInstance);
}
</Code Sample 4>
Right, this is going well. We have a window, and the code is relatively tidy. On the other hand, our app isn't very useful yet. We're going to need to add some functionality, and a good way to do that is to add some buttons. The buttons are going to trigger code and thus we have the potential to create a program with a useful (albeit limited) GUI to control it.
Now as far as Windows is concerned a Button is a Window. "Say what?" I hear you squeal. Don't panic, all will become clear. You probably though a window was a little square thing with a title bar and minimise button and resizeable border that you could fill with buttons and junk. Wrong! Ok, well half wrong anyway. A window is a rectangular area that is drawn by Windows (usually), and can receive input via the Windows API. The classic titlebared, bordered thing we just created is simply one type of window.
Now, Windows are hierarchical, ie they can contain other Windows, to a certain extent. You can't create an endless hall-of-mirrors-like cascade of nested MDI frames, but you can put a Button window inside the client area of your, well, Window window. Ahem.
The good news is that this is pretty easy to do. Firstly, we don't need to register a window class for our button because the standard Windows button is already defined as a window class by default. The default button window class specifies a built in window procedure function, which manages the drawing and basic action of the button. It also generates special WM_COMMAND messages which it sends to its parent window with parameters giving notice of various events or actions that have occurred which the button thinks are important, such as a doubleclick.
There are several other standard built in controls provided by Windows in the form of predefined window classes and built in window procedures. Those that send notification mostly do it by sending a WM_COMMAND message. Messages consist of the UINT Msg code (eg WM_COMMAND) but also two other parameters, lParam and wParam which are of type LPARAM and WPARAM respectively, which on 32bit platforms (ie Win 95 onwards) are both long (32bit) integer values. Extra data is usually crammed into these in order to provide more detail about the message in Msg. Often the upper and lower word (16bit chunk) of these are used independently. The windef.h header (which is automatically included by windows.h) defines two macros: LOWORD and HIWORD which do what you think they might. This is necessary because WM_COMMAND on its own is next to useless - all it says is that one of the controls has sent some kind of notification back to the window. If you want to know what the notification is, or which control sent it, you have to examine lParam and wParam. Specifically, the code for the notification (such as BN_CLICKED for a button click event notification) goes in HIWORD(wParam), the id of the control (which I'll explain in a moment) goes in LOWORD(wParam) and the window handle of the control (remember, controls are Windows too) goes in lParam, or to cast it properly, (HWND)lParam.
The id I just mentioned (from LOWORD(wParam)) can be set when the control is created, which is done using the CreateWindow function. Oops, dropped the ball there - What's the difference between CreateWindow and CreateWindowEx? Well the latter is the same as the former except for a single extra parameter, or to put it another way, CreateWindow is CreateWindowEx except without the dwExStyle parameter, which is the first parameter and specifies extended style information. You can use either function, unless you want some of those extended styles in which case you can only use CreateWindowEx. Buttons and other basic controls don't use any extended styles so you can get away with CreateWindow, or CreateWindowEx with the first param set to 0. Phew, I don't half make things hard for myself do I? OK, now, as I was saying, you can set the control id yourself, by putting a value in the 9th param of CreateWindow (10th of CreateWindowEx) which is of course called hMenu of type HMENU. Yes really. It's because that param is also used to attach a menu to a window. Luckily HMENU is just another HANDLE which basically means it's a 32bit integer. The reason that you use ids at all, and don't just test incoming messages based on the contents of the lParam (which contains the control's window handle remember) is because as well as creating windows manually in code, you can also use preconstructed 'dialogs' which are basically a bunch of data that describe the layout and basic properties of a window and a set of controls using a few integers and strings. The dialog data can be hand coded, but it's easier to use the dialog drawing tools in Visual C++ which then generates the data for you. The data is compiled into the exe, and then loaded in one go using the function CreateDialogParam, which specifies the dialog resource data using a string which matches one in the dialog data. Included in the dialog data is an id number for each control. Now if you're doing this in Visual C++ of course, then the Visual Arse-Wiping Wizard crawls out of its dank, festering lair and defecates all over your code. Sorry, what I mean is that it helpfully creates a set of preprocessor directives associating names like IDC_BOX1 with numbers like 101 (which corresponds to one of the buttons that you drew on your dialog, the name being a default, but customisable if you like), with a view to allowing you to use these names in your window procedure code to test for the buttons or other controls that have fired the various events. But since we're not using the Wizard, And we only have a single button, we can just store the HWND returned by CreateWindow for each control in a global, and test against lParam in the window procedure using an if statement. If we had more controls we'd want to use a switch statement, which requires constant integers, and in that case you'd need to use the ids.
OK, time to stop yacking and break out the final listing:
<Code Sample 5>
// WindowTest
// ==========
//
// A quick exploration of the windowing API
// main.c
// ------
//
// It's all in here
// DEFINES
// -------
// Lets us use OCR_NORMAL
#define OEMRESOURCE
// Some strings for the app
#define STRWCLASSNAME "WindowTestWClass"
#define STRWINDOWTITLE "Test Window"
#define STRBUTTONTEXT "Press Me"
// Some numbers
#define IWINDOWWIDTH 320
#define IWINDOWHEIGHT 200
#define IBUTTONTLX 100
#define IBUTTONTLY 50
#define IBUTTONW 120
#define IBUTTONH 60
// Numbers for custom function
#define noteA 880
#define noteB 988
#define noteDb 1109
// INCLUDES
// --------
#include <windows.h>
#include "resource.h"
// GLOBALS
// -------
HINSTANCE ghInstance;
HWND ghWndTestWindow;
HWND ghWndButton;
// PROTOTYPES
// ----------
int RegisterWindowClasses(void);
int BuildUserInterface(void);
int UnregisterWindowClasses(void);
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam);
void Horses(void);
// CODE
// ----
///////////////
// WinMain() //
///////////////
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nShowCmd)
{
MSG Msg;
ghInstance = hInstance;
if (BuildUserInterface())
{
UnregisterWindowClasses();
return 1;
}
// Message loop
while(GetMessage(&Msg, NULL, 0,0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
// Tidy up
UnregisterWindowClasses();
return 0;
}
///////////////////////
// WindowTestWProc() //
///////////////////////
LRESULT CALLBACK WindowTestWProc (HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_COMMAND:
if ((HWND)lParam == ghWndButton && HIWORD(wParam) == BN_CLICKED)
Horses();
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
//////////////////////////
// BuildUserInterface() //
//////////////////////////
int BuildUserInterface(void)
{
// Register the window classes
if (!RegisterWindowClasses())
return 1; // abort if there's a problem
// Create Window
ghWndTestWindow = CreateWindowEx(0, STRWCLASSNAME, STRWINDOWTITLE,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
IWINDOWWIDTH, IWINDOWHEIGHT,
NULL, NULL, ghInstance, NULL);
if (!ghWndTestWindow)
return 2; // again, abort if it goes wrong
// Create Button - not bothering with id but it would be... ------------o
ghWndButton = CreateWindow("BUTTON", STRBUTTONTEXT, // |
WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,// |
IBUTTONTLX, IBUTTONTLY, // |
IBUTTONW, IBUTTONH, // |
ghWndTestWindow, NULL, ghInstance, NULL);// |
// ^---here------------------o
// If the button creation fails then the abort procedure is
// a little bit more involved...
// The API function DestroyWindow sends messages including
// WM_DESTROY to the queue of the window we created first.
// We have to return 0 indicating success so that the
// message loop runs, so that the message gets picked up
// and sent to the window procedure, and the window
// closes down gracefully. If we just aborted the program
// then we'd be leaving Windows to do all the cleaning up
// for us, which might not happen properly (leaving
// memory allocated needlessly, and anyway it's bad manners.
if (!ghWndButton)
{
DestroyWindow(ghWndTestWindow);
return 0;
}
// Show the window
ShowWindow(ghWndTestWindow, SW_SHOWNORMAL);
return 0;
}
/////////////////////////////
// RegisterWindowClasses() //
/////////////////////////////
int RegisterWindowClasses(void)
{
WNDCLASSEX wcx;
// Check to see if we've already registered the windowclass
if (GetClassInfoEx(ghInstance, STRWCLASSNAME, &wcx))
return -1; // return a non-zero since a 0 indicates failure
// Define the Window Class
wcx.cbSize = sizeof(wcx);
wcx.style = CS_HREDRAW | CS_VREDRAW;
wcx.lpfnWndProc = WindowTestWProc;
wcx.cbClsExtra = 0;
wcx.cbWndExtra = 0;
wcx.hInstance = ghInstance;
wcx.hIcon = (HICON) LoadImage(ghInstance,
MAKEINTRESOURCE(IDI_ICON1),
IMAGE_ICON, 32, 32, 0);
wcx.hIconSm = (HICON) LoadImage(ghInstance,
MAKEINTRESOURCE(IDI_ICON1),
IMAGE_ICON, 16, 16, 0);
wcx.hCursor = (HCURSOR) LoadImage(0, MAKEINTRESOURCE(OCR_NORMAL),
IMAGE_CURSOR, 0, 0,
LR_SHARED);
wcx.hbrBackground = CreateSolidBrush(GetSysColor(COLOR_BTNFACE));
wcx.lpszMenuName = NULL;
wcx.lpszClassName = STRWCLASSNAME;
// Register Window Class and return
return RegisterClassEx(&wcx);
}
///////////////////////////////
// UnregisterWindowClasses() //
///////////////////////////////
int UnregisterWindowClasses(void)
{
// Tidy up by unregistering window class
return UnregisterClass(STRWCLASSNAME, ghInstance);
}
////////////// Custom function, fired by button click.
// Horses() //
//////////////
void Horses(void)
{
int data[] = {noteA, 80, 0, 80,
noteA, 40, 0, 40,
noteA, 40, 0, 40,
noteA, 40, 0, 120,
noteB, 80, 0, 80,
noteA, 80, 0, 80,
noteB, 80, 0, 80,
noteDb, 360};
int n = 0;
while (n < 15)
{
if (data[n*2])
Beep(data[n*2], data[(n*2)+1]);
else
Sleep(data[(n*2)+1]);
n++;
}
return;
}
</Code Sample 5>
The main things to notice are how the custom function (Horses) is called from within the window procedure (WindowTestWProc), and the slightly awkward abort procedure should the creation of the button (using CreateWindow) fail in BuildUserInterface. While you probably could just call the exit function or something here, and Windows probably would tidy everything up, it's a bad idea to ignore the procedure for freeing up resources managed by other processes (Windows in this case) because sooner or later you'll come across some resource that isn't automatically cleaned up. Java this aint.
Finally, you'll notice that the application now has some code to load an icon. There are two places in which an application icon are visible: When looking at the program in Windows Explorer (or a file 'open'/'save' dialog) and when the application is running, in the top left corner of the window.
Now while the latter can be loaded from disk programatically, it makes more sense to compile an icon into the executable as a resource because then the app also gets an icon in explorer.
The linker for Visual C++ reads a resource script (a .rc file) which contains information about resources like icons, string tables, dialog scripts (as created with the designer mentioned earlier) and so on. These resources are compiled into sections of the Windows 'Portable Executable' (.exe) designated for non-code resources along with the object code generated by the C/C++ compiler.
Much as I hate to enumerate a bunch of clicks and drags and then call it 'instructions', you can import icons into your project by selecting 'Resource' from the 'Insert' menu, choose 'Icon' then 'Import...' from the dialog, find the .ico file using the rebadged (as 'Import Resource') open dialogue. Close the icon editing tool, to reveal the resource script viewer, which will be showing a tree containing your icon's default assigned identifier (usually 'IDI_ICON1'). Close the viewer, which will make VC++ prompt you to save the resource script (which you need to do). You then need to add the new resource script (eg script1.rc) to the project (Project>Add To Project>Files...) as well as the file 'resource.h' which was magically generated at the same time. How very wizard.
Note that the icon has not been actually embedded in the project in any way, all that has happened is a resource script has been created which references the location of the icon and links it to a unique identifier. If you edit the icon, you will need to save your changes or save it as a new file somewhere. The resource script will reference the new file. If you move the file without tell VC++ then the resource won't be compiled into the exe. Obviously. The resource.h file is the link between the resource script and your code, defining the constant IDI_ICON1 for use in the program.
This constant is used in the function RegisterWindowClasses, to set the values of hIcon and hIconSm in the WNDCLASSEX structure. As you can see the actual values assigned are in fact returned by the function LoadImage which tells Windows to load the image into an internal resource database and assign it a HANDLE which is then used by the app as a shorthand to refer to the resource. LoadImage is one of those API functions that ostensibly does a basic function - load an icon into the resource database and assign it a handle - but it can do it in a lot of different ways. In C++ you would use function overloading, in Windows API C you start with a basic mode of operation and the parameters necessary to do it, and then you add functionality by mangling those params beyond all recognition. The second param of LoadImage is a glorified char*, but with the correct configuration of parameters you use it like a HANDLE, so some casting is necessary. MAKEINTRESOURCE does the job of converting a HANDLE (basically a long int) into a LPCTSTR (basically a char*), but if you don't like it, you can get away with a (char*) cast. MAKEINTRESOURCE is just a casting macro, nothing to be afraid of. The unloading of these resources is handled by Windows when we call UnregisterClass at shutdown.
For the sake of completeness, here's the contents of the resource script and header files:
script1.rc
<Code Sample 6>
//Microsoft Developer Studio generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.K.) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENG)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK
#pragma code_page(1252)
#endif //_WIN32
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_ICON1 ICON DISCARDABLE "FACE02.ICO"
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE DISCARDABLE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE DISCARDABLE
BEGIN
"#include ""afxres.h""\r\n"
"\0"
END
3 TEXTINCLUDE DISCARDABLE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
#endif // English (U.K.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
</Code Sample 6>
resource.h
<Code Sample 7>
//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by Script1.rc
//
#define IDI_ICON1 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
</Code Sample 7>
Play with things
Now it's your turn! Have a play about with the various settings and parameters. See if you can figure out how to make the program draw more than one window. (Note that the hard bit here is to handle the closing of one of the Windows gracefully - don't just let the program quit without destroying any other Windows!).
All the major API functions, structures and so on above are in hardlinked. An excellent way to get to grips with the inner workings of these is to read their documentation pages in the MDSN Library. If you do not have a copy of this on CD you can check out the online version at msdn.microsoft.com/library which is usually more up to date anyway.
Next time
That brings this tutorial to a close. I'm going to follow this up, assuming no-one beats me to it, with some more basic windowing API tutorials. Next time I hope to cover painting and drawing text in the window, adding further controls and possibly creating MDI (Multiple Document Interface) Windows. Check back periodically, I'll hardlink them here as I do them.
Feel free to /msg me if you find any of the above unclear, or would like some assistance with this stuff.
Happy Programming!