How do I? - Part 16

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 all. I'm sorry I missed last month; I was just too busy with other stuff. Some of the more exciting stuff was for the Intranet project I'm working on. Right now I've got one of my graduates working on a virtual conferencing project. In the sideline of this project we've looked at some very nice programs. I was very impressed by the O'Reilly webboard program -- a great program. (I was equally impressed with their web site software). It's really a pity that people like this won't develop for the OS/2 platform. Yesterday some Connectix cameras were brought in and I had the chance to toy with the CuSeeMe/2 Beta 1.6. This was another occasion I could show my colleagues that OS/2 matches anything Windows has to offer. What I'm lacking is the Netscape Communicator for OS/2; thank god something like WarpZilla's is going on. I strongly believe in Web applications. So Java and Browsers are the future. This means a strong browser for OS/2 might be critical.

Enough of this. What are we going to look at this month? We will continue to enhance the first version of the calculator and add some functions. The additions are rather small, but the discussion about them will be extensive. So I guess the learning takes precedence over new features right now.

Ok, let's enhance the calculator some more. I got an e-mail from Cindy, who pointed out a way to simplify some of my code. In the last column I described a way to strip the trailing zeros and dots from a calculated result. I used the following code:

sprintf(achOutput, "%f", flLeftMember);

while (achOutput[strlen(achOutput)-1] == '0')   // Strip zero's
       achOutput[strlen(achOutput)-1] = 0;
if (achOutput[strlen(achOutput)-1] == '.')      // Strip dot
    achOutput[strlen(achOutput)-1] = 0;

Cindy pointed out that this can be replaced with the following line of code:

sprintf(achOutput, "%g", flLeftMember);

This approach not only makes the code a lot smaller, but also handles the display of 'e...' power formatted numbers. Thanks, Cindy, for this improvement; I'd still been wondering about a way to solve this.

I found myself using this calculator more and more for simple calculations. While using it, I found some things annoying. You guessed it -- we're going to take care of these things, hopefully learning something in the process.

One of the things I didn't like was that the backspace can't be used when a wrong number is entered. Just like on a normal calculator, I have to use the clear button. However, this erases the whole entry, not just the wrong number -- this fact is the downside of the approach we took by using the Mnemonic capabilities of the dialog. The entryfield is read-only, so it won't handle the backspace for us. This means we've got to design a way to handle the event of a user pressing the backspace key.

Before we delve into that, how do we know the user pressed a key, or more specifically, the backspace key? Of course OS/2 has a message for that; the WM_CHAR message. This message is sent to the active window every time a user presses a key. The message parameters of the WM_CHAR message contain the character information we look for, but this is the point where things get tricky. The message parameters are filled as following:

mp1	31------24 The last byte (bits 31-24) contains the hardware scan code
	23------16 This byte contains the repeat count
	15------00 This first short contains flags. (We discuss them later)

mp2 The upper short contains the virtual key code
    The lower short contains the character code (16 bit)

I guess this needs some explanation. There are three keyboard codes available in the message parameters, so we'll take a short tour. From the mp1 parameter: the hardware scan code. This code is generated by your PC. You can be fairly sure this code will be different on non-PC machines, so I wouldn't recommend this value. If you do, this probably means redoing your program for every machine that will run OS/2.

The virtual key code is contained in the upper short of mp2. Virtual keys are those like the function keys, delete, backspace, etc. -- basically, all the non-character keys except the <ALT>, <SHIFT>; and <CTRL> keys. We'll take a look at virtual codes in a minute, since the backspace is one of these.

The last code is the character code contained in the lower short of mp2. This code contains the ASCII codes of the key pressed, so it will be sensitive to <SHIFT> and <CTRL> keys. (Although the <CTRL> is a special case.)

What we need is the backspace. How do we know it's pressed? Well, I stated that backspace is a virtual key. There are several ways to accomplish this. Of course we can dissect the message parameters, but there's a more convenient way. The OS/2 API contains a macro to handle the WM_CHAR message. (Using it is a little awkward) For example if I want the fs flag from mp1, I have to use the following:

CHARMSG(&msg)->fs

This can also be compared with another value, so if we want to check to see if a virtual key is pressed, it can be done with:

CHARMSG(&msg)->fs & KC_VIRTUALKEY

(We won't delve into the character flags right now.) In the same fashion a virtual key code can be extracted. Every virtual key in OS/2 has a defined value. These values are saved in the VK_ flags. For example VK_F1 is function key 1, VK_LEFT is the left cursor and VK_BACKSPACE is the backspace key. (All these values can be found in the OS/2 API description.) Knowing this makes processing the backspace easy -- we simply check to see if the vkey is equal to VK_BACKSPACE. This produces the following if statement to start the WM_CHAR handler with:

if (CHARMSG(&msg)->vkey == VK_BACKSPACE)

Note that handling keyboard events is an art in itself. We only scratched the surface of what's possible here.

Now that we know how to detect the backspace, let's build the event handler. What we want is to cut the last character of the string contained in the entryfield. So the first thing to be done is to query this string. The simplest way to proceed is to place the NULL character in the second-last place of the string. This will shift the end of the string one character, since a string is terminated by a NULL in C. To insert this NULL, we need to know the length of the string. We also can only process the string if it has nonzero length, so in this respect we need the string length too. This gives us the following code:

//-----------------------------------------------------------------------
// Handle the backspace char.
//-----------------------------------------------------------------------
case WM_CHAR:
	{
	if (CHARMSG(&msg)->vkey == VK_BACKSPACE)
		{
		char achValue[32];
		long ValueLength;

		WinQueryDlgItemText(hwndDlg , ENTRYFIELD1, 32, achValue);
		ValueLength = strlen(achValue);

		if (ValueLength)
			{
			achValue[ValueLength-1] = 0;
			WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achValue);
			}

		}
	}
break;

The second thing I wanted to change is the handling of the tallyroll; an option to be able to clear it. Besides that, I wanted to be able to click on a number in the tally and thereby transport it to the number entryfield. Both additions proved to be fairly simple to implement.

First the clearing of the tallyroll. What we want is a menu item, that when selected will discard all entries in the tally. We need to expand the menu first. I added an edit menu and in the process of doing so I also added an 'about' box with an accompanying menu option. This means a new event handler has to be created for the event that the user selects in the edit menu.

Now how can we clear a listbox? I usually browse through the control's messages when searching for something like this. It's such an obvious action that there has to be a message which handles it. There is -- the LM_DELETEALL message. So the only thing we have to do in clearing the tallyroll is send this message to the listbox:

//-------------------------------------------------------------
// Clear the Tally roll
//-------------------------------------------------------------
case IDM_CLEARTALLY:
	{
	WinSendDlgItemMsg(hwndDlg, LISTBOX1, LM_DELETEALL, 0L, 0L);
	}
return(0);

Now for the click and display trick. I want to be able to click on a number in the tallyroll. If we do, it must be transported to the entryfield. In doing so, it'll also delete the current entry. Because this action has a destructive effect on the already-entered number, we must take the approach that this can't be done accidentally, so a single click on a number won't do. We need at least a double click (this can hardly be done accidentally). First thing is to find a way to detect a double click on the listbox.

A few columns back I talked about the WM_CONTROL message. The WM_CONTROL message will notify a control of events happening on it. A double click is a typical event happening on a control, in this case the listbox. When browsing through the listbox notification, we'll find the LN_ENTER as the notification we want. Since there is only one listbox in our program, we can act directly on the LN_ENTER notification. Since we only handle a notification for the listbox on WM_CONTROL we only need an 'if' statement.

if (SHORT2FROMMP(mp1) == LN_ENTER)

Inside this we'll have to handle the click. First we need the contents of the listbox item selected; if we want the contents of a listbox line, we'll first need the index number of that line. For obtaining this index there is a convenient macro available, the WinQueryLboxSelectedItem macro. After that we can query the contents. The following code will do the trick:

hwndLbox  = WinWindowFromID(hwndDlg, LISTBOX1);
lSelected = WinQueryLboxSelectedItem(hwndLbox);
WinQueryLboxItemText(hwndLbox, lSelected, achListValue, 32);

Now that we've got the contents of a line, we need to think up a way to process them. Not all the lines can be taken up and placed in the entryfield; some lines only contain an action or equal sign. Therefore, we've got to check the contents if they have a length of 1. If this is the case and the line only contains an action, it can't be placed in the entryfield. Knowing this we can compose the code:

case WM_CONTROL:
	{
	if (SHORT2FROMMP(mp1) == LN_ENTER)
		{
		//-------------------------------------------------------------
		// Query selected listbox entry
		//-------------------------------------------------------------
		char achListValue[32];
		HWND hwndLbox;
		long lSelected;

		hwndLbox  = WinWindowFromID(hwndDlg, LISTBOX1);
		lSelected = WinQueryLboxSelectedItem(hwndLbox);
		WinQueryLboxItemText(hwndLbox, lSelected, achListValue, 32);
		//-------------------------------------------------------------
		// Check if entry is a number
		//-------------------------------------------------------------
		if (strlen(achListValue) !=1 ||
			(achListValue[0] != '=' && achListValue[0] != '-' &&
			 achListValue[0] != ' ' && achListValue[0] != '*' &&
			 achListValue[0] != '/'))
			//--------------------------------------------------------
			// Place selected item in the listbox
			//--------------------------------------------------------
			WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achListValue);
		}
	}
break;

So, that's it for this month. Next month we'll take a look at small code. One of the things I dislike about modern software packages is that they are enormously bulky. In my humble opinion, this size can be avoided in most cases. There are some simple tricks to avoid programs getting too big.

'til next month.

Source for this month's column: sample16.zip (24K)