Index

  1. About this tutorial
  2. Background
  3. Introduction
  4. How to start
  5. Paint Counter: A windows program outline
  6. Sort it out: A dialogue based Windows program
  7. How do I do...

 

An Introduction to Windows programming

Extend your programming skills and get ready for up to date graphical user interface (GUI) programming

edited by:

Don D. Hobson

for Prairie View A&M University, February 1997

 

1. About this tutorial

This tutorial is designed to help those who have already some experience in PASCAL, C or C++ programming under DOS or UNIX to experience the fancy world of GUI programming. If you are to write programs not just for yourself but for other people you won't get around providing a nice and easy to use graphical user interface for your applications anyway, sooner or later. But even if you do not intend to write programs for a living this tutorial may give you a sound understanding of how modern operating systems and applications work.

Although many of the principles introduced here apply to all GUIs this tutorial will focus on Microsoft Windows since it is the most commonly used GUI. Windows also offers the most sophisticated development tools and there are plenty of books and other sources of information available. I will try not to use too many development terms here straight away but in order to understand what I am talking about you should have worked with Microsoft Windows and be familiar with common Windows terms like icons, scrollbars, dialogue boxes etc.

 

1.1 How difficult is it to write a Windows program?

Let's start with the good news: if you have already programmed in C or C++ before you don't need to learn a new programming language for programming Windows. But as you will see, programming Windows is somewhat different from what is called ANSI-C programming under DOS or UNIX. However different does not mean more difficult, although there are some hurdles to overcome at the beginning. But once you've understood the basics and written your first program many things get easier with Windows and you get opportunities you have never had before.

Many people think that its easier to do non-Windows programming first and do the "difficult" windows stuff later. This is probably one reason why windows programming is still paid too little attention in education where they "want to teach you the basics not the details". But programming Windows is more than just using a few more functions, its a different philosophy, its event driven rather than sequential and its not just coding but also designing and creating dialogue boxes and other program resources.

In the same way many also believe that you should understand C first before starting with object orientated programming in C++ say. I don't know where these myths come from and I can't even prove that they are just myths but what would you say if an alien asked you whether its easier to understand capitalism by learning about communism first. I would say no and I also think its pointless to start with "ordinary" programming when you really want to do Windows and its not necessary either to become a master in C first when you are aiming at C++ in the long run.

However the combination of Windows and C++ bears some difficulties for beginners. Using so called "class libraries" makes understanding of how Windows works more difficult as these class libraries hide a lot of functionality away from you. Defining your own classes is a bit tricky with Windows especially at the beginning due to a very lose link between Windows' objects and your own program objects.

This and the fact, that people who have experience in C cannot read C++ program whereas vice versa it is generally not a problem is why I will give you all the code samples in C. This tutorial won't make you a master Windows programmer anyway and you are welcome once you've understood the philosophy of windows programming to go the much more promising way of C++ using either Microsoft's Foundation Classes (MFC) or Borland's Object Windows Library (OWL).

 

1.2 Software Development in the 90s

With the improvements in the way we use software, the way we develop it has also experienced considerable changes. Where a few years ago it was a challenge for a beginner to even type in a piece of source code from a book and compile it successfully we now get sophisticated so called Integrated Development Environments (IDE) that make things a lot easier for both the beginner and the expert.

Check you current way of programming. If you use an all purpose text editor to write your code and then call a compiler from a command line with something like comp -o myprog.c your not quite up to date. Even if you just want to taste what programming is like and especially if you are a beginner you might find features like source code highlighting, integrated source code debugging and full context sensitive help to name a few not just useful but crucial to get where you want to get. Writing a program is challenging enough and there's no need to fight your way though environment variables and makefiles.

In order to use this tutorial and to do windows programming all you need is a PC or at least access to a PC with Microsoft Windows and a Windows development package installed. The two most popular packages for developing Windows applications are Microsoft's Visual C++ and Borland's C++. Although both packages feature C++ compilers this does not mean you cannot write C programs with them. They both detect by default whether a source code file is a C or C++ file from the file extension which is .C for C and .CPP for C++ and compile them accordingly.

Which one of these packages you want to use is a matter of personal preference. I prefer Visual C++. With Borland you can also develop both 16-Bit Windows and 32-Bit Windows applications, whereas with Microsoft's Visual C++ you need Version 1.x (currently 1.52) to develop 16-Bit Apps and Version 4.x (currently 5.0) to create 32-Bit Apps. On the other hand Microsoft seems to have successfully establish its Foundation Classes (MFC) for C++ which make development especially of more advanced features of Windows such as OLE considerably easier.

Both packages include the following:

 

1.3 How to use this tutorial

In this tutorial I will first give you some background knowledge about windows and then introduce you to some important terms and conventions. Then in section 4 we start with a first simple windows program which you can download to your machine. The files are compressed and put together into a single .ZIP file. To be able to use the files you have to extract them first. For every ZIP file you should create a new directory.

In order to load and compile the sample programs you need to create a project from the IDE. For that you will find a NEW command in the project menu in Microsoft Visual C++ 4.0. A project file keeps track of all the files belonging to a project as well as compiler and linker options necessary to build the executable and is basically a modern version of a makefile (don't worry if you do not know what that is). Microsoft's project files have the extension .mdp (for makefile) and they are still plain ASCII text files. However they should not be changed with a text editor (they've got a line at the beginning saying "DO NOT EDIT").

The tutorial is designed that you don't need any additional books however it is essential that as we go though the code you have the project loaded in your IDE and that you are able to access the online help of the Windows SDK. This is because it is neither possible nor sensible to explain all the functions we are using in detail with all their parameters and in the online help you will find all the information about a particular function or message you need.

Just in case you don't know: there are two ways accessing the online help from the IDE. First you will find a command called SEARCH in the help menu which takes you to the index page from where you can use the hyperlinks or the search command to find what you are looking for or just to get an overview of what is available. The other possibility is to position the cursor on a particular function name or data structure and press F1.

 

2. Background

This chapter discusses what makes graphical user interfaces so special and it gives you some background information about Windows and its current versions.

 

2.1 About being friendly to the user

When computers were invented they were used as number crunchers to solve mathematical or other scientific problems. The focus was clearly on the processing task but as computers evolved the input and output i.e. the communication interface between the computer and the machine got more and more important. In the first place this was necessary to process and visualise more complex data which could not be analysed in the form of lists of numbers. However the second major benefit was the easier way of using computers which allowed more and more people to use them.

User friendliness has since become one of the most important marketing issues and big software companies are investing millions of dollars in usability labs in order to improve the usability of their software products. Today, as algorithms and libraries for all sorts of technical problems are largely available, the design, coding and testing of the user interface is the most expensive and time consuming part in the development process of an civil end-user application. Unfortunately this aspect of computer science, which is in many ways more an art than a science in the classical way, is still very often paid too little attention in academia.

How can we measure user friendliness? Obviously since the actions undertaken by a user are unpredictable scientific methods are largely inappropriate. Looking at a single application there are three major key factors that determine user friendliness

    1. Intuitiveness. This indicates how easy it is for the user to perform a desired task. Important factors are the grouping, ordering and naming of menu commands, the location, size and representation of buttons and the type of controls provided.
    2. Transparency. This describes the type and accuracy of response given to the user before, while or after performing an action. Example: If an action undertaken by the user requires a complex computation which takes some time, the program can either not respond for the time the computation is in progress (bad), or display a "please wait" message (better) or show a progress bar indicating the percentage of computation finished (best). The latter is the most transparent to the user and thus most user friendly.Bubble help explaining the meaning of a button or status reports/ accurate error messages are other examples for good transparency.
    3. Restrictiveness. The user friendliness also improves the less restricted the user is in his/ her actions. The user should e.g. be able to at any time cancel or undo operations and to customise the application's appearance and behaviour.

Taking the environment in which an application is operating into account user friendliness requires even more.

    1. Interoperability. In a multiuser/ multitasking environment it is important that information can be easily exchanged between applications. The clipboard is a good example for such a feature, others are DDE and OLE.
    2. Standardisation. This addresses a programs appearance and handling with respect to other applications. It requires that interfaces are the same or at least very similar for the same thing like e.g. dialogues for opening or saving a file or the configuration of the printer. The rule is: Once a user knows how to use one application he knows how to use them all.

 

2.2 Why Windows

The idea of a graphical user interface like windows is not new. In fact Microsoft started working on Windows as early as 1983 but the first two versions had little success to say the least. Apple was one of the pioneering companies that with the Macintosh introduced a Graphical User Interface Operating System for a mass market i.e. on an affordable personal computer system. And there are many similarities between the Mac-OS and Windows. On the less professional sector there were the Atari ST and the Commodore Amiga which first provided this type of advanced human computer interaction.

When Windows 3.0 came out in 1990 it was "as seven year overnight success" as Adrian King said in his book Inside Windows 95. Sitting on top of MS-DOS it was not the best technically possible solution for a GUI but it ran happily on ordinary PC hardware and the fact that you called it just like any other program let you the option whether to use it or not. And besides that there were in my opinion four other major reasons why Windows made it and is now dominating the computer world like no other GUI or operating system:

 

2.3 Windows != Windows

Since then Windows has developed and we now get three major versions of Windows: Windows 3.1 (including Windows for Workgroups 3.11), Windows 95 and Windows NT. Windows 3.1 is still fully depending on MS-DOS and thus based on a long outdated system architecture that Intel once designed for its 8086 processors. The programming interface for Windows 3.1 is called the Win16 API in opposition to the Win32 API offered by Windows 95 and Windows NT. Programming under Win16 is due different memory models and pointer constraints sometimes a bit tricky especially as programs get larger and more complex and many of the pitfalls can be avoided using the 32-bit programming model.

With Windows NT Microsoft got rid of all the compromises it had to make with MS-DOS and its 16 bit architecture and developed a fully independent operating system with ran exclusively in 32 bit mode. They also implemented a new file operating system called NTFS and network capabilities with the most sophisticated security features, a 3D graphics interface called OpenGL and even a new 16 bit character coding scheme (in opposition to the standard ASCI or ANSI character encoding schemes which feature only 8bit) called Unicode. Microsoft was aiming high to making Windows NT the state of the art operating system. Unfortunately this had its price: working with less than a 486-100 and 16MB of RAM was a pain and other system requirements were not moderate either.

Windows 95 is technically a compromise between the two, offering a 32 bit API but without all the advanced features of NT that I mentioned above. The major improvement of Windows 95 is the appearance and handling of the user interface which is somewhat different from the Windows 3.1 and Windows NT desktop. Coming versions of NT will also have this desktop interface.

Although there are considerable differences between all the three versions of Microsoft Windows the is good news for programmers: the source code is largely compatible. There are only a few API functions which underwent minor changes and the rest is just an extension of the API. Nevertheless you have still got to be a bit careful when converting a 16 bit program to 32 bit. One of the major changes is e.g. that the basic data type int which was 16bit wide in Win16 is now 32bit and there are no near pointers (16 bit pointers) any more.

Whereas Win16 programs can be used on all tree platforms, a Win32 program won't run directly under Windows 3.1. However there is a possibility to even do that with a operating system extension called Win32s. Provided that you do not use any API functions that are exclusively available under Windows NT or Windows 95 you can ship a single Win32 application for all three operating systems.

 

3. Introduction

This chapter introduces you to the basics of Windows programming and its event driven philosophy. It also explains why Windows programs look different from other C programs and introduces you to some major terms used in Windows programming.

 

3.1 Sequential programming vs. event-driven programming

It was only about a decade ago, when most computers had none or only very limited graphics capabilities. Interaction between the machine and the human was mainly done by typing in strange commands with a keyboard (and many self called UNIX Gurus still have not come over that yet) and receiving some sort of text message on the screen. Programs at that time had control over the interaction process as they gave the user a number of options what to do like pressing a key for example and they reacted by changing some of the say 2000 characters (25 lines by 80 columns) on the screen. Even if a program used a graphic mode this still applied, the only difference was, that instead of characters there were now a much larger number of pixels. So as a result there was a direct correlation of the way the computer saw the screen and the way the user saw it.

With GUI like Windows things have changed a bit. First you got this fancy device called mouse which enables you to go somewhere and click and even more important, you have now windows which you can drag around and arrange in any way you want. It would be a hard job for a program if it had to keep track of where it actually is and hence which of the thousands or millions of pixels it has to change to display a single character by also taking into account that some of the pixels may be hidden by another window. Someone's got to do the job though and this is where the GUI comes in. Now for a program that means that the correlation between the way the program sees the screen and the way the user sees it is lost. For the program there is only one window which is always visible and in which the coordinates of pixels never change.

This alone would not justify the need for a new programming philosophy because as you can see, many old non GUI programs still run happily in a "shell" window. However the reason why this works is that there is a windows program which suggests the old-style-application that the screen runs in text or full screen graphic mode and hence it does its output in the usual way. Since it would mess up the screen completely if the output would go directly to the screen the Windows program directs it to a buffer which it then displays on the screen. However this approach has the following disadvantages:

Another major problem is that although you might be able to change the size of such a window, it is not able to react to it i.e. if the program assumes a screen of 80x25 characters it won't give you more or less rows or columns just by changing the windows size.

All the old DOS or UNIX program did was processing every command sequentially. So apart from some programs which did their own interrupt handling (don't bother if you don't know what that is) it is always determinable with which command the program will succeed once the current command had been processed.

Under Windows things must obviously change a bit if we want to overcome the problems mentioned above. It can no longer be just us, telling the operating system or the GUI respectively what to do, here the OS must also be able to request services from us. Let me give you two examples of what that means:

So as you can see the main difference between a DOS or UNIX program and a Windows program is that the latter cannot just sequentially process the code but must have a mechanism of reacting to events which may occur at any time. This type of programming is called event-driven programming. As a result it depends mainly on the user's actions which part of your program will get executed and the user is controlling the program rather than the program controlling the user. An event-driven program might e.g. have to redraw the window's contents five consecutive times without any changes because of some action undertaken by the user whereas in a sequential program there has never been a need to do that.

 

3.2 Some code for starters

If you have ever read a book about learning a programming language you have probably come across the fist program which printed "Hello World" on the screen. They always do that and so do most books about Windows programming although there is no such thing as a new programming language.

So in a conventional C program you have probably tried the following:

#include <stdio.h>

int main()

{

   printf("Hello World");

   return 1;

}

Now with Windows you expect things to be a bit more complicated and indeed in most books they give you about 50 lines of code or more to do that, although it can be as easy as

#include <windows.h>

int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{

   MessageBox(NULL,"Hello World", "My first program", MB_OK);

   return TRUE;

}

Although this is a fully working Windows program there is a good reason why you won't find it in the books: This is not what you really want. The MessageBox command displays only a simple message to the user and all the work for creating and displaying the window is done by Windows. The is no functionality you can add to a message box apart from the options provided in the arguments of the command and the window is owned by Windows rather than yourself. But what you really want is to have your own window which you can fill with more complex contents such as menu's, buttons, text and graphics and which the user can resize, minimise or maximise.

Before we do that however I want to analyse the above program to and make you familiar with some important conventions that are required for understanding Windows and our further exercises.

 

3.3 There is C and C

If you have done some conventional C or C++ programming and someone would ask you to name some of the commands in C you might say something like: switch(), printf(), getc(), fopen(), etc. Among those four command however there are two which cannot be used under Windows, one that you can but should not and only one that is a truly valid and acceptable command in a Windows C program.

In fact the latter three are commands which themselves call one or more functions in the OS and are thus specific to a particular platform. The implementation of these commands is provided in so called libraries which are linked together with your program, whereas the core C commands such as switch() can be understood directly by the compiler and thus do not require header or library files.

Under Windows you still use the same syntax, the same brackets and the same variable types as in a conventional C program and of course you need all the core C commands plus the so called preprocessor commands which begin with a hash (like e.g. #include or #define). Here is a list of the most common core C commands:

switch     case     break     if     else     while goto     return     sizeof     typedef     struct

However with other commands you have to be careful. Library commands which are all defined in header files like stdio.h, stdlib.h, io.h, etc. cannot always be used under Windows. Printf (defined in stdio.h) is for example a command that you cannot use whereas localtime (defined in time.h) you can. For some functions that can be used, you'll find a similar function in the Windows API as for example for sprintf(). Sprintf does the same as printf except writing the output to a string rather than the screen (or any other stream). Sprintf (defined in the string.h file) can still be used under Windows and you'll find it in many of even Microsoft's own sample code. However I recommend not to use it and use wsprintf which is the equivalent Windows function instead.

The trouble with library commands is:

However there is one important difference between sprintf and wsprintf: wsprintf cannot deal with floating point numbers whereas sprintf can. So if you need to format floating point numbers you have to use sprintf.

If you got a bit confused now, here's a rule you can stick to: Before using a library function you should always check whether there is an equivalent or similar function in the Windows API and if so use it. Otherwise look up the description of the command in the manual or help file and check whether this command can be used under Windows.

 

3.4 The PASCAL calling convention

In every program things have got to start somewhere and in a conventional C program this is the function "main" which is executed by the OS when the program is called. Under Windows this function is called "WinMain" and it has got some additional parameters with it. If you look at the line of code with the implementation of the function WinMain you might realise a statement, that you have not seen before: PASCAL. You might have heard of the programming language PASCAL but since we're doing C does this make sense? It does and it actually tells the compiler that this function uses the PASCAL calling convention. Now what does that mean?

When a function is called by a program regardless whether it is an OS function or one defined in your own program, the parameters witch you specified in the function call have to be passed to the function. This is done by pushing these parameters on the stack and in C the order in which the parameters are pushed is from right to left (last parameter first) whereas in PASCAL it is vice versa. Not a big deal but as a result there are a couple of differences:

In C you can have functions with variable parameter lists as e.g. with printf where you can specify as many parameters as you wish (theoretically). With the PASCAL calling convention this is not possible but therefore functions using the PASCAL calling convention require 3 Bytes less of code when called (in 16-Bit programs). Now 3 Bytes may not seem very much, but by the time Microsoft designed Windows, Memory was a scarce resource and 3 Bytes saved for every of the thousands of function calls in Windows was a really big issue. 3 Bytes less mean also an improvement in speed since this also saves a few CPU cycles every time a function is called.

All of Window's API functions use the PASCAL calling convention except wsprintf because of the variable parameters required. And Windows expects, that all the functions it calls in your program also use the PASCAL calling convention. This is the reason why we have to specify the PASCAL keyword in the WinMain definition, otherwise the parameters would be messed up and you will most likely get a General Protection Fault (GPF) as soon a you use them. As we will see later there are other functions witch are called directly by windows such as window and callback procedures. These must in the same manner be declared with the PASCAL calling convention.

You might now ask yourself whether it is possible of using the PASCAL calling convention in general for all you functions and indeed most compilers offer an option to do that. However, I do not recommend to do so as you will get in trouble if you use other than the Windows API functions and which do not explicitly declare the calling convention in the associated header file. The compiler would then assume that these functions (which are provided in a LIB or OBJ file) also use the PASCAL calling convention and again you program would crash on execution. By default all C Compilers use the C calling convention an you should stick to that and only use PASCAL declaration where necessary.

 

3.5 The Hungarian notation

If you have a brief look at the above example again you might spot another difference between the two programs: the writing of the words. Where in the conventional C program you get only a rough idea if any at all what a command like e.g. printf does, the MessageBox command in the Windows program is much more understandable.

In the early days when C was invented programmers tended to be weird freaks, to lazy to write whole words and even too lazy to press the shift key. Programming commands were there sort of secret language and this might be one reason why many people have been afraid of programming.

In the Windows API all commands are concatenated full words in a natural language order and each of these words begins with a capital letter. To create a window e.g. the old C freaks would have probably introduced a command like crwnd or wcreate or createwnd whereas in Windows you can be sure to find a command called CreateWindow. This helps you the programmer a lot to find a command that you have never used before. In many cases you just think what you would call the command you are looking for according to these rules and you will find such a command.

In Windows not just functions are named in a special way but also variables and structures. Every variable or function parameter starts with a lowercase type prefix abbreviation followed by the meaning in natural language with a capital letter. This way of naming variables is called the Hungarian notation in honour of an apparently legendary Microsoft programmer called Charles Simonyi (if you see him, give him my regards). The following table shows examples of how the Hungarian notation is used on variables:

Type    Data     Example        Description                                     

Prefix  Type                                                                    

b       BOOL     bEnabled       Boolean variable which can be TRUE (0) or FALSE (=0)

c       char     cKey           Single character

h       HANDLE   hWnd           Handle to a Windows Object                      

dw      DWORD    dwRop          Unsigned 32-Bit value                           

l       LONG     lParam         Singed 32-Bit value                             

w       WORD     wParam         unsigned 16-Bit value                           

u       UINT                    unsigned integer value (can be 16 or 32 bit)    

n       int      nHeight        singed integer value (can be 16 or 32 bit)      

rc      RECT     rcWnd          Rectangle structure containing 4 coordinate values

sz      char[]   szFileName     Array of characters containing a String terminated

                                by a character value of zero (zero terminated string)

lp      void     lpCallbackProc Far pointer to something (e.g a hidden data structure)

        far*                    In 32-Bit Windows there are only far pointers so the type is (void *)

lpsz    LPSTR    lpszClassName  Far pointer to a zero terminated Sting

lprc    LPRECT   lprcWnd        Far Pointer to a Rectangle structure

lppt    LPPOINT  lpptPos        Far Pointer to a Point structure

When you define new data types or structure using the typedef statement all letters should be capital letters. This is e.g. how the types BOOL and POINT are defined in the windows.h header file:

     typedef int BOOL;

     typedef struct { int x,y } POINT;

Naming your variables and functions according to the Hungarian notation is not a must but I strongly recommend to adapt this style in your own programs. This will not only help others to understand your programs but also yourself it you look at them again after months or years. In fact I have not seen a single Windows program yet that does not use this notation.

 

3.6 Handling Windows objects

Now we've got all this fancy windows, icons, fonts etc. stuff but how do we handle it? Well, with handles of course. In the real world e.g. most glass windows have a handle and you use it to open and close it. In a windows program it basically the same thing. When you create the window you get a handle which you can then use to show, hide, move, size or destroy the window. And thanks to the Hungarian naming convention the functions are called respectively ShowWindow, HideWindow, MoveWindow (used for both moving and sizing) and DestroyWindow. All these functions require the handle of the window you want to perform the action on as the first parameter.

However window handles are just one particular type of handles and there are a lot more types in the Windows API. You get handles for all types of windows objects such as icons, bitmaps, dialogues, menus, fonts, color palettes and so on.

Now what exactly is an handle? Again if you have done some conventional programming you might have already come across one type of handles: file handles. A file handle is a positive integer value returned by either open or create and it identifies a file. Every time you read of write to/ from the file you need to specify the file handle and when you finished you close the file and the file handle becomes invalid.

In MS-DOS and UNIX the value of a file handles is an index number to a table owned by the OS where information about the file is stored. Since you cannot access the table the value of the file handle is more or less meaningless to you and it can only be used to call other file functions. Under Windows handles are usually pointers to a data structure describing the associated object and in the same manner it is meaningless to the application. However since the value of a handle is unique for a particular object type (i.e. there won't be two different objects of the same type with identical handles) you can compare two handles and if they have the same value they refer to the same object.

A common mistake in windows programming and the cause for many general protection faults (GPFs) is to pass an invalid handle to a function. A handle is only valid as long as an object exists. A window handle e.g. becomes invalid either if you explicitly call DestroyWindow in your program or if the user closes the window. If a handle becomes invalid you should set its value to NULL to be able to detect whether is valid or invalid later on.

 

3.7 Resources

In the real word nothing is unlimited and this also true for the computer world. For all those who own a car, parking space is (apart form money) probably one the things that is most limited and hard to find these days. In general we call things like that Resources.

What exactly is a resource? Well first it is something that is given by nature (or something/ someone else that beyond our control) i.e. it can't easily be produced, second it is limited and third we can decide when and how much we want to use of it. Real word resources are as we all know oil and coal for example.

When we look at the computer world hardware resources are the most obvious. There is a given amount of physical memory, the CPU operates at a given speed and there is a certain amount of hard disk space available. Obvious isn't it, but have you ever looked at the screen a resource? Well its properties are given by the manufacturer of your monitor and you can't easily change them, it is limited, as there is a maximum number of horizontal and vertical pixels and it is used up by all these programs that you simultaneously run in your GUI environment.

On the software bit there are also resources but they are not quite as easily to spot. First there are the so called System Resources. These are normally linked to hardware resources in some way like for example the available memory (but here its physical + virtual memory). Under Windows there are some other limits which are especially a problem under Windows 3.x and were divided into KERNEL, GDI and USER Resources, which all refer to subsystems of Windows. There is e.g. a limit on the number of file handles that can be obtained and that means that only a certain number of files can be opened simultaneously. Since IO operations are handled by the Kernel subsystem, this is a Kernel resource. GDI resources are limits set by the graphical subsystem of Windows and it actually means a limit of the memory available to store information about graphical objects as pens, brushes, bitmaps etc. Note that the limit only refers to the information stored about an object not the object itself i.e. for bitmaps that is amongst other things the size in pixels and a pointer to the bitmap data but not the bitmap data itself. The limit is also not set for each object type individually but for the sum of all GDI objects. So if the limit was 10 objects there could be either combination of the number of pens, brushes and bitmaps up to the total limit of 10. GDI resources were normally the most likely to run out of under Windows 3.1 and once you ran out, you were lucky if you were able to save your data and reboot windows. Finally USER resources are the number of Windows classes and individual windows that can be defined and opened throughout the system. As windows come and go there is very rarely a deficiency so this is generally not a problem to worry about.

As I mentioned before this is a particular problem of Windows 3.x. Under Windows NT there is no preallocated memory for resources and thus the only limit it the total memory available which will lead to other problems first once you are running out. Windows 95 is again somewhere in between. Technically its still the old inflexible system of Windows 3.x, but they have split resource types up and use more resource heaps, which basically means that it takes longer before a shortage occurs.

Since now resources have been a pain so let's now get on to a more friendly type of resources, the so called Program resources. Program resources are any kind of no-code information you create yourself in order to add it to your program. Examples are tables of any kind, bitmap images, icons, cursors, dialogue boxes etc. We'll go into more detail later on, when we create program resources.

From your point of view these might not look like resources but from the programs point of view they are since they are

In conventional DOS and UNIX program resources are normally stored in external files which you have to take care of yourself. In Windows program resources are linked together with your EXE file and there a plenty of functions to access and use these resources.

 

3.8 Compiling the lot

Obviously the most important thing you need to compile a Windows program is at least one C or CPP (C++) file containing your code. In addition every "real" Windows program needs another file type: a resource file with the file extension .RC. This is where you put all your program resources in such as e.g. menus and dialogue boxes and the first thing that should go in there is a program icon. Without an icon, Windows will use a default icon which gives little information about your application when installed in the Program Manager or the Windows 95 Desktop.

Most Windows projects contain another file, the definition file with the extension .DEF. This is required for 16 Bit Windows applications and optional for Win32. It contains general information about the program and sets some initial runtime parameters. DEF files are normally created at the beginning of the project and are hardly changed later on. If you set up a new project just copy and existing .DEF file and modify it according to your requirements.

Now what happens with all these files when you compile them? The following graphic shows what happens with your source files when compiling:

In the first stage all your code files are compiles into OBJ files. Then the resource compiler translates your RC file into a RES file. This is the binary representation of your resource description and contains also files included in the RC file such as .ICO and .BMP files.

In stage 2 all object files are linked to an EXE file. This is a temporary executable file which cannot be executed. In the next stage the resource compiler links the RES file with the temporary EXE file and marks it as a Windows program.

 

4. How to start

After all this theory you might feel like doing something practical now or at least to have a look at some real code. So how do we start with a windows program? Actually there are two different approaches.

In fact almost every windows program features at least one dialogue box and so you need the knowledge of approach two anyway. And vice versa if you want to extend your dialogues with say you own custom controls you need the knowledge of approach one. In fact a complex windows program is always a mixture of both techniques. However I reckon that even many people with Windows programming experience are not aware of the fact, that almost every application can be completely based on a dialogue box, simply because it is not in the books.

 

So how far do we get with say 200 lines of code?

200 lines of code is not much and you'll be surprised how far you'll get. The next two chapters give you an outline for a windows program based on each of the two approaches. You don't have to understand both approaches straight away. See which one you prefer and try to modify and extend it a little bit. You can always come back to the other approach later. Finally in chapter seven I will then give you some ideas on how to build on the knowledge you have acquired by giving you a free choice on where to go next.

 

5. Paint Counter: A windows program outline

In this chapter we analyse the code required for a simple windows program based on a custom window class. The purpose of the program is to show you how message are distributed and graphical output is performed. The only thing the program actually does is to count the number of paint messages the window is receiving in order to demonstrate which events cause a repaint message.

 

5.1 The files

To compile and modify the program you need the following files:

Example1.C - the main code file

Example.RC - the main resource file

Example1.h - Header file

Example1.ICO - Icon file containing program icon in binary form

Ex1W16.EXE - 16-bit executable file for Windows 3.x

Ex1W32.EXE - 32-bit executable file for Windows 95/ Windows NT

Example1.zip Click here to download the archive to your computer.

 

5.2 The procedure WinMain

Let's have a look at the program entry point, the procedure WinMain. This is where it all starts and what we get is four parameters.

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)

The first parameter hInst is a handle for the current instance of our program. Now what do we mean by instance? As you know, you can start most windows programs multiple times and you will get another window every time. Take Notepad e.g. and start it 10 times say. You've now got 10 instances of Notepad and each of them are independent from each other. Internally Windows loads the program's code only once in order to save memory space, but it gives every instance its own data area and stack and you do not have to bother about whether there is one or many instances of your program.

This handle is very important as we will need it later on as a parameter for various functions and so we store it in a global variable.

hInstance=hInst;

Many people would tell you its bad to use global variables at all and generally I agree. Global variables cause a number of problems and make programs less understandable and maintainable. In this case however its different. Basically this is because it is not really a variable but more of a constant, since it will not change at any time. Unfortunately C does not offer a feature to set the value of a variable only once but since we set it right at the beginning and we know it wont change all we have to make sure is that we do not change it by mistake.

The second parameter of WinMain is hPrevInst. Under Win16 this is a handle to a previous instance, if any, otherwise it is NULL. Basically this handle indicates in which memory segment the data of the previous instance is located. Under Win32 things have changed a bit. First there is no memory segmentation any more and second instances are more protected against each other which makes this parameter obsolete and it will thus always be NULL. The only function that makes use of this handle under Win16 is GetInstanceData, but since there is no such handle in Win32 it has been deleted and so its better to forget about it straight way.

However in Win16 this handle has another function - the detection of the first instance of a program which is the case when hPrevInst is NULL. This is important because only the first instance must register user defined window classes that the program wants to use (we will come to that in a minute). Again in Win32 a program does not know whether it is the first instance and thus it must always register the window classes required.

We use the following code to detect whether we are the first instance and if so we call our own function to register the window classes. Since in Win32 hPrevInst is always NULL this will also work fine for 32-bit programs. A description of what RegisterWindowClasses does can be found in the following paragraph.

if (hPrevInst == NULL)

      if (!RegisterWindowClasses(hInstance))

           return FALSE;  // registration failed!

After the window class has been registered it is now time to create our application window. For that we use the function CreateWindow and if the window can be created as specified we will receive a window handle in return. The CreateWindow function requires a number of parameter which I will not explain in detail since a precise description is provided in the online help. The window will have the title "My first Windows Program" and its size will be 400 by 300 pixels. With the CW_USEDEFAULT parameters we leave it up to Windows where on the screen the window will be shown later on.

// Create our Main Application Window

hAppWnd=CreateWindow(szWndClassName,"My First Windows Program",

                     WS_OVERLAPPEDWINDOW|WS_HSCROLL|WS_VSCROLL,

                     CW_USEDEFAULT,CW_USEDEFAULT,400,300,

                     NULL,NULL,hInstance,NULL

                    );

As it is always good practise to check the return value of API functions we now check whether the window handle is valid. If it is not i.e. if the window handle is NULL we terminate the program by returning FALSE.

if (!hAppWnd) return FALSE; // Create Window failed

The window is now created but it is not visible on the screen yet (Note: if you want a window to be visible immediately after creation add the window style WS_VISIBLE). To display the window we call ShowWindow which requires the window handle and a flag indicating how the window should be shown. The latter can be for example SW_SHOWNORMAL if the window should be shown in its given size, SW_SHOWMINIMIZED or SW_SHOWMAXIMIZED if it should be initially an icon or full screen or any other of the SW_ constants defined in windows.h (see function ShowWindow in the SDK help file).

For our main window however we should not set this ourselves but use the parameter nCmdShow which is the last parameter of the WinMain procedure. Usually this will be set to SW_SHOWNORMAL but the user can specify otherwise in the program manager. Afterwards we force the window to paint its contents by calling UpdateWindow.

ShowWindow(hAppWnd,nCmdShow);
UpdateWindow(hAppWnd);

Now that the window is created we come to the most important part of the WinMain procedure: the message loop. This is a simple while loop that will poll message form the message queue and dispatch them to the associated windows for further processing.

while (GetMessage(&msg, NULL, 0, 0))
  {
     TranslateMessage(&msg); /* translates virtual key codes    */
     DispatchMessage(&msg);  /* dispatches message to window    */
  }

There is another parameter in the WinMain procedure that I have not talked about yet. lpCmdLine is a pointer to a zero terminated string with the command line parameters specified in the program manger of Win 3.x or the shortcut properties of Win 95 respectively. Unlike in DOS or UNIX programs the parameters are not split up automatically i.e. if you call your program e.g. with "C:\TEST\MYPROG.EXE -f hello.tst -n" lpCmdLine will point to a string containing "-r hello.tst -n".

Although command line options are pretty unimportant in a GUI environment anyway there is one thing the lpCmdLine should always be used for: if your program deals with files you should always check whether lpCmdLine contains a file name and if so open it. This is important since many users start programs by double clicking on a document file in the file manager or the explorer. Provided that the file extension of the document file is assigned to your application, these programs will then call your program and specify the documents file name in the lpCmdLine Parameter.

 

5.3 Classifing a Window

In the description of the WinMain procedure above I have missed to explain what RegisterWindowClasses actually does. This function is defined next in the C file as follows:

static BOOL RegisterWindowClasses(HINSTANCE hFirstInstance)

This function, which is called from the WinMain for the first instance in a Win16 program and for every instance in a Win32 program, requires a handle to the instance and returns a boolean parameter indicating whether the function was successful (return value is TRUE) or not (return value is FALSE). I also added the keyword static which makes the function invisible i.e. inaccessible outside this module. It is good practise to declare every function that is not called from outside as static as you can find out more easily where this function is called from and you can use the function name again in other modules.

Lets just discuss what a window class is. A window class defines general properties for all windows that are derived from this class i.e. once a class has been defined you can create any number of windows based on this description and they will all have certain things in common like the background color, the icon that is shown when the window is minimised or most important the Windows procedure which handles all events concerning the window.

In order to register a window class we need to fill a structure of type WNDCLASS which is defined in windows.h. For that we define a local variable of this type and set the name of the new window class.

{ WNDCLASS wc;

  wc.lpszClassName = szWndClassName;  // Name of the Window Class

The name we set here for our class can be virtually any name that takes our fancy except the name of predefined window classes. Predefined classes are BUTTON, STATIC, LISTBOX, COMBOBOX, SCROLLBAR and EDIT which are the names of all standard controls provided by Windows. The class you register will be available for your application only and be invisible to others; hence you also do not need to worry whether the name you are giving your class has already been taken by another application.

The name is not given here directly but it is hidden in the constant string array szWndClassName which I have defined at the beginning of the file as:

static const char szWndClassName[]={ "MYWNDCLASS" };

Hence the name of the class we are registering is MYWNDCLASS. We need this name also for the function CreateWindow which is the reason why I have put it in a variable. Again as we only need the string in this module it is saver to define it as static which makes it invisible to other source code modules.

Next we set the instance handle and some class style flags. A concise description of possible class style flags can be found in the online help. The style I used here forces the window to redraw completely every time the size of the window changes. You might want to try out what effect it has if you do not specify these flags. For that set wc.style to zero and recompile the program.

  wc.hInstance = hFirstInstance;         // Handle of program instance
  wc.style     = CS_HREDRAW|CS_VREDRAW;  // Combination of Class Styles

Now we need to specify the address of a function handling all the events concerning the window. This function is called the windows procedure and you'll find the body for the function in the following paragraph. Since all windows derived from this class share the same window procedure we have a problem if more than one window per program instance is derived from this class. How can we give each window its own private data? The answer lies in the cbWndExtra parameter which gives the number of bytes allocated for each window to store user defined data. In C++ this is e.g. used to store a pointer to a C++ object. To set and retrieve the data Windows offers the functions SetWindowWord and SetWindowLong or GetWindowWord and GetWindowLong respectively.

  wc.lpfnWndProc   = AppWndProc; // Address of Window Procedure
  wc.cbClsExtra    = 0;   // Extra Bytes allocated for this Class
  wc.cbWndExtra    = 0;   // Extra Bytes allocated for each Window

The rest of the structure members defines the basic appearance of the windows created from this class. First we load the Icon that is displayed when the window is minimised from the resource file. LoadIcon does the job by returning a handle to the icon. To identify the resource we use the macro MAKEINTRESOURCE which is also defined in windows.h and which takes a numeric value uniquely identifying a resource. Its up to us to assign numbers or names to resources so I have defined ICON_APPWND in the example1.h header file as:

#define ICON_APPWND 100

If you want to have your own cursor when the user is in the window you can use LoadCursor accordingly. In this case we use one of Windows' standard cursors which is why we set the instance handle to NULL. Finally we set the background colour to white and the menu name to NULL.

  wc.hIcon         = LoadIcon(hFirstInstance, MAKEINTRESOURCE(ICON_APPWND));
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
  wc.lpszMenuName  = NULL;

Now that all members of the structure describing the window class have been set we register the window by passing a pointer to the structure on to windows.

  if (!RegisterClass(&wc))  /* Register the class */
      return FALSE;         /* RegisterClass failed */

When your program gets more complex you might need to register other windows classes.

 

5.4 The window procedure

Where all the code we have looked at so far is executed in the first few milliseconds after program execution we come now to the heart of your program, the windows procedure. As I mentioned before this is where Windows sends all events concerning the window to and where we can decide how to react to them. This is also the place where you will extend your program in order to add more functionality.

Let's have a look at the general structure of a window procedure first:

LRESULT FAR PASCAL WndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
  switch(msg)
   {
     case WM_...
          /* process a message */
          break;
     default:
          /* let Windows handle the message */
          return DefWindowProc(hWnd,msg,wParam,lParam);
   }
  return 0L;
}

As the window procedure is called by Windows it must always be defined as PASCAL i.e. it must use the Pascal calling convention. The keyword FAR is important for Win16 only but its no mistake to specify it for Win32 either. The window procedure receives four parameters from Windows and returns a 32 bit integer value.

The first parameter is the handle of the window which is exactly the one we received from the CreateWindow procedure in the WinMain. The second parameter is a message number indicating the type of event that has occurred. The value and meaning of the last two parameters are depending on the message.

All window messages are defined in windows.h and they all begin with the prefix WM_. There are hundreds of messages and you will never need to process all of them in a window procedure. Here are some of the most often used which you can look up in the help file.

WM_CREATE            WM_PAINT             WM_DESTROY            WM_SIZE
WM_LBUTTONDOWN       WM_ACTIVATE          WM_COMMAND

All messages that you do not process must be passed on to the default message handler of windows. To do that the function DefWindowProc is called in the default branch of the switch statement.

Now let's look how the Window procedure for our sample application has been implemented:

LRESULT FAR PASCAL AppWndProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
  static int iPaintCount;   // Count the number of Paint Messages received
  static HBRUSH hFillBrush; // handle to a brush which we use for filling the ellipse

First we define a variable which we use later on to count the number of WM_PAINT messages that have been received. This variable must be defined as static in order to preserve its value when we return control to Windows. Otherwise its value would be some random number every time Windows calls the window procedure. And we need another static variable to store the handle of the brush we want to fill the ellipse with.

Now we filter the messages and the first message we're going to process is WM_CREATE:

  switch(msg)
   {
     case WM_CREATE:
          iPaintCount=0;
          hFillBrush=CreateSolidBrush(RGB(0,255,255));   // Create a cyan brush
          break;

This message is received only once and that is when the window is created. We receive this message from Windows as a result of the call to CreateWindow we made in the procedure WinMain. So at this point, the program will still be in the WinMain waiting for CreateWindow to return, which will only be the case after we finished processing our WM_CREATE message. This message is now used for initialising variables and data structures and allocating all kinds of resources required by the window. In our case we set the counter variable to zero and create a brush that we will use for filling the ellipse. From CreateSolidBrush we receive a handle to a brush of the desired color which is specified in terms of its red, green and blue content using the RGB macro. This handle is a resource allocated by windows on behalf of your application and must be freed later on using DeleteObject.

The next message in line is the WM_COMMAND message. It deals with menu commands only. By examining the wParam parameter we can determine which menu command the user selected. All menu commands are associated with a number which is defined in the example1.h file; all beginning with IDM_ standing for IDentifier Menu.

     case WM_COMMAND:
          switch(wParam)
           { 
             case IDM_ABOUT:
                  DialogBox(hInstance,MAKEINTRESOURCE(DLG_ABOUT),hWnd,AboutDlgProc);
                  break;
             case IDM_QUIT:
                  DestroyWindow(hWnd);
                  break;
           }
          break;

There are only two commands in the menu the About and the Quit command. If the user decides to quit, we simply destroy the window using DestroyWindow. The WM_DESTROY message will then do everything else that is necessary. If the user selected the about command the DialogBox command is used to display a windows defined in the resource file. This is described in more detail in the Dialogue Example.

In order to force a repaint of the window we allow the user to press the left mouse button. This is again easy to detect as we will be sent the following message:

     case WM_LBUTTONDOWN:
          // Invalidate the window's client area
          InvalidateRect(hWnd,NULL,FALSE);
          break;

There are other messages similar to that indicating other mouse events such as WM_LBUTTONUP, WM_MOUSEMOVE etc.

What we do here is not repainting the window directly but rather invalidating the window's client area. The client area is the area of the window inside its borders i.e. all the space that is at your disposal and that you are responsible for drawing. InvalidateRect takes three parameters: the handle of the windows which client area you want to invalidate, the part of the client area you want to invalidate, and a flag indicating whether the invalidated parts should be erased first, or whether you want to paint over the existing contents. The part you want to invalidate is specified by a pointer to a rectangle containing the boundaries but is here (and in most cases this is absolutely sufficient) set to NULL which means that the entire client area is invalidated.

Now how does that help us? InvalidateRect will add a rectangle to the window's update region and post a WM_PAINT message to the window. If many events happen simultaneously each requesting to repaint the window, then if you'd do it directly you might end up, drawing the same thing several times. With this mechanism all requests are collected and processed later. Additionally this also takes into account, which part of the window is visible in case of overlapping windows. Window's will automatically minimise the area that must be repainted.

Now there has got to be some code to actually draw the contents of the window and the time and place for that is the WM_PAINT message. This message is received first when the window becomes visible and then every time that Windows wants us to repaint the entire window or a portion of it, which can virtually be any time. Since the whole purpose of the program is to give you an idea when and how often this message is received we increase the value of our counter every time and display its value. Now how do we do that?

     case WM_PAINT:
          { PAINTSTRUCT ps;
            HDC hdc;
            // Increase the Paint Message Counter
            iPaintCount++;
            // Get the handle to the Windows's Display Context
            hdc=BeginPaint(hWnd,&ps);
             // Now Paint the Windows's contents
             PaintAppWindow(hWnd,hdc,iPaintCount,hFillBrush);
            // Call EndPaint to release the DC and validate the client area
            EndPaint(hWnd,&ps);
          }
          break;

First we allocate a structure of type PAINTSTRUCT which we let Windows fill with values by calling BeginPaint. The meaning of the structure members is of minor importance at the moment and what we're really after is the handle to the display context (this is what HDC stands for). This handle is given to us in return to the call to BeginPaint and it is actually also contained in the PAINTSTRUCT structure. Fair enough, you might say, but what the heck is a display context?

In brief a display context is something that tells Windows where any output the window wishes to make has to go on the screen and where the borders of the area are. In fact you won't be able to draw or write anything in your window without having a valid handle to such a display context. All output is managed by the Graphic Device Interface (GDI) subsystem of Windows and I will tell you more about display contexts and the GDI in the next section.

So now that we've got the handle to display context we can actually draw our message which we do using the function PaintAppWindow which I will explain in the next paragraph. After we have done that, we must call EndPaint which tells Windows that we have finished painting and that the contents of the window are now in a valid state. End Paint will also free the display context i.e. the handle becomes invalid and we can't use it any more.

Processing the WM_PAINT message and calling BeginPaint and EndPaint is the single most important thing a window procedure has to provide. However messing around with it can cause a lot of trouble so I better warn you of some of the most dangerous pitfalls a beginner can step into:

The last message we have to process for our example application is the WM_DESTROY message and you probably guess that this is sent when the window is destroyed.

     case WM_DESTROY:
          DeleteObject(hFillBrush); // Delete the fill brush (never forget!)
          PostQuitMessage(0);       // Tell Windows we want to terminate
          break;

First we destroy the brush we allocated in the WM_CREATE message. If you have more resources allocated in the WM_CREATE message such as memory or file handles, this is the place and time to free them.

Now Windows does not know, that the destruction of this window means the end of our program. So what we have to do here in order to tell Windows that we want to terminate our application is to call PostQuitMessage. Window will then put a WM_QUIT message in our message queue and that will give our call to GetMessage in the procedure WinMain a return value of zero. If you look at the message loop we have defined in the WinMain again you will see that a return value of zero terminates the loop and the procedure WinMain returns. Now none of our code is executed any more and Window will remove our application from memory. Failing to call PostQuitMessage would keep our application waiting for a message but since there is no window any more there won't be any more messages. Unfortunately the only way to get rid of such an application (if you do not execute it from the IDE) is to reboot Windows.

That was heavy going now wasn't it? But do not worry if you do not understand it completely. If you stick to the example and do it like that you can't go wrong.

Once you have received the WM_DESTROY there is nothing you can do to keep the window alive. So what if we want to ask the user for confirmation first when he/ she closes the window?. In this case you have to process the WM_CLOSE message which you receive prior to WM_DESTROY. The example in the help file shows how to do that.

The only thing that is left for us to do now is to let Windows handle all messages that we did not handle. This is done by calling DefWindowProc in the default branch of the switch statement. For all the messages we have processed we return a value of zero.

     default:
          // We didn't process the message so let Windows do it
          return DefWindowProc(hWnd,msg,wParam,lParam);
   }
  return 0L;
}

 

5.5 Doing the painting

static void PaintAppWindow(HWND hWnd,HDC hdc,int iPaintCount,HBRUSH hEllipseBrush)

This function is declared as static since it will only be called within this module. It is passed the handle of the window, the handle of the display context, the count number of messages received so far, and a handle to a brush which we use to fill the ellipse. First we need to allocate the following local variables:

{ char   szText[50]; // Array holding the string displayed
  RECT   rcWnd;      // Dimensions of the Windows's client area
  HBRUSH hOrgBrush;  // Original Brush of display context

The first thing we do now, is to get the coordinates of the window's client area and store them in rcWnd which is a structure containing the left, top, bottom and right boundaries of the rectangle.

  GetClientRect(hWnd,&rcWnd);

Usually the upper left coordinate will be at 0/0 unless you change the windows origin. Hence the right and bottom values will contain the width and height of the client area.

We can now use this information to draw an ellipse touching each side of the window. Before we actually draw it though, we've got to select the fill brush we want to use into our display context.

All drawing primitives in Windows i.e. lines, rectangles, ellipses, arcs are always carried out with the pen and brush that is currently selected into the display context. The default is a black pen and a white brush. To change that you can call SelectObject and give it another pen, brush, color palette etc. SelectObject automatically detects the type of object you specified and replaces the current one. It also returns a handle to the object that was previously selected and this is a very important value to remember, since you are responsible to undo all changes before you release the display context again using EndPaint or ReleaseDC. Failing to restore the display context is a major offence and will most likely be punished by the need to reboot your system. The effect does not occur immediately though but after hours of use. This is why these bugs are very difficult to find and require special tools to detect such as BoundsChecker.

All right, this is how we do it now:

  hOrgBrush=SelectObject(hdc,hEllipseBrush);                   // select the brush
  Ellipse(hdc,rcWnd.left,rcWnd.top,rcWnd.right,rcWnd.bottom);  // Draw the ellipse
  SelectObject(hdc,hOrgBrush);                                 // deselect the brush

Now all we need to do is write the text into the centre of the window. To do that we use wsprintf to generate a string consisting of the text and the number value of iPaintCount. Then we set the background mode of the display context to transparent (otherwise it would be white) and draw the text using DrawText.

  wsprintf(szText,"Paint Count: %d - Click inside window to repaint!",iPaintCount);
  SetBkMode(hdc,TRANSPARENT);   // Make text transparent
  DrawText(hdc,szText,lstrlen(szText),&rcWnd,DT_SINGLELINE|DT_CENTER|DT_VCENTER);

DrawText has several options that are all given with the last parameter and which can be combined using a binary or. See the description in the help file for details. Another option for outputting text is the function TextOut.

 

6. Sort it out: A dialogue based Windows program

This Chapter describes a windows application based on a dialogue box which allows to enter a list of names and sort the list in ascending or descending order.

 

6.1 The Files

To compile and modify the program you need the following files:

DIALOG.C - the main code file

DIALOG.H - Header file

DIALOG.RC - the main resource file

DIALOG.ICO - Icon file containing program icon in binary form

DIALOG16.EXE - 16-bit executable file for Windows 3.x

DIALOG32.EXE - 32-bit executable file for Windows 95/ Windows NT

Example2.zip Click here to download the archive to your computer.

 

6.2 Designing the application

As I mentioned before, with this approach writing code only comes second. What you start with here, once you've created the project in the IDE is to call the ResourceWorkschop (Application Studio for the Microsofties) by double clicking on the .RC file. The RC files is created automatically by the resource editor, but since it is a plain ASCII file you can modify afterwards like any other text file. In this case we want to create a menu and a dialogue template for our main window. Most dialogues do not require menus, but I've implemented one here just to show you how it can be done.

In order to design our application window now, we first create a new resource of type Dialog (oooh, they speak American English...). You will be provided with an dialogue window that is empty except for the three default buttons OK, Cancel and Help that you will find in most dialogues. As we don't need them here they can be deleted. Now you add controls to this dialogue and place and size them as you want. There are five types of standard controls that you can use in Windows 3.1 and some more if you've got a resource editor for Windows95. Each control also has properties with determine the style and behaviour of the control. To access the properties double click on a control. If you want to find our more about a particular option in the property dialogue press the help button (I keep on saying that because most people don't use help files very much). At runtime i.e. during program execution you can send messages to controls in order to set them up or check their state and you will receive so called "Notification Messages" if the user changed data or the state of a control.

Here's a brief description for the six standard controls:

Now you've got the basic information you need and you can design something fancy. In all my years of Windows programming experience the design of dialogue boxes is still the most difficult and time consuming task. It sometimes takes me up to a couple of days to make a dialogue box look nice and intuitive to use. But if you are less of a perfectionist you can of course just throw the controls into the dialogue window. However it is not a very good idea to do it quick now, and perfectionise it later, because if you change the design after you've done the coding you'll face a lot more work than doing the design properly in the first place. The position and size of controls is not a problem since you can always change that without changing your code, but if you later on decide you need radio buttons instead of check boxes or you need to turn this list box into a combo box say, your up shit creek.

Another issue is the creation of control identifiers. In order to send and receive message from controls you need to give each of them a unique identifier which is simply an integer number. Both ResourceWorkshop and Application Studio help you by creating identifiers automatically although I prefer doing it myself. It's a bit more work, but you know exactly what's going on. Let me give you an example on this can be done:

Suppose you've got a dialogue which looks somewhat like this:

Then the resource script created by the ResourceWorkshop for this dialogue looks like this:

DIALOG_1 DIALOG 64, 99, 207, 46
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Authorisation"
FONT 8, "MS Sans Serif"
{
 CONTROL "Enter your name:", -1, "STATIC", SS_RIGHT | WS_CHILD | WS_VISIBLE | WS_GROUP, -1, 18, 60, 8
 CONTROL "", IDC_EDIT1, "EDIT", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 64, 16, 79, 12
 CONTROL "&OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 148, 6, 50, 14
 CONTROL "&Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 148, 24, 50, 14
}

The first line gives the dialogue name DIALOG_1 followed by the type of resource which is DIALOG and the position and size of it. Now change DIALOG_1 into something more meaningful like DLG_AUTHORISE and add a define statement in a header file which you include in both your code and the resource file. For example call the header file DIALOG.H and the follwoing line:

#define DLG_AUTHORISE      100

Then do the same thing with the identifier for the edit control which is set to IDC_EDIT1. If you change that to IDC_NAME say, you have to add a define for that as well in the header file like e.g.

#define IDC_NAME           100

Note that both values can be 100 since the first one is a resource and the second one only a identifier within this resource. By the way, IDC_ stands for IDentifier Control as you might have guessed. But you don't need to stick to it. I normally start with the name of the dialogue the control belongs to so I would call it DLGAUT_NAME (for DiaLoGAUTorise_NAME). You do not need control identifiers for the OK and Cancel buttons since IDOK and IDCANCEL are predefined constants in the windows.h file and so is IDHELP. And you can give all your static controls the value of -1 unless you want to set the text of the control at runtime.

Another thing you might want to do in the end is to put your controls in order. This can either be done in the resource editor or by editing the .RC file directly. The order is important for two things:

All right, that was the visual part, now lets go and implement the code for it.

 

6.3 The procedure WinMain

The procedure WinMain is the entry point of the application and it normally only features a few lines of code to create window classes and create the main application window. In this example the WinMain features as many as three lines of code. If you want more information about the WinMain and the parameters read the section about the procedure WinMain in the previous chapter.

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)

The first parameter hInst is the handle to the program instance which we will need later on for various other functions. Since it is constant and valid for the whole lifetime of this instance of the program we store it in the global variable hInstance.

hInstance=hInst;

Now we create our application window which is a dialogue box in this case. The function will not return before the dialogue is closed. This type of dialogue is called a "modal" dialogue. There is another type of dialogue called "modeless" which will not wait till the dialogue is closed and return immediately. If you want to find out more about those see the windows SDK help file for CreateDialog.

DialogBox(hInstance,MAKEINTRESOURCE(DLG_MAIN),NULL,MainDlgProc);

In its second parameter DialogBox requires the name of the template that is used for the dialogue. This is the one we defined earlier with the ResourceWorkshop. You can here use either a string or better use a numeric identifier. In the latter case you have to use MAKEINTRESOURCE to pass the value to the function. I used DLG_MAIN as a numeric identifier which I defined in the dialog.h header file. This file must then also be included by the resource file (dialog.rc).

Sometimes it can happen, that you call dialogue box, but nothing happens and DialogBox returns immediately with a return value of zero. In most cases this is due to an invalid dialogue template identifier. Check first, whether both the .C file and the .RC file include the header file where the numeric identifier is defined and that it is also used as the template name in the .RC file. If this is correct and there is still no dialogue box, check the dialogue box procedure (remember to return FALSE if you do not process a message) and if you use custom controls, whether all window classes are registered.

The third parameter of DialogBox is the handle to the parent window. In this case we do not have one, that's why we pass it NULL. The last parameter is a pointer to the dialogue box procedure that will handle all events. In the early days of windows this procedure address could not be given to the function directly and you had to a function called MakeProcInstance before you called DialogBox and FreeProcInstance afterwards. This was a major pain especially for modeless dialogues but luckily all modern windows compilers automatically deal with this problem. Under Win32 this is obsolete anyway and so there is no reason to bother you with the reason for that.

Well that was it really. If the dialogue is closed, we terminate by returning TRUE. It's as easy as that!

return TRUE;

 

6.4 The dialogue box procedure

The main difference between the previous approach described in the previous chapter and this one is, that the main window of the application is based on a dialogue box rather than a custom window. Of cause a dialogue box is also a window but the difference is, that the window procedure for a dialogue window is defined in Windows's itself. This window procedure will then call a dialogue procedure which we have to provide in our program in order to handle events.

A dialogue procedure gives you exactly the same parameters as a window procedure but the return value is here of type BOOL rather than of type LONG. Whereas in a window procedure we return zero (0L), if we have processed a message and call DefWindowProc if we didn't. In a dialogue procedure we return TRUE if we processed it and FALSE otherwise. Here is a general outline for a dialogue procedure:

BOOL FAR PASCAL DlgProc(HWND hDlg,UINT msg,WPARAM wParam,LPARAM lParam)
{
  switch (msg)
   {
     case WM_...
          /* process a message */
          break;
     default:
          /* let Windows handle the message */
          return FALSE;
   }
  return TRUE;
}

The most important messages to process for a dialogue procured are WM_INITDIALOG, WM_COMMAND and may be WM_DESTROY. WM_INITDIALOG is sent to the dialogue procedure instead of a WM_CREATE message (which is only sent to window procedures). It can be used to initialise dialogue controls and allocate required resources. Use WM_DESTROY to clean up afterwards if necessary. In a window procedure one of the most important messages to handle is the WM_PAINT message and you might wonder where that is here. Well, of course you get this message for a dialogue window well, but normally there is no reason to handle it.

6.4.1 The WM_INITDIALOG message

Let's now look at communication with controls and the initialisation of the dialogue with the WM_INITDIALOG message. In my example a good thing to start with, is to disable all controls that are useless at the beginning. These are the buttons for adding a name to the list and sorting the list. Note: All controls are enabled by default unless you specify otherwise in the dialogue template.

case WM_INITDIALOG:
     EnableWindow(GetDlgItem(hDlg,IDC_ADDNAME),FALSE);
     EnableWindow(GetDlgItem(hDlg,IDC_SORTLIST),FALSE);

Since EnableWindow requires a window handle we have to get the handle of the control first by calling GetDlgItem, which we give a handle to the dialogue window and the numeric control identifier. Set the second parameter of EnableWindow to TRUE if you want to enable the control or to FALSE to disable it. Disabled controls are displayed with grey text and are unavailable to the user. You can enable or disable all types of controls just like any other window.

Next we set the default options for the radio and check buttons. CheckRadioButton takes the identifier of the first and last radio button in the group and the one you want to set the check mark to. It is important that the identifiers for radio buttons all have consecutive numbers assigned to them i.e. if the numeric identifier for the first button is defined with a value of 100 then the control identifiers for the other radio buttons of that group have to be 101, 102, 103 and so on. CheckDlgButton is used for the check box.

     CheckRadioButton(hDlg,IDC_ASCENDING,IDC_DESCENDING,IDC_ASCENDING);
     CheckDlgButton(hDlg,IDC_CASEINSENSITIVE,TRUE);

And last not least, we set the maximum length of text the user can type into the edit control. There is no API function for that, but we can send a message and the message for this kind of thing is EM_LIMITTEXT. The maximum is then MAXNAMELEN which I've defined earlier on as 50 characters.

     SendDlgItemMessage(hDlg,IDC_NAME,EM_LIMITTEXT,MAXNAMELEN,0L);
     break;

Now that we've got that sorted, we can go on the WM_COMMAND message which.

6.4.1 the WM_COMMAND message

The WM_COMMAND is the heart of a dialogue box procedure since it handles both menu and control events. Unfortunately there is a slight difference here between Win16 and Win32. In Win16 where wParam is 16 bit and lParam 32 bit wide, wParam contains the identifier of the control and lParam contains both the handle of the control window (in the low order word) and a notification code (in the high order word). In Win32 wParam and lParam are 32 bit, wParam and contains both the control identifier (in the low order word) and a notification code (in the high order word) and lParam contains the handle to the control window only. Are you with me? Don't worry, here is how to keep everything nice, easy and independent:

First we define two macros, one for each API version:

#ifdef WIN32
  // Win32
  #define CTLID   LOWORD(wParam)       // Control ID for WM_COMMAND
  #define CTLMSG  HIWORD(wParam)       // Notification Message of Control
  #define HCTL   (HWND)lParam          // window handle of control

#else
  // Win16
  #define CTLID   wParam               // Control ID for WM_COMMAND
  #define CTLMSG  HIWORD(lParam)       // Notification Message of Control
  #define HCTL   (HWND)LOWORD(lParam)  // window handle of control

#endif

It is best to put that in your main header file, so it is available in every source code module. Then you can use it by processing the WM_COMMAND message in the following way:

  case WM_COMMAND:
       switch(CTLID)     // determine the control the message came from
        {
          case IDC_NAME: // Notification message from edit control
               if (CTLMSG==EN_UPDATE)
                 { 
                   ..... // something changed in the edit field
                 }
               break;
          case IDC_....  // add case statements for other controls
               ......    // and some code to handle the event
               break;
        }
       break;           // end of WM_COMMAND

The first thing I am doing in my example, is to check whether there is any text at all in the edit window called IDC_NAME and enable or disable the "Add name to list" push button accordingly. So we say:

          case IDC_NAME:
              if (CTLMSG==EN_UPDATE)
                { int len=SendDlgItemMessage(hDlg,IDC_NAME,EM_LINELENGTH,0,0L);
                  EnableWindow(GetDlgItem(hDlg,IDC_ADDNAME),len);
                }
              break;

This reads in natural language as follows: If text has been altered in the edit window [if (CTLMSG==EN_UPDATE)] then get the number of characters in the edit control [len=SendDlgItemMessage(hDlg,IDC_NAME,EM_LINELENGTH,0,0L)] and enable the button IDC_ADDNAME according to number of characters [EnableWindow(GetDlgItem(hDlg,IDC_ADDNAME),len)]. If you've got that, then you've understood the basic principle and the rest is just a variation.

The next thing the user is likely to do is to press the "Add name to list"-button or press Return, which is the same thing as this is our default push button. Normally the OK button is the default push button but we haven't go one here. You can make any button your default push button and in the resource editor you can specify which on it should be. For buttons we do not need to check the notification message since buttons have only one for the event that the button is pressed and hence CTLMSG will always be zero. What we need to do in this case now, is to get the text in the edit control and add it to the list of names. Therefore we allocate a buffer for the text first and fill it by calling GetDlgItemText, giving that the control identifier of the edit control, a pointer to the text buffer and the maximum number of characters we want to read.

         case IDC_ADDNAME:  // Button Add name was clicked
             { char szName[MAXNAMELEN];
               GetDlgItemText(hDlg,IDC_NAME,szName,MAXNAMELEN);

To add the text to the list, we send the message LB_ADDSTRING to the list control IDC_NAMELIST. To avoid compiler warnings we cast szName to the correct type.

        SendDlgItemMessage(hDlg,IDC_NAMELIST,LB_ADDSTRING,0,(LPARAM)(LPSTR)szName);

If you want to insert the name at a particular position then you can use LB_INSERTSTRING instead, giving it the list index where you want to insert it in the fourth parameter. Again there is lots of information about this in the SDK help file.

To make our program convenient to use, we clear the text in the edit control and set the input focus to it again so the user can type in another name immediately.

               SetDlgItemText(hDlg,IDC_NAME,NULL);
               SetFocus(GetDlgItem(hDlg,IDC_NAME));

Done that all that is left to do for this event is to enable the "Sort" button since there are now strings in the list box.

              EnableWindow(GetDlgItem(hDlg,IDC_SORTLIST),TRUE);
            }
            break; // end of IDC_ADDNAME

As it does not make sense sorting the list with only one item in, this could actually be improved by checking how many list items are in the list already. and enabling the button only if there is more than one. But you can add that easily yourself by sending a LB_GETCOUNT message to the list box and evaluating the return value.

The user can now type in names and add them to the list box. The next thing we've got to consider is the user pressing the "Sort" button to sort the list. This task is performed by the procedure SortList which is coded later on in the source file. But before we call this procedure we've got to get the sort options from the radio buttons and the checkbox control. Therefore we call IsDlgButtonChecked which will return TRUE if it is and FALSE if the button is not checked.

        case IDC_SORTLIST:
            { BOOL CaseInsensitive=IsDlgButtonChecked(hDlg,IDC_CASEINSENSITIVE);
              BOOL bDescendingOrder=IsDlgButtonChecked(hDlg,IDC_DESCENDING);
              SortList(hDlg,IDC_NAMELIST,bCaseInsensitive,bDescendingOrder);
            }
            break;

Now our application is fully operational. However there is one little serious problem left: the user won't be able to close the dialogue which will in this case also terminate the program. For that we have got the Quit command in the menu and in dialogues you will also get a IDCANCEL event though the WM_COMMAND if the user presses the escape key or closes the window with the close command in the system menu (in Windows95 there is also a close button in the title bar).

Let's do one after the other and deal with the Quit command in the menu first. As with control notifications we receive a WM_COMMAND message when the user selected the menu command. In this case we close the dialogue by calling EndDialog. The first parameter for this function is again the handle of the dialogue window and the second one is the return value for the calling function. You might have forgotten by now, but we called this dialogue in the WinMain with the DialogBox command. Since we do not evaluate the return value there we can return basically anything, but in an ordinary dialogue box with a OK and Cancel button you would return TRUE (or another positive value) if the user pressed OK and FALSE if he/ she cancelled. Anyway, this is what it looks like:

       case IDM_QUIT:
            EndDialog(hDlg,TRUE);
            break;

After you've called EndDialog you will receive some more messages like the WM_DESTROY for example. Use this to free resources like memory that you have allocated if any.

To deal with the other ways to close the dialogue mentioned above you need to add some code for IDCANCEL. In this case I ask the user whether he/ she really wants to close the program with a MessageBox. It will feature a little question mark icon (MB_ICONQUESTION) and a Yes and No button (MB_YESNO). If termination is confirmed then EndDialog is called.

case IDCANCEL:
    { int answer=MessageBox(hDlg,"Do you really want to quit?" ,"Confirm", MB_ICONQUESTION|MB_YESNO);
         if (answer==IDYES) EndDialog(hDlg,FALSE);
    }
    break;

 

6.5 Sorting the list

Finally let's have a brief look at the procedure used for sorting the list which uses a simple bubble sort algorithm. Again this is just to demonstrate how to communicate with controls and it is not a very efficient way of doing it. And after all in most cases there is not need to sort list boxes because you get the sort option for free if you specify this property for the list box.

Here's the procedure in which we first allocate a number of integers and two buffers to store both strings that we want to compare

void SortList(HWND hDlg,int idList,BOOL bCaseInsensitive,BOOL bDescendingOrder)
{ int  nItems,i,j;
  BOOL bSwap;
  char buffer1[MAXNAMELEN],buffer2[MAXNAMELEN];

Now we need to know how many items are in the list. Therefore we send a message LB_GETCOUNT message to the list box which will return the number of list items.

  nItems=SendDlgItemMessage(hDlg,idList,LB_GETCOUNT,0,0L);

In this case it is no problem if there are no entries in the list i.e. nItems is zero, since the following two for loops will handle it correctly. But how about other cases where you need a minimum of on list entry say. Well, normally it should not be necessary to check for that here. In this program for example we can be sure that there is at least one item in the list box otherwise the "Sort List" push button is disabled. This is an important matter! It is always better to prevent the user from making invalid input than to tell him/ her later with an error message that the input was invalid. And with windows you have the opportunity to do that easily. Take the editing of names as another example. Theoretically one could input an empty string to the list box, but since we only enable the "Add name to list"-button when at least one character is in the edit field, this can never happen. This rule applies not just to dialogue boxes. Menu command and toolbar buttons can and should be treated in the same manner.

Now we enter the first loop and copy the text of the first list item into our buffer. Again we send a message to the list box to do that. If you're not sure how long the string is and whether your buffer is big enough, you can send LB_GETTEXTLEN first and allocate sufficient space.

  for (i=nItems;i>0;i--)
   {
    SendDlgItemMessage(hDlg,idList,LB_GETTEXT,0,(LPARAM)(LPSTR)buffer1);

In the inner loop we get the next string in the list, compare the strings and if neccessary we swap them.

    for (j=1;j<i;j++)
       { // Get the text of the next item in the list
         SendDlgItemMessage(hDlg,idList,LB_GETTEXT,j,(LPARAM)(LPSTR)buffer2);
         // compare the two strings
         if (bCaseInsensitive) bSwap=(lstrcmpi(buffer1,buffer2)>0);
                          else bSwap=(strcmp(buffer1,buffer2)>0);
         if (bDescendingOrder) bSwap=!bSwap;
         // Swap the items if necessary
         if (bSwap)
           { // swap the strings
             SendDlgItemMessage(hDlg,idList,LB_DELETESTRING,j,0L);
             SendDlgItemMessage(hDlg,idList,LB_INSERTSTRING,j-1,(LPARAM)(LPSTR)buffer2);
           }
         else lstrcpy(buffer1,buffer2);
       }
   }
}

Unfortunately there is no message to set the text of a list box item. But what we can do is delete the current string by sending LB_DELETESTRING and then insert it again at the previous position with LB_INSERTSTRING.

 

7. How do I do...

Now this is the stage, where you should be able to stand on your own two feet, however weak they might still be. What you need now, is some guidelines on where to go, or better how to be able to go where you want to go. In this chapter I will give you some clues how to do certain things that are useful for many applications.

You don't have to read this chapter in the given order. Instead you can look up a topic you want further information about and try to implement it into one of the example programs.

 

Common Dialogues

Common Dialogues are a very important feature introduced with Windows 3.1 in order to standardise and simplify actions required most applications for both the users and the programmer. When Windows 3.0 started to get a really big success it soon became a problem that every application used a different methodology for similar things. The File Open dialogue is probably the best example for that. First it was not to easy to program although you'd need it in almost every program, and second users didn't find it too exciting facing a different interface every time. So Microsoft came up with the so called Common Dialogues which provide a consistent user interface for the most common tasks for every application. You can of course still do it all yourself, but you will find it a lot more convenient and easy to use them. The common dialogues reside in the dynamic-link library COMMDLG.DLL which provides the following functions:

ChooseColor Opens a dialogue in which the user can create and select a colour. You will find this dialogue coming up e.g. if you choose to customise your desktop colours in the control panel.

ChooseFont Allows to select a font and its type and size and colour properties. Optionally you get a preview of what a particular text with this font would look like.

FindText This is a modeless dialogue for searching text in a document. This will of course like all other common dialogues only provide a dialogue box not the actual search in your document.

ReplaceText The same as above but with the additional option to replace a particular text.

GetOpenFileName This is the nicest, quickest and most user friendly way to create a dialogue box that allows the user to select a file to open. There are many options and you can even use your own dialogue template, but the functionality of the dialougue like diplaying and changing directories and drives will be completely handled for you.

GetSaveFileName Ditto but for saving files.

PrintDlg A dialogue to set up printer properties

In order to common dialogues you first need to include the header file COMMDLG.H in your code file. You then call one of the above functions with the parameters described in the SDK help file. If you find the description given in there a bit too technical, here is a little example how to display a file open and file save dialogue.

First define the maximum length of the filename in your main header file:

#define MAXFILENAMELEN 80

Then define following function in one of your source modules. Change the member Flags as appropriate.

BOOL DlgGetFileName(HWND hWnd,LPSTR lpszTitle,LPSTR lpszFormat,LPSTR lpszFileName,BOOL save)
{  OPENFILENAME   of;
   int            result;
   // Initialize the OPENFILENAME members
   of.lStructSize       = sizeof(OPENFILENAME);
   of.Flags             = OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_SHOWHELP;
   of.hwndOwner         = hWnd;
   of.hInstance         = hInstance;
   of.lpstrFile         = lpszFileName;
   of.lpstrFilter       = lpszFormat;
   of.nMaxFile          = MAXFILENAMELEN;
   of.lpstrInitialDir   = NULL;
   of.lpstrTitle        = lpszTitle;
   of.lpTemplateName    = 0;
   of.lpfnHook          = NULL;
   // Display the dialog
   if (save) result=GetSaveFileName(&of);
        else result=GetOpenFileName(&of);
   return result;
}

In the module containing the window procedure of your main window you should then define a string containing the name and extension of your file format and allocate a buffer for the filename:

static const char szFileFormat[]={ "My file\0*.MYF\0" };
static char szFileName[MAXFILENAMELEN];

And finally you need to call the function in your window or dialogue procedure with somewhat like:

case WM_COMMAND:
    switch(wParam)
      {
       case IDM_OPEN:
            if (DlgGetFileName(hWnd,"Open a file",szFileFormat,szFileName,FALSE)) 
              { int hfile;
                hfile=_lopen(szFileName,OF_READ);
                ...
                ...
              }
            break;
       case IDM_SAVE:
            if (DlgGetFileName(hWnd,"Save a file",szFileFormat,szFileName,TRUE)) 
              { int hfile;
                hfile=_lcreat(szFileName,0);
                ...
                ...
              }
            break;

If you need to extend the functionality of your dialogue e.g. in order to offer a preview of the file you can do that by providing your own dialogue template and/ or specifying a hook procedure. Find information about this in the SDK help file.

 

Setting control colours and using 3D controls

When you implement your own dialogues you will find, that they still look somewhat different from all those fancy ones you see in other programs. What is missing is just a nice grey background and a nice 3D look of all the controls. Surely your application doesn't gain any more functionality with 3D of radio buttons, check boxes, edit controls and the lot, but if you want users to accept your program you'd better go for the fancy ones. Windows 95 already automatically provides a 3D look for controls, all that is missing here is a nice grey background color (the default as you can see is white).

Now if it just the background color that bothers you, then all you have to do is to process the WM_CTLCOLOR message. This message is sent from the control to the parent window (which is normally a dialogue) to allow different colours to be used. Unfortunately there is another difference between Win16 and Win32. Note, that it does not matter on which version of Windows you run the program only which you compile it for. In Win16 you only get the one message mentioned above and the lower word of lParam indicates what type of control sent the message. This can be a button, an edit control, a list box, a combo box, a static control or the dialogue itself. In Win32 there are six different messages, one for each control type.

This is how you'd do it for Win16:

case WM_CTLCOLOR:
     switch(HIWORD(lParam))
      {
         case CTLCOLOR_DLG:
         case CTLCOLOR_STATIC:
         case CTLCOLOR_BTN:
              SetBkColor((HDC)wParam,RGB(192,192,192));
              return GetStockObject(LTGRAY_BRUSH);
      }
     return FALSE; // use the defaults

and the equivialent for Win32:

case WM_CTLCOLORDLG:
case WM_CTLCOLORSTATIC:
case WM_CTLCOLORBTN:
     SetBkColor((HDC)wParam,RGB(192,192,192));
     return GetStockObject(LTGRAY_BRUSH);

In both cases you set the background color for text in the display context given in wParam using SetBkColor and return a handle to a brush. In this case I have obtained the handle to one of the predefined brushes from a call to GetStockObject. This function can also be used for pens and fonts as you can see when looking it up in the SDK help file. However if you want another color you have to create a brush using CreateSolidBrush which you best put in the WM_INITDIALOG. The handle you're receiving must be static declared as static and you can then return it on any call to WM_CTLCOLOR. Don't forget to destroy it in the WM_DESTROY using DeleteObject.

A disadvantage of this approach is, that you have to include it in every dialogue procedure and you won't get the nice 3D effect unless you're using Windows 95. Therefore you might rather go for using Microsoft's 3D control library. This library is called CTL3DV2.DLL and you'll probably have it already in your Windows system directory. If not, then you should have a look on your compiler CD or any other since it is installed by most programs and can be distributed royalty free. In order to use it, you have to add the file CTL3DV2.LIB to your project just like if you'd add another source code module. This will make the DLL load automatically when your program is started.

All you've got to do now, is to include the CTL3D.H file

#include <clt3d.h>

and add the following lines to the WinMain

  Ctl3dRegister(hInstance);
  Ctl3dAutoSubclass(hInstance);

This should be done prior to anything else. You then enter your dialogue box or message loop as usual and afterwards, just before you leave the WinMain you call

  Ctl3dUnregister(hInstance);

Now all your dialogues will have a grey background, nice looking dialogue boxes and 3D controls. If you are interested in more controls like tree views, multi column list boxes, spin controls etc. you might want to look at the COMMCTRL.DLL. Information about it can be found on the Microsoft Developer Network Library.

 

Customizing Dialogue Controls

In the dialogue example I have shown you how to use standard controls to input and output data. Standard controls like buttons and list boxes are handy because they make programming quick and easy. But how capable and flexible are they?

First of all, you can use standard controls not just in dialogue boxes but also in your "normal" windows. This for example creates a button inside the window specified in hWnd:

CreateWindow("BUTTON","&Delete",BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE, 10, 10, 50, 20, hWnd,
                       IDC_DELETE, hInstance, NULL);

Fair enough, but how about displaying other type of data like graphics, images or both mixed with text. And how about if you need a control with a different behaviour?

There are three techniques which all allow you to extend the functionality of your dialogues and controls:

Owner draw controls

Owner draw controls send you a WM_DRAWITEM message. The lParam of this messages contains a pointer to a DRAWITEMSTRUCT structure that contains the display context, the rectangle of the item in the display context and a pointer to the item data. Here is an example for an owner draw list box that displays a list of icons:

First create a list box inside your dialogue end set its properties of "owner draw" and "has strings". The resource statement of the list box in the .RC file should looks something like:

CONTROL "", IDC_ICONLIST, "LISTBOX", LBS_NOTIFY | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | WS_CHILD |
                WS_VISIBLE | WS_BORDER | WS_VSCROLL, 30, 127, 111, 55

Icons have a width and height of 32 pixels. In the WM_INITDIALOG message we first set the height of the list box items to 32:

SendDlgItemMessage(hDlg,IDC_ICONLIST,LB_SETITEMHEIGHT,0,MAKELPARAM(32,0));

Now you can add the items. Each requires text that we add with LB_ADDSTRING and and icon handle. This handle is set by sending a LB_SETITEMDATA message.

i=SendDlgItemMessage(hDlg,IDC_ICONLIST,LB_ADDSTRING,0,(LPARAM)(LPSTR)"myIcon");
SendDlgItemMessage(hDlg,IDC_ICONLIST,LB_SETITEMDATA,i,(LPARAM)hIcon));

Now you have to handle the WM_DRAWITEM message in the dialogue procedure:

case WM_DRAWITEM:
     { LPDRAWITEMSTRUCT lpdis=(LPDRAWITEMSTRUCT)lParam;
       HBRUSH  hOrgBrush;
       int     iBkColor,iTxtColor;
       RECT    rcText;
       char    szText[50];
       if (((int)lpdis->itemID)<0) break;
       // Determine the colors
       if (lpdis->itemState & ODS_SELECTED) 
         { iBkColor=COLOR_HIGHLIGHT;iTxtColor=COLOR_HIGHLIGHTTEXT; }
       else
         { iBkColor=COLOR_WINDOW;iTxtColor=COLOR_WINDOWTEXT; }
       // Erase the background
       hOrgBrush=SelectObject(lpdis->hDC,CreateSolidBrush(GetSysColor(iBkColor)));
       PatBlt(lpdis->hDC,1,lpdis->rcItem.top,lpdis->rcItem.right-2,lpdis->rcItem.bottom-lpdis->rcItem.top,PATCOPY);
       DeleteObject(SelectObject(lpdis->hDC,hOrgBrush));
       // Draw the icon
       DrawIcon(lpdis->hDC,1,lpdis->rcItem.top,(HICON)lpdis->itemData);
       // Draw the text
       rcText=lpdis->rcItem;
       rcText.left=36;
       SendMessage(lpdis->hwndItem,LB_GETTEXT,lpdis->itemID,(LPARAM)(LPSTR)szText);
       SetBkMode(lpdis->hDC,TRANSPARENT);
       SetTextColor(lpdis->hDC,GetSysColor(iTxtColor));
       DrawText(lpdis->hDC,szText,lstrlen(szText),&rcText,DT_SINGLELINE|DT_VCENTER);
       // Draw the Focus rectangle
       if (lpdis->itemState & ODS_FOCUS)
           DrawFocusRect(lpdis->hDC,&lpdis->rcItem);
     }
     break;

Subclassing a control

To understand what subclassing can do, consider a list box that outputs a list of tasks that can only be processed in a sequential order. You indicate the current task by highlighting the corresponding list item (using LB_SETCURSEL). What you now don't want is that the user can select any other item. To prevent the user from doing this, you can disable the list box, but unfortunately this will also disable the list box's scrollbar and all items are greyed.

A solution for this is to subclass the window procedure for the list box and filter out all mouse and keyboard messages. All other messages must be processed as normal so we need to pass them on to the original list box procedure.

static FARPROC lpOrgListboxProc;

LRESULT FAR PASCAL ListSubclassProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
  switch(msg)
   {
     case WM_LBUTTONDOWN:
     case WM_MOUSEMOVE:
     case WM_LBUTTONUP:
     case WM_CHAR:
          // do nothing
          break;
     default:
          // process as normal
          return CallWindowProc(lpOrgListboxProc,hWnd,msg,wParam,lParam);
   }
return 0L;
}

Now you can subclass the control in the WM_INITDIALOG procedure by storing the original window procedure in lpOrgListboxProc and setting the window procedure for the list box control to your own procedure.

lpOrgListboxProc=(FARPROC)GetWindowLong(GetDlgItem(hWnd,IDC_TASKLIST),GWL_WNDPROC);
SetWindowLong(GetDlgItem(hWnd,IDC_TASKLIST),GWL_WNDPROC,ListSubclassProc);

 

Bitmaps

Bitmaps are images consisting of n times m pixels (picture elements). You can create and modify bitmaps (stored in .BMP files, which is the standard bitmap format for Windows) with Paintbrush, MS Paint or any other pixel orientated drawing program. The easiest way to use a bitmap image in your application is to include it as a resource. The resource file code for a bitmap is:

ID_IMAGE BITMAP "myimage.bmp"

Where ID_IMAGE it the name or numeric identifier of the bitmap and "myimage.bmp" is the name of the file containing the bitmap.You can then load it in your program with the command LoadBitmap which gives you a handle to the bitmap (type HBITMAP). e.g.

hImage=LoadBitmap(hInstance,MAKEINTRESOURCE(ID_IMAGE));

The only problem you have now, is to bring the bitmap on the screen. This requires a little bit of preparation. First you need a memory display context. Let me just briefly explain what that is. A memory display context is basically the same as a window but instead of mapping output to the screen all drawing goes to memory. Where the output area of a "normal" display context is the client area of a window which is constrained by the window size and the colour depth (bits required to display the maximum number of colours) of the current display mode, the output area of a memory display context is constrained by the width, height and colour depth of the bitmap selected into it. Therefore a memory display context needs a bitmap just as a window needs the screen.

The reason why we need a memory display context is that we cannot select a bitmap directly into a window display context, but we can copy data from one display context into another, provided that they are compatible. The following code creates a memory display context that is compatible with the display context of the screen:

hMemDC=CreateCompatibleDC(NULL);

The value of NULL always creates a display context that is compatible with the screen. Optionally you can give it the handle of your window's display context, but it won't make any difference. If you use bitmaps more than once I recommend that you create a memory display context at the beginning in the WinMain and make the handle global. It is always useful to have a memory display context around.

Next let's see what we can do with that. Suppose your bitmap is 300 times 200 pixels big and you want to display it at position 0,0 of your window then your WM_PAINT message handling could look somewhat like this:

case WM_PAINT:
     { PAINTSTRUCT ps;
       HBITMAP hOrgBitmap;
       HDC hdc;
       hdc=BeginPaint(hWnd,&ps);
        hOrgBitmap=SelectObject(hdc,hImage);
        BitBlt(hdc,0,0,300,200,hMemDC,0,0,SRCCOPY);
        SelectObject(hdc,hOrgBitmap);
       EndPaint(hWnd,&ps);
     }
     break;

The function which does the job is BitBlt which stands for Bit Block Transfer. It copies an area from a source display context, which is the memory display context in this case, into a destination display context, which is our window in this case. This works also the other way round of course and you can also copy from one memory context into another or from window context into another. Before you use a BitBlt with a memory display context however, you have to select a bitmap into it, which you do with SelectObject. As with all other GDI objects (pens, brushes, fonts and palettes) you have to remember the original object and restore the display context if you do not need it any more.

Instead of BitBlt you can also use StretchBlt which stretches or compresses the bitmap from its original size into the destination rectangle.

There is now only one thing to remember: Cleaning up afterwards. We have allocated two resources, a bitmap and a display context, which you have to delete before terminating the program. Therefore we use

DeleteObject(hImage);

to delete the bitmap and

DeleteDC(hMemDC);

to delete the memory display context.

This was just scratching the surface of bitmaps. There is so much to say and know about bitmaps that I could easily write another tutorial of this size just about it. The problems begin, when you start dealing with Device Independent Bitmaps (DIBs) and Color Palettes. But if you need to know more about that, you really need a book.

 

Printing

Have you ever tried to print out a nice graphic (with your own program of course) in a DOS or UNIX program? It not, then I can tell you that it takes ages (and a lot of reading in the printer manual) to get a reasonable result on your own printer and is virtually impossible to get it right for every printer. If you aim for WYSIWYG (What You See Is What You Get) or even just a proper print preview, you just have to forget it.

Is Windows any better than? Well it is, and not just a bit. Printing anything under Windows is really trivial and you can not just use almost any font, font size and font style you like but also print out all kind of graphics including bitmaps without any difficulties.

Suppose you've got a function called PaintAppWindow which you normally call in the WM_PAINT branch of your window procedure like:

case WM_PAINT:
     { PAINTSTRUCT ps;
       HDC hdc;
       hdc=BeginPaint(hWnd,&ps);
        PaintAppWindow(hdc);
       EndPaint(hWnd,&ps);
     }
     break;

No matter what this function does, you can have exactly the same output on your printer just by calling it with a printer display context instead of a screen display context. Hence the only thing you need to do is something like the following function, which reads the name, driver file name and port number of the current printer (the one you've specified as the standard printer in the control panel) and then creates a display context for that printer:

PrintWindow(void)
{ char sDevice[160];
  char sName[64],sDriver[80],sPort[16];
  HDC hPrinterDC;
  GetProfileString("WINDOWS", "DEVICE", "", sDevice,sizeof(sDevice));
  sscanf(sDevice, "%64[^,],%80[^,],%16[^,]", sName, sDriver, sPort);
  hPrinterDC=CreateDC(sDriver,sName,sPort,NULL);
PaintAppWindow(hPrinterDC);
  DeleteDC(hPrinterDC);
}

Not too bad, is it? OK, may be I have exaggerated a bit. This works, but in reality you'll have to do a bit more to get a WYSIWYG result. For example to get the equivalent font size or a font you're using for the screen you have to do the following:

int GetFontSize(HDC hPrinterDC,int nScreenHeight)
{ POINT pt;
  pt.x=0;
  pt.y=-MulDiv(nScreenHeight,GetDeviceCaps(hPrinterDC,LOGPIXELSY),72);
  DPtoLP(hPrinterDC,&pt,1); 
  return pt.y;
}

Use the return value now to fill the lfHeight property of a LOGFONT structure and create a font for printing using CreateFontIndirect, select it into your display context using SelectObject and off you go.

To retrieve the properties of the printer display context you can call GetDeviceCaps as in the example above. To determine the width and height of a printer page in pixels for example you can call GetDeviceCaps with the handle of the printer display context and either HORZSIZE or VERTSIZE.

Finally you may want to provide a printer configuration dialogue with which the user can set up the printer. Just make use of the common dialogues for that and call PrintDlg.

 

Sound

To play sound you can easily make use of the Multimedia Control Interface (MCI). Information about this can be found in the Multimedia Reference help file (WIN31MWH.HLP). The easiest way is to call SndPlaySound to play back a .WAV file (digital sound). This is how you do it:

#include <mmsystem.h>
....
sndPlaySound("C:\\WINDOWS\\DING.WAV",SND_SYNC);
....

Midi files (extension .MID) are a bit more difficult to play back. Basically what you have to do is to call mciSendCommand to open a MCI device and then send commands to this device to play them back. There is lots more you can do with mciSendCommand like recording and playing back WAV files as well.

 

Getting more information

Windows programming is a never-ending field of learning you'll probably never be able to know everything about it (I would not dare to say I do). Now getting a book is not a bad idea, but to get all the information you could possibly want you'd need more of a library than a single book. Most books I have ever seen therefore cover either the basics of Windows programming or a very special topic. And every few month there are some new Software Development Kits coming out which allow you to do even more with Windows. Take the Video for Windows Development Kit (VFWDK) for example. With around 12 MB of hard disk space it is certainly one of the smaller SDKs but it gives you all the tools, include files, libraries and sample application to create, manipulate and play back AVI files(Audio Video Interleaved, the Microsoft format for Video files).

Fortunately there is something that covers everything, and it is even small enough to fit in your room: Microsoft System Development Library. This is a CD full of information about everything you ever wanted to know about Windows including OLE, Visual Basic and Visual Basic for Applications Programming, VBX and OCX Development and all features of Windows 95 and Windows NT. It also contains the complete book "Programming Windows 3.1" by Charles Petzold and all articles ever published in the Microsoft System Journal, a monthly developer magazine, which is also very good source of information. And if you're really interested in what's brewing, you can pop in at Dr. GUI's Espresso Stand which is virtually on the CD (but remember he's not a real doctor!).

To obtain this CD which is updated every three months you've got to subscribe to the Microsoft Developer Network (MSDN). You will then not just get this CD but also all SDK and operating system software you need. To get information about the MSDN write to or e-mail the:

Microsoft Developer Network

One Microsoft Way

Redmond, WA 98052-6399

USA

Fax: (206) 936-7329, Attn: Developer Network

Internet: msdn@microsoft.com