Order the book from O'Reilly

Previous PageTable Of ContentsIndexNext Page

In this chapter:

 5.  Forms and
Form Objects

This chapter describes forms and form objects. Before we cover these subjects, however, we explain how the resources associated with the forms are created and used. Your application is stored in the form of resources. Once we discuss resources and forms in general, we give you some programming tips for creating specific types of forms (like alerts). Last, we turn to a discussion of resources and forms in the sample application.

Resources

Top Of Page

A resource is a relocatable block marked with a four-byte type (usually represented as four characters, like CODE or tSTR) and a two-byte ID. Resources are stored in a resource database (on the desktop, these files end in the extension .PRC).

An application is a resource database. One of the resources in this database contains code, another resource contains the application's name, another the application's icon, and the rest contain the forms, alerts, menus, strings, and other elements of the application. The Palm OS uses these resources directly from the storage heap (after locking them) via the Resource Manager.

The two most common tools to create Palm OS application resources are CodeWarrior's Constructor tool or PilRC as part of the GCC collection of tools. Our discussion turns to Constructor first and PilRC second.

Creating Resources in Constructor

CodeWarrior's Constructor is a visual resource editor: you lay out the user interface object resources using a graphical layout tool.

In the following example, we take a peek at the Forms section of the resource file. You will see how to use the New Form Resource menu item and select the name and change it to be called "Main" (see Figure 5-1).

Figure 5- 1. Creating a form resource in Constructor

The following discussion is not a tutorial on how to use Constructor. The Code Warrior documentation does a fine job at that. Rather, it is intended to be just enough information to give you a clear idea of what it's like to create a resource using Constructor.

To add a particular type of object resource to a form-a button, for instance-you drag it from the catalog window and drop it into the form (see Figure 5-2). Clicking on any item that has been dropped into the form allows you to edit its attributes. Double-clicking brings up a separate window.

If you look at Figure 5-3, you will see several windows: one shows you all the items in your form; another shows you the hierarchy of your form and its objects (as shown in the Object Hierarchy window); and last, but not least, another shows editing a form. In Figure 5-3, the top left window is the form window used to edit the form shown at the top right. The bottom left window is an editor for the Done button. The bottom right window shows the hierarchy of items on this particular form.

Figure 5- 2. The catalog window from which you can drag-and-drop an item to a form

Figure 5- 3. Editing a form

There are a couple of worthwhile things to know about creating resources in Constructor.

Use constants rather than raw resource IDs

When using Constructor to create resources, you won't be embedding resource IDs directly in your code as raw numbers like this:

FrmAlert(1056);

Instead, you should create symbolic constants for each of the resource IDs:

#define  TellUserSomethingAlert 1056

Thus, when you use the resource with a function, you will have code that looks like this:

FrmAlert(TellUserSomethingAlert);

Using constants for your resources

Constructor rewards you for creating symbolic constants for your resources. When it saves the resource file, it also saves a corresponding header file with all symbolic constant definitions nicely and neatly laid out for you (Figure 5-4 shows you how to choose the header filename). The names it creates are based on the type of the resource and the resource name you've provided.

Figure 5- 4. Specifying the header file Constructor generates with ID definitions

This is Constructor's way of keeping you from editing the resource file directly-that's Constructor's job and strictly hands off to you. For one thing, Constructor can change IDs on an item. Further, your project development or maintenance will not work correctly. You are supposed to use Constructor only for resource editing, whether that is adding, deleting, or renumbering them. To keep things all lined up nicely, Constructor regenerates the header file after any change, ensuring that your constant definitions match exactly the resources that exist.

NOTE:

Constructor creates constants not only for resource IDs, but for form object IDs (see "Form Objects," later in this chapter) as well.

Here's the header file generated by Constructor for the resource file we created in Figure 5-3. As you can see in the comments, you are not supposed to fiddle with this file:

// Header generated by Constructor for Pilot 1.0.2
//
// Generated at 10:55:44 AM on Friday, July 10, 1998
//
// Generated for file: Macintosh HD::MyForm.rsc
//
// THIS IS AN AUTOMATICALLY GENERATED HEADER FILE FROM
// CONSTRUCTOR FOR PALMPILOT;
// - DO NOT EDIT - CHANGES MADE TO THIS FILE WILL BE LOST
//
// Pilot App Name:    "Untitled"
//
// Pilot App Version: "1.0"

// Resource: tFRM 8000
#define MainForm                                  8000
#define MainDoneButton                            8002
#define MainNameField                             8001
#define MainUnnamed8003Label                      8003

Constructor has generated constants for every resource in the file; one for the form and three for the form objects.

Creating Resources in PilRC

PilRC is a resource compiler that takes textual descriptions (stored in an .RCP file) of your resources and compiles them into the binary format required by a .PRC file. Unlike Constructor, PilRC doesn't allow you to visually create your resources; instead, you type in text to designate their characteristics. There is a way to see what that PilRC text-based description will look like visually, however. You can use PilRCUI, a tool that reads an .RCP file and displays a graphic preview of that file. This allows you to see what your resource objects are going to look like on the Palm device (see Figure 5-5).

Figure 5- 5. PilRCUI displaying a preview of a form from an .RCP file

The pretty points of PilRC

PilRC does do some of the grunt work of creating resources for you. For example, you don't need to specify a number for every item's top and left coordinates, and every item's width and height. PilRC has a mechanism for automatically calculating the width or height of an item based on the text width or height. This works especially well for things like buttons, push buttons, and checkboxes.

It also allows you to specify the center justification of items. Beyond this, you can even justify the coordinates of one item based on those of another; you use the width/height of the previous item. These mechanisms also make it possible to specify the relationships between items on a form, so that changes affect not just one, but related groups of items. Thus, you can move an item or resize it and have that change affect succeeding items on the form as well.

PilRC example

Here's a PilRC example. It is a simple form that contains:

Figure 5-5 shows you what this text description looks like graphically:

FORM ID 1 AT (2 2 156 156)
USABLE
MODAL
BEGIN
   TITLE "Foo"
   LABEL "Choose one:" 2001  AT (8 16) 

   CHECKBOX "Check 1" ID 2002 AT (PrevRight PrevBottom+3 AUTO AUTO) GROUP 1
   CHECKBOX "Another choice" ID 2003 AT (PrevLeft PrevBottom+3 AUTO AUTO) 
      GROUP 1
   CHECKBOX "Maybe" ID 2004 AT (PrevLeft PrevBottom+3 AUTO AUTO) GROUP 1

   BUTTON "Test1" ID 2006 AT (7 140 AUTO AUTO)
   BUTTON "Another" ID 2007 AT (PrevRight+5 PrevTop AUTO AUTO)
   BUTTON "3rd" ID 2008 AT (PrevRight+5 PrevTop AUTO AUTO)
END

Just as Constructor discourages you from embedding resource IDs directly into your code as raw numbers (see Figure 5-3), similarly, you shouldn't embed resource IDs directly into your .RCP files. The right way to do this with PilRC is to use constants.

Using constants for your resources

PilRC doesn't automatically generate symbolic constants, as Constructor does. PilRC does, however, have a mechanism for unification. If you create a header file that defines symbolic constants, you can include that header file both in your C code and in your PilRC .RCP definition file. PilRC allows you to include a file using #include and understands C-style #define statements. You'll simply be sharing your #defines between your C code and your resource definitions.

NOTE:

PilRC does have an -H flag that automatically creates resource IDs for symbolic constants you provide.

Here's a header file we've created, ResDefs.h, with constant definitions (similar to the kind that Constructor generates automatically):

#define MainForm         8000
#define MainDoneButton   8002
#define MainNameField    8001

We include that in our .c file and then include it in our resources.rcp file:

#include "ResDefs.h"

FORM ID MainForm AT (0 0 160 160)
BEGIN
   TITLE "Form title"
   LABEL "Name:" AUTOID AT (11 35) FONT 1
   FIELD ID MainNameField AT (PrevRight PrevTop 50 AUTO) UNDERLINED
      MULTIPLELINES MAXCHARS 80
   BUTTON "Done" ID MainDoneButton AT (CENTER 143 AUTO AUTO) 
END

Note that the label doesn't have an explicit ID but uses AUTOID. An ID of AUTOID causes PilRC to create a unique ID for you automatically. This is handy for items on a form that you don't need to refer to programmatically from your code as is often the case with labels, for example.

Reading Resources

Occasionally, you may need to use the Resource Manager to directly obtain a resource from your application's resource database. Here's what you do:

1. Get a handle to the resource.

2. Lock it.

3. Mess with it, doing whatever you need to do.

4. Unlock it.

5. Release it.

You modify a resource with a call to  DmGetResource. This function gives you a handle to that resource as an unlocked relocatable block. To find the particular resource you want, you specify the resource type and ID when you make the call. DmGetResource searches through the application's resources and the system's resources. When it finds the matching resource, it marks it busy and returns its handle. You lock the handle with a call to MemHandleLock. When you are finished with the resource, you call  DmReleaseResource to release it.

Here's some sample code that retrieves a string resource, uses it, and then releases it:

Handle  h;
CharPtr s;

h = DmGetResource('tSTR', 1099);
s = MemHandleLock(h);
// use the string s
MemHandleUnlock(h);
DmReleaseResource(h);

Actually, DmGetResource searches through the entire list of open resource databases, including the system resource database stored in ROM. Use  DmGet1Resource to search through only the topmost open resource database; this is normally your application.

Writing Resources

Although it is possible to write to resources (see "Modifying a Record" on page 149), it is uncommon; most resources are used only for reading.

Forms

Top Of Page

As we discussed earlier, a form is a container for the application's visual elements. A form is created based on information from a resource (of type "tFRM") that describes the elements. There are both modal and modeless forms in an application. The classic example of a modal form is an alert. Other forms can be made modal but require extra work on your part.

NOTE:

A modal dialog is different from a modeless form in:

· Appearance: a modal dialog has a full-width titlebar with the title centered and with buttons from left to right along the bottom. Most modal dialogs should have an info button that provides additional help.

· Behavior: the Find button doesn't work while a modal dialog is being displayed.

In the following material, we first discuss alerts and then modal forms. We also offer several tips in each section.

Alerts

An alert is a very constrained form (based on a "Talt" resource); it is a modal dialog with an icon, a message, and one or more buttons at the bottom that dismiss the dialog (see Figure 5-6). As we discussed in Chapter 3, Designing a Solution, there are four different types of alerts (information, warning, confirmation, and error). The user can distinguish the alert type by the icon shown.

-Figure 5- 6. An alert showing an icon, a message, and a button

The return result of  FrmAlert is the number of the button that was pressed (where the first button is number 0).

Customizing an alert

It is worth noting that you can customize the message in an alert. You do so with runtime parameters that allow you to make up to three textual substitutions in the message. In the resource, you specify a placeholder for the runtime text with ^1, ^2, or ^3. Instead of calling FrmAlert, you call  FrmCustomAlert. The first string replaces any occurrence of ^1, the second replaces any occurrence of ^2, and the third replaces occurrences of ^3.

NOTE:

When you call FrmCustomAlert, you can pass NULL as the text pointer only if there is no corresponding placeholder in the alert resource. If there is a corresponding placeholder, then passing NULL will cause a crash; pass a string with one space in it (" ") instead.

NOTE:

That is, if your alert message is "My Message ^1 (^2)", you can call:

FrmCustomAlert(MyAlertID, "string", " ", NULL)

NOTE:

but not this:

FrmCustomAlert(MyAlertID, "string", NULL, NULL)

User interface guidelines recommend that modal dialogs have an info button at the top right that provides help for the dialog. To do so, create a string resource with your help text and specify the string resource ID as the help ID in the alert resource.

NOTE:

Make sure that any alerts you display with FrmAlert don't have ^1, ^2, or ^3 in them. FrmAlert(alertID) is equivalent to FrmCustomAlert(alertID, NULL, NULL, NULL). The Form Manager will try to replace any occurrences of ^1, ^2, or ^3 with NULL, and this will certainly cause a crash.

Alert example

Here's a resource description of an alert with two buttons:

#define MyAlert 1000

ALERT ID MyAlert
CONFIRMATION
BEGIN
    TITLE "My Alert Title (^1)"
    MESSAGE "My Message (^1) (^2)  (^1)"
    BUTTONS "OK" "Cancel"
END

If you display the alert with FrmCustomAlert, it appears as shown in Figure 5-7:

if (FrmCustomAlert(MyAlert, "foo", "bar", NULL) == 0) {
   // user pressed OK
} else {
   // user pressed Cancel
}    

Figure 5- 7. An alert displayed with FrmCustomAlert; note that FrmCustomAlert doesn't replace strings in the title

 

Tips on creating alerts

Here are a few tips that will help you avoid common mistakes:

Button capitalization

Buttons in an alert should be capitalized. Thus, a button should be titled "Cancel" and not "cancel".

OK buttons

An "OK" button should be exactly that. Don't use "Ok", "Okay", "ok", or "Okey-dokey". OK?

Using ^1, ^2, ^3

The ^1, ^2, ^3 placeholders aren't replaced in the alert title or in buttons but are replaced only in the alert message.

Modal Dialogs

The easiest way to display a modal dialog is to use FrmAlert or FrmCustomAlert. The fixed structure of alerts (icon, text, and buttons) may not always match what you need, however. For example, you may need a checkbox or other control in your dialog.

Modal form template

If you need this type of flexible modal dialog, use a form resource (setting the modal attribute of the form) and then display the dialog using the following code:

// returns object ID of hit button
static Word DisplayMyFormModally(void)
{
    FormPtr previousForm = FrmGetActiveForm();
    FormPtr frm = FrmInitForm(MyForm);
    Word    hitButton;
    
    FrmSetActiveForm(frm);
    
// Set an event handler, if you wish, with FrmSetEventHandler
// Initialize any form objects in the form

    hitButton = FrmDoDialog(frm);
    
    // read any values from the form objects here
    // before the form is deleted
    
    if (previousForm)
        FrmSetActiveForm(previousForm);
    FrmDeleteForm(frm);
    return hitButton;
}
NOTE:

 FrmDoDialog is documented to return the number of the tapped button, where the first button is 0. Actually, it returns the button ID of the tapped button.

NOTE:

For example, if you've got a form with an icon, a label, and two buttons, where the first button has a button ID of 1002 and the second button has a button ID of 1001, FrmDoDialog will return either 1002 or 1001, depending on whether the first or second button is pressed.

Modal form example

Here we have an example that displays a modal dialog with a checkbox in it (see Figure 5-8). The initial value of the checkbox is determined by the parameter to TrueOrFalse. The final value of TrueOrFalse is the value of the checkbox (if the user taps OK) or the initial value (if the user taps Cancel). This demonstrates setting form object values in a modal form before displaying it and reading values from a modal form's objects after it is done:

// takes a true/false value and allows the user to edit it
static Boolean TrueOrFalse(Boolean initialValue)
{
   FormPtr previousForm = FrmGetActiveForm();
   FormPtr frm = FrmInitForm(TrueOrFalseForm);
   Word    hitButton;
   ControlPtr  checkbox = FrmGetObjectPtr(frm, 
            FrmGetObjectIndex(frm, TrueOrFalseCheckbox));
   Boolean  newValue;

   FrmSetActiveForm(frm);
    
// Set an event handler, if you wish, with FrmSetEventHandler

   CtlSetValue(checkbox, initialValue);

   hitButton = FrmDoDialog(frm);
    
   newValue = CtlGetValue(checkbox);

   if (previousForm)
      FrmSetActiveForm(previousForm);
   FrmDeleteForm(frm);
   if (hitButton == TrueOrFalseOKButton)
      return newValue;
   else
      return initialValue;
}

Figure 5- 8. The modal form that allows you to edit a true/false value with a checkbox

A tip for modal forms

When you call  FrmDoDialog with a modal form, your event handler won't get a frmOpenEvent, and it doesn't have to call FrmDrawForm. Since your event handler won't be notified that the form is opening, any form initialization must be done before you call FrmDoDialog.

Modal form sizes

You don't want your modal form to take up the entire screen real estate. Center it horizontally at the bottom of the screen, and make sure that the borders of the form can be seen. You'll need to inset the bounds of your form by at least two pixels in each direction.

Help for modal forms

The Palm user interface guidelines specify that modal dialogs should provide online help through the "i" button at the top right of the form (see Figure 5-9). You provide this help text as a string resource (tSTR) that contains the appropriate help message. In your form (or alert) resource, you then set the help ID to the string resource ID. The Palm OS takes care of displaying the "i" button (only if the help ID is nonzero) and displaying the help text if the button is tapped.

Figure 5- 9. Modal dialog (left) with "i" button bringing up help (right)

Form Objects

Top Of Page

The elements that populate a form are called form objects. Before we get into the details of specific form objects, however, there are some very important things to know about how forms deal with all form objects.

Many of the form objects post specific kinds of events when they are tapped on, or used. To use a particular type of form object, you need to consult the Palm OS documentation to see what kinds of events that form object produces.

Dealing with Form Objects in Your Form Event

Form objects communicate their actions by posting events. Most of the form objects have a similar structure:

1. When the stylus is pressed on the object, it sends an enter event.

2. In response to the enter event, the object responds appropriately while the stylus is pressed down. For example: a button highlights while the pen is within the button and unhighlights while it is outside the button; a scrollbar sends sclRepeatEvents while the user has a scroll arrow tapped; a list highlights the row the stylus is on and scrolls, if necessary, when the pen reaches the top or bottom of the list.

3. When the stylus is released:

In all these events, the ID of the tapped form object and a pointer to the form object itself are provided as part of the event. The ID allows you to distinguish between different instances that generate the same types of events. For example, two buttons would both generate a ctlSelectEvent when tapped; you need the IDs to know which is which.

Events generated by a successful tap

Most often, you want to know only when an object has been successfully tapped; that is, the user lifts the stylus while still within the boundaries of the object. You'll be interested in these events:

Events generated by repeated taps

Sometimes, you'll need to be notified of a repetitive action while a form object is being pressed. The events are ctlRepeatEvent (used for repeating buttons) and sclRepeatEvent.

Events generated by the start of a tap

Occasionally, you'll want to know when the user starts to tap on a form object. For example, when the user starts to tap on a pop-up trigger you may want to dynamically fill in the contents of the pop-up list before it is displayed. You'd do that in the ctlEnterEvent, looking for the appropriate control ID. The events sent when the user starts to tap on a form object are:

Events generated by the end of an unsuccessful tap

Rarely, you'll want to know when a form object has been unsuccessfully tapped (the user tapped the object, but scuttled the stylus outside the boundaries before lifting it). For example, if you allocate some memory in the enter event, you'd deallocate the memory in both the select event and in the corresponding exit event (covering all your bases, so to speak). The events are ctlExitEvent, lstExitEvent, sclExitEvent, and tblExitEvent.

NOTE:

Note that although there is a frmTitleSelectEvent, there is no corresponding frmTitleExitEvent. We know of no reason why this is so.

Getting an Object from a Form

 Whenever you need to do something with an object, you will need to get it from the form. You do this with a pointer and the function FrmGetObjectPtr. Note that FrmGetObjectPtr takes an object index and not an object ID. The return result of FrmGetObjectPtr depends on the type of the form object.

Types of form object pointers

FrmGetObjectPtr returns one of the following, depending on the type of the form object kept at that index:

Code example

If you pass the correct index into FrmGetObjectPtr, you can safely typecast the return result. For example, here we get a field from the form and cast it to a FieldPtr:

FormPtr frm = FrmGetActiveForm();
FieldPtr fld = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, MainMyField));
NOTE:

C doesn't require an explicit typecast when casting from a void * such as the return result of FrmGetObjectPtr. It automatically typecasts for you. C++, on the other hand, requires an explicit typecast in that situation.

Error checking

You can use  FrmGetObjectType with FrmGetObjectPtr to ensure that the type of the form object you retrieve is the type you expect. Here's an example that retrieves a FieldPtr, using additional error checking to verify the type:

FieldPtr GetFieldPtr(FormPtr frm, Word objectIndex)
{
    ErrNonFatalDisplayIf(FrmGetObjectType(frm, objectIndex <> frmFieldObj,
      "Form object isn't a field"
   return (FieldPtr) FrmGetObjectPtr(frm, objectIndex);
}

In a finished application, of course, your code shouldn't be accessing form objects of the wrong type. During the development process, however, it is frightfully easy to accidentally pass the wrong index to FrmGetObjectPtr. Thus, using a safety checking routine like GetFieldPtr can be helpful in catching programming errors that are common in early development. 

Form Object "Gotchas"

Here are a couple of problems to watch out for when dealing with form functions and handling form objects:

Remember which form functions require object IDs versus object indexes

You must keep track of which form functions require form object IDs and which require form object indexes. If you pass an object ID to a routine that expects an object index, you'll probably cause the device to crash. Remember that you can translate back and forth between these two using FrmGetObjectID and FrmGetObjectIndex whenever it's necessary.

Bitmaps don't have object IDs

Bitmaps on a form don't have an associated object ID. This can be a problem if you want to do hit-testing on a bitmap, for instance. In such cases, you can create a gadget that has the same bounds as the bitmap and do hit-testing on it. This has an associated object ID.

Specific Form Objects

Now that you have an idea how forms interact with form objects, it is time to look at the quirks associated with programming particular form objects. Concerning these form objects there is both good news and bad. Let's start with the good.

We don't discuss any of the following objects, because their creation and coding requirements are well documented and straightforward:

The bad news is that the rest of the form objects require further discussion. Indeed, some objects, like editable text field objects, require extensive help before you can successfully add them to an application. Here is the list of objects that we are going to discuss further:

Label Objects

Label objects can be a little bit tricky if you are going to change the label at runtime. They are a snap if the label values don't switch.

Changing the text of a label

To change the text of a label form object, use  FrmCopyLabel. Unfortunately, FrmCopyLabel only redraws the new label, while not erasing the old one. You can have problems with this in the case where the new text is shorter than the old text; remnants of the old text are left behind. One way to avoid this problem is to hide the label before doing the copy and then show it afterward. Here is an example of that:

FormPtr frm = FrmGetActiveForm();
Word    myLabelObjectIndex = FrmGetObjectIndex(frm, MainMyLabel);

FrmHideObject(frm, myLabelObjectIndex);
FrmCopyLabel(FrmGetActiveForm(), MainMyLabel, "newText");
FrmShowObject(frm, myLabelObjectIndex);
NOTE:

To change the label of a control (like a checkbox, for instance), use CtlSetLabel, not FrmCopyLabel.

Problems with labels longer than the resource specification

You will also have trouble if the length of the new label is longer than the length specified in the resource. Longer strings definitely cause errors, since FrmCopyLabel blindly writes beyond the space allocated for the label.

In general, you should realize that labels aren't well suited for text that needs to change at runtime. In most cases, if you've got some text on the screen that needs to change, you are better off not using a label. A preferable choice, in such instances, is a field that has the editable and underline attributes turned off.

Gadget Objects

Once you have rifled through the other objects and haven't found anything suitable for the task you have in mind, you are left with using a gadget. A gadget is the form object you use when nothing else will do.

A gadget is a custom form object with an on-screen bounds that can have data programmatically associated with it. (You can't set data for a gadget from a resource.) It also has an object ID. That's all the Form Manager knows about a gadget: bounds, object ID, and a data pointer. Everything else you need to handle yourself.

What the gadget is responsible for

The two biggest tasks the gadget needs to handle are:

There are two times when the gadget needs to be drawn-when the form first gets opened and whenever your event handler receives a frmUpdateEvent (these are the same times you need to call FrmUpdateForm).

If you'll be saving data associated with the gadget, use the function FrmSetGadgetData. You also need to initialize the data when your form is opened.

NOTE:

Although you could draw and respond to taps without a gadget, it has three advantages over a totally custom-coded structure:

· The gadget maintains a pointer that allows you to store gadget-specific data.

· The gadget maintains a rectangular bounds specified in the resource.

· Gremlins, the automatic random tester (see page 293), recognizes gadgets and taps on them. This is an enormous advantage, because Gremlins relentlessly tap on them during testing cycles. While it is true that it will tap on areas that lie outside the bounds of any form object, it is a rare event. Gremlins are especially attracted to buttons and objects and like to spend time tapping in them. If you didn't use gadgets, your code would rarely receive taps during this type of testing.

A sample gadget

Let's look at an example gadget that stores the integer 0 or 1 and displays either a vertical or horizontal line. Tapping on the gadget flips the integer and the line. Here's the form's initialization routine that initializes the data in the gadget and then draws it:

FormPtr  frm = FrmGetActiveForm();
VoidHand h = MemHandleNew(sizeof(Word));

if (h) {
   * (Word *) MemHandleLock(h) = 1;
   MemHandleUnlock(h);
   FrmSetGadgetData(frm, FrmGetObjectIndex(frm, MainG1Gadget), h);
}

// Draw the form.
FrmDrawForm(frm);
GadgetDraw(frm, MainG1Gadget);

When the form is closed, the gadget's data handle must be deallocated:

VoidHand h;
FormPtr  frm = FrmGetActiveForm();

h = FrmGetGadgetData(frm, FrmGetObjectIndex(frm, MainG1Gadget));
if (h)
   MemHandleFree(h);

Here's the routine that draws the horizontal or vertical line: 

// draws  | or - depending on the data in the gadget
static void GadgetDraw(FormPtr frm, Word gadgetID)
{
   RectangleType  bounds;
   UInt           fromx, fromy, tox, toy;
   Word           gadgetIndex = FrmGetObjectIndex(frm, gadgetID);
   VoidHand       data = FrmGetGadgetData(frm, gadgetIndex);
   
   if (data) {
      WordPtr        wordP = MemHandleLock(data);
      
      FrmGetObjectBounds(frm, gadgetIndex, &bounds);
      switch (*wordP) {
      case 0:
         fromx = bounds.topLeft.x + bounds.extent.x / 2;
         fromy = bounds.topLeft.y;
         tox = fromx;
         toy = fromy + bounds.extent.y - 1;
         break;
      case 1:
         fromx = bounds.topLeft.x;
         fromy = bounds.topLeft.y + bounds.extent.y / 2;
         tox = fromx + bounds.extent.x - 1;
         toy = fromy;
         break;
      default:
         fromx = tox = bounds.topLeft.x;
         fromy = toy = bounds.topLeft.y;
         break;
      }
      MemHandleUnlock(data);
      WinEraseRectangle(&bounds, 0);
      WinDrawLine(fromx, fromy, tox, toy);
   }
}

Every time the user taps down on the form, the form's event handler needs to check to see whether the tap is on the gadget. It does so by comparing the tap point with the gadget's bounds. Here is an example:

case penDownEvent:
   {
      FormPtr        frm = FrmGetActiveForm();
      Word           gadgetIndex = FrmGetObjectIndex(frm, MainG1Gadget);
      RectangleType  bounds;
      
      FrmGetObjectBounds(frm, gadgetIndex, &bounds);
      if (RctPtInRectangle (event->screenX, event->screenY, &bounds)) {
         GadgetTap(frm, MainG1Gadget, event);
         handled = true;
      }
   }
   break;

The  GadgetTap function handles a tap and acts like a button (highlighting and unhighlighting as the stylus moves in and out of the gadget):

// it'll work like a button: Invert when you tap in it.
// Stay inverted while you stay in the button. Leave the button, uninvert,
// let go outside, nothing happens; let go inside, data changes/redraws
static void GadgetTap(FormPtr frm, Word gadgetID, EventPtr event)
{
   Word           gadgetIndex = FrmGetObjectIndex(frm, gadgetID);
   VoidHand       data = FrmGetGadgetData(frm, gadgetIndex);
   SWord          x, y;
   Boolean        penDown;
   RectangleType  bounds;
   Boolean        wasInBounds = true;
   
   if (data) {
      FrmGetObjectBounds(frm, gadgetIndex, &bounds);
      WinInvertRectangle(&bounds, 0);
      do {
         Boolean  nowInBounds;
         
         PenGetPoint (&x, &y, &penDown);
         nowInBounds = RctPtInRectangle(x, y, &bounds);
         if (nowInBounds != wasInBounds) {
            WinInvertRectangle(&bounds, 0);
            wasInBounds = nowInBounds;
         }
      } while (penDown);
      if (wasInBounds) {
         WordPtr  wPtr = MemHandleLock(data);
         *wPtr = !(*wPtr)
         MemHandleUnlock(data);
         
         // GadgetDraw will erase--we don't need to invert
         GadgetDraw(frm, gadgetID);
      } // else gadget is already uninverted
   }
}

If we wanted to have multiple gadgets on a single form, we'd need to modify the form open and close routines to allocate and deallocate handles for each gadget in the form. In addition, we'd have to modify the event handler to check for taps in the bounds of each gadget, rather than just the one.

List Objects

A list can be used as is without any programmatic customization. In the resource, you can specify the text of each list row and the number of rows that can be displayed at one time (the number of visible items). The list will automatically provide scroll arrows if the number of items is greater than the number that can be shown.

Lists are used both alone and with pop-up triggers (see "Pop-up Trigger Objects" later in this chapter). If you are using a standalone list, you'll receive a lstSelectEvent when the user taps on a list item. The list manager highlights the selected item.

You can manipulate the display of a list in two ways:

You can get information from it using three different routines:

Sample that displays a specific list item

Here's some sample code that selects the 11th item in a list (the first item is at 0) and scrolls the list, if necessary, so that it is visible:

FormPtr  frm = FrmGetActiveForm();
ListPtr  list = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, MainMyList));

LstSetSelection(list, 10);
LstMakeItemVisible(list, 10);

Custom versus noncustom lists

If you want to specify the contents of the list at runtime, there are two ways to do it:

You'll find that the second way is almost always easier than the first. Let's look at a sample written twice, using the first approach and again using the second.

The sample draws a list composed of items in a string list resource. A string list resource contains:

There's no particular significance to retrieving the items from a string list resource; we just needed some example that required the runtime retrieval of the strings.

Here's a C structure defining a string resource:

typedef struct StrListType {
   char prefixString;       // we assume it's empty
   char numStringsHiByte;   // we assume it's 0
   char numStrings;         // low byte of the count
   char firstString[1];     // more than 1-all concated together
} *StrListPtr;
NOTE:

This sample asssumes that the prefix string is empty, and that there are no more than 255 strings in the string list. For a sample that has been modified to correctly handle more general cases, see http://www.calliopeinc.com/PalmProgramming.

Using the first approach, we need to create an array with each element pointing to a string. The easiest way to create such an array is with  SysFormPointerArrayToStrings. This routine takes a concatenation of null-terminated strings and returns a newly allocated array that points to the beginning of each string in the concatenation. We lock the return result and pass it to  LstSetListChoices:

static void MainViewInit(void)
{
   FormPtr        frm = FrmGetActiveForm();
   
   gStringsHandle = DmGetResource('tSTL', MyStringList);
   if (gStringsHandle) {
      ListPtr  list = FrmGetObjectPtr(frm, 
         FrmGetObjectIndex(frm, MainMyList));
      StrLstPtr   stringsPtr = = MemHandleLock(gStringsHandle);
      
      gStringArrayH = SysFormPointerArrayToStrings(
         stringsPtr->firstString, stringsPtr->numStrings);
      LstSetListChoices(list, MemHandleLock(gStringArrayH), 
         stringsPtr->numStrings);
   }
   // Draw the form.
   FrmDrawForm(frm);
}

The resource handle and the newly allocated array are stored in global variables so that they can be deallocated when the form closes:

static VoidHand  gStringArrayH = 0;
static VoidHand  gStringsHandle = 0;

Here's the deallocation routine where we deallocate the allocated array, and unlock and release the resource:

static void MainViewDeInit(void)
{
   if (gStringArrayH) {
      MemHandleFree(gStringArrayH);
      gStringArrayH = NULL;
   }

   if (gStringsHandle) {
      MemHandleUnlock(gStringsHandle);
      DmReleaseResource(gStringsHandle);
      gStringsHandle = NULL;
   }
}

Here's the alternative way of customizing the list at runtime. Our drawing function to draw each row is similar. Our initialization routine must initialize the number of rows in the list and must install a callback routine:

static void MainViewInit(void)
{
   FormPtr        frm = FrmGetActiveForm();
   
   VoidHand stringsHandle = DmGetResource('tSTL', MyStringList);
   if (stringsHandle) {
      StrListPtr  stringsPtr;
      ListPtr  list = FrmGetObjectPtr(frm, 
         FrmGetObjectIndex(frm, MainMyList));
      
      stringsPtr = MemHandleLock(stringsHandle);
      LstSetListChoices(list, NULL, stringsPtr->numStrings);
      MemHandleUnlock(stringsHandle);
      DmReleaseResource(stringsHandle);
      
      LstSetDrawFunction(list, ListDrawFunc);
   }

   // Draw the form.
   FrmDrawForm(frm);
}

 ListDrawFunc gets the appropriate string from the list and draws it. If the callback routine had wanted to do additional drawing (lines, bitmaps, etc.), it could have:

static void ListDrawFunc(UInt itemNum, RectanglePtr bounds, CharPtr *data)
{
   VoidHand stringsHandle = DmGetResource('tSTL', MyStringList);
   if (stringsHandle) {
      StrListPtr  stringsPtr;
      FormPtr     frm = FrmGetActiveForm();
      ListPtr     list = FrmGetObjectPtr(frm, 
         FrmGetObjectIndex(frm, MainMyList));
      CharPtr     s;
      
      stringsPtr = MemHandleLock(stringsHandle);
      s = stringsPtr->firstString;
      while (itemNum-- > 0)
         s += StrLen(s) + 1;  // skip this string, including null byte
      WinDrawChars(s, StrLen(s), bounds->topLeft.x, bounds->topLeft.y);
      MemHandleUnlock(stringsHandle);
      DmReleaseResource(stringsHandle);
   }
}

There is no cleanup necessary when the form is completed.

Note that the two different approaches had roughly the same amount of code. The first used more memory (because of the allocated array). It also kept the resource locked the entire time the form was open, resulting in possible heap fragmentation.

The second approach was somewhat slower, since, for each row, the resource was obtained, locked, iterated through to find the correct string, and unlocked. Note that if we'd been willing to keep the resource locked as we did in the first case, the times would have been very similar. The second approach had more flexibility in that the drawing routine could have drawn text in different fonts or styles, or could have done additional drawing on a row-by-row basis.

Pop-up Trigger Objects

Pop-up triggers need an associated list. The list's bounds should be set so that when it pops up, it will be equal to or bigger than the trigger. Otherwise, you get the ugly effect of a telltale fragment of the original trigger under the list. In addition, the usable attribute must be set to false so that it won't appear until the pop-up trigger is pressed.

When the pop-up trigger is pressed, the list is displayed. When a list item is chosen, the pop-up label is set to the chosen item. These actions occur automatically; no code needs to be written. When a new item is chosen from the pop-up, a popSelectEvent is sent. Some associated data goes with it that includes the list ID, the list pointer, a pointer to the trigger control, and the indexes of the previously selected item and newly selected items.

Here's an example resource:

#define MainForm        1100
#define MainTriggerID   1102
#define MainListID      1103

FORM ID 1100 AT (0 0 160 160)
BEGIN
    POPUPTRIGGER "States" ID MainTriggerID AT (55 30 44 12) 
        LEFTANCHOR NOFRAME FONT 0
        POPUPLIST ID MainTriggerID MainListID
    LIST "California" "Kansas" "New Mexico" "Pennsylvania" "Rhode Island"
        "Wyoming" ID MainListID AT (64 29 63 33) NONUSABLE DISABLED FONT 0
END

Here's an example of handling a popSelectEvent in an event handler:

case popSelectEvent: 
      // do something with following fields of event->data.popSelect
      //   controlID
      //   controlPtr
      //   listID
      //   listP
      //   selection
      //   priorSelection
   break;

Text Objects

Editable text objects require attention to many details.

Setting text in a field

 Accessing an editable field needs to be done in a particular way. In the first place, you must use a handle instead of a pointer. The ability to resize the text requires the use of a handle. You must also make sure to get the field's current handle and expressly free it in your code. Here is some sample code that shows you how to do this:

static FieldPtr SetFieldTextFromHandle(Word fieldID, Handle txtH)
{
   Handle      oldTxtH;
   FormPtr     frm = FrmGetActiveForm();
   FieldPtr    fldP;


   // get the field and the field's current text handle.
   fldP     = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, fieldID));
   ErrNonFatalDisplayIf(!fldP, "missing field");
   oldTxtH  = FldGetTextHandle(fldP);
   
   // set the field's text to the new text.
   FldSetTextHandle(fldP, txtH);
   FldDrawField(fldP);

   // free the handle AFTER we call FldSetTextHandle().
   if (oldTxtH) 
      MemHandleFree(oldTxtH);
   
   return fldP;
}  

The previous bit of code is actually quite tricky. The Palm OS documentation doesn't tell you that it's your responsibility to dispose of the field's old handle. (We get the field handle with FldGetTextHandle and dispose of it with MemHandleFree at the end of the routine.)

Were we not to dispose of the old handles of editable text fields in the application, we would get slowly growing memory leaks all over the running application. Imagine if every time an editable field were modified programmatically, its old handle were kept in memory, along with its new handle. It wouldn't take long for our running application to choke the application heap with its vampire-like hunger for memory. Further, debugging such a problem would require diligent sleuthing as the cause of the problem would not be readily obvious.

Last, we redraw the field with  FldDrawField. If we had not done so, the changed text wouldn't be displayed.

Note that when a form closes, each field within it frees its handle. If you don't want that behavior for a particular field, call  FldSetTextHandle(fld, NULL) before the field is closed. If a field has no handle associated with it, when the user starts writing in the field, the Field Manager automatically allocates a handle for it.

Here are some utility routines that are wrappers around the previous routine. The first one sets a field's text to that of a string, allocates a handle, and copies the string for you: 

// Allocates new handle and copies incoming string
static FieldPtr SetFieldTextFromStr(Word fieldID, CharPtr strP)
{
   Handle      txtH;
   
   // get some space in which to stash the string.
   txtH  = MemHandleNew(StrLen(strP) + 1);
   if (!txtH)
      return NULL;

   // copy the string to the locked handle.
   StrCopy(MemHandleLock(txtH), strP);

   // unlock the string handle.
   MemHandleUnlock(txtH);
   
   // set the field to the handle
   return SetFieldTextFromHandle(fieldID, txtH);
} 

The second utility routine clears the text from a field:

static void  ClearFieldText(Word fieldID)
{
   SetFieldTextFromHandle(fieldID, NULL);
}

Modifying text in a field

One way to make changes to text is to use  FldDelete, FldSetSelection, and FldInsert. FldDelete deletes a specified range of text.  FldInsert inserts text at the current selection ( FldSetSelection sets the selection). By making judicious calls to these routines, you can change the existing text into whatever new text you desire. The routines are easy to use. They have a flaw, however, that may make them inappropriate to use in some cases: FldDelete and FldInsert redraw the field. If you're making multiple calls to these routines for a single field (let's say, for example, you were replacing every other character with an "X"), you'd see the field redraw after every call. Users might find this distracting. Be careful with FldChanged events, as well, as they can overflow the event queue if they are too numerous.

An alternative approach exists that involves directly modifying the text in the handle. However, you must not change the text in a handle while it is being used by a field. Changing the text while the field is using it confuses the field and its internal information is not updated correctly. Among other things, line breaks won't work correctly.

To properly change the text, first remove it from the field, modify it, and then put it back. Here's an example of how to do that:

FormPtr     frm = FrmGetActiveForm();
FieldPtr    fld;
Handle      h;

// get the field and the field's current text handle.
fld      = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, Main1Field));
h = FldGetTextHandle(fld);
if (h) {
   CharPtr  s;
   
   FldSetTextHandle(fld, NULL);
   s = MemHandleLock(h);
  // change contents of s
   while (*s != '\0') {
      if (*s >= 'A' && *s <= 'Z')
         StrCopy(s, s+1);
      else
         s++;
   }

   MemHandleUnlock(h);
   FldSetTextHandle(fld, h);
   FldDrawField(fld);
}

This no-brainer example simply removes any uppercase characters in the field.  

Getting text from a field

To read the text from a field, you can use  FldGetTextHandle. It is often more convenient, however, to obtain a pointer instead by using  FldGetTextPtr. It returns a locked pointer to the text. Note that this text pointer can become invalid if the user subsequently edits the text (if there isn't enough room left for new text, the field manager unlocks the handle, resizes it, and then relocks it).

If the field is empty, it won't have any text associated with it. In such cases, FldGetTextPtr returns NULL. Make sure you check for this case.

Other aspects of a field that require attention

When a form containing editable text fields is displayed, one of the text fields should contain the focus; this means it displays an insertion point and receives any Graffiti input. You must choose the field that has the initial focus by setting it in your code. The user can change the focus by tapping on a field. The Form Manager handles changing the focus in this case.

You must also handle the prevFieldChr and nextFieldChr characters; these allow the user to move from field to field using Graffiti (the Graffiti strokes for these characters are and ).

To move the focus, use  FrmSetFocus. Here's an example that sets the focus to the MyFormMyTextField field:

FormPtr frm = FrmGetActiveForm();

FrmSetFocus(frm, FrmGetObjectIndex(frm, MyFormMyTextField));
NOTE:

Do not use FldGrabFocus. It changes the insertion point, but doesn't notify the form that the focus has changed. FrmSetFocus ends up calling FldGrabFocus anyway.

Field "gotchas"

As might be expected with such a complicated type of field, there are a number of things to watch out for in your code:

Preventing deallocation of a handle

When a form containing a field is closed, the field frees its handle (with FldFreeMemory). In some cases, this is fine (for instance, if the field automatically allocated the handle because the user started writing into an empty field). In other cases, it is not. For example, when you've used FldSetTextHandle so that a field will edit your handle, you may not want the handle deallocated-you may want to deallocate it yourself or retain it.

To prevent the field from deallocating your handle, call  FldSetTextHandle(fld, NULL) to set the field's text handle to NULL. Do this when your form receives a frmCloseEvent.

Preventing memory leaks

When you call FldSetTextHandle, any existing handle in the field is not automatically deallocated. To prevent memory leaks, you'll normally want to:

Don't use FldSetTextPtr and FldSetTextHandle together

  FldSetTextPtr should be used only for noneditable fields for which you'll never call FldSetTextHandle. The two routines do not work well together.

Remove the handle when editing a field

If you're going to modify the text within a field's handle, first remove the handle from the field with FldSetTextHandle(fld, NULL), modify the text, and then set the handle back again.

Compacting string handles

The length of the handle in a field may be longer than the length of the string itself, since a field expands a handle in chunks. When a handle has been edited with a field, call  FldCompactText to shrink the handle to the length of the string (actually, one longer than the length of the string for the trailing null byte).

Scrollbar Objects

A scrollbar doesn't know anything about scrolling or about any other form objects. It is just a form object that stores a current number, along with a minimum and maximum. The user interface effect is a result of the scrollbar's allowing the user to modify that number graphically within the constraints of the minimum and maximum.

NOTE:

Scrollbars were introduced in Palm OS 2.0 and therefore aren't available in the 1.0 OS. If you intend to run on 1.0 systems, your code will need to do something about objects that rely on scrollbars.

Scrollbar coding requirements

There are a few things that you need to handle in your code:

Here is how you do that. Your event handler receives a sclRepeatEvent while the user holds the stylus down and a sclExitEvent when the user releases the stylus. Your code is on the lookout for one or the other event, depending on whether your application wants to scroll immediately (as the user is scrolling with the scrollbar) or postpone the scrolling until the user has gotten to the final scroll position with the scrollbar.

Updating the scrollbar based on the insertion point

 Let's look at the code for a sample application that has a field connected to a scrollbar. We need a routine that will update the scrollbar based on the current insertion point, field height, and number of text lines ( FldGetScrollValues is designed to return these values):

static void UpdateScrollbar(void)
{
   FormPtr        frm = FrmGetActiveForm();
   ScrollBarPtr   scroll;
   FieldPtr       field;
   Word           currentPosition;
   Word           textHeight;
   Word           fieldHeight;
   Word           maxValue;
   
   field = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, Main1Field));
   FldGetScrollValues(field, &currentPosition, &textHeight, &fieldHeight);

   // if the field is 3 lines, and the text height is 4 lines
   // then we can scroll so that the first line is at the top 
   // (scroll position 0) or so the second line is at the top
   // (scroll postion 1). These two values are enough to see
   // the entire text.
   if (textHeight > fieldHeight)
      maxValue = textHeight - fieldHeight;
   else if (currentPosition)
      maxValue = currentPosition;
   else
      maxValue = 0;
      
   scroll = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, MainMyScrollBar));
   
   // on a page scroll, want to overlap by one line (to provide context)
   SclSetScrollBar(scroll, currentPosition, 0, maxValue, fieldHeight - 1);
}

We update the scrollbar when the form is initially opened:

static void MainViewInit(void)
{
   UpdateScrollbar();
   // Draw the form.
   FrmDrawForm(FrmGetActiveForm());
}

Updating the scrollbar when the number of lines changes

We've also got to update the scrollbar whenever the number of lines in the field changes. Since we set the hasScrollbar attribute of the field in the resource, when the lines change, the fldChangedEvent passes to our event handler (in fact, this is the only reason for the existence of the hasScrollbar attribute). Here's the code we put in the event handler:

case fldChangedEvent:
   UpdateScrollbar();
   handled = true;
   break;

At this point, the scrollbar updates automatically as the text changes.

Updating the display when the scrollbar moves

Next, we've got to handle changes made via the scrollbar. Of the two choices open to us, we want to scroll immediately, so we handle the sclRepeatEvent:

case sclRepeatEvent:
   ScrollLines(event->data.sclRepeat.newValue - 
      event->data.sclRepeat.value, false);
   break;

 ScrollLines is responsible for scrolling the text field (using  FldScrollField). Things can get tricky, however, if there are empty lines at the end of the field. When the user scrolls up, the number of lines is reduced. Thus, we have to make sure the scrollbar gets updated to reflect this change (note that up and down are constant enumerations defined in the Palm OS include files):

static void ScrollLines(int numLinesToScroll, Boolean redraw)
{
   FormPtr        frm = FrmGetActiveForm();
   FieldPtr       field;
   
   field = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, Main1Field));
   if (numLinesToScroll < 0)
      FldScrollField(field, -numLinesToScroll, up);
   else
      FldScrollField(field, numLinesToScroll, down);
      
   // if there are blank lines at the end and we scroll up, FldScrollField
   // makes the blank lines disappear. Therefore, we've got to update
   // the scrollbar
   if ((FldGetNumberOfBlankLines(field) && numLinesToScroll < 0) ||
      redraw)
      UpdateScrollbar();
}

Updating the display when the scroll buttons are used

Next on the list of things to do is handling the Scroll buttons. When the user taps either of the Scroll buttons, we receive a keyDownEvent. Here's the code in our event handler that takes care of these buttons:

case keyDownEvent:
  if (event->data.keyDown.chr == pageUpChr) {
      PageScroll(up);
      handled = true;
    } else if (event->data.keyDown.chr == pageDownChr) {
      PageScroll(down);
      handled = true;
   }
   break;

Scrolling a full page

Finally, here's our page scrolling function. Of course, we don't want to scroll if we've already scrolled as far as we can. FldScrollable tells us if we can scroll in a particular direction. We use ScrollLines to do the actual scrolling and rely on it to update the scrollbar:

static void PageScroll(DirectionType direction)
{
   FormPtr        frm = FrmGetActiveForm();
   FieldPtr       field;
   
   field = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, Main1Field));
   if (FldScrollable(field, direction)) {
      int linesToScroll = FldGetVisibleLines(field) - 1;
      
      if (direction == up)
         linesToScroll = -linesToScroll;
      ScrollLines(linesToScroll, true);
   }
}

Resources, Forms, and Form Objects
in the Sales Application

Top Of Page

Now that we have given you general information about resources, forms, and form objects, we will add them to the Sales application. We'll show you the resource definitions of all the forms, alerts, and help text. We won't show you all the code, however, as it would get exceedingly repetitious and not teach you anything new. In particular, we won't show the code to bring up every alert. We also postpone adding the table to the order form until "Tables in the Sample Application" on page 216.

We cover the forms and the code for them in order of increasing complexity. This yields the following sequence:

All the resources are shown in text as PilRC format. (This format is easier to explain than a bunch of screen dumps from Constructor.)

Alerts

Here are the defines for the alert IDs and for the buttons in the Delete Item alert (this is the alert that has more than one button):

#define RomIncompatibleAlert                      1001
#define DeleteItemAlert                           1201
#define DeleteItemOK                              0
#define DeleteItemCancel                          1
#define NoItemSelectedAlert                       1000
#define AboutBoxAlert                             1100

Here are the alerts themselves:

ALERT ID NoItemSelectedAlert
INFORMATION
BEGIN
    TITLE "Select Item"
    MESSAGE "You must have an item selected to perform this command. " \
            "To select an item, tap on the product name of the item."
    BUTTONS "OK"
END
ALERT ID RomIncompatibleAlert
ERROR
BEGIN
    TITLE "System Incompatible"
    MESSAGE "System Version 2.0 or greater is required to run this " \
            "application."
    BUTTONS "OK"
END

ALERT ID DeleteItemAlert
CONFIRMATION
BEGIN
    TITLE "Delete Item"
    MESSAGE "Delete selected order item?"
    BUTTONS "OK" "Cancel"
END

ALERT ID AboutBoxAlert
INFORMATION
BEGIN
   TITLE "Sales v. 1.0"
   MESSAGE "This application is from the book \"Palm Programming: The " \
      Developer's Guide\" by Neil Rhodes and Julie McKeehan."
   BUTTONS "OK"
END

We won't show every call to FrmAlert (the call that displays each of these alerts). Here, however, is a piece of code from OrderHandleMenuEvent, which shows two calls to FrmAlert. The code is called when the user chooses to delete an item. If nothing is selected, we put up an alert to notify the user of that. If an item is selected, we put up an alert asking if they really want to delete it:

if (!gCellSelected)
   FrmAlert(NoItemSelectedAlert);
else if (FrmAlert(DeleteItemAlert) == DeleteItemOK) {
      // code to delete an item
}

Delete Customer

Our Delete Customer dialog has a checkbox in it, so we can't use an alert. We use a modal form, instead. Here are the resources for the form:

#define DeleteCustomerForm                   1400
#define DeleteCustomerOKButton               1404
#define DeleteCustomerCancelButton           1405
#define DeleteCustomerSaveBackupCheckbox     1403

We have only one define to add:

#define DeleteCustomerHelpString                  1400

Here is the Delete Customer dialog:

STRING ID DeleteCustomerHelpString "The Save Backup Copy option will " \
   "store deleted records in an archive file on your desktop computer " \
   "at the next HotSync. Some records will be hidden but not deleted " \
   "until then."

FORM ID DeleteCustomerForm AT (2 40 156 118)
MODAL
SAVEBEHIND
HELPID DeleteCustomerHelpString
BEGIN
   TITLE "Delete Customer"
   FORMBITMAP AT (13 29) BITMAP 10005
   LABEL "Delete selected customer?" ID 1402 AT (42 30) FONT 1
   CHECKBOX "Save backup copy on PC?" ID DeleteCustomerSaveBackupCheckbox 
      AT (12 68 140 12) LEFTANCHOR  FONT 1 GROUP 0 CHECKED
   BUTTON "OK" ID DeleteCustomerOKButton AT (12 96 36 12) LEFTANCHOR FRAME
      FONT 0
   BUTTON "Cancel" ID DeleteCustomerCancelButton AT (56 96 36 12)
      LEFTANCHOR FRAME FONT 0
END

The bitmap is a resource in the system ROM; the Palm OS header files define ConfirmationAlertBitmap as its resource ID.

Here's the code that displays the dialog. Note that we set the value of the checkbox before calling  FrmDoDialog. We take a look at it again to see if the user has changed the value after FrmDoDialog returns but before we delete the form:

static Boolean  AskDeleteCustomer(void)
{
   FormPtr  previousForm = FrmGetActiveForm();
   FormPtr frm = FrmInitForm(DeleteCustomerForm);
   Word  hitButton;
   Word  ctlIndex;
   
   FrmSetActiveForm(frm);
   // Set the "save backup" checkbox to its previous setting.
   ctlIndex = FrmGetObjectIndex(frm, DeleteCustomerSaveBackupCheckbox);
   FrmSetControlValue(frm, ctlIndex, gSaveBackup);
   
   hitButton = FrmDoDialog(frm);
   if (hitButton == DeleteCustomerOKButton)
   {
      gSaveBackup = FrmGetControlValue(frm, ctlIndex);
   }
   if (previousForm)
      FrmSetActiveForm(previousForm);
   FrmDeleteForm(frm);
   return hitButton == DeleteCustomerOKButton;
}

Edit Customer

We have a bunch of resources for the Edit Customer form. Here are the #defines:

#define CustomerForm                              1300
#define CustomerOKButton                          1303
#define CustomerCancelButton                      1304
#define CustomerDeleteButton                      1305
#define CustomerPrivateCheckbox                   1310
#define CustomerNameField                         1302
#define CustomerAddressField                      1307
#define CustomerCityField                         1309
#define CustomerPhoneField                        1313

Now we get down to business and create the form:

FORM ID CustomerForm AT (2 20 156 138)
MODAL
SAVEBEHIND
HELPID CustomerhelpString
MENUID DialogWithInputFieldMenuBar
BEGIN
   TITLE "Customer Information"
   LABEL "Name:" AUTOID AT (15 29) FONT 1
   FIELD ID CustomerNameField AT (54 29 97 13) LEFTALIGN FONT 0 UNDERLINED
      MULTIPLELINES MAXCHARS 80
   BUTTON "OK" ID CustomerOKButton AT (7 119 36 12) LEFTANCHOR FRAME 
      FONT 0
   BUTTON "Cancel" ID CustomerCancelButton AT (49 119 36 12) LEFTANCHOR
      FRAME FONT 0
   BUTTON "Delete" ID CustomerDeleteButton AT (93 119 36 12) LEFTANCHOR 
      FRAME FONT 0
   LABEL "Address:" AUTOID AT (10 46) FONT 1
   FIELD ID CustomerAddressField AT (49 46 97 13) LEFTALIGN FONT 0   
      UNDERLINED MULTIPLELINES MAXCHARS 80
   LABEL "City:" AUTOID AT (11 67) FONT 1
   FIELD ID CustomerCityField AT (53 66 97 13) LEFTALIGN FONT 0 UNDERLINED 
      MULTIPLELINES MAXCHARS 80
   CHECKBOX "" ID CustomerPrivateCheckbox AT (54 101 19 12) LEFTANCHOR 
      FONT  0 GROUP 0
   LABEL "Private:" AUTOID AT (9 102) FONT 1
   LABEL "Phone:" AUTOID AT (12 86) FONT 1
   FIELD ID CustomerPhoneField AT (51 86 97 13) LEFTALIGN FONT 0 
      UNDERLINED  MULTIPLELINES MAXCHARS 80
END

Here's the event handler for the form. It's responsible for bringing up the Delete Customer dialog if the user taps on the Delete button: 

static Boolean CustomerHandleEvent(EventPtr event)
{
#ifdef __GNUC__
   CALLBACK_PROLOGUE
#endif
   if (event->eType == ctlSelectEvent && 
      event->data.ctlSelect.controlID == CustomerDeleteButton) {
      if (!AskDeleteCustomer())
         return true;   // don't bail out if they cancel the delete dialog
   } else if (event->eType == menuEvent) {
      if (HandleCommonMenuItems(event->data.menu.itemID))
         return true;
   }
#ifdef __GNUC__
   CALLBACK_EPILOGUE
#endif
   return false;
}

Last, but not least, here is the code that makes sure the customer was handled correctly:

static void EditCustomerWithSelection(UInt recordNumber, Boolean isNew,
   Boolean *deleted, Boolean *hidden, struct frmGoto *gotoData)
{
   FormPtr  previousForm = FrmGetActiveForm();
   FormPtr  frm;
   UInt     hitButton;
   Boolean  dirty = false;
   ControlPtr  privateCheckbox;
   UInt     attributes;
   Boolean     isSecret;
   FieldPtr nameField;
   FieldPtr addressField;
   FieldPtr cityField;
   FieldPtr phoneField;
   Customer theCustomer;
   UInt     offset = offsetof(PackedCustomer, name);
   VoidHand    customerHandle = DmGetRecord(gCustomerDB, recordNumber);
   
   *hidden = *deleted = false;
   // code deleted that initializes isSecret based on the record
   
   frm = FrmInitForm(CustomerForm);
   FrmSetEventHandler(frm, CustomerHandleEvent);
   FrmSetActiveForm(frm);

   UnpackCustomer(&theCustomer, MemHandleLock(customerHandle));
   
   nameField = GetObjectFromActiveForm(CustomerNameField);
   addressField = GetObjectFromActiveForm(CustomerAddressField);
   cityField = GetObjectFromActiveForm(CustomerCityField);
   phoneField = GetObjectFromActiveForm(CustomerPhoneField);

   SetFieldTextFromStr(CustomerNameField,    (CharPtr) theCustomer.name);
   SetFieldTextFromStr(CustomerAddressField, 
      (CharPtr) theCustomer.address);
   SetFieldTextFromStr(CustomerCityField,    (CharPtr) theCustomer.city);
   SetFieldTextFromStr(CustomerPhoneField,   (CharPtr) theCustomer.phone);
      
   // select one of the fields
   if (gotoData && gotoData->matchFieldNum) {
      FieldPtr selectedField =   
         GetObjectFromActiveForm(gotoData->matchFieldNum);
      FldSetScrollPosition(selectedField, gotoData->matchPos);
      FrmSetFocus(frm, FrmGetObjectIndex(frm, gotoData->matchFieldNum));
      FldSetSelection(selectedField, gotoData->matchPos, 
         gotoData->matchPos + gotoData->matchLen);
   } else {
      FrmSetFocus(frm, FrmGetObjectIndex(frm, CustomerNameField));
      FldSetSelection(nameField, 0, FldGetTextLength(nameField));
   }
   // unlock the customer
   MemHandleUnlock(customerHandle);

   privateCheckbox = GetObjectFromActiveForm(CustomerPrivateCheckbox);
   CtlSetValue(privateCheckbox, isSecret);

   hitButton = FrmDoDialog(frm);
   
   if (hitButton == CustomerOKButton) {      
      dirty = FldDirty(nameField) || FldDirty(addressField) ||
         FldDirty(cityField) || FldDirty(phoneField);
      if (dirty) {
         theCustomer.name = FldGetTextPtr(nameField);
         if (!theCustomer.name)
            theCustomer.name = "";
         theCustomer.address = FldGetTextPtr(addressField);
         if (!theCustomer.address)
            theCustomer.address = "";
         theCustomer.city = FldGetTextPtr(cityField);
         if (!theCustomer.city)
            theCustomer.city = "";
         theCustomer.phone = FldGetTextPtr(phoneField);
         if (!theCustomer.phone)
            theCustomer.phone = "";
      }
      PackCustomer(&theCustomer, customerHandle);
      if (CtlGetValue(privateCheckbox) != isSecret) {
         // code deleted that sets information about secret records
      }
   }
   
   if (hitButton == CustomerDeleteButton) {
      // code deleted that deletes the record
   }
   else if (hitButton == CustomerOKButton && isNew && 
      !(StrLen(theCustomer.name) || StrLen(theCustomer.address) ||
      StrLen(theCustomer.city) || StrLen(theCustomer.phone))) {
      // code deleted that deletes the record
   }
   else if (hitButton == CustomerCancelButton && isNew) {
      // code deleted that deletes the record
   }
   
   if (previousForm)
      FrmSetActiveForm(previousForm);
   FrmDeleteForm(frm);
}

Note that in the code we set CustomerHandleEvent as the event handler, and we initialize each of the text fields before calling  FrmDoDialog. After the call to FrmDoDialog, the text from the text fields is copied if the OK button was pressed and any of the fields have been changed.

Item Details

This modal dialog allows editing the quantity and product for an item. The interesting part of this dialog is the pop-up trigger that contains both product categories and products.

The code uses the following globals:

static UInt          gCurrentCategory = 0;
static Long          gCurrentSelectedItemIndex = -1;
static UInt          gNumCategories;

gCurrentCategory contains the current category number. ProductsOffsetInList shows where in the list the products start.

When the Item Details form opens, here is the code that gets called: 

static void ItemFormOpen(void)
{
   ListPtr  list;
   
   FormPtr  frm = FrmGetActiveForm();
   FieldPtr fld = GetObjectFromActiveForm(ItemQuantityField);
   char  quantityString[kMaxNumericStringLength];
   
   // initialize quantity
   StrIToA(quantityString, gCurrentItem->quantity);
   SetFieldTextFromStr(ItemQuantityField, quantityString);
   
   // select entire quantity (so it doesn't have to be selected before 
   // writing a new quantity)
   FrmSetFocus(frm, FrmGetObjectIndex(frm, ItemQuantityField));
   FldSetSelection(fld, 0, StrLen(quantityString));
   
   list = GetObjectFromActiveForm(ItemProductsList);
   LstSetDrawFunction(list, DrawOneProductInList);
   
   if (gCurrentItem->productID) {
      Product  p;
      VoidHand h;
      UInt     index;
      UInt     attr;
      
      h = GetProductFromProductID(gCurrentItem->productID, &p, &index);
      ErrNonFatalDisplayIf(!h, "can't get product for existing item");
      // deleted code that sets finds attr--the category;
      SelectACategory(list, attr & dmRecAttrCategoryMask);
      LstSetSelection(list, 
         DmPositionInCategory(gProductDB, index, gCurrentCategory) + 
         (gNumCategories + 1));
      
      CtlSetLabel(GetObjectFromActiveForm(ItemProductPopTrigger), 
         (CharPtr) p.name);
      MemHandleUnlock(h);
   } else
      SelectACategory(list, gCurrentCategory);
}

First, we set the quantity field. Next, we set a custom draw function. Finally, if the current item already has a product selected, we initialize the list using SelectACategory. We use LstSetSelection to set the current list selection and CtlSetLabel to set the label of the trigger. If no product is selected, we initialize the list using whatever category has been previously used.

Here's  SelectACategory, which sets the current category, initializes the list with the correct number of items, and sets the list height (the number of items shown concurrently):

static void SelectACategory(ListPtr list, UInt newCategory)
{
   Word     numItems;

   gCurrentCategory = newCategory;
   // code deleted that sets numItems based on the 
   // product category
   LstSetHeight(list, numItems);
   LstSetListChoices(list, NULL, numItems);
}

When the user taps on the trigger, the list is shown. We've used  DrawOneProductInList to draw the list. It draws the categories at the top (with the current category in bold), a separator line, and then the products for that category:

static void DrawOneProductInList(UInt itemNumber, RectanglePtr bounds, 
   CharPtr *text)
{
   FontID   curFont;
   Boolean  setFont = false;
   const char  *toDraw = "";  
   
#ifdef __GNUC__
   CALLBACK_PROLOGUE
#endif
   if (itemNumber == gCurrentCategory) {
      curFont = FntSetFont(boldFont);
      setFont = true;
   }
   if (itemNumber == gNumCategories)
      toDraw = "---";
   else if (itemNumber < gNumCategories) {
      // code deleted that sets toDraw based on category name
   } else {
      // code deleted that sets toDraw based on product name
   }
   DrawCharsToFitWidth(toDraw, bounds);
   if (setFont)
      FntSetFont(curFont);
#ifdef __GNUC__
   CALLBACK_EPILOGUE
#endif
}

When the user selects an item from the pop-up, a popSelectEvent is generated. Here's the event handler for that event:

static Boolean  ItemHandleEvent(EventPtr event)
{
   Boolean     handled = false;

#ifdef __GNUC__
   CALLBACK_PROLOGUE
#endif
   switch (event->eType) {
      // code deleted that handles other kinds of events
               
      case popSelectEvent: 
         if (event->data.popSelect.listID == ItemProductsList){
            HandleClickInProductPopup(event);
            handled = true;
         }
         break;
         
      }
#ifdef __GNUC__
   CALLBACK_EPILOGUE
#endif
   return handled;
}

 HandleClickInProductPopup actually handles the selection. If a product is selected, the trigger's label is updated (as is the item). If a new category is selected, the list is updated with a new category, and CtlHitControl is called to simulate tapping again on the trigger. This makes the list reappear without work on the user's part:

static void HandleClickInProductPopup(EventPtr event)
{
   ListPtr     list = event->data.popSelect.listP;
   ControlPtr  control = event->data.popSelect.controlP;
   
   if (event->data.popSelect.selection < (gNumCategories + 1)) {
      if (event->data.popSelect.selection < gNumCategories)
         SelectACategory(list, event->data.popSelect.selection);
      LstSetSelection(list, gCurrentCategory);
      CtlHitControl(control);
   } else {
      // code deleted that sets s.name to product name
      CtlSetLabel(control, (CharPtr) s.name);
   }
}

Customers Form

Here's the form containing only one form object, the list. Here are the resource definitions of the form, the list, and a menu:

#define CustomersForm                             1000
#define CustomersCustomersList                    1002
#define CustomersMenuBar                          1000

Here is the Customers form:

FORM ID CustomersForm AT (0 0 160 160)
MENUID CustomersCustomerMenu
BEGIN
    TITLE "Sales"
    LIST "" ID CustomersCustomersList AT (0 15 160 132) DISABLED FONT 0
END

Our initialization routine (which we call on a frmOpenEvent) sets the draw function callback for the list and sets the number (by calling InitNumberCustomers):

static void CustomersFormOpen(void)
{
   ListPtr  list = GetObjectFromActiveForm(CustomersCustomersList);
   InitNumberCustomers();
   LstSetDrawFunction(list, DrawOneCustomerInListWithFont);

   // code deleted that sets different menus on a pre-3.0 device
}

 InitNumberCustomers calls LstSetListChoices to set the number of elements in the list. It is called when the form is opened and when the number of customers changes (this happens if a customer is added):

static void InitNumberCustomers(void)
{
   ListPtr  list = GetObjectFromActiveForm(CustomersCustomersList);
   // code deleted that sets numCustomers from the databas
   LstSetListChoices(list, NULL, numCustomers);
}

Our event handler handles an open event by calling CustomersFormOpen, then draws the form:

case frmOpenEvent:   
   CustomersFormOpen();
   FrmDrawForm(FrmGetActiveForm());
   handled = true;
   break;

A lstSelectEvent is sent when the user taps (and releases) on a list entry. Our event handler calls OpenNthCustomer to open the Order form for that customer:

case lstSelectEvent: 
   OpenNthCustomer(event->data.lstSelect.selection);
   handled = true;
   break;

 OpenNthCustomer calls SwitchForm to switch to a different form:

static void OpenNthCustomer(UInt customerIndex)
{
   Long  customerID = GetCustomerIDForNthCustomer(customerIndex);
   
   if ((gCurrentOrder =  GetOrCreateOrderForCustomer(
      customerID, &gCurrentOrderIndex)) != NULL)
      SwitchForm(OrderForm);
}

  SwitchForm calls FrmGotoForm to open a new form (and to save the ID of the new form):

static void SwitchForm(Word formID)
{
   FrmGotoForm(formID);
   gCurrentView = formID;
}

The event handler has to handle the up and down scroll keys. It calls the list to do the actual scrolling (note that we scroll by one row at a time, instead of by an entire page):

case keyDownEvent:   
   if (event->data.keyDown.chr == pageUpChr || 
      event->data.keyDown.chr == pageDownChr) {
      ListPtr  list = GetObjectFromActiveForm(CustomersCustomersList);
      enum directions   d;
      if (event->data.keyDown.chr == pageUpChr)
         d = up;
      else
         d = down;
      LstScrollList(list, d, 1);
   }
   handled = true;
   break;

When a new customer is created, code in CustomerHandleMenuEvent calls  EditCustomer to put up a modal dialog for the user to enter the new customer data. When the modal dialog is dismissed, the Form Manager automatically restores the contents of the Customers form. The Customers form also needs to be redrawn, as a new customer has been added to the list. CustomerHandleMenuEvent calls FrmUpdateForm, which sends our event handler a frmUpdateEvent:

EditCustomer(recordNumber, true);
FrmUpdateForm(CustomersForm, frmRedrawUpdateCode);

By default, the Form Manager redraws the form when a frmUpdateEvent occurs. However, it doesn't erase the form first. We need to have the list erased before it is redrawn, since we've changed the contents of the list. So, we erase the list with  LstEraseList and then update the list with the new number of customers. We set handled to false so the default behavior (redrawing the form) will occur.

case frmUpdateEvent:
   LstEraseList(GetObjectFromActiveForm(CustomersCustomersList));
   InitNumberCustomers();
   handled = false;
   break;

Switching Forms

The  ApplicationHandleEvent needs to load forms when a frmLoadEvent occurs (not necessary for forms shown with FrmDoDialog):

static Boolean ApplicationHandleEvent(EventPtr event)
{
   FormPtr  frm;
   Int      formId;
   Boolean  handled = false;

   if (event->eType == frmLoadEvent)
   {
      // Load the form resource specified in event then activate the form.
      formId = event->data.frmLoad.formID;
      frm = FrmInitForm(formId);
      FrmSetActiveForm(frm);

      // Set the event handler for the form.  The handler of the currently 
      // active form is called by FrmDispatchEvent each time it receives 
      // an event.
      switch (formId)
      {
      case OrderForm:
         FrmSetEventHandler(frm, OrderHandleEvent);
         break;
         
      case CustomersForm:
         FrmSetEventHandler(frm, CustomersHandleEvent);
         break;
         
      }
      handled = true;
   }
   return handled;
}

We keep a variable that tells us which is the current form, the CustomersForm or the OrderForm. This variable can be saved in the application's preferences entry so that when the application is reopened, it can return to the form the user was last viewing:

static Word gCurrentView = CustomersForm;

In our PilotMain, we open the form specified by gCurrentView. We also check to make sure that we're running on a 2.0 OS or greater (since we want our application to take advantage of some calls not present in the 1.0 OS):    

error = RomVersionCompatible(0x02000000, launchFlags);
if (error)
   return error;

if (cmd == sysAppLaunchCmdNormalLaunch)
{
   error = StartApplication();
   if (!error)
   {
      FrmGotoForm(gCurrentView);
      EventLoop();
      
      StopApplication();
   }
}

The RomVersionCompatible checks whether the OS version of the handheld device is at least that required to run. It puts up an alert telling the user that a newer OS is required (only if the application's launch flags specify that it should interact with the user):

static Err RomVersionCompatible(DWord requiredVersion, Word launchFlags)
{
   DWord       romVersion;
   // See if we're on a minimum required version of the ROM or later.
   // The system records the version number in a feature.  A feature is a
   // piece of information that can be looked up by a creator and feature
   // number.
   FtrGet(sysFtrCreator, sysFtrNumROMVersion, &romVersion);
   if (romVersion < requiredVersion)
      {
      // If the user launched the app from the launcher, explain
      // why the app shouldn't run.  If the app was contacted for 
      // something else, like it was asked to find a string by the  
      // system find, then don't bother the user with a warning dialog.   
      // These flags tell how the app was launched to decided if a 
      // warning should be displayed.
      if ((launchFlags & 
         (sysAppLaunchFlagNewGlobals | sysAppLaunchFlagUIApp)) 
         == (sysAppLaunchFlagNewGlobals | sysAppLaunchFlagUIApp)) {
         FrmAlert(RomIncompatibleAlert);
      
         // Pilot 1.0 will continuously relaunch this app unless we switch  
         // to another safe one.  The sysFileCDefaultApp is 
         // considered "safe".
         if (romVersion < 0x02000000) {
            Err err;

            AppLaunchWithCommand(sysFileCDefaultApp,
               sysAppLaunchCmdNormalLaunch, NULL);
         }
      }
      return sysErrRomIncompatible;
   }
   return 0;
}

That is all there is of interest to the resources, forms, and form objects in the Sales application. This material took so much space simply because of the large number of objects we needed to show you, rather than because of the complexity of the subject material. This is all good news, however, as a rich set of forms and form objects means greater flexibility in the types of applications you can create for Palm OS devices.


Palm Programming: The Developer's Guide
Copyright © 1999, O'Rielly and Associates, Inc.
Published on the web by permission of O'Rielly and Associates, Inc. Contents modified for web display.

Previous PageTop Of PageTable Of ContentsIndexNext Page