Introduction to Programming the Windows GUI with Pelles C

This is a tutorial and introduction to GUI programming with Pelles C that I originally wrote for neworder. For the intro you will need to install Pelles C and should familiar with programming in C (to include the use of pointers). You can download the final example file locally from here.

My waiver. i've never taken any formal training in Windows Programming. I do have formal training in C Programming (college). lately most of my coding has been in php, powershell, and C with the majority in C. I am not a programmer by profession but i do code at work to solve real world problems.

Let's start by exploring Pelles C for a bit. If you installed Pelles C in the default location it is located in C:\Program Files\PellesC. Go ahead and navigate to that location and you'll see several sub-directories. These include Bin, Include, Lib and Projects. The Bin directory includes the compiler, IDE and linker, the Projects directory is the default location for your Pelles C Projects, the Include directory is the default location for your include files (Header files) and the Lib directory is the default location for your C Libraries. Both the Include directory and the Lib directory container Win subdirectories. inside the Win subdirectories you will find the necessary include files and header files for windows programming. Because these resources come with Pelles C by default you don't need to download and install the Platform SDK from microsoft to start programming in windows.

You should take the time to look in the header files and explore a bit. Be sure to open up the Pelles C help file and go through it as well. This help file is a great resource, containing a C99 language reference, an intro to the IDE and Pelles C commandline tools as well as documentation for the #include files. This documentation is very important as it includes documentation on non standard C include files (tchar.h or conio.h for example). tchar.h is particularly important as it provides character neutral macros so that you can code for either ANSI or UNICODE.

Let's go ahead and start coding. Fire up Pelles C and go to File, New, Project. Scroll down and choose "Win32 Program (EXE)" as your file type. It is located under "Empty projects". Then give your project a name. I've called mine intro2GUI.

Now that we have our workspace set up we need a blank source file. Goto File, New, Source code. This is where we will be working.

As with any C program a windows program includes a few parts. broken down we'll have our include files, macro definitions, function declarations, our main function, and any other auxillory functions that may be needed by our program. For us, this is going to look something like this:

/* 
    Filename - intro2GUI.c
    Date - Februrary 23, 2008

    This program is an example program for the Intro to GUI Programming on neworder.box.sk
*/

#include < windows.h >

#define szAppName TEXT ("intro2GUI")

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM)  ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
...
    return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
...
}

now, this isn't going to compile just yet. we've still got alot to work through. The first thing your going to notice is the include files. We include the windows.h header file. this header file is the main windows header that includes other subsidiary windows headers (don't know if i described that right). This header is heavy on the macro definitions and using it you can define stuff like WIN32_LEAN_AND_MEAN which will exclude extra apis like APIs such as Cryptography or windows sockets. You could also describe this header as being equivalent to stdio.h in regular C programming as it allows you to interact (input output) with windows.

The next thing you're going to notice is

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM)  ;

here we declare a callback function call WndProc. WndProc stands for Windows Procedure. This function is a message processing function for our window. It will process any messages Windows sends to our program.

What is a callback function? A callback function is a function in your program that is called by the operating system. You see, in our program we will never actually call the function WndProc(). Windows will do that when ever it sends a message to our program. more on that later.

Now what are all these parameters we're passing to this our WndProc function? We see stuff like: HWND, UINT, WPARAM, LPARAM. As you probably know, these are not your standard C types. It's an ancient windows practice to defines many standard as something totally undescriptive of what they actually are. This goes back all the way to the first edition of windows (back in the eighties, sometime around the same time they invented hungarian notation).

These specific types are message parameters that are passed throughout windows. When something happens with your program, whether you resize it, move it, minimize it or close it, windows tells your program, 'heh, this happened to you, what are you going to do?' by sending your program a message consisting of these parmeters. Your callback function receives this message and processes it usually using a switch statement. any message that you do not explicit handle within your switch statement gets handled by windows in the default manner.

I mentioned HWND, UINT, WPARAM, and LPARAM above. HWND is the handle to the window whose procedure receives the message. UINT is the message identifier and both wParam and lParam specify additional information, depenting on the UINT parameter. All messages also include the time when the message was posted and the cursor position, in screen coordinates, when the message was posted...though you won't be using these in your windows procedure.

now how the heck did i get to messaging already? i had wanted to tell you about the funky identifiers in windows. These funky identifiers are defined in the windows header files (did you go exploring?) and often contain two or three letters an underscore followed by more letters. the two or three letters usually describe the category the defined constant belongs to. for example:

  • CS - Class style option
  • CW - Create window option
  • DT - Draw text option
  • IDI - ID Number for an Icon
  • IDC - ID Number for a cursor
  • MB - Message box option
  • SND - Sound option
  • WM - Window message
  • WS - Windows style

also the hungarian notation that i mentioned earlier that many windows programmers use follows similar rules. usually a prefix is appended to the name of a variable. for example:

  • c - char (WCHAR or TCHAR)
  • l - LONG (long)
  • sz - zero terminated string
  • h - handle to something

anyways, i hope you get what's going on. Lets move on to our main function.

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)

the Platform SDK defines this function as 'the initial entry point for a Windows-based application'. I define it as main for windows ; ) anyways, this function takes several parameters. hInstance, which is the handle to the current instance of your program. hPrevInstance, the handle to the previous instance of the application. i believe this one is antiquated as it is always NULL. maybe they used to use it back in the eighties before they came up with that funky hungarian stuff. next you have szCmdLine, which is a pointer to a zero terminated string that specifies the commandline for our program. and finally you have iCmdShow, which specifies how the window should be shown. you can check the SDK or msdn for possible values of this parameter.

    HWND                hwnd ;
    MSG                	msg ;
    WNDCLASS        	wndclass ;

    wndclass.style           =     CS_HREDRAW | CS_VREDRAW ;
    wndclass.lpfnWndProc     =     WndProc ;
    wndclass.cbClsExtra      =     0 ;
    wndclass.cbWndExtra      =     0 ;
    wndclass.hInstance       =     hInstance ;
    wndclass.hIcon           =     LoadIcon (NULL, IDI_APPLICATION) ;
    wndclass.hCursor         =     LoadCursor (NULL, IDC_ARROW) ;
    wndclass.hbrBackground   =     (HBRUSH) GetStockObject (WHITE_BRUSH) ;
    wndclass.lpszMenuName    =     NULL ;
    wndclass.lpszClassName   =     szAppName ;

here we are going set up our preliminary window class. All windows are based on a window class structure. this structure sets the attributes of your window to include its style, icon, cursor, and the accompanying windows procedure. Every window has a window class. We specify our style which can be any combination of windows class styles. for our intro program we'll use the CS_HREDRAW and CS_VREDRAW styles. this will cause our program to redraw the window if there is any change in size (read more on class styles here). Next we have the pointer to our window procedure (lpfnWndProc in funky notation, in english it's a long pointer to a function called WndProc). in the rest i tell windows which icon and cursor to load for my program, what brush to use for the background and the name of the application. you can read more detailed information on msdn here.

Next we need to register our class with Windows.

    if(!RegisterClass(&wndclass)){
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
            szAppName, MB_ICONERROR) ;
        return 0;
    }

we do this of course with the RegisterClass function.

Finally, with the necessary preliminaries out of the way we create our window by calling the CreateWindow function. In this function we define the windows procedure, the windows name, it's default behavior and it's size and position.

So let's go through the steps we use to create the window. First we define a structure of a windows class, initializing the necessary fields. Next we register that class with windows so it knows what our window is going to act like and look like. finally we call our create our window by calling CreateWindow, which will cause windows to create the window and its associated window procedure. At this point your window is there, in memory, created internally in windows.

Now that it's created we need to get the window on screen, update it's information and set up a message loop so that it can handle any messages that are being passed to it from windows.

    ShowWindow (hwnd, iCmdShow) ;
    UpdateWindow (hwnd) ;
    
    BOOL bGotMessage ;

    while( (bGotMessage = GetMessage( &msg, NULL, 0, 0 )) != 0){ 
        if (bGotMessage == -1) {
        // handle the error and possibly exit
        } else {
            TranslateMessage(&msg) ; 
             DispatchMessage(&msg) ; 
           }
    }

    return msg.wParam ;
}

We do this by calling the ShowWindow function and the UpdateWindow function. This will get our application window on screen and up to date by sending a WM_PAINT message to our window procedure.

Once our window is on screen we are going to have to make it interactive. It needs to be able to handle messages sent to it from windows. in order to do this we use a three step process. First we get the message using GetMessage, then we call TranslateMessage to translate any virtual-key messages into character messages. Finally we send our retrieved message to our windows procedure via the DispatchMessage function.

and with all of this you should now have a pretty good idea of how a window is created and sets up it's message loop. next we'll be going over your windows procedure.

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    HDC            hdc;
    PAINTSTRUCT        ps;
    RECT            rect;

    switch (message){
        case WM_PAINT:
            hdc = BeginPaint(hwnd, &ps);

            GetClientRect(hwnd, &rect);

            DrawText (hdc, TEXT("Hello neworder!"), -1, &rect,
                DT_SINGLELINE | DT_CENTER | DT_VCENTER);
            EndPaint (hwnd, &ps);
            return 0;

        case WM_DESTROY:
            PostQuitMessage (0);
            return 0;

        default:
            return DefWindowProc (hwnd, message, wParam, lParam);
        }
}

You should recognize this as the callback function we declared earlier in the program. We had mentioned that this function is ultimately responsible for the processing messages your program receives. To do this we use a switch statement. In our program, whenever we receive a WM_PAINT message we repaint our client area by calling GetClientRect, making sure to draw some text in the center of our window with the function DrawText. Any messages sent to our program that we don't want to explicitly handle we pass on to DefWindowProc, which calls the default windows procedure to handle the message. This ensures that our program processes every message and does so consistently.

In windows, buttons, text fields and all those other little controls that show up in a program are what they call child controls. These are really windows of themselves, you may have noticed this if you took the time to read about the CreateWindow Function on msdn. These child controls basically use a built in Window class that defines their behavior, so we don't need to set up a window class for them in our program.

The first thing we're going to want to do is setup identifiers for each of our controls. For the purpose of our little tutorial we will be using three controls. One to get input data, one to provide direction and one to process the data. So that would be an Edit Control, a Static Control and a Button respectively.

#define szAppName TEXT ("intro2GUI")

// We define our Child Controls here:
#define ID_ED_DATAIN 1
#define ID_STATIC 2
#define ID_PUSH 3

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM)  ;

Next we're going to need to add a handle to our new little windows and variables to hold our data. We'll do this in our callback function and not in main to keep these local.

    static HWND hwndEditDataIn, hwndButton ;
    static TCHAR szDataIn[512] ;
    static TCHAR szDataInLabel[] = TEXT("Data in:     __________") ;

You should already know what HWND is for. A TCHAR is a macro that can either be ANSII or Unicode, depending on what you have defined. We wrap any string in the TEXT macro so that it does the conversion for us (Unicode or ANSII). The buffer for our data is szDataIn.

Next, we need to get our child controls onto our program. We want to do this as soon as the window is created, so we are going to add WM_CREATE to our switch statement in the callback function. This way whenever windows sends the WM_CREATE message we can draw our controls.

    switch (message){
        case WM_CREATE:
            // When we create our window we need to make sure we create the child controls.
            hwndEditDataIn = CreateWindow(TEXT("edit"), NULL,
                    WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_CENTER,
                    70, 10, 100, 20, hwnd, (HMENU) ID_ED_DATAIN,
                    ((LPCREATESTRUCT) lParam)->hInstance, NULL ) ;

            // We are going to set a limit of 10 characters for our Edit Box. Always control the input.
            SendMessage( hwndEditDataIn, EM_LIMITTEXT, 10 * sizeof( char), 0L ) ;

            hwndButton = CreateWindow(TEXT("button"), TEXT("Push Me!"),
                    WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, 
                    70, 50, 100, 20, hwnd, (HMENU) ID_PUSH,
                    ((LPCREATESTRUCT) lParam)->hInstance, NULL) ;
            return 0 ;

You can see that our controls are created by calling the CreateWindow function. I've already explained this above. Review the CreateWindow documentation on msdn here if you don't understand what's going on.

An extra thing that i've done that deserves pointing out is the use of SendMessage to limit how much data we are going to accept through our edit control. Controlling the flow of input in your program is an important security principle, as well as verifying that input.

Now we go over processing WM_COMMAND messages sent to our program through the child controls.

       case WM_COMMAND :

            if(LOWORD(wParam) == ID_ED_DATAIN) {

                if (HIWORD ( wParam) == EN_MAXTEXT ) {
                        //Make sure the control did not exceed our limit of ten characters.
                        MessageBox (hwnd, TEXT ("We are only accepting ten characters!"),
                                szAppName, MB_OK | MB_ICONSTOP) ;
                        return 0 ;
                    }
                }

            if(LOWORD(wParam) == ID_PUSH) {

                if ( GetWindowText(hwndEditDataIn, szDataIn, 511)  )
                    MessageBox(hwnd, szDataIn, szAppName, MB_OK) ;

                InvalidateRect(hwnd, NULL, TRUE) ;

            }

            return 0 ;

Because we want to have our program respond to commands from the user we need to process WM_COMMAND messages sent to our program. Specifically we want to know when our edit control hit's the maximum text (remember, we set this by sending our edit control an EM_LIMITTEXT message). If this happens we want to display an error message telling the user what happened.

We also want to know when the user pushes our button. Once it's pushed we want to display the data that whatever they entered. We do this by calling the GetWindowText function which allows us to pull the text out of the edit control into a buffer that we specify. We display the contents of that buffer with the a MessageBox just to demonstrate clearly what's going on. then we invalidate our client area which will cause the system to redraw the region. this is important if you are going to interactively display text in a static control in your window (something we are not doing here).

   case WM_SETFOCUS :
            SetFocus(hwndEditDataIn) ;

            return 0 ;

We also process WM_SETFOCUS, this way when our program starts up the focus is already on hwndEditDataIn. The user doesn't need to click into the edit control to start entering information. That wraps it up for WM_COMMAND messages. Now we look at WM_PAINT messages.

        case WM_PAINT:
            hdc = BeginPaint(hwnd, &ps);

            GetClientRect(hwnd, &rect);

            DrawText (hdc, TEXT("Hello neworder!"), -1, &rect,
                DT_SINGLELINE | DT_CENTER | DT_VCENTER);

             TextOut (hdc, 10, 15, szDataInLabel, lstrlen (szDataInLabel)) ;       

            EndPaint (hwnd, &ps);
            return 0;

        case WM_DESTROY:
            PostQuitMessage (0);
            return 0;

        default:
            return DefWindowProc (hwnd, message, wParam, lParam);
        }
}

much of this is the same. the only real additions here is the call to TextOut, which displays the contents of our label.

That's pretty much it. The only thing left is to save and compile (compile and run the program in Pelles C by hitting the Execute button on your toolbar). I invite and encourage anyone to comment or send me corrections. Also, if you have questions, need help or just want to let me know that you've tried it out then you can comment about this on my blog (see the link below). Before i close this out, let me share a little secret about programming in windows (and really, programming in general) for the people new to it. it is very rare that you'll find a programmer who has all of this memorized. Usually people reuse their code and must refer back to documentation. even for this tutorial i just stripped down an old project to almost nothing so that i could use it as an example. hope you were able to learn from it. - nabiy

Tools and Links of Interest:
---
The windows API Reference on msdn
The Pelles C Website
Pelles C Forums

feedback? comment on livejournal