A Better Approach to WinCE/Pocket PC Forms

Wednesday Dec 18th 2002 by Nancy Nicolaisen
Share:

When it comes to building forms for mobile device-based applications, the fastest way isn't always the best way. This becomes most glaringly apparent is in the case of form-based data entry applications.

           Good
           Cheap
           Fast:
           Pick any two.
                         -Dwight Dial

In the last few installments, we saw how to rapidly and simply move a user interface from Win 32 to Windows CE. The fastest way isn't always the best way, or even an adequate way, though. From my point of view, the place this becomes most glaringly apparent is in the case of form based data entry applications.

On the desktop, dialog box based forms are a good, self-documenting way to collect data from users. For Windows CE devices, because of screen size limitations, most non-trivial dialog based forms must be re-implemented. The "quick and dirty" route to re-implementation is to subdivide a large dialog into a set of tabbed dialog pages.

However, in terms of the user's prior experience, reorganizing one dialog-based form into a series of dialogs representing the same form sets up a subtle contradiction. Tabbed dialogs on the desktop usually imply a collection of related, but independent, information. Using a tabbed dialog to represent a form violates the user's intuition and prior experience about what is expected of her. Should all of the tabs be visited? In order? Is there an obvious way to tell when input is required and when input has been validated? The tabbed dialog has real drawbacks, because in most cases, the information in a form is related and dependent. A first name is meaningless without a last name; a shipping address is not useful without a billing address; and so forth.

Command Bands: The Win CE Forms Solution

Windows CE CommandBands provide the tool for creating intuitive forms that accommodate the limitations of the HPC and PPC, wringing meaning out of every last pixel on the tiny Windows CE screen. The CommandBands approach to form design allows you to retain the visual metaphor of a single form, but in a way that efficiently uses screen resources.

CommandBands controls are related to the CommandBar control. In the MenuBar example in a previous article, we saw that CommandBars act as containers for controls, managing painting, visual changes in the control states, and message routing from the controls to the window procedure. CommandBands build on and extend this concept, by providing a container for CommandBars. This powerful construct will let you create a "form" composed of an aggregation of optionally moveable, resizable CommandBars. Each CommandBar contains one control. To put this another way, the Command Band represents the form as a whole, and the CommandBars represent the individual fields of the form.

This approach overcomes the limitations of the tabbed dialog as a form based data entry interface. First, it allows you to put all of the fields of the form on the screen at once, because the user can resize CommandBands. For example, you can show a few fields at full size, and the rest as "grippers" the user can drag to open the field. Second, it simplifies and rationalizes data validation, because the fields in a form enjoy the unifying relationship established by containment in the CommandBands. And finally, it improves your options if you choose not to let users move and resize the bands containing the fields. In this case, you can create "pages" of bands that users navigate using next / previous controls. This kind of navigation behavior is less prone to confusing the user than the tabbed dialog approach, as well as being more amenable to application specific specialization.

The first example shows how to create and use CommandBands. Creating and using CommandBands entails these steps:

  • Create the CommandBands control, setting the overall band style, the base band ID ( the lowest individual band ID in the control ), and optionally, the controls image list

  • Allocate and array of REBARBANDINFO structures, one per band.

  • Initialize each of the band structures.

  • Add the bands.

  • Loop through the bands, inserting a control in the CommandBar contained in each band.

  • Initialize controls as necessary.

Steps To A Form Using CommandBands

Let's assume that the sole purpose of our application is to collect data using the UFO CommandBands based form. We'll call CreateDataBands(), the function that creates the form when we process the WM_CREATE message for the application. This has the same functional effect as if we created a modeless dialog. In other words, the form will persist until we destroy it or until the user closes the application. Here's the code for this:

case WM_CREATE:
   // build the bands that 
   // replace the Win32 
   // dialog box form
   CreateDataBands( hWnd, 
                    message, 
                    wParam, 
                    lParam);
   break;

The job of building the bands is handled in the function CreateDataBands(). Notice the parameters to this function are simply copies of the parameters to WindProc that came with the WM_CREATE message. Now let's look more closely at the CreateDataBands() function. First, notice this declaration:

LPREBARBANDINFO prbi;

This datum is a long pointer to a REBARBANDINFO structure. This structure contains the information used to size, position, and specify the behavior and appearance of each band. This is a subtlety that I can't emphasize enough. The band is the real estate where the CommandBar and it's control live. If you want to access the bar, and by extension, it's a contained control, you have to do so through it's band.

Porting Tip
As you are most probably aware, all of these controls are implemented as windows, and they have ownership relationships among themselves. I mention this, because if you are porting a real application, it's very tempting to try "clever" solutions that will preserve your existing code, and a lot of Win32 forms code does interact directly with the windows that underlie dialog elements. There are very legitimate reasons for doing this ( data validation and input masking are two good examples ). I have found that trying to treat any of the CommandBands constituents ( bands, bars, or controls) as individual windows ( as opposed to bands that contain bars that contain controls) produces highly unpredictable behavior.

Here is the declaration of the REBARBANDINFO structure, along with the meanings of it's members:

typedef struct tagREBARBANDINFO
{
  UINT          cbSize;       //size of this structure
  UINT          fMask;        //flags that tell ether what 
                              //    info is being passed
                              // or what info is being requested
  UINT          fStyle;       //flags for band styles
  COLORREF      clrFore;      //band foreground color 
  COLORREF      clrBack;      //band background color
  LPSTR         lpText;       //ptr to band label text
  UINT          cch;          //size of text in the band label
                              //    in bytes
  int           iImage;       //0 based index of image list image
                              // to be displayed in this band
  HWND          hwndChild;    //handle to child window 
                              //    contained in band
  UINT          cxMinChild;   //min child window width in pixels
  UINT          cyMinChild;   //min child window height in pixels
  UINT          cx;           //band width, pixels
  HBITMAP       hbmBack;      // handle to a bitmap for 
                              //     band background
                              // if this member is filled, 
                              // clrFore and clgBack are ignored
  UINT          wID;          // ID the CommandBand control uses to
                              //notify this band
  UINT          cyChild;      //initial height of child window
  UINT          cyMaxChild;   //max child height - not used unless 
                              // style RBBS_VARIABLEHEIGHT is set
  UINT          cyIntegral;   //step value for band height increase
  UINT          cxIdeal;      //ideal band width in pixels
  LPARAM        lParam;       //application specific data
}   REBARBANDINFO

Whew. It's a truckload of structure. In most cases, you'll only need to fill in a few of these members to create a CommandBands control. One structure is required for each band in the control. We use this structure to initialize an existing CommandBands control.

Here's how to create the control:

// Create a command band; save the handle in a global
// variable.  We need to preserve it to access the CommandBands 
// control
hwndCB = CommandBands_Create(hInst, hWnd, 
                             IDC_BAND_BASE_ID,
                             RBS_BANDBORDERS | 
                             RBS_AUTOSIZE | 
                             RBS_FIXEDORDER, NULL);

Notice that we don't specify any details about the individual bands, not even their number. The parameters to CommandBands_Create(), in the order shown, are the instance handle of this app, the main window's handle, the ID of the first band in the control ( which is the zeroth band ), and the style flags that define the appearance and behavior of the CommandBands control. These styles apply to all the individual bands. RBS_BANDBORDERS means the bands will be separated by thin lines, which give the control a gridded appearance. RBS_AUTOSIZE means the bands will be sized to fit within the control if the position of the control changes. RBS_FIXEDORDER means the bands will maintain their initial ordering, even if the user moves a band using its gripper. The last parameter is the optional handle to an image list. We aren't using any images in this control, so it's NULL here. If successful, CommandBands_Create() returns the handle to the control.

Next, we allocate an array of REBARBANDINFO structures.

// Allocate space for the REBARBANDINFO array
// Need one entry per ctl band ( nBands ) plus the menu
prbi = (LPREBARBANDINFO)LocalAlloc(LPTR,
                   (sizeof (REBARBANDINFO) * nBands) );

//always test returns if you attempt to allocate memory
if (!prbi) {
        MessageBox( hWnd, 
                    TEXT("LocalAlloc Failed"), 
                    TEXT("Oops!"), 
                    MB_OK); 
        return 0;
}

These structures are quite large, and it's a definite possibility that your allocation request could fail. Always check the return from an allocation request. Also, notice that we called LocalAlloc() with the LPTR flag as its first parameter. This flag allocates fixed memory that is zero initialized. It's important to initialize the structures before you use them because many of the fields and their interpretations are interdependent. For example, if there is a value in the hbmBack member, the members for foreground and background colors are ignored. The fmask flags define your intentions about what structure members are valid and how they should be used, but if you zero initialize the structures, you won't be as vulnerable to subtleties of the mask flags.

There are several REBARBANDINFO structure fields that are the same for every band. We initialize these first.

// Initialize common REBARBANDINFO structure fields.
for (i = 0; i < nBands; i++) {  
   prbi[i].cbSize = sizeof (REBARBANDINFO);
   prbi[i].fMask = RBBIM_ID | 
                   RBBIM_SIZE |
                   RBBIM_BACKGROUND |
                   RBBIM_STYLE |
                   RBBIM_IDEALSIZE ; 
   prbi[i].wID    = IDC_BAND_BASE_ID+i;
   prbi[i].hbmBack = hBmp;
}

The structure members initialized here, in the order they appear, are the size field , which gives the size of the REBARBANDINFO structure; the mask flags; the ID of this band, calculated using the band base ID and the loop index; and the handle to the bitmap that serves as the background for this band.

It may seem superfluous to pass the size of the structure, but notice that when we call the function that adds the bands using this structure, we pass its address. Since the very first member of the structure contains the size, the called function can accurately determine the size of the structure by dereferencing only the DWORD at the address in the pointer. Supplying the structure size makes for more reliable allocation behavior in the called function.

Porting Tip:
Always initialize the size member of structure if it has one.

The fmask member of the structure tells which other members of the REBARBANDINFO structure are valid. Some of the masks define more than one member to be valid or specifically exclude others from being valid. The ones specified in the call above validate the following members of the structure:

  • wID
  • cx
  • hbmBack
  • fStyle
  • cxIdeal

Recall that in the Menubar example in Chapter 1, the CommandBar menu replaced the resource based-based menu. When you use a CommandBands control, the CommandBar in the 0th band serves as the menu. For this reason the 0th REBARBANDINFO is initialized differently than the bands that hold the controls that make up the form.

// Initialize REBARBANDINFO structure for Menu band
prbi[0].cx = GetSystemMetrics(SM_CXSCREEN ) / 4;
prbi[0].fStyle = prbi[1].fStyle = RBBS_NOGRIPPER |
                                  RBBS_BREAK;

We limit the width of the menu bar to 1/4 of the screen dimension, but it could be larger, because no other bands are inserted between it and the Adornments. We set its style flags and the style flags of the immediately following band to RBBS_NOGRIPPER | RBBS_BREAK. This is an important point. Here's why:

The RBBS_NOGRIPPER style means that the user can't move the band to a new location by dragging it with the gripper control. We want the menu to stay at the top of the screen, so we explicitly eliminate the gripper, which is a default with out this style.

We also specify the RBBS_BREAK, which means the band begins on a new line. (Of course, the 0th band always begins on a new line.) To make sure the 0th band occupies the full screen width, the band following it must have the RBBS_BREAK style as well.

Next, we prepare to initialize the structures for the bands that contain the controls. This is where we get an initial glimpse of the effect of variability in Windows CE device sizes. Obviously, the bigger devices can legibly display more bands in a single row of screen real estate that the smaller devices. We choose the number of bands per row based on runtime information about the platform type of the host.

//Set bands per row based on platform type
memset( &tszPlatType, 0x0, sizeof(tszPlatType));
rc = SystemParametersInfo( SPI_GETPLATFORMTYPE, 
                           sizeof(tszPlatType),
                           &tszPlatType, 0);
if( lstrcmp( tszPlatType, TEXT("Jupiter") ) == 0 )
   { nBandsPerRow = 4;}
else if( lstrcmp( tszPlatType, TEXT("HPC") ) == 0 )
   { nBandsPerRow = 3;}
else if( lstrcmp( tszPlatType, TEXT("Palm PC") ) == 0 )
   { nBandsPerRow = 1;}

Notice that to distinguish between the different form factors we use the SystemParametersInfo() function. The flag SPI_GETPLATFORMTYPE request a string that describes the class of the Windows CE device on which we are running, rather than an OS version. Here's why this is the most reliable way to distinguish between hosts at runtime.

The Windows CE Platform Developers' Kit is designed in a highly componentized fashion, essentially allowing device vendors to "roll their own" version of the OS to implement on their devices. This offers them a great competitive advantage, to wit, the opportunity to innovate. Not surprisingly, they do. For this reason, Windows CE version numbers don't provide the same kind of definite information as Windows version numbers on the desktop, because hardware developers have great latitude in choosing what parts of CE they implement and how they do it. Put another way, you can't safely or effectively use OS version numbers for decision-making at runtime.

The real business of creating the form is accomplished when we initialize the structures for the bands that contain the form's fields.

//Initialize data entry band attributes
for (i = 1; i < nBands; i++) 
{
   //  Common Combobox ctrl band attributes
   prbi[i].fMask |= RBBIM_IDEALSIZE;
   prbi[i].cx = (GetSystemMetrics(SM_CXSCREEN ) - 20 ) / 
          nBandsPerRow ;
   prbi[i].cxIdeal = (GetSystemMetrics(SM_CXSCREEN ) - 20 ) /
          nBandsPerRow ;

   //Set style for line break at the end of a row
   if(((i - 1 ) % nBandsPerRow ) == 0 ) 
   {
      prbi[i].fStyle |= RBBS_BREAK | RBBS_NOGRIPPER;
   }
   else
   {
      prbi[i].fStyle |= RBBS_NOGRIPPER;
   }
}

Notice that we "OR" the RBBIM_IDEALSIZE flag to the fmask member. Next we set the initial size, cx, and the ideal band size, cxIdeal, to a fraction of the screen width that is determined by the platform type. Finally, we use a conditional to assign styles that will assure that we have the correct number of bands in each row. The structure array is fully initialized, and we are ready to add the bands to the CommandBands control.

// Add the bands-- we've reserved real estate  // for our dlg controls.
CommandBands_AddBands (hwndCB, hInst, nBands, prbi);

The parameters to the CommandBands_AddBands() function, in the order shown, are the handle to the command bands control to which the bands are being added, the instance handle, the number of bands being added (this is the count of the bands, not the highest zero based index ), and the address of the array of REBARBANDINFO structures.

We have a CommandBands control, the control is initialized with bands, and now all that remains is to insert the form's controls and set their behaviors. A word about the strategy behind the "form" building steps is in order her. We save conserve screen real estate in a couple of different ways using command bands. First, and most obvious is the CommandBands control itself is fairly condensed. The second is that we eliminate the need for control captions by making all of the controls in the form list boxes or combo boxes.

If the field really is a list ( that is, the user must pick one or more items, but can't add items of their own), then the first item in the list is the string that would normally be the caption text for the control if we were using a dialog based form.. If, on the other hand, the user can type input to the control (the combo box style ), then the first item in the combo box list is the caption, and user input to the edit control is always inserted at the end of the list.

Initially, all the controls are displayed with the caption string selected, which has the effect of labeling the controls, but without using additional screen real estate to do so. If the user visits a control and provides input, we change the current list selection from the field's caption string to the user's typed input or list selection. This has the dual advantages of making it easy for the user to see where she has entered data and easy for the programmer to test for and validate input.

Now let's set up the controls for the form. Here's the initialization array where we store the initialization information for each control.

// Setup information for the controls that implement
//the fields of the forms
const struct CmdBarParms structCmdBarInit[] = 
{
  //Ctrl ID  //Label text //Ctrl Styles   //String Index  //#strings

  IDC_NAME,
          IDS_NAME,    ES_AUTOHSCROLL,    NULL,             0,
  IDC_BADGE,   
          IDS_BADGE,   ES_AUTOHSCROLL,    NULL,             0,
  IDC_DESC,
          IDS_DESC,    ES_MULTILINE | ES_AUTOVSCROLL,
                                          NULL,             0,
  IDC_CAT,    
          IDS_CAT,     CBS_DROPDOWNLIST,  IDS_CAT1,         2,
  IDC_MODE_TRAVEL,
          IDS_MODE_TRAVEL, CBS_DROPDOWNLIST,    
                                          IDS_MODE_TRAVEL1,
                                                            3,
  IDC_WHERE_SEEN,
          IDS_WHERE_SEEN, CBS_DROPDOWNLIST,  
                                          IDS_WHERE1        3,
  IDC_PHYS_EVIDENCE,
          IDS_PHYS_EVIDENCE,
                       CBS_DROPDOWNLIST,   
                                          IDS_PHYS_EVIDENCE1, 
                                                            3,
  IDC_OFFICE,   
          IDS_OFFICE,  CBS_DROPDOWNLIST,     
                                          IDS_OFFICE1,      3,
  IDC_ABDUCTIONS,
          IDS_ABDUCTIONS, CBS_DROPDOWNLIST,    
                                          IDS_ABDUCTIONS1,
                                                            3,
  IDC_FRIENDLY,  
          IDS_FRIENDLY, CBS_DROPDOWNLIST, 
                                          IDS_FRIENDLY1,    3,
  IDC_TALKATIVE,   
          IDS_TALKATIVE, CBS_DROPDOWNLIST,  
                                          IDS_TALKATIVE1,   3,
  IDC_APPEARANCE,    
          IDS_APPEARANCE, CBS_DROPDOWN,   IDS_APPEARANCE1,
                                                            5
};

In order to set up the controls, we loop the bands of the CommandBand control and the control initialization array, inserting controls and setting their attributes using the control ID, label text string, and control styles specified in the structCmdBarInit array.

//Move past the menu bar and set the control behaviors
for (i = 1; i < nBands; i++) 
{
   hwndBand = CommandBands_GetCommandBar (hwndCB,i);
   CommandBar_InsertComboBox (hwndBand, hInst, 
                              prbi[i].cx - 6, 
                              structCmdBarInit[i].lStyles, 
                              IDC_COMBO_BASE_ID + (i - 1), 0);

Next, we initialize each control with it's list of strings. If the control is intended to behave as an edit control, it has only 1 string: its caption. If the control is a list, its array entry specifies its caption, a manifest constant that gives the index of its first string resource in the string table, and a count of the strings.

In order to initialize the control, you must get its handle from the band which contains it.

//get a handle to the combo in this band
hwndCombo = GetDlgItem(hwndBand,IDC_COMBO_BASE_ID + (i - 1));

As a recap, the steps necessary to gain access to an individual control are:

  • Get a handle to a band by calling CommandBands_GetCommandBar()
  • Get a handle to the dialog control GetDlgItem() for the hwnd returned by the previous step

Next load and select the string for the caption. (Every control needs this initializtion. )

//get the caption string
LoadString(hInst, structCmdBarInit[i - 1].uiCaption, 
           (LPTSTR)&tszCaptionBuff,sizeof(tszCaptionBuff));

//insert the label string
SendMessage (hwndCombo, CB_INSERTSTRING, 0, 
            (long)&tszCaptionBuff);
//highlight the caption
SendMessage (hwndCombo, CB_SETCURSEL, 0, 0);

Test the number of list strings. If this control needs a list of selection items, we'll insert the list strings now.

//does this combo have a string list?
if(structCmdBarInit[i - 1].iNumStrings)
{

The strings are stored in the resource file in the order that they appear in the list, and so have consecutive resource Ids. We load the strings by incrementing the based string ID and limit the loop iteration using the count of strings, both of which are stored in the structCmdBarInit structure.

    //if so, insert them in the combo
    for( j = 0; j < structCmdBarInit[i - 1].iNumStrings; j ++ )
    {
      LoadString(hInst, structCmdBarInit[i - 1].iStringIndexBase + j,
          (LPTSTR)&tszCaptionBuff,sizeof(tszCaptionBuff));
      //add strings to the end of the list
      SendMessage (hwndCombo, CB_INSERTSTRING, -1, 
          (long)&tszCaptionBuff);
    } //end for(iNumStrings)
  }//end if(iNumStrings)
}

There you have it. We created a for using controls of fixed size and order, all of which are fully displayed when the form is created. To query the controls for input, you use the same sort of looping construction that we saw in the initialization process, followed with a call to GetDlgItemText() to retrieve user input.. Here's a code fragment that demonstrates how to retrieve input from a control in a CommandBand:

//a stack based buffer for the control's input
TCHAR szSelString[48];

   //get a handle to the bar contained by the band at index i
     hwndBand = CommandBands_GetCommandBar (hwndCB,i);

     //get a handle to the combo in this band
     hwndCombo = GetDlgItem(hwndBand,IDC_COMBO_BASE_ID + (i - 1));

    //ask the list what is selected 
    j = SendMessage(hWndCombo, CB_GETCURSEL , 0, 0 );

    //copy the string at index j to the buffer
    SendMessage(hWndCombo, CB_GETLBTEXT , j, (long)&szSelString );

Here are a couple of things to note about this code fragment. First, notice that the type of the buffer szSelString is TCHAR. Control strings, filenames and the like are Unicode under CE. Consistently using TCHAR types when dealing with controls saves you from having to think about the physical size of strings you are manipulating, and thus greatly reduces the likelihood of memory overwrites. Second, this code work for single selection list strings, but not for multiple selection string. Multiple selection list don't have a "current" selection. For multiple selection lists, you must first query the control for number of selections, allocate a buffer to hold an array of selection item indices, get the sel indices, and then loop through the list or combo control and get the strings. Here's a code fragment that demonstrates this strategy:

//get the count of selections
j = SendMessage(hWndCombo, LB_GETSELCOUNT , 0, 0 );

//allocate an integer array to hold the selection indices
piSelIndices = (LPINT)LocalAlloc( LPTR, sizeof(int) * j;
//ALWAYS check the return of an allocation call
if( ! piSelIndices )
{
   //no alloc, fail & bail
   return FALSE;
}

//get the array of sel indices
SendMessage(hWndCombo, LB_GETSELITEMS, j, (long) piSelIndices);

//loop thru the array and get the strings at the selection indices
for( k = 0; k < j; k++, piSelIndices++ )
{
   //copy the string at index j to the buffer
   SendMessage(hWndCombo, LB_GETLBTEXT , j, (long)&szSelString );
         .
   //do something with the string
          .
}

Wrapping Up

This is a tidy solution for displaying forms on small devices, but again we face limitations of screen real estate. What if your form simply can't be condensed sufficiently to fit on single screen of command bands? In the next installment, we'll learn how to create "pages" of command bands. I'll show you how to set up the controls so that users can move back and forth between screens of bands, using controls that look and behave like the "forward" and "back" buttons on a web browser.

Download the Source

DataBands.zip - 7 kb

About the Author

Nancy Nicolaisen is a software engineer who has designed and implemented highly modular Windows CE products that include features such as full remote diagnostics, CE-side data compression, dynamically constructed user interface, automatic screen size detection, entry time data validation.

In addition to writing for Developer.com, she has written several books including Making Win 32 Applications Mobile. She has also written numerous articles on programming technology for national publications including Dr. Dobbs, BYTE Magazine, Microsoft Systems Journal, PC Magazine; Computer Shopper, Windows Sources and Databased Advisor.

# # #

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved