The Infinitely Floating Spinbutton

From EDM2
Jump to: navigation, search

by Marc Mittelmeijer and Eric Slaats

Introduction

Most of the OS/2 controls are very useful and flexible. But every once in a while you bounce against the limits of what's possible with the given controls. (Handling floats in spinbuttons and containers for one thing is an issue.) When we were building an interface for a neural network problem we needed a spinbutton control to handle an infinite range of (undetermined) numeric values.

This isn't a big problem; the spinbutton can accept boundaries when handling integers which can be set or reset using the SPBM_SETLIMITS message. The problem occurred when we wanted the spinbutton to handle floats. The control is incapable of handling floats. Of course its possible to convert floats to strings and a spinbutton can accept strings. But this would mean a limited range of predefined (float to string) values. We needed a infinite range of undetermined values! In this article we'll discuss our solution to this problem. But first, we need to dissect the spinbutton.

How Do Spinbuttons Work?

The spinbutton is one the most useful controls OS/2 has to offer. It's described in the book The Art of OS/2 2.1 programming but for all those lost souls who do not have a copy, here is a short description.

A spinbutton is a combination of an entryfield and a set of up and down arrow buttons. The buttons can be used to spin through a list of predefined choices. If the spinbutton isn't read only, the user can type in a value of choice. (Which doesn't necessarily have to be in the predefined list of choices.) Spinbuttons are typically used to cycle through choices as months of the year, days of the week, hours of the day.

An example of the use of spinbuttons is found in the settings of the clock applet. Here spinbuttons are used to set the time and the date.

Spinbuttons can be defined in a number of ways. One of the most attractive features (which will not be described here) is the ability to link several spinbuttons together, so they react to one set of up and down buttons. The following styles can be used creating a spinbutton.

Style Description
SPBS_ALLCHARACTERS Any char can be typed in. This is the default.
SPBS_NUMERICONLY Only the digits 0-9 are accepted.
SPBS_READONLY Nothing can be typed in the spinfield.
SPBS_MASTER When more spinfields are coupled this spinbutton provides the up and down keys and acts as a master which controls the others.
SPBS_SERVANT A servant spinbutton will have no buttons and will spin under the master's control.
SPBS_JUSTLEFT Left justify the spinbutton text.
SPBS_JUSTRIGHT Right justify the spinbutton text.
SPBS_JUSTCENTER Center the spinbutton text.
PBS_NOBORDER Surpress drawing a border.
PBS_FASTSPIN The speed in which the spinbutton cycles through the choices will increase (every two seconds) when one of the buttons is held down.
SPBS_PADWITHZEROS The entryfield is padded with zeros (up to a maximum of 11) to the first non zero digit.

A set of values for the spinbutton can be set up in two ways. You can define upper and lower integer boundaries with a message to the spinbutton and let it spin between them or you can supply the spinbutton with an array of predefined (character) values.

These two types of spinbuttons are demonstrated in the following simple program. We used a dialog box to demonstrate the spinbuttons because this eliminated the hassle necessary to build a 'normal' window. The .RC file for dialog box is build using the resource workshop of Borland C++ for OS/2 and is straightforward.

 //------------------------------------------------------------------------
 // FILE: Spinbut1.rc
 //------------------------------------------------------------------------
 
 #include "Spinbut1.H"
 
 DLGTEMPLATE SPINDLG
 BEGIN
 
    DIALOG "Spin example1", 100, 14, 92, 122, 71, NOT FS_DLGBORDER |
       FS_SIZEBORDER | WS_VISIBLE, FCF_SYSMENU | FCF_TITLEBAR | FCF_MINBUTTON |
       FCF_MAXBUTTON
 
    BEGIN
 
    CONTROL "", SPINBUT1, 9, 40, 73, 12, WC_SPINBUTTON, SPBS_MASTER |
       SPBS_ALLCHARACTERS | SPBS_JUSTLEFT | WS_VISIBLE
 
    CONTROL "", SPINBUT2, 8, 8, 74, 12, WC_SPINBUTTON, SPBS_MASTER |
       SPBS_NUMERICONLY | SPBS_JUSTLEFT | SPBS_FASTSPIN | WS_VISIBLE
 
    CONTROL "With array of values", 103, 11, 55, 98, 8, WC_STATIC,
       SS_TEXT | DT_LEFT | DT_TOP | DT_MNEMONIC | WS_VISIBLE | WS_GROUP
 
    CONTROL "With integer boundaries", 104, 9, 22, 106, 8, WC_STATIC,
       SS_TEXT | DT_LEFT | DT_TOP | DT_MNEMONIC | WS_VISIBLE | WS_GROUP
 
    END
 END

This .RC file defines a dialog box with two spinbuttons and some text above each. Both spinbuttons are defined as SPBS_MASTER. This means they both have arrow-buttons. SPINBUT1 will take any character as input,SPINBUT2 will accept only numeric (integer) input. This dialog is the base for the following program which will set SPINBUT1 to a set of predefined values and SPINBUT2 to a range between two given boundaries.

This dialog box will look like this:

Ifs1.png

 //-------------------------------------------------------------------------
 // FILE: Spinbut1.h
 //-------------------------------------------------------------------------
 #define SPINBUT1     101
 #define SPINBUT2     102
 #define SPINDLG     1
 
 //-------------------------------------------------------------------------
 // FILE: Spinbut1.cpp
 //-------------------------------------------------------------------------
 #define  INCL_WIN
 #include <os2.h>
 #include "spinbut1.h"
 
 PCHAR achSpin1Array[] = {  "One",
                            "Two",
                            "Three",
                            "Four",
                            "Five",
                            "Six",
                            "Seven",
                            "Eight",
                            "Nine",
                            "Ten",
                            "And once again"
                         };
 
 //-------------------------------------------------------------------------
 // Prototypes
 //-------------------------------------------------------------------------
 MRESULT EXPENTRY SpinDlg (HWND, ULONG ,MPARAM, MPARAM);
 
 //-------------------------------------------------------------------------
 // Main
 //
 // Sets up a simple dialogbox to demonstrate spinbuttons. By using a dialog
 // none of the usual window control has to be included.
 //-------------------------------------------------------------------------
 void main(void)
      {
      HAB  hab;
      HMQ  hmq;
 
      hab = WinInitialize(0);
      hmq = WinCreateMsgQueue(hab,0);
 
      WinDlgBox(HWND_DESKTOP,
                HWND_DESKTOP,
                SpinDlg,
                NULLHANDLE,
                SPINDLG,
                0);
 
      WinDestroyMsgQueue(hmq);
      WinTerminate(hab);
      }
 
 
 //-------------------------------------------------------------------------
 // dialog procedure
 //-------------------------------------------------------------------------
 
 MRESULT EXPENTRY SpinDlg(HWND hwndDlg, ULONG ulMsg, MPARAM mpParm1,
                          MPARAM mpParm2)
 
      {
      switch (ulMsg)
         {
         case WM_INITDLG:
            {
 
            WinSendDlgItemMsg(hwndDlg,      // Spinbut1 to predefined
                              SPINBUT1,
                              SPBM_SETARRAY,
                              MPFROMP (achSpin1Array),
                              MPFROMSHORT (11));
 
            WinSendDlgItemMsg(hwndDlg,      // Spinbut2 to range
                              SPINBUT2,
                              SPBM_SETLIMITS,
                              MPFROMLONG (100),
                              MPFROMLONG (0));
            }
         }
      return WinDefDlgProc(hwndDlg, ulMsg, mpParm1, mpParm2);
      }

The complete working code can be found in SPINBUT1.ZIP.

In the dialog procedure, the only message that is intercepted is the WM_INITDLG message. This message is send when the dialog box is created. This message functions just like the WM_CREATE message, only it's specially for dialog boxes. When the WM_INITDLG message is generated, we can initiate the two spinbuttons.

SPINBUT1 is initiated using the SPBM_SETARRAY message. In this message the array achSpin1Array is attached to the spinbutton. If the spinbutton is used, the values as they are declared in this array are shown.

SPINBUT2 is initiated using the SPBM_SETLIMITS message. In this case the upper limit is 100 and the lower limit is 0. The spinbutton will show the integer values from 0 to 100.

The Infinite Spinbutton

Our goal was to build a spinbutton with no predefined boundaries. Also the next value a spinbutton shows after a button-up or button down action should be derived from the current value. So if a user types in a value and uses the buttons afterwards, the new value should be derived from the typed value.

NOTE: the spinbutton we're building will be working with numeric values. So the derived values are also numeric. It's possible to take the same technique and use it with alphanumeric values.

The trick used to accomplish this is to set up a new value array for the spinbutton each time the value in the button is changed. This value array should have three entries, the middle one being the current value. If a button is pressed, the button shows one of the other two values. At that point a new array of values should be calculated and should be attached to the spinbutton. "1.04" is the current value in the spinbutton. The value array looks something like this:

'1.03' '1.04' '1.05'

The up button is pressed. So '1.05' will be the next current value. '1.05' should also be the next center value in the value array. The array will change to:

'1.04' '1.05' '1.06'

To accomplish such a task, we could intercept the SPBN_UPARROW and the SPBN_DOWNARROW notification messages and change the values in the value array when the up or down arrow has been pressed. The code to accomplish this may look like this:

case WM_CONTROL:
   {
   switch (SHORT2FROMMP(mpParm1))
      {
      case SPBN_DOWNARROW:
      case SPBN_UPARROW:
         {
         char achRetValue[12];

         WinSendDlgItemMsg(hwndDlg, SPINBUT1,
                           SPBM_QUERYVALUE,
                           MPFROMP (achRetValue),
                           MPFROM2SHORT (sizeof(achRetValue), 0));

         sprintf(achValues[0], "%.*f", 2, atof(achRetValue)-0.01);
         sprintf(achValues[1], "%.*f", 2, atof(achRetValue));
         sprintf(achValues[2], "%.*f", 2, atof(achRetValue)+0.01);

         WinSendDlgItemMsg(hwndDlg, SPINBUT1,   // Spinbut1 to predefined
                           SPBM_SETARRAY,
                           MPFROMP (achValues),
                           MPFROMSHORT (3));

         WinSendDlgItemMsg(hwndDlg, SPINBUT1,   // Make middle one current
                           SPBM_SETCURRENTVALUE,
                           MPFROMLONG (1),
                           MPFROMLONG (0));
         }
     return MRFROMSHORT (TRUE);
     }
  }

NOTE: the array which will be attached to the spinbutton must contain 3 values!

The complete code for this example can be found in SPINBUT2.ZIP.

This example code will increment or decrement the spinbutton value with 0.01. Of course, this is a choice, it's very simple to change the code so the change will be 0.5.

After a button click, the spinbutton will switch to the next value in the value array and will generate a notification message. The code reacts to the SPBN_DOWNARROW and the SPBN_UPARROW. With the SPBM_QUERYVALUE the spinbutton is queried. The returned value in achRetValue is a value of the value array. With the returned value the new values for the achValue array will be set. In this example a (float) in/decrement of .01 is chosen. The changed array has to be attached to the spinbutton, and the middle value has to be made current.

Building an infinite spinbutton this way has one major flaw. The button will not derive it's next value from a value entered by a user. The reason is: when the spinbutton gets a SPBM_QUERYVALUE, it will read from the attached value array using an index. It won't read from the entry field attached to the spinbutton. So the value entered will be discarded if it isn't in the values array! Also, it would be nice to leave all the notification messages untouched and take a universal approach in constructing an infinite spinbutton. In the next section we will analyze this problem.

Adding Spoilers and Other Gadgets

In the previous section the major trick to create an infinite spinbutton which can handle floats is explained. There is a more elegant way to achieve an infinite spinbutton, which will also react to user input. Before we use a new bag of tricks, let's first examine a spinbutton more precisely.

There is a nice tool to examine windows in an application. It's called PMTREE and it is an applet from IBM. We got it from the Developers Connection CD-ROM, but it is also available from a number of anonymous sites. If we examine a spinbutton with this tool, we will see that a spinbutton is build from several childwindows. The top-most window in a spinbutton is an entryfield window. This is useful information, because this gives us a way to directly query the value in a spinbutton. Even if a user entered a value which can't be found in the attached array, the entered value will be retrieved.

Retrieving the Value in the Entryfield

We can query the string in an entryfield by using WinQueryWindowText(). To use this API, we need the window-handle of the entryfield. We know the entryfield is the top child of the spinbutton childwindows. Thus, if we know the handle of the spinbutton, we can retrieve the handle of the corresponding entryfield using the WinQueryWindow() function and specifying QW_TOP as the parameter.

WinQueryWindow (hwndSpin, QW_TOP);

By combining these two functions, we can think of some code which will retrieve the value in the spinbutton-entryfield given the handle of the spinbutton.

char achSpinValue[32];

WinQueryWindowText(WinQueryWindow(hwndSpin, QW_TOP),
          sizeof(achSpinValue),
          achSpinValue);

If the value in the spinfield is known, the value array can be set up. We calculate the float value from the string returned by WinQueryWindowText() and use this value to calculate the value array values.

flSpinValue = atof(chSpinValue);
sprintf(achValues[0],"%.*f", 2, flSpinValue + -0.01);
sprintf(achValues[1],"%.*f", 2, flSpinValue + 0);
sprintf(achValues[2],"%.*f", 2, flSpinValue + 0.01);

If we use the code this way, we can only in/decrement the value in the spinbutton with 0.01. It's more interesting to in/decrease the value in the spinbutton with a fraction of its current value. The next code increases the value with 2% for values outside the range -1,1. It can easily be modified to work with another value, or even with a parameter.

//-----------------------------------------------------------------------
// Set the multiplication factor and fill values array
//-----------------------------------------------------------------------
if (fabs(flSpinValue) < 1)
     factor = 0.01;
else
     factor = (float) ceil(fabs(flSpinValue/50));

sprintf(achValues[0],"%.*f", 2, flSpinValue + -factor);
sprintf(achValues[1],"%.*f", 2, flSpinValue + 0);
sprintf(achValues[2],"%.*f", 2, flSpinValue + factor);

The next step is to attach the array to the spinbutton and set the middle value as the current. This can be done using the messages SPBM_SETARRAY and SPBM_SETCURRENTVALUE.

And now for the big trick! PM generates a SPBM_SPINUP or SPBM_SPINDOWN message if one of the spinbutton arrow buttons are pressed. If we set the new value array before the messages SPBM_SPINUP and SPBM_SPINDOWN are processed, we know for certain that the values array will be build upon the current value in the spinbutton, whether or not it was user-entered! So the processing of SPBM_SPINUP or SPBM_SPINDOWN will use this new value array and display the correct value.

But how can we intercept these messages so that the value array can be changed before it's used? The technique to achieve this is called subclassing.

And Now For Some Subclassing

Really the most elegant way of creating an infinite spinbutton is to use subclassing. Subclassing is a technique in which a call to the standard procedure for a particular kind of window is intercepted. This way, a programmer can define their own handling of a message. We want to intercept the messages SPBM_SPINUP and SPBM_SPINDOWN. These messages are send by the PM when the arrowbuttons attached to a spinbutton are pressed and cause the value in the spinbutton to skip one value in the attached array.

The spinbutton can be subclassed when handling the WM_INITDLG message. The function to call is WinSubclassWindow(). With this function we can insert a function which will be called instead of the standard window function.

NOTE: because the standard function won't be called, the subclass function should call the standard function for all the messages it doesn't compute!

pfnwOldProc = WinSubclassWindow(WinWindowFromID(hwndDlg, SPINBUT1), CalcSpin);

pfnwOldProc is the return pointer to the original window function. This pointer value should be available in the subclass function so the subclass function can call the original window function. The general way to deal with this is putting the pointer in a window word. Because we're not talking about window words and we're a little lazy, we define a global variable for it.

The name of the subclass function is CalcSpin. This function has the same anatomy as a normal window procedure. It's prototype looks like this:

MRESULT EXPENTRY CalcSpin (HWND, ULONG ,MPARAM, MPARAM);

At this point we should be able to write the complete subclass function. The function always calls the original window procedure. Only if the messages SPBM_SPINUP or SPBM_SPINDOWN are received, the function should interfere. The subclass function looks like this.

//---------------------------------------------------------------------
// Subclass for spinbutton
//---------------------------------------------------------------------

MRESULT EXPENTRY CalcSpin(HWND hwndDlg, ULONG ulMsg, MPARAM mpParm1,
                          MPARAM mpParm2)
   {
   if (ulMsg == SPBM_SPINUP || ulMsg == SPBM_SPINDOWN)
      {
      float  flSpinValue, factor;
      char   chSpinValue[12];

      WinQueryWindowText(WinQueryWindow(hwndDlg,QW_TOP),12,chSpinValue);
      flSpinValue = atof(chSpinValue);
      //---------------------------------------------------------------
      // Set the multiplication factor and fill values array
      //---------------------------------------------------------------
      if (fabs(flSpinValue) < 1)
         factor = 0.01;
      else
         factor = (float) ceil(fabs(flSpinValue/50));

      sprintf(achValues[0],"%.*f", 2, flSpinValue + -factor);
      sprintf(achValues[1],"%.*f", 2, flSpinValue + 0);
      sprintf(achValues[2],"%.*f", 2, flSpinValue + factor);

      //----------------------------------------------------------------
      // Set the new array for the spinbutton and make the middle item the
      // current one so the user can go up or down.
      //----------------------------------------------------------------

      pfnwOldProc(hwndDlg, SPBM_SETARRAY, MPFROMP (achValues), MPFROMSHORT(3));
      pfnwOldProc(hwndDlg, SPBM_SETCURRENTVALUE, (MPARAM)1, (MPARAM)0);
      }
   return pfnwOldProc(hwndDlg, ulMsg, mpParm1, mpParm2);
   }

The complete working code can be found in SPINBUT3.ZIP.

It should be noted that subclassing takes a performance penalty because more functions have to be called for every message generated! So the EXAMPLE2 version which works with notification messages might be slightly faster. On a 486DX system this difference should be insignificant.

Add the Spare Tire!

Now that we have a spinbutton which is reacting to user input, and can handle floats, it would be nice to have some functions to read a float from the spinbutton, or to put a float in the spinbutton.

The first function reads the current value in the spinbutton and returns a float value. In this function we'll use the knowledge that the entryfield is the first child of a spinbutton.

//--------------------------------------------------------------------------------------------
// ReadSpin
//
// Read the value in the sle field of the and return the float equevalent
//--------------------------------------------------------------------------------------------
float ReadSpin(HWND hwndDlg, ULONG ulId)
   {
   HWND  hwndSpinSle;        // Handle off sb sle field (child sb)
   HWND  hwndSpinbut;        // Handle Spin button
   char  Output[16];

   hwndSpinbut = WinWindowFromID(hwndDlg, ulId);      // Query spinbutton handle
   hwndSpinSle = WinQueryWindow(hwndSpinbut, QW_TOP); // Query first child spinbuuton
                                                      // (= entry field)
   WinQueryWindowText(hwndSpinSle, 16, Output);

   return (atof(Output));
   }

The WriteSpin() function uses the same knowledge. It puts a given float value as text in the entryfield of the spinbutton. Because the way it's constructed the subclass will use this value if an up or down arrow is used.

NOTE: if you want the message SPBM_QUERYVALUE to return a valid value, you've got to set the value array in the writespin function and attach it to the spinbutton. This can easily be achieved by using code from calcspin.

//--------------------------------------------------------------------------
// WriteSpin
//
// Write the float value in Input as a char[12] in the sle of a Spinbutton
//--------------------------------------------------------------------------

void WriteSpin(HWND hwndDlg, ULONG ulId, float WriteValue)
   {
   char  Value[16];

   sprintf(Value,"%.*f", 2, WriteValue);

   WinSetWindowText(WinQueryWindow(WinWindowFromID(hwndDlg, ulId), QW_TOP), Value);
   }

A complete working code example is found in the file SPINBUT4.ZIP.

In the SPINBUT4 program both of the above functions are used to fill and read a spinbutton.

Concluding Notes

In this article we described some possible algorithm for spinbuttons to handle an infinite range of values which can be floating point numbers. Two different approaches were discussed, using notification messages and using subclassing.

The infinite spinbutton could be refined by adding input control so the user can't enter illegal characters. This can be done by capturing the WM_CHAR message.

The examples were built and compiled using Borland C++ version 1.5.