I haven’t done everything with tables, so this HOWTO will not explain everything about a table. In fact I have done very little with tables, so this HOWTO explains very little. This HOWTO gives the recreational programmer a starting point. It is expected that after mastering the techniques described here, the reader will find better and more sophisticated ways to manipulate tables. If you improve on these methods, please send me a note describing your techniques and I will immortalize your achievement in an updated version of this HOWTO.
The HOWTO was written in MS Word 97 and (tediously) converted to HTML for cross-platform distribution.
When you tap in one of the table’s cells PalmOS enters a tblSelectEvent on the event queue. The tblSelectEvent data structure includes the row and column of the cell that was tapped. This allows you to execute different actions for each column, row, and/or cell. In this way the table can be thought of as a grid of buttons.
|
|
|
|
|
Required | Assigned | |||
Field One | ||||
Field Two | ||||
Field Three | ||||
… | ||||
Total: |
|
|
In my tables, each row is associated with a record of information. The
record has several fields. The problem is to figure out which fields will
be displayed, how many characters of each field will be displayed, the
number of pixels required to display the entire field and the pixel width
assigned to the field.
Some things to remember when planning your column widths:
TABLE ID <Id.n> AT (<Left.p> <Top.p> <Width.p> <Height.p>) ROWS <NumRows.n> COLUMNS <NumCols.n> COLUMNWIDTHS <Col1Width.n> <Col2Width.n.>...
|
|
|
|
|
Required | Assigned | |||
dbID |
|
|
|
|
Resource/Data |
|
|
|
R for resource, D for data. Bold font 6 pixels wide. |
Type |
|
|
|
|
CreatorID |
|
|
|
|
Name |
|
|
|
Suppose we need at least six characters to identify a database name, plus room for the ellipsis. |
Our form will need a title bar, and we want some room at the bottom of the form to display other information and maybe controls. So we have 130 pixel-height to work with, which gives us eleven rows in the table (using standard and bold fonts).
We now have all the information we need to define the table resource
and it's form.
FORM formID_FileList 0 0 160 160
MENUID menuID_MainMenu
USABLE
NOFRAME
BEGIN
TITLE "File Manager 1.0"
TABLE tableID_FileList AT (0 20 160 140) ROWS 11 COLUMNS 5 COLUMNWIDTHS 15 10 30 30 75
END
You set rows usable/non-usable with the TblSetRowUsable API call. Its
prototype is:
void TblSetRowUsable ( TablePtr table, Word row, Boolean usable)
Row is zero based (first row is row zero) and usable is 1 for usable, 0
for non-usable.
void TblMarkRowInvalid (TablePtr table, Word row)
void TblSetColumnUsable ( TablePtr table, Word row, Boolean usable )
void TblSetItemStyle ( TablePtr table, Word row, Word column, TableItemStyleType type )
void TblSetCustomDrawProcedure( TablePtr table, Word column, VoidPtr drawCallback )
static void FileListDrawForm(void) { FormPtr frm; TablePtr tableP; UInt dbIndex; Word row, numRows; int currFont; char string[30]; WinEraseWindow(); frm = FrmGetActiveForm(); tableP = FrmGetObjectPtr (frm, FrmGetObjectIndex (frm, tableID_FileList));NumDatabases is a global variable containing the number of databases on card zero. If there are no databases on card 0, then return from the function now.
if (NumDatabases == 0) { FrmDrawForm (frm); return; }Now we get to the guts of the function. TopRow is a global which is equal to the dbIndex of the database which will be drawn in the top row (row zero) of the table. Get the number of rows in the table using TblGetNumberOfRows. Stuff the value of TopRow into the RowData storage location that is part of the table structure (I'll explain why we have to do this in section 4).
dbIndex = TopRow; numRows = TblGetNumberOfRows (tableP); TblSetRowData ( tableP, 1, TopRow);Next we loop through all the rows and set them usable/non-usable, and if usable set them invalid and set the item style. We have to set the values for all the rows, but we may not have enough databases on the card to populate the entire table. If dbIndex is less than NumDatabases, then we have an entry for that row so we set the row usable, mark the row invalid, and set the item style of every cell in the row to customTableItem. If dbIndex is not less than NumDatabases, then we set the row non-usable.
for (row = 0; row < numRows; row++, dbIndex++) { if (dbIndex < NumDatabases) { TblSetRowUsable ( tableP, row, true); TblMarkRowInvalid ( tableP, row); TblSetItemStyle ( tableP, row, 0, customTableItem); TblSetItemStyle ( tableP, row, 1, customTableItem); TblSetItemStyle ( tableP, row, 2, customTableItem); TblSetItemStyle ( tableP, row, 3, customTableItem); TblSetItemStyle ( tableP, row, 4, customTableItem); } else { TblSetRowUsable ( tableP, row, false); } }We have to set the custom cell draw procedure for each column. We decide now that the name of the custom cell draw function will be "FileListDrawCell". We also have to set all the columns to usable. When that's done we can call FrmDrawForm.
TblSetCustomDrawProcedure ( tableP, 0, FileListDrawCell); TblSetCustomDrawProcedure ( tableP, 1, FileListDrawCell); TblSetCustomDrawProcedure ( tableP, 2, FileListDrawCell); TblSetCustomDrawProcedure ( tableP, 3, FileListDrawCell); TblSetCustomDrawProcedure ( tableP, 4, FileListDrawCell); TblSetColumnUsable ( tableP, 0, true); TblSetColumnUsable ( tableP, 1, true); TblSetColumnUsable ( tableP, 2, true); TblSetColumnUsable ( tableP, 3, true); TblSetColumnUsable ( tableP, 4, true); FrmDrawForm (frm);When using a scrollable table it's a good idea to indicate the total number of rows in the data source. You can do this in several ways. Some apps use FrmCopyTitle to indicate the number of records right in the form title. This app uses WinDrawChars to print a string at the bottom of the LCD panel. Because we use the WinDrawChars API call, we had to include the WinEraseWindow call at the beginning of the function.
StrIToA (string, NumDatabases); StrCat (string, " databases on card 0"); WinDrawChars (string, StrLen(string), 0, 149); }
When PalmOS calls the custom cell draw procedure PalmOS passes the table pointer of the relevant table, the row and column of the cell to be drawn, and a Rectangle structure which describes the location and size of the cell. If you have multiple tables in your application, it is conceivable but not recommendable that you could use one custom cell draw procedure and switch on the table pointer to implement different functions for different tables. It would be way easier to write different custom cell draw procedures for different tables.
Your cell draw procedure processes the row and column arguments to determine how to populate the cell. Exactly how the row and column arguments determine the contents of the cell depends on how you choose to organize your data. PalmOS leaves a lot of room for programmer imagination.
/*********************************************************************** * * Trim String To Fit Cell * * I don't want to use the FntCharsInWidth function for two reasons: * * 1. The use of a boolean causes strange problems with GCC * 2. I want ellipsis ("...") to appear at the end of truncated strings * **********************************************************************/ void TrimStringToFitCell (char *string, int cell_width_pixels) { if (FntCharsWidth(string, StrLen(string)) > cell_width_pixels) { cell_width_pixels -= 6; do { string[StrLen(string) - 1] = 0; } while (FntCharsWidth(string, StrLen(string)) > cell_width_pixels); StrCat(string, "..."); } }
static void FileListDrawCell (VoidPtr tableP, Word row, Word column, RectanglePtr bounds) { DWord dbID; short font; char DBName[32], *display_string, string[5]; short TextLen; FontID currFont; unsigned long type, creator, RowTop; unsigned short attributes;We want to get the database id of database to be displayed in this row. To figure this out, we need to know the dbIndex of the row. From the Form Draw routine we know that the dbIndex is equal to TopRow plus row. But first we have to get the value from where we stashed it – that is, out of the row data area of row 1. I call TopRow RowTop within the custom cell draw procedure to remind me that it isn’t really the global variable.
RowTop = TblGetRowData(tableP, 1); dbID = DmGetDatabase (0, RowTop + row);Once we have the dbID, we can retrieve information about that database.
DmDatabaseInfo (0, dbID, (char *) &DBName, &attributes, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &type, &creator);Now we must display that information. What we display depends on which column PalmOS wants us to populate. So we switch on column. For each case, we need three pieces of information: a pointer to a string, the length of the string, and which font to use for the string.
switch (column) { case 0: // column 0 shows index StrIToA(string, RowTop+row); display_string = string; TextLen = StrLen(string); font = 0; break; case 1: // column 1 shows Res/Data character if (attributes && dmHdrAttrResDB) string[0] = 'R'; else string[0]='D'; display_string = string; TextLen = 1; font = 1; break; case 2: // column 2 shows type display_string = (char *) &type; TextLen = 4; font = 0; break; case 3: // column 3 shows creatorID display_string = (char *) &creator; TextLen = 4; font = 0; break; case 4: // column 4 shows dbName display_string = (char *) &DBName; TrimStringToFitCell(display_string, bounds->extent.x - 2); TextLen = StrLen(DBName); font = 0; break; }And now all we have to do is display the string.
currFont = FntSetFont (font); WinDrawChars(display_string, TextLen, bounds->topLeft.x, bounds->topLeft.y); FntSetFont (currFont); // Restore the font. }For columns one to four I know that the string will fit in the cell, but in the last column the string might be larger than the cell. Therefore I used the TrimStringToFitCell function when processing column five (case 4).
case keyDownEvent: if (event->data.keyDown.chr == pageUpChr) { FileListScroll (pageUpChr); FileListDrawForm(); handled = true; } else if (event->data.keyDown.chr == pageDownChr) { FileListScroll (pageDownChar); FileListDrawForm(); handled = true; } break;You see that I implement scrolling using a FileListScroll function. I pass one of two values: pageUpChr or pageDownChr. These values are defined in PalmOS header files. After FileListScroll does its thing, I redraw the form, using the custom form draw function discussed in section 3.
So what exactly does FileListScroll do? Not much. If the argument was pageUpChr then FileListScroll subtracts numRows from the TopRow global, where numRows is the number of rows in the database. If the argument was pageDownChr, then FileListScroll adds numRows to TopRow. Of course, we don’t want TopRow to be greater than our maximum number of data rows. In the example NumDatabases is the maximum number of rows, so if TopRow is larger than NumDatabases, then we set TopRow equal to NumDatabases minus numRows. In fact, there's a better way to do this. You can make sure that the last page displayed is always a full page by using the expression "((TopRow + numRows) >= NumDatabases)". I'll leave it for you to figure out why that works.
Similarly, we don’t want TopRow to be negative,
so if TopRow is less than zero we set it equal to zero. Notice that the
negative test comes after the maximum test. This is important. Consider
the case where TopRow=0, NumDatabases=5 and numRows=11. Someone pushes
the pageDownChr key. FileListScroll adds numRows to TopRow, so TopRow=11.
But TopRow + numRows is greater than NumDatabases, so FileListScroll sets TopRow
= NumDatabases - numRows = 5 - 11 = -6. But now TopRow is negative, so
the negative test has to come at the end. Since TopRow is less than zero,
FileListScroll sets TopRow = 0.
static void FileListScroll (int direction)
{
TablePtr tableP;
Word numRows;
FormPtr frm;
frm = FrmGetActiveForm();
tableP = FrmGetObjectPtr (frm, FrmGetObjectIndex (frm, tableID_FileList));
numRows = TblGetNumberOfRows (tableP);
if (direction == pageUpChr) TopRow = TopRow - numRows;
else TopRow = TopRow + numRows;
if ((TopRow + numRows) >= NumDatabases) TopRow = NumDatabases - numRows;
if (TopRow < 0) TopRow = 0;
}
pTable = FrmGetObjectPtr (FrmGetActiveForm(), FrmGetObjectIndex(FrmGetActiveForm(), TableID));
Show_database_index = TopRow + event->data.tblSelect.row; FrmGotoForm(formID_ShowDatabase); handled = true; break;In the example, when the user taps one of the rows in the table we must show the database information for the corresponding database. The global integer "Show_database_index" contains the index value of the database to show. The ShowDatabase form provides the user interface for displaying this information. The function ShowDatabaseDrawForm shows the information and the function ShowDatabaseEventHandler handles the events. The ShowDatabase form and its two functions are very simple and have little to do with table implementation, so they will not be discussed here.
Andrew Howlett
howlett@iosphere.net
Ottawa, Canada
March 1998