How do I? - Part 9

From EDM2
Jump to: navigation, search
How Do I? / Part
1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19

by Eric Slaats

Hi, and welcome to our next episode on dialogs. Last month we had a small introduction on dialogs in which some essential concepts were looked at. The most important were the concepts of "parent" and "owner" windows and the concept of "modeless" and "modal" dialogs. We also took a look at a special OS/2 API which enabled us to quickly produce a simple dialog in which the user can receive a message and give a simple reply.

This month, on the brink of my summer-holiday, we delve deeper into the way dialogs are made. We will take a look at resources and what they have to do with dialogs. I will discuss how to handle Modeless and Modal dialogs (again <g>) and how to get some interface functionality from dialogs without programming by introducing the "group" concept.

Remember, some columns back (in the Dec. '96 issue) when I put down my philosophy on how programs should be made? Be lazy (don't do what can be done for you) and KISS (Keep It Simple Stupid). In that column we had our first taste of resources. Also in that December issue, a resource script for a menu was created. This was done by writing a text file, but it sure was a lot easier than having to program every menu option with an API call or by letting your program send messages to the menu window. We also gave a simple definition of resource-files. Here's a recap:

In a resource file (.RC) we can describe controls with a scripting kind of language. This isn't exactly a programming language, but more a language that is used to describe the way things look. Some examples of resources that can be defined in resource files are:

  • Menus
  • Accelerator tables
  • Dialog and window templates
  • Icons
  • Fonts
  • Bit maps
  • Strings

In most cases, we don't have to bother to write the resource scripts ourselves. With most compilers, as well as with the OS/2 toolkit, come dedicated resource editors. These tools offer a graphical environment in which you can paint the resources you'd like to use. (See also the description of IBM VisualAge C++, Borland C++ and Watcom C/C++).

Image 1: Dialog Editor
Image 2: Borland Resource Workshop

We won't look at the scripting language for dialogs in depth just now. (Although a deeper understanding of this language comes in handy every once in a while.) I will just assume that everyone can get hold of a visual dialog editor. Almost every compiler for OS/2 has such a tool. If you use a public domain compiler, there is always dlgedit.exe (Image 1) from IBM that comes with the developer's toolkit. Personally, I prefer the BCC 2.0 for OS/2 resource workshop (Image 2). But there are also other excellent products on the market. Prominare Designer is one of them (this is actually a LOT more than a dialog editor).

How does a dialog editor work? Well if you have ever worked with Delphi, Visual Cafe or other visual tools, you won't have any problems with the way dialog editors work. You can drag various controls like notebooks, entry fields, spin buttons, etc. to the dialog window. Here you can resize them and change their attributes like the colour and font used. Now, this is the simple part. Probably anyone can learn dropping controls on a window in a matter of minutes.

Now is a good time to start the sample application for this month (ZIP, 16.7k). When you start it, you'll notice it has two menu options. We come to those in a minute. For now, click one of the menu options and this dialog will appear.

Image 3: Dialog

Now try the following:

  • Select radio button 1 and use the cursor keys
  • Select radio button A and use the cursor keys
  • Select one of the entry fields and use the cursor keys
  • Select the X-button and use the cursor keys

The effect you should experience is caused by grouping the controls in the dialog. By using the cursor keys, you can select one of the controls in the dialog (give it the focus). This is a very common way to handle radio buttons, but is almost never used when the dialog contains entry fields. Especially with entry field controls, I find the grouping effect very convenient. You can walk through the entry fields by simply using the cursor keys in a natural, intuitive way. It's unbelievable that this feature is so infrequently used!

This grouping effect can be achieved with most controls in the OS/2 palette. There are, of course, controls that react to the cursor keys themselves; for these controls you can always use the Tab key to select the next control. The dialog in this month's sample demonstrates this effect for buttons, radio buttons and entry fields.

Now for the good part. To achieve these effects, no programming is needed. In fact, we won't write a single line of dialog code this month.

Now, let's take a look at the .RC (resource file) in this month's sample. I've arranged the groups of controls in this file with one line of spacing. It's as simple as this. The resource compiler will put the controls in the dialog in the order they are put in the RC file. This will also dictate the order in which the controls are selected when the tab key is used to select the next control in a dialog. So the first thing you've got to do after you've put all the controls in the dialog window is to make sure the controls are in a logical sequence. Almost every resource editor I've ever seen has a method for ordering these controls. Here's a screen shot of the BCC 2.0 resource editor (Image 4) when it's in ordering mode.

Image 4: Ordering Screenshot

By simply ordering the controls, we can't achieve the grouping effect. Normally, every control would have been handled as a group of its own. This means the effect that we achieve in this month's example would not have been achieved.

Now let's take another look at the .RC file. You can see that some of the lines feature a WS_GROUP attribute. If a line contains the attribute, it marks the beginning of a group. The controls that are added to the group are all the controls after the control with the WS_GROUP attribute that don't have this attribute. You can look at the RC file in the example to see this effect. To make things a little clearer, here's another simple example.

Say you would like to build a simple dialog box with three buttons and four entry fields. The buttons must form a group as well as the entry fields. This means the RC script must have the following structure:

DLGTEMPLATE DIALOG 
BEGIN
	CONTROL "", ENTRYFIELD1 ....................  | WS_GROUP
	CONTROL "", ENTRYFIELD2 ....................  
	CONTROL "", ENTRYFIELD3 ....................  
	CONTROL "", ENTRYFIELD4 ....................  

	PUSHBUTTON "A",.............................  | WS_GROUP
	PUSHBUTTON "B",.............................
	PUSHBUTTON "C",.............................
END

If you don't want to use the group effect, simply give all the controls a WS_GROUP (done automatically by most resource editors).

Now that we can create resources, take a look at the API that is used to activate dialogs that reside in a resource file. As you may have guessed, there are two API's available to start a dialog. One for a Modal dialog and one for a Modeless dialog.

With the WinDlgBox API we can start a modal dialog. The syntax for this API looks like this:

HWND	hwndParent;     //  Parent-window handle.
HWND	hwndOwner;      //  Owner-window handle.
PFNWP	pfnDlgProc;     //  Dialog procedure.
HMODULE	hmod;           //  Where is the resource.
ULONG	idDlg;          //  Dialog-template id in the resource file.
PVOID	pCreateParams;  //  Pointer to application-defined data area.
ULONG	ulResult;       //  Reply value.

ulResult = WinDlgBox(hwndParent, 
                     hwndOwner,
                     pfnDlgProc, 
                     hmod, 
                     idDlg, 
                     pCreateParams);

How does this API work? Let's go through all the parameters one by one. First, the return value ulResult. This isn't too exciting. It's a value describing if the system was able to create the dialog or not. It will return a system constant DID_ERROR if something went wrong. The next two parameters speak for themselves, the parent and the owner window handles. (If you're unsure what the effect of the parent and owner was, check last month's How Do I? column.)

The next: pfnDlgProc. Well, this one needs a little explanation. Dialogs are handled by a sort of window procedure as well as the main window of an application (see the Oct. 96 issue of How Do I? for an explanation of the window procedure). In this parameter we should mention the name of the window procedure that handles the dialog (we'll check into that somewhere in a future column). For now we use the default window procedure to handle the messages sent to the dialog.

So a dialog has its own window procedure, should this mean it can also use the default window procedure WinDefWindowProc? NO! This is one of the most common errors made by programmers and can lead to erratic and uncontrolled behaviour. Dialogs have a special default procedure. This procedure is called WinDefDlgProc. If we use this name for pfnDlgProc, the dialog will show and react to our actions. However, it won't do a specific task. For that we've got to program.

Next: hmod. This is a handle to a resource file. It's possible to store resources in DLL files. This can be very convenient when you wish to release a multi-language package. Simply make more RC files, one for each language, and compile them into DLL's. Pick the right DLL at install time and you can set up an application in any language you like. For now we won't delve into this great possibility. We will simply link the resources to the .exe file. (Normally it's done like this.) If the resources reside in the exe file, hmod should be filled with a NULLHANDLE.

Then: dDlg. This is the resource ID of the dialog we want to display. It's simply the number we gave the dialog when creating it. This is normally dome by a predefined constant in a header file. In our case this is the DIALOG1 constant.

Finally, we have pCreateParams. It's possible to pass a pointer to the dialog to provide it "from" information. In our case, this isn't necessary, so we fill in a 0. When we talk again about the dialog window procedure, this value will reappear.

Knowing all this we can create the call needed to display a modal version of the DIALOG1 dialog.

WinDlgBox(HWND_DESKTOP,        // Parent
		 hwnd,        	// Owner
		 WinDefDlgProc,	// Dialog window procedure
		 NULLHANDLE,	// Where is the dialog resource?
		 DIALOG1,	// Dialog Resource ID
		 0);		// Creation parameters (used in WM_INITDLG)

The API call to create a modeless dialog is very similar. Even the parameters are the same. There is one very important difference though: the return value of this call is the window handle of the dialog. We will see where this comes in handy in moment. The call is called WinLoadDlg.

Last month I said that the possibility of an application to create multiple modeless dialogs of the same dialog can be seen as an error when this effect isn't explicitly wanted. I will discuss a simple trick to prevent this behaviour. When I tried to handle this situation for Smalled, I found some things that weren't as straightforward as I thought they would be. Let's first define the problem we want to handle. It breaks up into two different parts:

  1. A modeless dialog can only exist once. This means that activating the menu item that will create the dialog must not create a new dialog.
  2. If the modeless dialog already exists, it should be made active when the menu item that normally creates the dialog is activated.

To handle the first part of this little problem, if the menu option that creates the dialog is activated, it should first check if the dialog already exists. For this, the minimum we need is the window handle. Besides that we need to remember it the next time this part of the window procedure is called. For this purpose we'll create a static variable. This variable will be a sort of global var, but it only has a local scope.

The next thing that we've got to decide is if we need to create the dialog. We can do this by checking if the dialog is showing. It took me quite a long time to figure this out. It would be more logical to use WinIsWindow function, but strangely enough, this function still returns a TRUE for the hwndDlg after the dialog is closed! So I started to look for a function that could be used instead. I decided on WinIsWindowShowing. This function will return a TRUE when any part of the window is showing.

So the first thing to do is to make sure the dialog is showing! We can do this by calling WinSetActiveWindow. This will make the dialog visible if it exists. After that, we can call WinIsWindowShowing function. If the dialog exists, this will return a TRUE. This gives the following code:

static HWND hwndDlg = 0;

WinSetActiveWindow(HWND_DESKTOP,hwndDlg);
if (!WinIsWindowShowing(hwndDlg))
	{
	// Create dialog
	}

This simple piece of code will take care of business for the first part of our problem. The nice thing is that the second part of the problem is also fixed! The WinSetActiveWindow takes care of that. So the complete code looks like this:

case IDM_DIALOG2:
	{
	static HWND hwndDlg = 0;

	WinSetActiveWindow(HWND_DESKTOP,hwndDlg);
	if (!WinIsWindowShowing(hwndDlg))
		{
		hwndDlg = WinLoadDlg(HWND_DESKTOP, // Parent
			    hwnd,        	   // Owner
			    WinDefDlgProc,	// Dialog window procedure
			    NULLHANDLE,		// Where is the dialog resource?
                           DIALOG1,		// Dialog Resource ID
			    0);			// Creation parameters (used in WM_INITDLG)
		}
	}
break;

Why does this work? Well, we are sure of the fact that every window in the system will have a unique HWND at every moment. So we always know that we are querying the right window.

Well, that's it for this month, I hope you enjoyed it. Next month we will delve deeper into the treasures of dialogs. Next month we will take a look at how the Dialog procedure should look.