In this chapter:
This chapter is a grab bag of items that have no particular programmatic relationship to each other. We put them together here because they need to be discussed, and they had to go somewhere.
Tables |
In this section, we do three things. First, we talk in general about tables, the kinds of data they contain, what they look like, what features are automatically supported, and what you need to add yourself. Second, we create a small sample application that shows you how to implement all the available table data types. Third, we show you the implementation of a table in our Sales order application. We also discuss the problems that we encountered in implementing tables and offer you a variety of tips.
An Overview of Tables
Tables are useful forms if you need to display and edit multiple columns of data. (Use a list to display a single column; see "List Objects" in Chapter 5, Forms and Form Objects, on page 91). Figure 8-1 contains three examples of tables from the built-in applications. As you can see, tables can contain a number of different types of data-everything from text to dates to numbers.
Figure 8- 1.
Sample tables from the built-in applications; the first item in the To Do list has a note icon associated with it
Scrolling in tables
While the List Manager automatically supports scrolling, the Table Manager does not. You have to add that support if you need it.
Adjusting width and height
The height and width of table columns and rows are independently adjustable (in fact, editing a text field automatically makes a row change size).
Data types in tables
The Palm OS Table Manager offers greater built-in support for displaying data than for editing it. The following sections list the data types and whether the Table Manager supports them for display purposes only or for editing as well.
Display-only data types
The following are display-only data types:
Edit and display data types
The following are edit and display data types:
Unlike other controls, tables require some programming in order to work. The table stores data for each cell in two parts-an integer and a pointer. The data is used differently, depending on the type of the column. Because of this, you must specify a data type for each column. Here are the possible specifications you can make.
NOTE: The source code for the 1.0 OS Table Manager can be found at http:// www.palmpilot.com/devzone. Be aware that the Table Manager has changed since the 1.0 OS. It is still useful, however, as it gives you a good idea of how the manager works. |
Display-only data types
These are the actual names of data types supported by the Table Manager. These display-only types cannot be edited.
This displays a date (as month/day). The data for a cell should be an integer that can be cast to a DateType. If the value is -1, a hyphen (-) is displayed; otherwise, the actual date is shown. If the displayed date is earlier than the handheld's current date, an exclamation point (!) is appended to it. Tapping on a date highlights the cell.
This displays the text stored in the pointer portion of the cell with an appended colon (:). Tapping on a label highlights the cell.
This displays the number stored in the integer portion of the cell. Tapping on a numeric cell highlights the cell.
Editable data types
These are the types of data that the user can change or edit as necessary:
This displays a checkbox with no associated label. The data for a particular cell should be an integer with a value of either 0 or 1. Clicking on the checkbox toggles the value. Tapping on a checkbox doesn't highlight the row.
This displays an item from a pop-up list (with an arrow before it). The list pointer is stored in the pointer data of the cell; the item from the list is stored in the integer data of the cell. Tapping on a pop-up trigger displays the pop-up, allowing the user to change the value in the integer.
This displays a text cell that can be edited. The column that contains these cells needs a load routine that provides a handle. This handle has an offset and length that are used when editing the text cell. An optional save routine is called after editing.
This is similar to textTableItem, but it also displays a note icon at the righthand side of the cell. Tapping on the note icon highlights the cell.
This is like textTableItem, but it reserves space at the righthand side of the cell. The number of pixel spaces reserved is stored in the integer data of the cell. This is often used for text fields that have 0 or more icons and need to reserve space for them.
This is used for a custom cell. A callback routine needs to be installed for the column; it will be called to draw the contents of each cell at display time. The callback routine can use the integer and pointer data in the cell for whatever it likes. Tapping on a custom table cell highlights the cell.
Initializing tables
There are some difficulties with initializing tables. When you initialize a table, you should first set the types of each column. You can further mark each row and column as usable or unusable. By dynamically switching a column (or row) from unusable to usable (or usable to unusable), you can make it appear (or disappear).
NOTE: Although Table.h defines a timeTableItem type, this type doesn't actually work. |
If you make changes to the data in a cell, you need to mark that row invalid so that it will be redisplayed when the table is redrawn. For some mysterious reason, by default rows are usable, but by default columns are not. If you don't explicitly mark your columns as usable, they won't display.
You can set a two-byte ID and a two-byte data value, which are associated with each row. It's common to set the row ID to the record number of a record in a database.
Simple Table Sample
The following sections describe a table sample in a simple application that shows you how to use all the table data types available in the Table Manager. Figure 8-2 shows the running application. You can see that it contains one table with nine columns and eight rows. Figure 8-3 contains the resource descriptions as they are created in Constructor. Note that the columns go from the easiest data types to code to the hardest.
-Figure 8- 2.
The table sample
Figure 8- 3.
The table resource in Constructor
Initialization of the simple table sample
Initializing this table requires initializing the style and data for each cell in the table. Example 8-1 shows you the entire initialization method. First, look at the entire block of code; then we discuss it, bit by bit.
-Example 8- 1. Initialization of Table
void MainViewInit(void) { FormPtr frm; TablePtr tableP; UInt numRows; UInt i; static char * labels[] = {"0", "1", "2", "3", "4", "5", "6", "7"}; DateType dates[10]; ListPtr list; // we"ll have a missing date, and then some dates before and // after the current date * ((IntPtr) &dates[0]) = noTime; for (i = 1; i < sizeof(dates)/sizeof(*dates); i++) { dates[i].year = 1994 + i - 1904; // offset from 1904 dates[i].month = 8; dates[i].day = 29; } // Get a pointer to the main form. frm = FrmGetActiveForm(); tableP = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, MemoPadMainTableTable)); list = FrmGetObjectPtr(frm, FrmGetObjectIndex (frm, MemoPadMainListList)); numRows = TblGetNumberOfRows (tableP); for (i = 0; i < numRows; i++) { TblSetItemStyle(tableP, i, 0, textWithNoteTableItem); TblSetItemStyle(tableP, i, 1, numericTableItem); TblSetItemInt(tableP, i, 1, i); TblSetItemStyle(tableP, i, 2, checkboxTableItem); TblSetItemInt(tableP, i, 2, i % 2); TblSetItemStyle(tableP, i, 3, labelTableItem); TblSetItemPtr(tableP, i, 3, labels[i]); TblSetItemStyle(tableP, i, 4, dateTableItem); TblSetItemInt(tableP, i, 4, DateToInt(dates[i])); TblSetItemStyle(tableP, i, 5, textTableItem); TblSetItemInt(tableP, i, 5, i * 2); TblSetItemStyle(tableP, i, 6, popupTriggerTableItem); TblSetItemInt(tableP, i, 6, i % 5); TblSetItemPtr(tableP, i, 6, list); TblSetItemStyle(tableP, i, 7, narrowTextTableItem); TblSetItemInt(tableP, i, 7, i * 2); TblSetItemStyle(tableP, i, 8, customTableItem); TblSetItemInt(tableP, i, 8, i % 4); } TblSetRowUsable(tableP, 1, false); // just to see what happens for (i = 0; i < kNumColumns; i++) TblSetColumnUsable(tableP, i, true); TblSetLoadDataProcedure(tableP, 0, CustomLoadItem); TblSetLoadDataProcedure(tableP, 5, CustomLoadItem); TblSetSaveDataProcedure(tableP, 5, CustomSaveItem); TblSetLoadDataProcedure(tableP, 7, CustomLoadItem); TblSetCustomDrawProcedure(tableP, 8, CustomDrawItem); // Draw the form. FrmDrawForm(frm); }
Let's look at the columns not in column order, but in terms of complexity.
Column 1-handling numbers
The code starts with a numeric column that is quite an easy data type to handle. We use the row number as the number to display. Here's the code that executes for each row. As you can see, there is not a lot to it:
TblSetItemStyle(tableP, i, 1, numericTableItem); TblSetItemInt(tableP, i, 1, i);
Column 2-a checkbox
This second column displays a simple checkbox. We set the initial value of the checkbox to be on for even row numbers and off for odd row numbers:
TblSetItemStyle(tableP, i, 2, checkboxTableItem); TblSetItemInt(tableP, i, 2, i % 2);
Column 3-a label
This column displays a label that contains a piece of noneditable text. We set the text to successive values from a text array. The table manager appends a colon to the label:
static char * labels[] = {"0", "1", "2", "3", "4", "5", "6", "7"}; // for each row: TblSetItemStyle(tableP, i, 3, labelTableItem); TblSetItemPtr(tableP, i, 3, labels[i]);
Column 4-a date
In the date column, we create an array of dates that are used to initialize each cell. Note that the first date is missing, which is why the "-" is displayed instead of a date. The remaining dates range over successive years; some dates are before the current time, and others are after it:
DateType dates[10]; ListPtr list; // we"ll have a missing date, and then some before and after // the current date * ((IntPtr) &dates[0]) = noTime; for (i = 1; i < sizeof(dates)/sizeof(*dates); i++) { dates[i].year = 1994 + i - 1904; // offset from 1904 dates[i].month = 8; dates[i].day = 29; } // for each row: TblSetItemStyle(tableP, i, 4, dateTableItem); TblSetItemInt(tableP, i, 4, DateToInt(dates[i]));
Column 6-a pop-up trigger
As with any pop-up trigger, we've got to create a list in our resource. We've created one that has the values "1", "2", "3", "4", and "5". For each cell in the column, we set the pointer value to the list itself, then set the data value as the item number in the list:
ListPtr list; list = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, MemoPadMainListList)); // for each row: TblSetItemStyle(tableP, i, 6, popupTriggerTableItem); TblSetItemInt(tableP, i, 6, i % 5); TblSetItemPtr(tableP, i, 6, list);
Columns 0, 5, and 7-handling text
Now let's look at the text columns. Notice that we use all three of the available text column types:
TblSetItemStyle(tableP, i, 0, textWithNoteTableItem); TblSetItemStyle(tableP, i, 5, textTableItem); TblSetItemStyle(tableP, i, 7, narrowTextTableItem);
With the narrow text table item, we set the integer data as a pixel reserve on the righthand side. We give each row a different pixel reserve so that we can see the effect:
TblSetItemInt(tableP, i, 7, i * 2);
Each of the text items requires a custom load procedure to provide the needed handle for the cell. Actually, we have the option of providing only a portion of the handle as well:
TblSetLoadDataProcedure(tableP, 0, CustomLoadItem); TblSetLoadDataProcedure(tableP, 5, CustomLoadItem); TblSetLoadDataProcedure(tableP, 7, CustomLoadItem);
We customize the saving of the second text column:
TblSetSaveDataProcedure(tableP, 5, CustomSaveItem);
We'll look at the custom load and save routines that we just called after we discuss the eighth column.
Column 8-handling custom content
The final column is a custom column that displays a line at one of four angles. The angle is determined by the integer data in the cell. We initialize the integer data to a value between 0 and 3, depending on the row:
TblSetItemStyle(tableP, i, 8, customTableItem); TblSetItemInt(tableP, i, 8, i % 4);
We set a custom draw procedure for that column:
TblSetCustomDrawProcedure(tableP, 8, CustomDrawItem);
Displaying the columns
In order to make the columns display, we've got to mark them usable:
for (i = 0; i < kNumColumns; i++) TblSetColumnUsable(tableP, i, true);
Just as an exercise, we mark row 1 as unusable (now it won't appear in the table):
TblSetRowUsable(tableP, 1, false); // just to see what happens
Custom load routines
The custom load routines that we used with the text columns need to return three things:
The Table Manager calls on the Field Manager to display and edit the range within the handle. It's our job to allocate one (null-terminated) handle for every text cell:
#define kNumTextColumns 3 Handle gHandles[kNumTextColumns][kNumRows]; static Boolean StartApplication(void) { int i; int j; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif for (i = 0; i < kNumTextColumns; i++) for (j = 0; j < kNumRows; j++) { CharPtr s; gHandles[i][j] = MemHandleNew(1); s = MemHandleLock(gHandles[i][j]); *s = '\0'; MemHandleUnlock(gHandles[i][j]); } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return false; }
A utility routine converts a table column number to an appropriate index in our handles array:
static int WhichTextColumn(int column) { if (column == 0) return 0; else if (column == 5) return 1; else //column == 7 return 2; }
Once we have the handles for each text cell, we can set the offset and length within each one. We set our offset to 0 and the size to the appropriate length of data:
static Err CustomLoadItem(VoidPtr table, Word row, Word column, Boolean editable, VoidHand * dataH, WordPtr dataOffset, WordPtr dataSize, FieldPtr fld) { #ifdef __GNUC__ CALLBACK_PROLOGUE #endif *dataH = gHandles[WhichTextColumn(column)][row]; *dataOffset = 0; *dataSize = MemHandleSize(*dataH); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return 0; }
Custom save routine
This save routine customizes the saving of the first cell in the second text column. If the text has been edited, the text converts from uppercase to lowercase. Note that the save routine returns true in this case to show that the table needs to be redrawn:
static Boolean CustomSaveItem(VoidPtr table, Word row, Word column) { int textColumn; Boolean result = false; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif textColumn = WhichTextColumn(column); // the handle that we provided in CustomLoadItem has been modified // We could edit that (if we wanted). // If it's been edited, let's make the first row // convert to lower-case and redraw if (row == 0 && textColumn == 1) { FieldPtr field = TblGetCurrentField(table); if (field && FldDirty(field)) { VoidHand h = gHandles[textColumn][row]; CharPtr s; int i; s = MemHandleLock(h); for (i = 0; s[i] != '\0'; i++) if (s[i] >= 'A' && s[i] <= 'Z') s[i] += 'a' - 'A'; MemHandleUnlock(h); TblMarkRowInvalid(table, row); result = true; } } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return result; // should the table be redrawn }
Custom draw routine
We need a drawing routine that creates our rotating line:
// draws either \, |, /, or - static void CustomDrawItem(VoidPtr table, Word row, Word column, RectanglePtr bounds) { UInt fromx, fromy, tox, toy; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif switch (TblGetItemInt(table, row, column)) { case 0: fromx = bounds->topLeft.x; fromy = bounds->topLeft.y; tox = fromx + bounds->extent.x; toy = fromy + bounds->extent.y; break; case 1: fromx = bounds->topLeft.x + bounds->extent.x / 2; fromy = bounds->topLeft.y; tox = fromx; toy = fromy + bounds->extent.y; break; case 2: fromx = bounds->topLeft.x + bounds->extent.x; fromy = bounds->topLeft.y; tox = bounds->topLeft.x; toy = fromy + bounds->extent.y; break; case 3: fromx = bounds->topLeft.x; fromy = bounds->topLeft.y + bounds->extent.y / 2; tox = fromx + bounds->extent.x; toy = fromy; break; default: fromx = tox = bounds->topLeft.x; fromy = toy = bounds->topLeft.y; break; } WinDrawLine(fromx, fromy, tox, toy); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif }
Handling a table event
If we tap on a cell in the custom column, we want the angle of the line to change. We do that by changing the integer value. The tblSelectEvent is posted to the event queue when a custom cell is successfully tapped (that is, the user taps on and releases the same cell).
NOTE: While you might assume that the tblSelectEvent is where to change the value and redraw, this isn't the case. The Table Manager highlights the selected cell, and we overwrite the highlighting when we redraw. If we switch to a new cell, the Table Manager tries to unhighlight by inverting. As these are certainly not the results we want, we need to handle the call in another place. |
We're going to handle the redraw in tblEnterEvent, looking to see whether the tapped cell is in our column:
static Boolean MainViewHandleEvent(EventPtr event) { Boolean handled = false; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif switch (event->eType) { // code deleted case tblSelectEvent: // handle successful tap on a cell // for a checkbox or popup, tblExitEvent will be // called instead of tblSelectEvent // if the user cancels the control break; case tblEnterEvent: { UInt row = event->data.tblEnter.row; UInt column = event->data.tblEnter.column; if (column == 8) { TablePtr table = event->data.tblEnter.pTable; int oldValue = TblGetItemInt(table, row, column); TblSetItemInt(table, row, column, (oldValue + 1) % 4); TblMarkRowInvalid(table, row); TblRedrawTable(table); handled = true; } } break; } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return handled; }
This is all that is worth mentioning in the simple example of a table. It should be enough to guide you in the implementation of these data types in your own tables.
Tables in the Sample Application |
In our sample application, we use a table in the Order form. There are three columns: the product ID, the product, and the quantity. Note that we don't use the numeric cell type for either the product ID or quantity, because we need editing as well as display.
We don't use the text cell type for the product ID or quantity, either. These are numbers that we want displayed as right-justified-the text cell type doesn't provide for right-justified text. Therefore, we don't use table's built-in types. We use the custom cell type to create our own data type, an editable numeric value, instead.
Tables with Editable Numeric Values
If we ignore the Table Manager APIs and create our own data type, we have the advantage of having a preexisting model on which we can rely-the built-in applications for which source code is available use this approach. The major disadvantage to this approach is that we won't be able to rely on the Table Manager for help with all the standard little details (such as key events). For our application, all the Table Manager provides is some iterating through cells for drawing and indicating which cell has been tapped. Thus, we will need to write additional code for the following:
Initialization
Here's the code for the one-time initialization done when we load the Order form:
static void InitializeItemsList(void) { TablePtr table = GetObjectFromActiveForm(OrderItemsTable); Word rowsInTable; Word row; ErrNonFatalDisplayIf(!gCurrentOrder, "nil gCurrentOrder"); TblSetCustomDrawProcedure(table, kProductNameColumn, OrderDrawProductName); TblSetCustomDrawProcedure(table, kQuantityColumn, OrderDrawNumber); TblSetCustomDrawProcedure(table, kProductIDColumn, OrderDrawNumber); rowsInTable = TblGetNumberOfRows(table); for (row = 0; row < rowsInTable; row++) { TblSetItemStyle(table, row, kProductIDColumn, customTableItem); TblSetItemStyle(table, row, kProductNameColumn, customTableItem); TblSetItemStyle(table, row, kQuantityColumn, customTableItem); } TblSetColumnUsable(table, kProductIDColumn, true); TblSetColumnUsable(table, kProductNameColumn, true); TblSetColumnUsable(table, kQuantityColumn, true); LoadTable(); } tblSelectEvent
Refreshing the form
Since the contents of the rows change (as scrolling takes place or as items are added or deleted), we need a routine to update the contents of each row. LoadTable updates the scrollbars, sets whether each row is visible or not visible, and sets the global gTopVisibleItem:
static void LoadTable(void) { TablePtr table = GetObjectFromActiveForm(OrderItemsTable); Word rowsInTable = TblGetNumberOfRows(table); Word row; SWord lastPossibleTopItem = ((Word) gCurrentOrder->numItems) - rowsInTable; if (lastPossibleTopItem < 0) lastPossibleTopItem = 0; // If we have a currently selected item, make sure that it is visible if (gCellSelected) if (gCurrentSelectedItemIndex < gTopVisibleItem || gCurrentSelectedItemIndex >= gTopVisibleItem + rowsInTable) gTopVisibleItem = gCurrentSelectedItemIndex; // scroll up as necessary to display an entire page of info gTopVisibleItem = min(gTopVisibleItem, lastPossibleTopItem); for (row = 0; row < rowsInTable; row++) { if (row + gTopVisibleItem < gCurrentOrder->numItems) OrderInitTableRow(table, row, row + gTopVisibleItem); else TblSetRowUsable(table, row, false); } SclSetScrollBar(GetObjectFromActiveForm(OrderScrollbarScrollBar), gTopVisibleItem, 0, lastPossibleTopItem, rowsInTable - 1); }
Displaying the quantity and product data
OrderInitTableRow actually makes the Table Manager calls to (1) mark this row usable, (2) set the row ID to the item number (so we can go from table row to an item), and (3) mark the row as invalid so that it will be redrawn:
static void OrderInitTableRow(TablePtr table, Word row, Word itemNum) { // Make the row usable. TblSetRowUsable(table, row, true); // Store the item number as the row id. TblSetRowID(table, row, itemNum); // make sure the row will be redrawn TblMarkRowInvalid(table, row); }
Instead of creating one field for each numeric cell, we create a field when it's time to draw the cell or when it's time to edit a numeric cell.
Using this programming strategy is a big win for memory use, which, you will remember, is quite tight on the handheld. Because we are creating a field for only one cell at a time, we need to allocate memory for only that one field. If we created all the fields all at once, we would have to reserve a great deal of precious memory, as well.
The custom draw routine for the quantity and product ID is OrderDrawNumber:
static void OrderDrawNumber(VoidPtr table, Word row, Word column, RectanglePtr bounds) { FieldPtr field; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif field = OrderInitNumberField(table, row, column, bounds, true); FldDrawField(field); FldFreeMemory(field); OrderDeinitNumberField(table, field); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif }
Dynamically adjusting number fields
We want to dynamically adjust the number fields as an optimization of memory usage, as well. Unfortunately, there is no documented way prior to 3.0 to dynamically set aspects of a field (bounds, etc.). Therefore, like the built-in applications, we need a routine that fills in the fields of the structure by hand. On a 3.0 (or later) OS, there is a documented FldNewField routine to create a new field in the form (although there is still no way to modify an existing field).
NOTE: Rather than not show you the code at all, we've defined a constant, qUseDynamicUI. If it's false, the code isn't actually used. In the event that we don't yet have a solution, we would be glad to hear from you if you find the answer (neil@pobox.com, julie@pobox.com). |
We've provided code that uses FldNewField:
#define qUseDynamicUI 0 #define kDynamicFieldID 9999 // a field ID not present in the form // WARNING, the form, and any controls, table, etc. on the form may change // locations in memory after this call; don't keep pointers to them while // calling this routine. static FieldPtr OrderInitNumberField(TablePtr table, Word row, Word column, RectanglePtr bounds, Boolean temporary) { VoidHand textH; CharPtr textP; char buffer[10]; ULong number; UInt itemNumber = TblGetRowID(table, row); FieldPtr fld; if (!qUseDynamicUI || sysGetROMVerMajor(gRomVersion) < 3) { if (temporary) fld = &gTempFieldType; else gCurrentFieldInTable = fld = TblGetCurrentField(table); MemSet(fld, sizeof(FieldType), 0); RctCopyRectangle(bounds, &fld->rect); fld->attr.usable = true; fld->attr.visible = !temporary; fld->attr.editable = true; fld->attr.singleLine = true; fld->attr.dynamicSize = false; fld->attr.underlined = true; fld->attr.insPtVisible = true; fld->attr.numeric = true; fld->attr.justification = rightAlign; fld->maxChars = kMaxNumericStringLength; } else { FormPtr frm = FrmGetActiveForm(); fld = FldNewField((VoidPtr) &frm, kDynamicFieldID, bounds->topLeft.x, bounds->topLeft.y, bounds->extent.x, bounds->extent.y, stdFont, kMaxNumericStringLength, true, true, true, false, rightAlign, false, false, true); if (!temporary) gCurrentFieldInTable = fld; } if (column == kQuantityColumn) number = gCurrentOrder->items[itemNumber].quantity; else number = gCurrentOrder->items[itemNumber].productID; buffer[0] = '\0'; // 0 will display as empty string if (number) StrIToA(buffer, number); textH = MemHandleNew(StrLen(buffer) + 1); textP = MemHandleLock(textH); StrCopy(textP, buffer); MemPtrUnlock(textP); FldSetTextHandle(fld, (Handle) textH); if (temporary) return fld; else return NULL; }
Deallocating number fields
If the qUseDynamicUI macro is set to true, the deinitialization routine deallocates the field on a 3.0 or later OS:
// WARNING, the form, and any controls, table, etc. on the form may change // locations in memory after this call; don't keep pointers to them while // calling this routine. static void OrderDeinitNumberField(TablePtr table, FieldPtr fld) { if (qUseDynamicUI && sysGetROMVerMajor(gRomVersion) >= 3) { FormPtr frm = FrmGetActiveForm(); FrmRemoveObject(&frm, FrmGetObjectIndex(frm, kDynamicFieldID)); } if (fld == gCurrentFieldInTable) gCurrentFieldInTable = NULL; }
NOTE: FldNewField and FrmRemoveObject both change the form pointer and can change the pointers to any objects in the form. Make sure not to reuse any pointer (like the table or the field) after calling either of these routines. |
Adding product names
Here's the routine that draws a product name (since it's called by the Table Manager, it must have the CALLBACK macros for GCC):
static void OrderDrawProductName(VoidPtr table, Word row, Word column, RectanglePtr bounds) { VoidHand h = NULL; Product p; UInt itemNumber; ULong productID; CharPtr toDraw; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif itemNumber = TblGetRowID(table, row); productID = gCurrentOrder->items[itemNumber].productID; if (productID) { h = GetProductFromProductID(productID, &p, NULL); toDraw = (CharPtr) p.name; } else toDraw = "-Product-"; DrawCharsToFitWidth(toDraw, bounds); if (h) MemHandleUnlock(h); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif }
Adding scrolling support
We've got to handle scrolling if we want items to display properly. In the routine OrderHandleEvent, we look for a sclRepeatEvent:
case sclRepeatEvent: OrderDeselectRowAndDeleteIfEmpty(); OrderScrollRows(event->data.sclRepeat.newValue - event->data.sclRepeat.value); handled = false; // scrollbar needs to handle the event, too break;
OrderScrollRows is straightforward. It updates gTopVisibleItem, then reloads the table and redraws it:
static void OrderScrollRows(SWord numRows) { TablePtr table = GetObjectFromActiveForm(OrderItemsTable); gTopVisibleItem += numRows; if (gTopVisibleItem < 0) gTopVisibleItem = 0; LoadTable(); TblUnhighlightSelection(table); TblRedrawTable(table); }
The table event handler
We handle a great number of things in our code and rely on the Table Manager for very little. As a result, we've got quite a complex event handler. Here's how we handle the tblEnterEvent:
case tblEnterEvent: { Word row = event->data.tblEnter.row; Word column = event->data.tblEnter.column; TablePtr table = event->data.tblEnter.pTable; // if the user taps on a new row, deselect the old row if (gCellSelected && row != table->currentRow) { handled = OrderDeselectRowAndDeleteIfEmpty(); // if we delete a row, leave everything unselected if (handled) break; } if (gCellSelected) { // if the user taps a prod in the currently selected row, edit it if (column == kProductNameColumn) { ListPtr list = GetObjectFromActiveForm(OrderProductsList); int selection; UInt index; UInt attr; LstSetDrawFunction(list, DrawOneProductInList); if (gCurrentOrder->items[gCurrentSelectedItemIndex].productID) { // initialize the popup for this product GetProductFromProductID( gCurrentOrder->items[gCurrentSelectedItemIndex].productID, NULL, &index); DmRecordInfo(gProductDB, index, &attr, NULL, NULL); SelectACategory(list, attr & dmRecAttrCategoryMask); LstSetSelection(list, DmPositionInCategory(gProductDB, index, gCurrentCategory) + (gNumCategories + 1)); } else SelectACategory(list, gCurrentCategory); do { selection = LstPopupList(list); if (selection >= 0 && selection < gNumCategories) SelectACategory(list, selection); } while (selection >= 0 && selection < (gNumCategories + 1)); if (selection >= 0) { UInt index = 0; VoidHand h; PackedProduct *packedProduct; Product s; Int oldSelectedColumn = table->currentColumn; gCurrentProductIndex = 0; DmSeekRecordInCategory(gProductDB, &gCurrentProductIndex, selection - (gNumCategories + 1), dmSeekForward, gCurrentCategory); ErrNonFatalDisplayIf(DmGetLastErr(), "Can't seek to product"); h = DmQueryRecord(gProductDB, gCurrentProductIndex); gHaveProductIndex = true; ErrNonFatalDisplayIf(!h, "Can't get record"); packedProduct = MemHandleLock(h); UnpackProduct(&s, packedProduct); DmWrite(gCurrentOrder, offsetof(Order, items[gCurrentSelectedItemIndex].productID), &packedProduct->productID, sizeof(packedProduct->productID)); MemHandleUnlock(h); // Redraw current row. Can't have anything selected or the // table will highlight it. OrderSaveAmount(table); LoadTable(); TblRedrawTable(table); OrderSelectNumericCell(NULL, OrderItemsTable, row, oldSelectedColumn); } } else { if (column == table->currentColumn) { // the user tapped in the current field OrderTapInActiveField(event, table); } else { // the user tapped in another field in the row OrderSaveAmount(table); OrderSelectNumericCell(event, OrderItemsTable, row, column); } } } else { // user tapped in a new row if (column == kQuantityColumn || column == kProductIDColumn) { OrderSelectNumericCell(event, OrderItemsTable, row, column); } else { OrderSelectRow(OrderItemsTable, row); } } handled = true; } break;
Handling taps
We need to convert a tblEnterEvent (tap in a numeric cell) into a fldEnterEvent so that the Field Manager will handle the event and set the insertion point, or start drag-selecting. Here is how we do that:
static void OrderTapInActiveField(EventPtr event, TablePtr table) { EventType newEvent; FieldPtr fld; fld = gCurrentFieldInTable; // Convert the table enter event to a field enter event. EvtCopyEvent(event, &newEvent); newEvent.eType = fldEnterEvent; newEvent.data.fldEnter.fieldID = fld->id; newEvent.data.fldEnter.pField = fld; FldHandleEvent(fld, &newEvent); }
Handling key events
We've got to handle scrolling when our table receives key-down events. If the user is writing in a cell, we filter to allow only arrows, backspace, and digits. If the user has no cell selected and writes a digit, we add a new item and insert the new digit in the quantity cell.
Note that the character we retrieve from the event is a two-byte word, not a one-byte char:
static Boolean OrderHandleKey(EventPtr event) { Word c = event->data.keyDown.chr; // bottom-to-top screen gesture can cause this, depending on // configuration in Prefs/Buttons/Pen if (c == sendDataChr) return OrderHandleMenuEvent(RecordBeamCustomer); else if (c == pageUpChr || c == pageDownChr) { SWord numRowsToScroll = TblGetNumberOfRows(GetObjectFromActiveForm(OrderItemsTable)) - 1; OrderDeselectRowAndDeleteIfEmpty(); if (c == pageUpChr) numRowsToScroll = -numRowsToScroll; OrderScrollRows(numRowsToScroll); } else if (c == linefeedChr) { // The return character takes us out of edit mode. OrderDeselectRowAndDeleteIfEmpty(); } else if (gCellSelected) { if ((c == backspaceChr) || (c == leftArrowChr) || (c == rightArrowChr) || IsDigit(GetCharAttr(), c)) FldHandleEvent(gCurrentFieldInTable, event); } else { // writing a digit with nothing selected creates a new item if (IsDigit(GetCharAttr(), c)) { UInt itemNumber; OrderDeselectRowAndDeleteIfEmpty(); if (AddNewItem(&itemNumber)) { OrderSelectItemNumber(itemNumber, kQuantityColumn); FldHandleEvent(gCurrentFieldInTable, event); } } } return true; }
Handling numeric cell selection
Here's how we handle the user's tapping on a numeric cell:
static void OrderSelectNumericCell(EventPtr event, Word tableID, Word row, Word column) { TablePtr table; table = GetObjectFromActiveForm(tableID); // make this cell selected, if it isn't already if (row != table->currentRow || column != table->currentColumn || !table->attr.editing) { RectangleType r; FormPtr frm; table->attr.editing = true; table->currentRow = row; table->currentColumn = column; TblGetItemBounds(table, row, column, &r); OrderInitNumberField(table, row, column, &r, false); // reacquire the table, since OrderInitNumberField may have // made it invalid table = GetObjectFromActiveForm(tableID); gCurrentSelectedItemIndex = TblGetRowID(table, row); gCellSelected = true; OrderHiliteSelectedRow(table, true); frm = FrmGetActiveForm(); FrmSetFocus(frm, FrmGetObjectIndex(frm, tableID)); FldGrabFocus(gCurrentFieldInTable); } // if there's an event, pass it on if (event) OrderTapInActiveField(event, table); }
We (like the built-in applications) modify the table fields attr.editing, currentRow, and currentColumn directly, since there is no API to change these values.
Find |
In this section, we discuss the Find feature of the Palm OS. First, we give you an overview of Find, the user interface, and its intended goals. Second, we walk through the entire Find process from the beginning to the end. Third, we implement Find in our sample application and discuss important aspects of the code.
Overview of Find
The Palm OS user interface supports a global Find-a user can find all the instances of a string in all applications. The operating system doesn't do the work, however. Instead, it orders each application, in turn, to search through its own databases and return the results.
There is much to be said for this approach. The most obvious rationale is that the operating system has no idea what's inside the records of a database: strings, numbers, or other data. Therefore, it's in no position to know what's a reasonable return result and what's nonsense. Indeed, the application is uniquely positioned to interpret the Find request and determine the display of the found information to the user.
Find requests are sent from the OS by calling the application's PilotMain (see "Other Times Your Application Is Called" on page 88) with a specific launch code, sysAppLaunchCmdFind, along with parameters having to do with interpreting the Find.
The objectives of Find
Remembering that speed on the handheld is essential, Find is intended to be a very quick process. Here are some of the things that the OS does to ensure this:
No global variables
An application's global variables are not created when it receives the sysAppLaunchCmdFind launch code, as creating, initializing, and releasing every application's globals would be a time-consuming process.
Only one screenful of items at a time
The Find goes on only long enough to fill one screen with items. If the user wants to see more results, the Find resumes where it left off until it has another screenful of found items, then stops again. This process continues until it runs out of return results.
Long Finds are easy to stop
Applications check the event queue every so often to see whether an event has occurred. If so, the application prematurely quits the Find. Thus, a simple tap outside the Find prevents a long search of a large database that would otherwise lock up the handheld.
Another goal is to minimize the amount of memory used. Remember that the Find request could well occur while an application other than yours is running. In such cases, it would be very rude, indeed, to suck away the application's dynamic heap. To prevent such bad manners, memory use is minimized in the following ways:
No global variables
No unopen application global variables are created.
Minimal information about each found item is stored
An application doesn't save much about the items it finds. Rather, the application draws a summary of the found items and passes the Find Manager six bits of information: the database, the record number, the field number, the card number, the position within the field, and an additional integer.
Only one screenful of items at a time
Only one screenful of found items is maintained in memory. If the user requests more, the current information is thrown out and the search continues where it left off.
A Walkthrough of Finding Items
The following is a walkthrough of what happens when the user writes in a string to be found and taps Find. First, the current application is sent the launch code sysAppLaunchCmdSaveData, which requests that the application save any data that is currently being edited but not yet saved in a database. Then, starting with the open application, each application is sent the launch code sysAppLaunchCmdFind.
The application's response to a Find request
Each application responds with these steps:
1. The application opens its database(s) using the mode specified in its Find parameters. This can be specified as read-only mode and may also (depending on the user's Security settings) specify that secret records should be shown.
2. The application draws an application header in the Find Results dialog. Figure 8-4 contains some examples of application headers as they appear in the dialog. The application uses FindDrawHeader to retrieve the application header from the application's resource database. If FindDrawHeader returns true, there is no more space in the Find Results dialog, and step 3 is skipped. If there is room in the dialog, it is on to step 3.
Figure 8- 4.
Find results dialog showing application headers
3. The application iterates through each of its records in the database. If it is sent a Find request and there is room to fit all of the found items on the screen, the application iterates through the records starting at record 0. If some records from the application have already been displayed, the application has the Find Manager store the record number of the last displayed record and continues the iteration with the next record when the user taps the More button.
a. Most applications retrieve the next record by using DmQueryNextInCategory, which skips private records, if necessary. If an error occurs, the application exits the loop.
b. It looks for a string that matches. An application should normally ignore case while determining a match. The application can use FindStrInStr to determine whether there is a match and where the match occurs.
c. If the application finds a match, it saves information about the match using FindSaveMatch. If FindSaveMatch returns true, no more items can be drawn in the Find Results dialog. In this case, the application has finished iterating and goes to step 4. Otherwise, it draws to the Find Results dialog a one-line summary of the matching item (FindGetLineBounds returns the rectangle in which to draw). The summary should, if possible, include the searched-for string, along with other contextual information.
In addition, the application increments the lineNumber field of the Find parameters.
d. The application should check the event queue every so often (using EvtSysEventAvail). If an event has occurred, the application should set the more field of the Find parameters to true and go to step 4.
4. The application closes any databases it has opened and returns.
When the Find Results dialog is displayed, the user can choose Find More. In this case, the Find Manager starts the process again, skipping any applications that have been completely searched.
NOTE: In the documentation for Find that was current at the time of this book's writing, some Find functions and a field in the Find parameters are incorrectly documented as being for system use only. The following functions are necessary to correctly support Find: FindDrawHeader, FindGetLineBounds, FindStrInStr, FindSaveMatch. This Find parameter field is also necessary: lineNumber. |
Handling a Find request with multiple databases
If your application supports searching in multiple databases, you've got to carefully handle continuing a search (Find More). The Find parameters provide the last matched record number (as saved by FindSaveMatch), but not the last matched database. Because of this, your Find routine doesn't know which database was last searched.
Our recommendation is to use system preferences as a place to store the name of the last database. When you call FindSaveMatch, you can retrieve the information. When you receive the Find launch code, if the continuation field of the Find parameters is false, mark the last database as invalid and start the search with your first database. If the continuation field of the Find parameters is true, start your search with the saved database (if it is valid).
NOTE: Remember that you can't store information in global variables, because when the sysAppLaunchCmdFind launch code is sent, your application's global variables don't get allocated. |
Alternatively, you could use the record number field as a combination record number and database. You could store the indicated database (0, 1, 2, etc.) in the upper few bits, and the actual record number in the remaining bits.
Navigating to a found item
When the user taps on an item in the Find Results dialog, that item's application is sent the sysAppLaunchCmdGoTo launch code. That application may or may not be the current application. If it is, the application just switches to displaying the found item. If it isn't, the application must call StartApplication and enter a standard event loop.
The Find parameters are sent, along with the sysAppLaunchCmdGoTo launch code. These parameters are all the items that were passed to FindSaveMatch, along with an additional one: the length of the searched-for string. Your application should then display the found item, highlighting the searched-for string within the found item.
Displaying a found item from a running application
Here's the step-by-step process your open application will go through when it receives the sysAppLaunchCmdGoTo launch code:
1. Close any existing forms (using FrmCloseAllForms).
2. Open the form appropriate to display the found item (using FrmGotoForm).
3. Create a frmGotoEvent event record with fields initialized from the go to parameters, and post it to the event queue (using EvtAddEventToQueue).
4. Respond to the frmGotoEvent event in your form's event handler by navigating to the correct record and highlighting the found contents (using FldSetScrollPosition and FldSetSelection).
NOTE: Note that we must find the unique ID of the specified recordNumber before we close all the forms. There are many cases that call for this, but, as an example, the user might be viewing a blank form immediately prior to the Find request. Before displaying the found item, the application needs to delete the blank Customer dialog and close the form. If this occurs, however, the records in the database may no longer be numbered the same. Therefore, we find the unique ID of the found record. After closing the forms, we then find the record based on its unchanging unique ID instead of the possibly compromised record number. |
Displaying a found item from a closed application
If your application is closed when it receives the sysAppLaunchCmdGoTo launch code, you need to do a few more things:
1. As specified by the sysAppLaunchFlagNewGlobals launch flag, call StartApplication.
2. Create a frmGotoEvent event record with fields initialized from the goto parameters and post it to the event queue (using EvtAddEventToQueue).
3. Enter your EventLoop.
4. Respond to the frmGotoEvent event in your form's event handler by navigating to the correct record and highlighting the found contents (using FldSetScrollPosition and FldSetSelection).
5. Call StopApplication after the EventLoop is finished.
Find in the Sales Application
From the earlier description of Find, you can see that supporting it in your application, while straightforward, does require handling a number of steps and possible situations.
Let's look now at how we handle these steps in the Sales application.
Handling the Find request
The PilotMain handles the save data and the Find launch codes. Here's the bit of code from PilotMain that shows the call to sysAppLaunchCmdFind:
// Launch code sent to running app before sysAppLaunchCmdFind // or other action codes that will cause data searches or manipulation. else if (cmd == sysAppLaunchCmdSaveData) { FrmSaveAllForms(); } else if (cmd == sysAppLaunchCmdFind) { Search((FindParamsPtr)cmdPBP); }
Searching for matching strings
Here's the Search routine that actually handles the searching through our customer database. The part of the code that's specific to our application is emphasized; the remaining code is likely to be the standard for most applications:
static void Search(FindParamsPtr findParams) { Err err; Word pos; UInt fieldNum; UInt cardNo = 0; UInt recordNum; CharPtr header; Boolean done; VoidHand recordH; VoidHand headerH; LocalID dbID; DmOpenRef dbP; RectangleType r; DmSearchStateType searchState; // unless told otherwise, there are no more items to be found findParams->more = false; // Find the application's data file. err = DmGetNextDatabaseByTypeCreator(true, &searchState, kCustomerDBType, kSalesCreator, true, &cardNo, &dbID); if (err) return; // Open the expense database. dbP = DmOpenDatabase(cardNo, dbID, findParams->dbAccesMode); if (! dbP) return; // Display the heading line. headerH = DmGetResource(strRsc, FindHeaderString); header = MemHandleLock(headerH); done = FindDrawHeader(findParams, header); MemHandleUnlock(headerH); if (done) { findParams->more = true; } else { // Search all the fields; start from the last record searched. recordNum = findParams->recordNum; for(;;) { Boolean match = false; Customer customer; // Because applications can take a long time to finish a find // users like to be able to stop the find. Stop the find // if an event is pending. This stops if the user does // something with the device. Because this call slows down // the search we perform it every so many records instead of // every record. The response time should still be short // without introducing much extra work to the search. // Note that in the implementation below, if the next 16th // record is secret the check doesn't happen. Generally // this shouldn't be a problem since if most of the records // are secret then the search won't take long anyway! if ((recordNum & 0x000f) == 0 && // every 16th record EvtSysEventAvail(true)) { // Stop the search process. findParams->more = true; break; } recordH = DmQueryNextInCategory(dbP, &recordNum, dmAllCategories); // Have we run out of records? if (! recordH) break; // Search each of the fields of the customer UnpackCustomer(&customer, MemHandleLock(recordH)); if ((match = FindStrInStr((CharPtr) customer.name, findParams->strToFind, &pos)) != false) fieldNum = CustomerNameField; else if ((match = FindStrInStr((CharPtr) customer.address, findParams->strToFind, &pos)) != false) fieldNum = CustomerAddressField; else if ((match = FindStrInStr((CharPtr) customer.city, findParams->strToFind, &pos)) != false) fieldNum = CustomerCityField; else if ((match = FindStrInStr((CharPtr) customer.phone, findParams->strToFind, &pos)) != false) fieldNum = CustomerPhoneField; if (match) { done = FindSaveMatch(findParams, recordNum, pos, fieldNum, 0, cardNo, dbID); if (done) break; //Get the bounds of the region where we will draw the results. FindGetLineBounds(findParams, &r); // Display the title of the description. DrawCharsToFitWidth(customer.name, &r); findParams->lineNumber++; } MemHandleUnlock(recordH); if (done) break; recordNum++; } } DmCloseDatabase(dbP); }
Displaying the found item
First, here's the code from PilotMain that calls StartApplication, EventLoop, and StopApplication, if necessary (if using GCC and the application was already running, the code must have the CALLBACK macros, since PilotMain was called as a subroutine from a system function):
// This launch code might be sent to the app when it's already running else if (cmd == sysAppLaunchCmdGoTo) { Boolean launched; launched = launchFlags & sysAppLaunchFlagNewGlobals; if (launched) { error = StartApplication(); if (!error) { GoToItem((GoToParamsPtr) cmdPBP, launched); EventLoop(); StopApplication(); } } else { #ifdef __GNUC__ CALLBACK_PROLOGUE #endif GoToItem((GoToParamsPtr) cmdPBP, launched); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif } }
Here's the GoToItem function that opens the correct form and posts a frmGotoEvent:
static void GoToItem (GoToParamsPtr goToParams, Boolean launchingApp) { EventType event; UInt recordNum = goToParams->recordNum; // If the current record is blank, then it will be deleted, so we'll use // the record's unique id to find the record index again, after all // the forms are closed. if (! launchingApp) { ULong uniqueID; DmRecordInfo(gCustomerDB, recordNum, NULL, &uniqueID, NULL); FrmCloseAllForms(); DmFindRecordByID(gCustomerDB, uniqueID, &recordNum); } FrmGotoForm(CustomersForm); // Send an event to select the matching text. MemSet (&event, 0, sizeof(EventType)); event.eType = frmGotoEvent; event.data.frmGoto.formID = CustomersForm; event.data.frmGoto.recordNum = goToParams->recordNum; event.data.frmGoto.matchPos = goToParams->matchPos; event.data.frmGoto.matchLen = goToParams->searchStrLen; event.data.frmGoto.matchFieldNum = goToParams->matchFieldNum; event.data.frmGoto.matchCustom = goToParams->matchCustom; EvtAddEventToQueue(&event); }
Remember that this code needs to take into account the possibility of records that change numbers in between closing open forms and displaying the found record. We do this using DmRecordInfo and DmFindRecordByID. The first takes the record and finds the unique idea associated with it; the second returns a record based on the unique idea.
Note also that we're opening the CustomersForm, even though we really want the CustomerForm. The reason we do this is that we can't get to the CustomerForm directly. It is a modal dialog that is displayed above the CustomersForm. Thus, the CustomersForm needs to be opened first, because it is that bit of code that knows how to open the CustomerForm. Here's the code from CustomersHandleEvent that opens the CustomerForm:
case frmGotoEvent: EditCustomerWithSelection(event->data.frmGoto.recordNum, false, &deleted, &hidden, &event->data.frmGoto); handled = true; break;
Here's the portion of EditCustomerWithSelection that scrolls and highlights the correct text:
static void EditCustomerWithSelection(UInt recordNumber, Boolean isNew, Boolean *deleted, Boolean *hidden, struct frmGoto *gotoData) { // code deleted that gets the customer record and initializes // the fields // 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); } // code deleted that displays the dialog and handles updates // when the dialog is dismissed }
That is all there is to adding support for Find to our application. Indeed, the trickiest part of the code is figuring out the type of situations you might encounter that will cause Find to work incorrectly. The two most important of these are searching applications with multiple databases correctly and making sure that you don't lose the record in between closing forms and displaying results.
Beaming |
In this section, we discuss beaming. First, we give you a general overview of beaming, describe the user interface, and offer you a few useful tips. Next, we provide a checklist that you can use to implement beaming in an application. Last, we implement beaming in the Sales application.
Beaming and the Exchange Manager
The Exchange Manager is in charge of exchanging of information between Palm OS devices and other devices. This manager is new to Palm OS 3.0 and is built on industry standards.
Currently, the Exchange Manager works only over an infrared link, although it may be enhanced in the future to work over other links (such as TCP/IP or email). The exchange manager uses the ObEx Infrared Data Association (IrDA) standard to exchange information. As a result, it should be possible to exchange information between Palm OS devices and other devices that implement this ObEx standard.
NOTE: For information on IrDA standards, see http://www.irda.org. For information on Multipurpose Internet Mail Extensions (MIME), see http://www.mindspring.com/~mgrand/mime.html or http://www.cis.ohio-state.edu/hypertext/faq/usenet/mail/mime-faq/top.html. |
How Beaming Works
Applications that support this feature usually allow beaming either a single item or an entire category. When the user chooses the Beam menu item, a dialog appears showing that the beam is being prepared. Then it searches for another device using infrared. Once it finds the other device, it beeps and starts sending the data. After the remote device receives all the data, it beeps and presents a dialog to the user, asking whether the user wants to accept the data. If the user decides to accept the data, it is put away; if not, it is thrown away. The creator type of the item is matched to an appropriate application on the receiving device, which then displays the newly received data.
Newly received items are always placed in the Unfiled category. This is true even when both sending and receiving units have the same categories. While problematic for a few users, this it the right solution for most situations. Users will have one consistent interface for receiving items. After all, who is to say that a user wants beamed items filed in the same name category that the sending handheld uses?
The user can also send an entire category. When a category is sent, private records are skipped (to avoid accidentally sending unintended records). Newly received items are placed in the Unfiled category.
Programming Tips
The following sections present a set of miscellaneous tips to help you implement beaming. The first ones are optimization suggestions, the next will help you when debugging your code, and the last are a grab bag of helpful ideas.
Optimization tips
Debugging tips
General tips
Figure 8- 5.
Alert shown when user attempts to beam on a device that has the beaming APIs (3.0 OS or greater), but no IR hardware
Step-by-Step Implementation Checklist
Beaming lends itself well to a checklist approach of implementation. If you follow these steps in a cookbook-like fashion, you should get beaming up in a jiffy.
Determine data interchange format
1. You first need to decide whether you'll use a file extension or MIME type (or both). You also have to determine the format of the transmitted data (for both a single entry and an category).
Add beam user interface
2. Add a Beam menu item to beam the current entry.
3. Add a Beam Category item to the overview Record menu to beam the current category.
Send an entry
4. Add <ExgMgr.h> to your include files.
5. Declare an ExgSocketType and initialize it to 0.
6. Initialize the description field of the ExgSocketType.
7. Initialize type, target, and/or name.
8. Initialize localMode to 1 (this is for testing with one device; it's optional).
9. Call ExgPut to begin the beam.
10. Call ExgSend in a loop to send the actual data.
11. Call ExgDisconnect to terminate the beam.
Receive an entry
12. Register for receiving based on the MIME type and/or file extension (optional) you set up in step 1.
In PilotMain, when a sysAppLaunchCmdSyncNotify launch code occurs, call ExgRegisterData with exgRegExtensionID and/or call ExgRegisterData with exgRegTypeID. This setup is optional, however. If a sender beams data specifying your target application creator, your application will get sent a launch code even if it hasn't registered for a specific extension and/or MIME type. You should do this registration if you have a specific kind of data that you want to handle; senders of that data may not have a specific application in mind when they do the send.
13. Handle the receive beam launch code.
In PilotMain, check for the sysAppLaunchCmdExgReceiveData launch code. You won't have global variables unless you happen to be the open application.
14. Call ExgAccept.
15. Call ExgReceive repeatedly and until ExgReceive returns 0. A zero is returned when no more data is being received or an error has occurred.
16. Call ExgDisconnect to hang up properly.
17. Set gotoLaunchCode and gotoParams.
Set gotoLaunchCode to your creator's application. Set the following fields in gotoParams with the appropriate values: uniqueID, dbID, dbCardNo, recordNum.
Display received item
This feature is a free gift thanks to the work you did in supporting Find. If your application already correctly handles Find, displaying received items is no work.
Send an entire category
The code for sending an entire category is very similar to the code for sending one item (the actual data you send will be different, of course). You must make sure that your data format allows you to distinguish between one item and multiple items.
18. Declare an ExgSocketType and initialize it to 0.
19. Initialize the description field of the ExgSocketType.
20. Initialize type, target, and/or name.
21. Initialize localMode to 1 (this is for testing with one device; it's optional).
22. Call ExgPut to begin the beam.
23. Call ExgSend in a loop to send the actual data.
24. Call ExgDisconnect to terminate the beam.
Receive an entire category
Receiving an entire category is similar to receiving one item.
25. Call ExgAccept.
26. Call ExgReceive repeatedly.
27. Call ExgDisconnect.
28. Set gotoLaunchCode and gotoParams.
Test all possibilities
You need to run a gamut of tests to make sure you haven't forgotten any of the details. Test every one of the following combinations of sending and receiving and any other tests that come to mind.
29. Send a record while your application is open on the remote device.
30. Send a record while your application isn't open on the remote device.
31. Send a category with lots of records (so that the ExgReceive can't read all its data at one time).
32. Tap No when the Accept dialog is presented on the remote device.
33. Send a category with a private record. Verify that the private record isn't received.
34. Verify that beaming an empty category does nothing (doesn't try to send anything).
35. If you've registered a MIME type or extension, send using the ExgSend test application to make sure your application correctly receives (rather than relying strictly on the target).
36. Try the test on a 3.0 device that lacks IR capability (for example, POSE).
Sales Application
The Sales application doesn't have categories, so we don't have a Beam Category menu item; instead, we support Beam all Customers for times when the user wants to beam all the customer information. We also support beaming a single customer.
NOTE: We don't support beaming an entire order, although that would be a reasonable thing to add to the application, particularly if it were a commercial product. Our interests are pedagogical rather than commercial, so we are skipping that bit; adding this support would not teach you anything new. |
When beaming a single customer, we send the customer record itself, with a name ending in .CST. When beaming all customers, we send:
- A two-byte record length for the record
- The customer record itself
Let's look at handling a single customer first and then turn to dealing with them all.
Sending a single customer
We add support for beaming to OrderHandleMenuEvent, where we add the Beam menu item:
case RecordBeamCustomer: BeamCustomer(GetRecordNumberForCustomer(gCurrentOrder->customerID)); handled = true; break;
When the user selects the menu item, the BeamCustomer routine we have created gets called into play. BeamCustomer beams a single customer:
static void BeamCustomer(UInt recordNumber) { ExgSocketType s; Handle theRecord = DmQueryRecord(gCustomerDB, recordNumber); PackedCustomer *thePackedCustomer = MemHandleLock(theRecord); Err err; MemSet(&s, sizeof(s), 0); s.description = thePackedCustomer->name; s.name = "customer.cst"; s.target = salesCreator; err = ExgPut(&s); if (!err) err = BeamBytes(&s, thePackedCustomer, MemHandleSize(theRecord)); MemHandleUnlock(theRecord); err = ExgDisconnect(&s, err); }
BeamCustomer relies on BeamBytes to actually send the data. Here is that code:
static Err BeamBytes(ExgSocketPtr s, void *buffer, ULong bytesToSend) { Err err = 0; while (!err && bytesToSend > 0) { ULong bytesSent = ExgSend(s, buffer, bytesToSend, &err); bytesToSend -= bytesSent; buffer = ((char *) buffer) + bytesSent; } return err; }
That is all the code for beaming one customer. Let's look at what we need to do to receive that information on the other end.
Receiving a record
First, we need to register with the Exchange Manager in PilotMain. Note that we check to make sure we are running OS 3.0 or greater before setting to work:
} else if (cmd == sysAppLaunchCmdSyncNotify) { DWord romVersion; FtrGet(sysFtrCreator, sysFtrNumROMVersion, &romVersion); if (sysGetROMVerMajor(romVersion) >= 3) ExgRegisterData(kSalesCreator, exgRegExtensionID, "cst"); // code deleted that resorts our databases }
Next, we've got to handle the receive data launch code, which we also put into our PilotMain:
} else if (cmd == sysAppLaunchCmdExgReceiveData) { DmOpenRef dbP; // if our app is not active, we need to open the database // The subcall flag is used to determine whether we are active if (launchFlags & sysAppLaunchFlagSubCall) { #ifdef __GNUC__ CALLBACK_PROLOGUE #endif dbP = gCustomerDB; // save any data we may be editing. FrmSaveAllForms(); error = ReceiveBeam(dbP, (ExgSocketPtr) cmdPBP); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif } else { dbP = DmOpenDatabaseByTypeCreator(kCustomerDBType, kSalesCreator, dmModeReadWrite); if (dbP) { error = ReceiveBeam(dbP, (ExgSocketPtr) cmdPBP); DmCloseDatabase(dbP); } } }
We open the customer database if our application isn't already running. If our application is running, and if we're using GCC, we must use CALLBACK_PROLOGUE and CALLBACK_EPILOGUE, since PilotMain is being called as a subroutine call from the Palm OS (if we don't put in the callback macros, we'll crash if we try to access global variables like gCustomerDB). Then, we call FrmSaveAllForms to save any data currently being edited. ReceiveBeam handles much of this work. Note that since new customers need to have unique customer IDs, we assign a new customer ID to the newly received customer, just as we would if the user used the New Customer... menu item.
This version of ReceiveBeam doesn't receive all customers yet. See "Receiving all customers" on page 246 for the final version, which does.
// NB: First version that doesn't support receiving all customers yet static Err ReceiveBeam(DmOpenRef db, ExgSocketPtr socketPtr) { Err err; UInt index; SDWord newCustomerID = GetLowestCustomerID() - 1; err = ExgAccept(socketPtr); if (!err) { err = ReadIntoNewRecord(db, socketPtr, 0xffffffff, &index); // must assign a new unique customer ID if (!err) { VoidHand h = DmGetRecord(db, index); DmWrite(MemHandleLock(h), offsetof(Customer, customerID), &newCustomerID, sizeof(newCustomerID)); MemHandleUnlock(h); DmReleaseRecord(db, index, true); } } err = ExgDisconnect(socketPtr, err); if (!err) { DmRecordInfo(db, index, NULL, &socketPtr->goToParams.uniqueID, NULL); DmOpenDatabaseInfo(db, &socketPtr->goToParams.dbID, NULL, NULL, &socketPtr->goToParams.dbCardNo, NULL); socketPtr->goToParams.recordNum = index; socketPtr->goToCreator = salesCreator; } return err; }
ReadIntoNewRecord reads until there is no more to read (or the number of bytes specified, a feature we use when reading all customers). It returns the new record number in the indexPtr parameter:
// read at most numBytes into a new record. // Don't use very much dynamic RAM or stack space--another app is running static Err ReadIntoNewRecord(DmOpenRef db, ExgSocketPtr socketPtr, ULong numBytes, UInt *indexPtr) { char buffer[100]; Err err; UInt index = 0; ULong bytesReceived; VoidHand recHandle = NULL; CharPtr recPtr; ULong recSize = 0; Boolean allocatedRecord = false; do { ULong numBytesToRead; numBytesToRead = min(numBytes, sizeof(buffer)); bytesReceived = ExgReceive(socketPtr, buffer, numBytesToRead, &err); numBytes -= bytesReceived; if (!err) { if (!recHandle) recHandle = DmNewRecord(db, &index, bytesReceived); else recHandle = DmResizeRecord(db, index, recSize + bytesReceived); if (!recHandle) { err = DmGetLastErr(); break; } allocatedRecord = true; recPtr = MemHandleLock(recHandle); err = DmWrite(recPtr, recSize, buffer, bytesReceived); MemHandleUnlock(recHandle); recSize += bytesReceived; } } while (!err && bytesReceived > 0 && numBytes > 0); if (recHandle) { DmReleaseRecord(db, index, true); } if (err && allocatedRecord) DmRemoveRecord(db, index); *indexPtr = index; return err; }
That is all there is to sending and receiving a single customer. Next, let's look at what additional changes you need to make to beam or receive them all at once.
Sending all customers
Once again, we add something to our CustomersHandleMenuEvent that handles sending all customers:
case CustomerBeamAllCustomers: BeamAllCustomers(); handled = true; break;
It calls BeamAllCustomers which beams the number of records, then the size of each record and the record itself:
#define kMaxNumberLength 5 static void BeamAllCustomers(void) { DmOpenRef dbP = gCustomerDB; UInt mode; LocalID dbID; UInt cardNo; Boolean databaseReopened; UInt numCustomers; // If the database was opened to show secret records, reopen it to not // see secret records. The idea is that secret records are not sent // when a category is sent. They must be explicitly sent one by one. DmOpenDatabaseInfo(dbP, &dbID, NULL, &mode, &cardNo, NULL); if (mode & dmModeShowSecret) { dbP = DmOpenDatabase(cardNo, dbID, dmModeReadOnly); databaseReopened = true; } else databaseReopened = false; // We should send because there's at least one record to send. if ((numCustomers = DmNumRecordsInCategory(dbP, dmAllCategories)) > 0) { ExgSocketType s; VoidHand recHandle; Err err; UInt index = dmMaxRecordIndex; MemSet(&s, sizeof(s), 0); s.description = "All customers"; s.target = kSalesCreator; s.localMode = 1; err = ExgPut(&s); if (!err) err = BeamBytes(&s, &numCustomers, sizeof(numCustomers)); while (!err && numCustomers-- > 0) { UInt numberToSeekBackward = 1; if (index == dmMaxRecordIndex) numberToSeekBackward = 0; // we want the last one err = DmSeekRecordInCategory(dbP, &index, numberToSeekBackward, dmSeekBackward, dmAllCategories); if (!err) { UInt recordSize; recHandle = DmQueryRecord(dbP, index); ErrNonFatalDisplayIf(!recHandle, "Couldn't query record"); recordSize = MemHandleSize(recHandle); err = BeamBytes(&s, &recordSize, sizeof(recordSize)); if (!err) { PackedCustomer *theRecord = MemHandleLock(recHandle); err = BeamBytes(&s, theRecord, MemHandleSize(recHandle)); MemHandleUnlock(recHandle); } } } err = ExgDisconnect(&s, err); } else FrmAlert(NoDataToBeamAlert); if (databaseReopened) DmCloseDatabase(dbP); }
BeamAllCustomers uses BeamBytes, which we've already seen.
In order to receive all customers, ReceiveBeam must change just a bit (changes are in bold):
static Err ReceiveBeam(DmOpenRef db, ExgSocketPtr socketPtr) { Err err; UInt index; Boolean nameEndsWithCst = false; SDWord newCustomerID = GetLowestCustomerID() - 1; // we have a single customer if it has a name ending // in ".cst". Otherwise, it's all customers. "All customers" // will have a name // because the exchange manager provides one automatically. if (socketPtr->name) { CharPtr dotLocation = StrChr(socketPtr->name, '.'); if (dotLocation && StrCaselessCompare(dotLocation, ".cst") == 0) nameEndsWithCst = true; } err = ExgAccept(socketPtr); if (!err) { if (nameEndsWithCst || socketPtr->type) { // one customer err = ReadIntoNewRecord(db, socketPtr, 0xffffffff, &index); // must assign a new unique customer ID if (!err) { VoidHand h = DmGetRecord(db, index); DmWrite(MemHandleLock(h), offsetof(Customer, customerID), &newCustomerID, sizeof(newCustomerID)); MemHandleUnlock(h); DmReleaseRecord(db, index, true); } } else { // all customers UInt numRecords; ExgReceive(socketPtr, &numRecords, sizeof(numRecords), &err); while (!err && numRecords-- > 0) { UInt recordSize; ExgReceive(socketPtr, &recordSize, sizeof(recordSize), &err); if (!err) { err = ReadIntoNewRecord(db, socketPtr, recordSize, &index); // must assign a new unique customer ID if (!err) { VoidHand h = DmGetRecord(db, index); DmWrite(MemHandleLock(h), offsetof(Customer, customerID), &newCustomerID, sizeof(newCustomerID)); newCustomerID--; MemHandleUnlock(h); DmReleaseRecord(db, index, true); } } } } } err = ExgDisconnect(socketPtr, err); if (!err) { DmRecordInfo(db, index, NULL, &socketPtr->goToParams.uniqueID, NULL); DmOpenDatabaseInfo(db, &socketPtr->goToParams.dbID, NULL, NULL, &socketPtr->goToParams.dbCardNo, NULL); socketPtr->goToParams.recordNum = index; socketPtr->goToCreator = kSalesCreator; } return err; }
That is all the code needed to support beaming. Use the checklist to make sure you take care of all the little details, and review the sample application if you have any further questions. Otherwise, it's on to another topic.
Barcodes |
The Symbol SPT 1500 has a built-in barcode scanner (see Figure 8-6). The two buttons at the top of the device start the scan, and the barcode scanner runs along the top of the device. As you might imagine, for some vertical applications, this can be quite useful (for example, salespeople could have a catalog with barcoded items; warehouse workers could read barcodes from boxes).
Figure 8- 6.
Symbol SPT 1500 with barcode scanner
If you want to implement barcode scanning in an application, there are some special requirements and APIs from Symbol to use. First, let's look at some of the API calls that barcode reading brings to the Palm 3.0 OS. Then we do a code walkthrough of a sample application that scans barcodes.
For more information on the Symbol APIs or the SDK, contact Symbol Technologies (http://www.symbol.com/palm). Symbol also has a neat utility program that lets you turn the SPT 1500 device into a unit dedicated to your application. The utility allows you to reflash the device ROM to include or dedicate it to your application. For more information on this, contact Symbol.*
Handling Scanning in an Application
There are some minor additions to the Palm 3.0 OS. The basic support for scanning barcodes requires these simple steps:
1. Your code needs to make sure you have a Symbol device by calling the function ScanIsPalmSymbolUnit. If it isn't a Symbol device, make no further Symbol calls. This routine is provided as part of a library; thus, you can call it whether or not you are on a Symbol device. You should normally call this once at the beginning of your program and store its result in a global.
2. The next step is to load the Symbol library with ScanOpenDecoder. Once you've done this, the scanner provides power to the scanning hardware. To save battery life, you usually don't do this at the start of your application, where scanning is inappropriate (for instance, only some forms may allow scanning, so you may enable scanning when such a form opens). After calling this routine, the scan hardware has power, but the user still can't press the buttons to do a scan.
3. Once the Symbol library is open, you have a few optional alternatives to consider. These involve initializing various scanning options, such as:
- The type of barcodes to be recognized.
- The feedback options you want to give when a barcode is scanned-you can have the unit beep or flash the green LED or both.
- Options on the barcodes that include lengths, conversions, checksums, etc.
4. When you are actually ready for the user to scan, call ScanCmdScanEnable. Don't just blindly call this routine after opening the scanning library. Enable scanning only when it actually makes sense for the user to scan (for example, when the user enters a particular field). Otherwise, the user might accidentally press one of the two built-in scan buttons, which will:
- Cause the laser to activate while the unit isn't pointing at a barcode. Activating lasers that are pointing at random locations is a bad idea (think lawsuit).
- Unnecessarily drain the battery.
5. Your application needs to respond to two new events while scanning is enabled:
- scanDecodeEvent. This is sent when a scan (successful or unsuccessful) has occurred. In response to such an event, call ScanGetDecodedData. You get the scanned ASCII data from this call as well as what kind of barcode (there are many different symbologies) was scanned.
- scanBatteryErrorEvent. This event is sent when the battery is too low to do a scan. As a scan requires more power from the battery than simply running the handheld, there may be enough battery life to run the handheld, but not enough to do the scan. This event is sent so that you can alert the user to the problem.
NOTE: It is very important that you handle scanBatteryErrorEvent correctly in your application. Without a proper alert, the user will have no idea why the scan did not occur. |
6. When scanning is no longer appropriate (for instance, the user leaves a field in which they are allowed to scan), call ScanCmdScanDisable.
7. When you're ready to shut down the scanner, call ScanCloseDecoder. This may be at the end of your application for an application that allows scanning everywhere. It may be as a form closes, if you've got some forms that allow scanning and others that don't.
A Scanning Sample
Some applications might be written so that any field could be written into with Graffiti or, alternatively, scanned into with the barcode scanner. The code we've written is designed to retrofit an existing application to allow input to any field from the scanner. A successful scan takes the scanned data and inserts it in the field that contains the insertion point.
Starting up the application
In AppStart, we check to make sure we're running on a Symbol unit. If so, we initialize the library, set parameters so that we can scan every type of barcode and enable scanning (remember that this application allows scanning anywhere):
static Boolean gScanManagerInitialized = false; static Err AppStart(void) { // other initialization code deleted if (ScanIsPalmSymbolUnit()) { Err err = ScanOpenDecoder(); // load the scanner library if (err == 0) { int i; // we want to be able to scan everything we can get our hands on // If we just wanted the default types, we could jump directly // to calling ScanCmdScanEnable BarType allTypes[] = { barCODE39, barUPCA, barUPCE, barEAN13, barEAN8, barD25, barI2OF5, barCODABAR, barCODE128, barCODE93, barTRIOPTIC39, barUCC_EAN128, barMSI_PLESSEY, barUPCE1, barBOOKLAND_EAN, barISBT128, barCOUPON}; for (i = 0; i < sizeof(allTypes) / sizeof(*allTypes); i++) err = ScanSetBarcodeEnabled(allTypes[i], true); err = ScanCmdSendParams(No_Beep); // send all the accumulated // settings to scanner // allow scanning as of now (uses some battery life) err = ScanCmdScanEnable(); gScanManagerInitialized = true; } } return 0; }
The application shutdown
In AppStop, we disable scanning and close the library:
static void AppStop(void) { // other termination code deleted if (gScanManagerInitialized) { ScanCmdScanDisable(); // turn scanner off ScanCloseDecoder(); } }
Event handling
In AppHandleEvent, we put up an alert (see Figure 8-7) if the battery is too low to scan.
Figure 8- 7.
The application's alert that is posted when a scanBatteryErrorEvent occurs
Here is the code that accomplishes this task:
if (eventP->eType == scanBatteryErrorEvent) { FrmAlert(LowScanBatteryAlert); return true; }
Also in AppHandleEvent, we need to handle a scan event:
if (eventP->eType == scanDecodeEvent) { MESSAGE decodeDataMsg; int status = ScanGetDecodedData( &decodeDataMsg ); // if we successfully got the decode data from the API... if( status == STATUS_OK ) { FormPtr form = FrmGetActiveForm(); // a response of NR means no scan happened. If so, ignore it if (decodeDataMsg.length == 2 && decodeDataMsg.data[0] == 'N' && decodeDataMsg.data[1] == 'R') return true; // find the focused field and insert there if (form) { Word focusedIndex = FrmGetFocus(form); //focusedIndex is documented to return -1 but is also documented // to return a (unsigned) Word. Instead of returning -1, then, // it returns 65536 if (focusedIndex >= 0 && focusedIndex < 65535) { if (FrmGetObjectType(form, focusedIndex) == frmFieldObj) { FieldPtr focusedField = (FieldPtr) FrmGetObjectPtr(form, focusedIndex); if (focusedField->attr.editable) FldInsert(focusedField, (CharPtr) decodeDataMsg.data, decodeDataMsg.length); } } } } return true; }
This is all there is to supporting barcode reading in an application. With a dandy device like the Symbol SPT 1500 and such an easy set of changes required to support barcode scanning, you should expect to see a variety of applications.
* If you are new to barcode technologies, there is an excellent reference work available: The Bar Code Book, by Roger C. Palmer, 1995, Third Ed. (Helmers, ISBN: 0-91126-109-5).
Palm Programming: The Developer's Guide