In this chapter:
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 |
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
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 |
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: |
· 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 |
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:
a. If it is on the object, it sends a select event.
b. If it is outside the object, it sends an exit event.
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 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.
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:
1. Get the old handle with FldGetTextHandle
2. Set the new handle with FldSetTextHandle
3. Deallocate the old handle
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, ¤tPosition, &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 |
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.