How do I? - Part 14

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. It's been two months. I was so busy the last months of the year that I couldn't possibly make the December deadline (sorry). However, I had a great holiday period and I'm back again refreshed. I got involved in a really exciting project. When things evolve I let you all know more. Besides kicking off from working, I managed to release Smalled 2.00 just after New Year's. I'm rather pleased with this version. It contains some code that took a long time to develop and needed some experimentation. But then again it always feels good when you pull off something like that.

What are we going to do this month? I guess I wrote enough stuff last year to make the regular reader a little more comfortable in doing something more worthwhile. We're going to make the first application! (ZIP, 22K) Yep, in this article and some of the next months' articles we're doing a working application. After this month we'll have the basics. These basics will be working and functional though.

I received a number of suggestions about applications (thanks). I'd like to use a number of them in the future, but for now, we're going for the easy controls. This month we'll start to build a simple calculator. (I miss that thing from OS/2 2.11.) You'll find that it's easy to build. It basically contains two types of controls, an Entryfield and a load of buttons. With an eye on next month's events we'll also include an MLE box. The whole will look something like this:

Calculator Screenshot

After the previous lessons this should be easy enough to create. A calculator has a few nasty things that can give a programmer a splitting headache when dealing with it. A number of them can be solved by being Smart and Lazy (remember KISS?). We'll just let the dialog do a lot of the work for us.

1) If we let the user simply type in the numbers he wants into the Entryfield, we've got to build all sorts of code in the WM_CONTROL message area. This to prevent him from entering letters or other unwanted characters. Well, how do we fix that? We simply won't allow the user to use the entry field. Make it read-only in the dialog editor. (Use the ES_READONLY style).

2) So that was easy enough. But how is the user going to enter his data into the calculator? Also simple, use the buttons on the calculator for that. Hmmmm, not very satisfying. I like to be able to use the keyboard for entering data and starting the calculations. Let's assume the code is there that puts the numbers on the buttons in the entry field. The minus sign and the decimal dot are also handled. (We'll come to that in a minute). How can we let the user use a keyboard? Well, in an earlier article I've mentioned mnemonics. These are the letters underlined in a menu with which the menu can be executed. On a button we can do the same. Simply put a tilde (~) in front of the character you'd like to use as a mnemonic. If we do that for all the buttons on the calculator, the user can use his keyboard. This will activate the buttons, for which the code will put characters into the entry field.

What have we achieved? A simple character filter. The user can only enter those characters into the entry field for which we've created buttons (so kiss good-bye to the code for filtering the user input.)

Well, now we've made this move, we've got to build some code that handles the buttons. But before we do that, let's make some assumptions on how the internals of the editor will function. I've defined the following two simple rules:

  1. The editor uses only two variables of the type double
  2. One holds the left member of the operation, the other the right one.

Now on to the code for the number buttons. There are a few things that need to be considered when writing this code. First of all, every button does, in principle, the same thing. So what we want is to write the code once and apply it to every WM_COMMAND message that a number button generates. How to do this? Luckily the case statement we normally use to handle messages can take care of one part of the problem.

If a case statement is constructed as follows:

switch (number)
        {
        case 1:
        case 2:
        case 3:
                ACTIONS
        break;

        case 4:
        case 5:
        case 6:
                ACTIONS
        break;
        }

then if number contains 1,2 or 3 the same action is performed. The same goes for 4,5 and 6. So we can construct that part of the WM_COMMAND handler of the dialog so that for all the number buttons the same lines of code are executed.

Before we can consider what the code must do, we've got to provide a simple way to detect which button is pressed. This is a slight problem because we just decided to let all the number buttons execute the same code. So we can't make custom code for every button. Again, being lazy and thinking simple comes to the rescue (as does the dialog editor). The trick is to give the buttons the same resource number as the value on it. So button 0 will have the ID 0, button 1 will have ID 1 etc. This is reflected in the define lines in the header file.

#define BUTTON1         1
#define BUTTON2         2
#define BUTTON3         3
#define BUTTON4         4
#define BUTTON5         5
#define BUTTON6         6
#define BUTTON7         7
#define BUTTON8         8
#define BUTTON9         9
#define BUTTON0         0

If the number buttons in the dialog use these IDs we can know which button is pressed. Having solved this we can start to think about how to handle things when a number button is pressed. Several things have to be taken into account.

  1. First the typed number has to be added to the value displayed in the entry field
  2. 1 isn't always true. If the current value in the entry field is a result of a previous computation, we like to empty the entry field before we add the number because we're starting to input a new value.

For 2 we make the following assumption; if a computation is executed, the result will be placed in the left member. So the following goes:

RightMember (operation) Leftmember = Leftmember

This way a new rightmember can be added, subtracted etc. from a previous result. (And this is one of the things we certainly want in a calculator).

Let's solve these problems one by one. Add the currently typed number to the value presented in the entry field. Remember that what's presented in the entry field is a character-string. The value of the typed button is a number. How can we convert this? The simplest way to go is convert the typed number to a string and catenate it to the contents of the entry field. This can be done with sprintf. This is a standard C function that can create a string. Remember to include <stdio.h> in your C file for this. (I won't go any further into this because it involves using standard C). The string to contain the number must be 2 characters long. This is because a string always contains a NULL as terminator. So the value is the first char, the terminator the second.

After this we can merge the value from the entry field with the value in the number. At this moment we'll assume the value in the entry field is contained in the string achValue (we'll come back to this in a minute). To catenate the strings we'll use the strcat function. This is another default C function (remember to include <string.h> in your C file).

char achButton[2];
sprintf(achButton, "%d", SHORT1FROMMP(mp1));
strcat(achValue, achButton);
WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achValue);

Now we've taken care of 1, let's proceed. The second item isn't that hard to implement. We know that if a result has been computed, it resides in the left member. If this is true, the rightmember should contain 0. (This has to be taken care of further on.) So the second point is taken care of with an if statement:

if (flRightMember == 0)
        strcpy(achNumber, "");

This way the newly typed number will be catenated to an empty string. So it will be the first number in the Entry field. Doing this means we've got to fill the rightmember with some value that it isn't 0. This way we know something is inserted. This leads to the following code for the number buttons:

//-------------------------------------------------------------
// Process number button
//-------------------------------------------------------------
case BUTTON1:   case BUTTON2:   case BUTTON3:
case BUTTON4:   case BUTTON5:   case BUTTON6:
case BUTTON7:   case BUTTON8:   case BUTTON9:
case BUTTON0:
        {
        if (flRightMember == 0)
                strcpy(achNumber, "");

        char achButton[2];
        sprintf(achButton, "%d", SHORT1FROMMP(mp1));
        strcat(achNumber, achButton);
        WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achNumber);
        flRightMember = 1;      // Rightmember filled
        }
return(0);

There is one other point in putting values in the entry field (the minus sign will come in a minute). It's the decimal separator, the dot. We should only allow a dot to be added when there is no dot present in the current entry field. Again we can use a function of the string.h library. The strchr function. This function return a TRUE when a given character value is present in a given string. This gives the following code to handle the WM_COMMAND for the BUTTONDOT ID.

if (!strchr(achValue, '.'))
        {
        strcat(achValue, ".");
        WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achValue);
        }

Before we look at a way to process the action keys as '-', ' ', '/' and '*' we take a look at how to process an action. Besides having to calculate it on an action button press, it also has to be calculated on an '=' action. Imagine the following. We enter 10, after that a ' ', after that a 5, and after that a '-'. When the minus is pressed, we've got to compute the ' '. So the last action pressed should be remembered. For that we introduce the LastAction variable. So when a computation has to be performed we know which based on the value in LastAction. We can define a procedure based on this. We know the result has to be placed in the left member, which gives us the following procedure:

//---------------------------------------------------------------------------------
// Calculate the result of the last action and return it
//---------------------------------------------------------------------------------
void proceslastaction( void )
        {
        switch (chLastAction)
                {
                case '-': flLeftMember -= flRightMember; break;
                case ' ': flLeftMember  = flRightMember; break;
                case '*': flLeftMember *= flRightMember; break;
                case '/': flLeftMember /= flRightMember; break;
                }
        }

When an action button is pressed, we first have to see if the left member is empty. If so, we fill the left member with the value of the rightmember, clear the rightmember and store the action in the LastAction variable. This way when the rightmember is filled and '=' is pressed we can calculate the result.

When the left member is filled with a valid value and we press an action key, the result has to be calculated and put in the left member. Again the rightmember has to be cleared (remember the code in the number button handler!)

After processing the action key, we've got to store the new action in the LastValue. This leads to the following code:

//-------------------------------------------------------------
// Process action buttons ( , -, /, *)
//-------------------------------------------------------------
case BUTTONSUB:
case BUTTONADD:
case BUTTONMUL:
case BUTTONDEV:
        {
        flRightMember = atof(achValue);         // Get value in Entry field

        if (flLeftMember == 0)
                {
                flLeftMember  = flRightMember;
                flRightMember = 0;
                WinSetDlgItemText(hwndDlg, ENTRYFIELD1, "");
                }
        else
                {
                //---------------------------------------------------
                // Process last action
                //---------------------------------------------------
                proceslastaction( );
                flRightMember = 0;
                sprintf(achValue, "%f", flLeftMember);
                WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achValue);
                }
        //--------------------------------------------------------
        // Set new last action
        //--------------------------------------------------------
        switch (SHORT1FROMMP(mp1))
                {
                case BUTTONSUB: chLastAction = '-'; break;
                case BUTTONADD: chLastAction = ' '; break;
                case BUTTONMUL: chLastAction = '*'; break;
                case BUTTONDEV: chLastAction = '/'; break;
                }
        }
return(0);

There is one exception to the action buttons. When a '-' is pressed it can also be used to used to insert a negative value. This is only a valid action if the '-' is pressed as the first character. This means, inserting the '-' as only a character in the entry field and filling the rightmember with some value to make sure the number-button handler doesn't overwrite it. This gives the following code:

//--------------------------------------------------------
// Set negative sign
//--------------------------------------------------------
if (flRightMember == 0)
        {
        WinSetDlgItemText(hwndDlg, ENTRYFIELD1, "-");
        flRightMember = 1;      // Rightmember filled
        return(0);
        }

Remember we said earlier that the string contained in the Entry field is contained in the achValue string. Well, because almost every action performed on pressing a key needs this string, it is positioned on top of the WM_COMMAND event handler procedure. So before we handle the keys we fill the string like this:

//------------------------------------------------------------------
// Query the value in the Entry field and put it in flRightMember
//------------------------------------------------------------------
char achValue[32];
WinQueryDlgItemText(hwndDlg , ENTRYFIELD1, 32, achValue);

We're almost done. What's left? The '=' key, and the 'C' and 'CA' keys. These last two I'll leave to your own vivid imagination (take a look at the sample). The '=' needs some attention. After an '=' the result should not be used in the next computation. (When we want to cascade computations, simply press one of the calculate actions keys.)

So after the '=' we need to purge all values contained in the calculator and display the result.

//-------------------------------------------------------------
// Process the = button
//-------------------------------------------------------------
case BUTTONEQ:
        {
        flRightMember = atof(achValue); // Get value in Entry field
        proceslastaction( );                    // Process last action
        sprintf(achValue, "%f", flLeftMember);
        WinSetDlgItemText(hwndDlg, ENTRYFIELD1, achValue);
        //-----------------------------------------------------
        // Reinit
        //-----------------------------------------------------
        flRightMember = 0;                              // Rightmember becomes 0
        flLeftMember  = 0;                              // Leftmember becomes 0
        chLastAction  = ' ';                    // No last action
        }
return(0);

Well, glad I explained that... It isn't that much PM programming, but we've established a nice platform to create the first OS/2 e-Zine! HOW DO I? application. Right now we've got a working calculator (with a lot to be improved!).

Next month we'll take a look at how to improve the interface. This can be done a lot nicer, and remember, a nice interface gives a lot more fun to using the application. Next week we also provide access to the tally roll (the MLE control at the top of the calculator).

Let me know what you think of this. Besides that, any ideas for improvements are welcome. Until next week... Oh yeah, have a happy new year. <g>