The Infinitely Floating Spinbutton

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 spin buttons and containers for one thing is an issue.) When we were building an interface for a neural network problem we needed a spin button control to handle an infinite range of (undetermined) numeric values.

This isn't a big problem; the spin button can accept boundaries when handling integers which can be set or reset using the SPBM_SETLIMITS message. The problem occurred when we wanted the spin button to handle floats. The control is incapable of handling floats. Of course its possible to convert floats to strings and a spin button 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 spin button.

How Do Spinbuttons Work?
The spin button 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 spin button is a combination of an entry field and a set of up and down arrow buttons. The buttons can be used to spin through a list of predefined choices. If the spin button 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.) Spin buttons 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 spin buttons is found in the settings of the clock applet. Here spin buttons are used to set the time and the date.

Spin buttons 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 spin buttons together, so they react to one set of up and down buttons. The following styles can be used creating a spin button.

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

These two types of spin buttons are demonstrated in the following simple program. We used a dialog box to demonstrate the spin buttons 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 // 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
 * 1) include "Spinbut1.H"

This .RC file defines a dialog box with two spin buttons and some text above each. Both spin buttons 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:



//- // FILE: Spinbut1.h //- //- // FILE: Spinbut1.cpp //- 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); }
 * 1) define SPINBUT1    101
 * 2) define SPINBUT2    102
 * 3) define SPINDLG    1
 * 1) define INCL_WIN
 * 2) include 
 * 3) include "spinbut1.h"

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 spin buttons.

SPINBUT1 is initiated using the SPBM_SETARRAY message. In this message the array achSpin1Array is attached to the spin button. If the spin button 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 spin button will show the integer values from 0 to 100.

The Infinite Spinbutton
Our goal was to build a spin button with no predefined boundaries. Also the next value a spin button 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 spin button 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 spin button 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 spin button. "1.04" is the current value in the spin button. The value array looks something like this:

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:

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 spin button must contain 3 values!

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

This example code will increment or decrement the spin button 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 spin button 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 spin button 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 spin button, and the middle value has to be made current.

Building an infinite spin button 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 spin button 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 spin button. 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 spin button. In the next section we will analyse this problem.

Adding Spoilers and Other Gadgets
In the previous section the major trick to create an infinite spin button which can handle floats is explained. There is a more elegant way to achieve an infinite spin button, which will also react to user input. Before we use a new bag of tricks, let's first examine a spin button 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 spin button with this tool, we will see that a spin button is build from several child windows. The top-most window in a spin button is an entry field window. This is useful information, because this gives us a way to directly query the value in a spin button. 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 entry field by using WinQueryWindowText. To use this API, we need the window-handle of the entry field. We know the entry field is the top child of the spin button child windows. Thus, if we know the handle of the spin button, we can retrieve the handle of the corresponding entry field 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 spin button. char achSpinValue[32]; WinQueryWindowText(WinQueryWindow(hwndSpin, QW_TOP),          sizeof(achSpinValue),           achSpinValue);

If the value in the spin field 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 spin button with 0.01. It's more interesting to in/decrease the value in the spin button 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 spin button 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 spin button 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 spin button 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 arrow buttons attached to a spin button are pressed and cause the value in the spin button to skip one value in the attached array.

The spin button 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.