第八章 表和滚动条
在这一章中,我们将讨论Palm OS的两个很重要的用户界面元素:表和滚动条。表能够显示或编辑较大的数据量。在嵌入式应用程序中都它使用的很广泛。滚动条的功能很出色,但由于滚动条不支持1.0版本的Palm OS系统,所以只有在不想支持较早的Pilot 1000和5000时,才可以使用滚动条。我们将同时添加滚动按钮(它可以被所有的Palm设备使用)和滚动条(请不要在一个真正的应用程序中使用!可能会系统崩溃的喔!),然而这些还不够,我们还将论及如何
支持PAGE UP和PAGE DOWN键。
保存工程
现在你已经有了这个习惯了吧,步骤如下:
1.运行Windows浏览器;
2.找到工程存放的文件夹;
3.选中文件夹,按CTRL+C来复制文件夹;
4.选择一个文件夹用来保存副本;
5.按CTRL+V把项目副本粘贴到备份文件夹中;
6.把项目名重命名为你容易记的名字,我把它命名为Contacts CH.7。
删除旧的资源
既然已用表代替了Contact List窗体中的列表框,那么我们需要把列表框删除。
1.运行Metrowerks 构造器;
2.打开资源文件Contacts.rsrc。它位于项目文件夹中的Src文件夹中;
3.双击打开Contact List窗体;
4.点击资源列表中名为List的资源,按DELETE键来删除;
5.Contact List窗体现在看起来如图8-1所示。
删除旧代码
既然已经将列表框删除了,函数buildList()和deleteList()也就不再需要了。找到并删除这两个函数及其有关的内容。你可以将光标放在文件的开始处,在菜单栏中选中Search | Find,输入buildList。在删除了所有与buildList有关内容后,你可以再对deleteList做相似的操作。
另外,删除在Contact List事件处理函数中响应1stSelectEvent事件的代码。这些代码是:
// CH.7 Respond to a list selection
case lstSelectEvent:
{
// CH.7 Set the database cursor to the selected contact
cursor = event->data.lstSelect.selection;
// CH.7 Go to contact details
FrmGotoForm( ContactDetailForm );
}
break;
表
相对其它的UI(用户界面)元素来说,表是容器。表中的UI元素和表外的Palm OS系统中 UI元素不太一样。在表中的每一个单元(Cell)(行+列)可以有不同的类型,也就是说,它可以支持不同类型UI元素,在接下来的部分中将描述这些类型。
在捕获事件和将单元中的数据传递给UI元素时,表也有所不同。一些普通的函数,对表却可以执行一些特殊的函数。表看起来很像静态的Palm OS,只是要保证将每个可用UI元素的类型区分开来。
不幸地是,许多表中可使用的UI元素,在我的工程中却用不到。本章中,我只是将如何建立了自己的定制单元的技巧做一些论述。
条目类型
表中的每一个单元都有自己的类型。例如,单元可以是一个字编辑框资源或一个复选框资源。表8-1中是这些类型及其操作的纲要。
类型 使用
CheckboxTableItem
除了没有文本和选项框关联外,这种类型的单元操作和一般的选项框一样,可以通过调用TblSetItemInt()将其选中或清除,0表示没有选中,1表示选中。
CustomTableItem
这是一个非常有用的单元类型。你必须为此类型每一列定义一个定制函数,在本章以后的部分中,我们更多讲述了如何处理这个类型的内容。这种类型是可编辑的。
DateTableItem
在此单元中显示的日期已定义为DateType格式,可用TblSetItemPtr()函数将指向DateType的游标传给表。这种格式的缺点是在过去的任何日期后都会写出一个感叹号。有时这或许是件好事,但有时它会强制你使用定制日期显示。这种类型是不可编辑的。
LabelTableItem
它用来显示一个标签。使用TblSetItemPtr()将字符串传递给表。此格式的缺点就是表经常在所传递的字符串后面加上一个冒号(:),并且文本通常是右对齐。这就是为什么在这一章中,我们要使用定制类型地原因。这种类型是不可编辑的。
numericTableItem
显示一个右对齐的数字。这种类型很好,不会加上一些怪异的内容。可调用TblSetItemInt()函数来设置数字。这种类型是不可编辑的。
popupTriggerTableItem
这种类型类似于弹出触发按纽。使用TblSetItemPtr()函数可以指向列表框的游标使列表框显示出来,使用TblSetItemInt()可以设置列表框到底选中哪一个条目。
TextTableItem
这种类型类似于编辑框,它是可编辑的。编辑框的长度可以改变和重叠。使用TblSetLoadDataProcedure()定义一个定制导入函数,将编辑框的句柄传递给表。使用TblSetSaveDataProcedure()定义一个保存函数,可以将数据保存在此句柄的编辑框中。所以你必须写这两个定制函数来支持表中的编辑框操作。
textWithNoteTableItem
这种类型会在一般的文本条目右边加入一个小的提示图标。这提示图标看起来象单独地被选中。当单元被选中后,你须调用TblEditing()看一下编辑框是否为可编辑模式。如果不是,Note图标已经被选中了,你就要切换到你的Note窗体去处理。
narrowTextTableItem
除了可以使用TblSetItemInt()在字段末尾处定义空间的大小,使之符合所填内容外,这个类型和一般的TextTableItem类型相同。例如,在日历窗体中,为了在条目的右边放置小的警告钟图标,Date Book程序就用这种类型来提供空间。
因为所有存在的类型都有其专用性,所以只有自己定制类型才能完成自己想实现的功能。
表的属性
表8-2中是表的属性描述。
和其它资源属性一样,在窗体中选中表资源后,就可以在构造器中进行编辑。
名称 描述
Object Identifier 在资源头文件中,构造器用之代表资源ID
Table ID 表的资源ID号。
Left Origin 水平方向上控件的最左端位置
Top Origin 垂直方向上控件的最顶端位置
Width 表的宽度
Height 表的高度。
Editable 定义表中可编辑的数据是否能被用户输入
Rows 表中可见的行数。
Column Widths 每一列的宽度,如果要定义一个新的列,按CTRL-K
添加一个表
现在将表添加到Contact List窗体中:
1.运行Metrowerks 构造器;
2.打开资源文件Contacts.rsrc,它位于工程文件夹中的Src文件夹中;
3.双击打开Contact List窗体。
4.在菜单中选择Window | Catalog来打开Catalog;
5.拖动表资源到窗体中;
6.设置表的属性:Object Identifier=Table,Left Origin=0,Top Origin=15,Width=153,Height=130。这样就有足够的空间放置十行,然后设置Rows为10,这样设置也可以在窗体右边留有足够的空间放置滚动条;
7.定义Column Widths。设Column Width从1到40。选中Column Width 1,按CTRL-K创建一个新的列。设置此列宽度从2到40。选中Column Width 2,按CTRL-K创建第三列。设置Column Width从3到73;
8.Contact List窗体看起来如图8-2所示。
在表中显示记录
我们将添加表的两个基本函数:drawTable()和drawCell()。drawTable()在光标的当前状态绘制表。函数drawCell()是定制的单元输入函数,当Palm OS要向表中输入一个条目时,就会执行这个函数。我们先加入这些函数的原型:
static void drawTable( void );
static void drawCell( VoidPtr table, Word row, Word column,
RectanglePtr bounds );
drawCell()的函数原型必须和订制字单元输入的回馈函数原型相匹配。在Palm OS文献的TblSetCustomDrawProcedure()中有这个原型的定义。
为了整洁起见,最好在文件的开头定义常量:
// CH.8 Table constants
#define TABLE_NUM_COLUMNS 3
#define TABLE_NUM_ROWS 11
#define TABLE_COLUMN_DATE 0
#define TABLE_COLUMN_TIME 1
#define TABLE_COLUMN_NAME 2
#define BLACK_UP_ARROW "\x01"
#define BLACK_DOWN_ARROW "\x02"
#define GRAY_UP_ARROW "\x03"
#define GRAY_DOWN_ARROW "\x04"
常量TABLE_NUM_COLUMNS和TABLE_NUM_ROWS定义了窗体中表显示的大小,这与以后的很多运算与迭代有关。接下去的三个常量TABLE_COLUMN_DATE、TABLE_COLUMN_TIME和TABLE_COLUMN_NAME定义了每列所填写的信息。最后的四个常量BLACK_UP_ARROW,BLACK_DOWN_ARROW,GRAY_UP_ARROW和GRAY_DOWN_ARROW是Palm Os中Symol 7字体中代表这些图的ASCII值。当滚动条到达顶部或底部,我们使用这些常量给箭头加上灰晕。值得注意的是,在Palm Os中,只有这个控件可以添加灰晕。
函数contactListHandleEvent()的修改
找到Contact List窗体的事件处理函数contactListHandleEvent(),在这里需要添加drawTable()函数调用:
// CH.7 Form open event
case frmOpenEvent:
{
// CH.7 Draw the form
FrmDrawForm( form );
// CH.8 Populate and draw the table
drawTable();
}
break;
接着,处理表中记录被选中后的操作,在选中一条记录后应该调用Contact Detail窗体来显示其详细信息。请注意这些代码与处理列表框记录选中后的代码很相似。为使Contact Detail窗体显示相应的记录,我们设置了游标(Cursor)变量。
// CH.7 Respond to a list selection
case tblSelectEvent:
{
// CH.7 Set the database cursor to the selected contact
cursor += event->data.tblSelect.row;
// CH.7 Go to contact details
FrmGotoForm( ContactDetailForm );
}
break;
因为数据库要根据了不同的标准排序,所以每次排序后都要重新画表来显示新的记录顺序。为此,在DmQuickSort()后加入drawTable()函数来响应popSelectEvent事件。
// CH.7 Sort the contact database by the new criteria
DmQuickSort( contactsDB, (DmComparF*)sortFunc, sortBy );
// CH.8 Rebuild the table
drawTable();
}
break;
这样对这个函数的修改就完成了。
添加drawTable()函数
下面添加drawTable()函数。先定义一些变量,并获取表的指针。
// CH.8 Draw our list of choices using a table object
static void drawTable( void )
{
FormPtr form;
TablePtr table;
Int column;
Int count;
ControlPtr upArrow;
ControlPtr downArrow;
// CH.8 Get the form pointer
form = FrmGetActiveForm();
// CH.8 Get the table pointer
table = getObject( form, ContactListTableTable );
我们将对表中的列做两件事情。首先,每一列都要有一个定制的规则(Routine)。虽然条目类型是基于单元的,但如果单元是定制的,每一单元在特定的列上都要使用相同的规则。在例子中,我们将创建一个定制规则——drawCell(),在表的每个单元中都将使用这个规则。
另外一个要做的事情是使列为可见。列的缺省值是不可见的,为了显示需要将其设置为可见。
// CH.8 For all columns
for( column = 0; column < TABLE_NUM_COLUMNS; column++ )
{
// CH.8 Set the draw routine
TblSetCustomDrawProcedure( table, column, drawCell );
// CH.8 Make the column visible
TblSetColumnUsable( table, column, true );
}
下面,再来讲述表的行。由于表中的每一单元都需要定义一个类型,所以我们对列进行了操作。对于表中的不用(Unused)的行来说,就不需这样做。如果数据库包含的记录少于可见的行数,就需把表中不用的行关闭。这是很重要的,如果不关掉这些不用的行,当写代码时,我们就会试图向行中写不存在的记录,说不定会使系统崩溃的。既然表中的记录数是在变化的,我们就要保证在有记录时,标记行为可用,在没有记录时,标记行为不可用。
// CH.8 Initialize the table styles
for( count = 0; count < TABLE_NUM_ROWS; count++ )
{
// CH.8 If there is data
if( count < numRecords )
{
// CH.8 Show the row
TblSetRowUsable( table, count, true );
// CH.8 Set the cell styles
for( column = 0; column < TABLE_NUM_COLUMNS; column++ )
TblSetItemStyle( table, count, column, customTableItem );
}
else
// CH.8 Hide unused rows if any
TblSetRowUsable( table, count, false );
}
// CH.8 Draw the table
TblDrawTable( table );
一旦表的类型确定,通过命令TblDrawTable()将表画出来。
值得注意的是,使用TblSetRowUsable()函数可以在浏览表时,只显示所览数据库的一列,这种方法的缺点是它比我们后面章节使用的方法要耗费更多的内存。
添加drawCell()函数
通过前面的准备,现在终于可以调用我们定制函数drawCell()了,每次它都会在表中绘制一条目(Item)。下面是函数的开始部分:
// CH.8 The custom drawing routine for a table cell
static void drawCell( VoidPtr table, Word row, Word column,
RectanglePtr bounds )
{
Int record;
CharPtr precord;
Char string[DB_FIRST_NAME_SIZE + DB_LAST_NAME_SIZE];
SWord width;
SWord len;
Boolean noFit;
由于这个函数是通过调用TblSetCustomDrawProcedure()设置的回馈(CallBack)函数,所以它的参数和返回值就由此而决定。我们会从中得到表的指针、每一单元的行和列、每一单元在窗体上的矩形框。
// CH.8 Calculate our record
record = cursor + row;
// CH.8 Get our record
hrecord = DmQueryRecord( contactsDB, record );
precord = MemHandleLock( hrecord );
// CH.8 Get the date and time
MemMove( &dateTime, precord + DB_DATE_TIME_START,
sizeof( dateTime ) );
首先,我们得到一条和这一行相关联的记录,然后提取日期和时间,使之更容易被输入。
// CH.8 Switch on the column
switch( column )
{
// CH.8 Handle dates
case TABLE_COLUMN_DATE:
{
if( dateTime.year != NO_DATE )
{
DateToAscii( dateTime.month, dateTime.day,
dateTime.year,
(DateFormatType)PrefGetPreference(
prefDateFormat ), string );
}
else
StrCopy( string, "-" );
}
break;
根据列的类型,我们创建了要显示的字符串。对日期来说,所用的函数和列表框中显示时间的函数相同,在没有日期的地方将以短划线表示。
// CH.8 Handle times
case TABLE_COLUMN_TIME:
{
if( dateTime.hour != NO_TIME )
{
TimeToAscii( dateTime.hour, dateTime.minute,
(TimeFormatType)PrefGetPreference(
prefTimeFormat ), string );
}
else
StrCopy( string, "-" );
}
break;
下一列显示时间。它和列表框中显示时间的函数相同,如果没有日期,我们以短划线来代替。
// CH.8 Handle names
case TABLE_COLUMN_NAME:
{
StrCopy( string, precord + DB_FIRST_NAME_START );
StrCat( string, " " );
StrCat( string, precord + DB_LAST_NAME_START );
}
break;
第三列也就是最后一列显示名和姓。我们写入了为单元新建的文本。
// CH.8 Unlock the record
MemHandleUnlock( hrecord );
因为我们已经创建了合适的文本字符串,现在就可以将记录解锁(Unlock)向里面写入了。注意,这种方法没有使用永久(Permanently)内存存储单元数据,因此无论数据库中有多少记录,这个函数都能很好的工作。在表和列表框中都使用订制函数写入数据的好处可见一斑。
// CH.8 Set the text mode
WinSetUnderlineMode( noUnderline );
FntSetFont( stdFont );
// CH.8 Truncate the string if necessary
width = bounds->extent.x;
len = StrLen( string );
noFit = false;
FntCharsInWidth( string, &width, &len, &noFit );
下面,为了使WinDrawChars()能达到我们的要求,必须把文本模式设置好。名字或许不能在屏幕上能显示的空间中完全显示出来,所以需要检查字符串避免不要太长而超出单元显示的范围。如果太长,我们只好去掉多余的部分。事实上,如果你很充分的想象力的话,可以想办法在字符串的末尾添上省略号(……)表示其多余的部分。
// CH.8 Draw the cell
WinEraseRectangle( bounds, 0 );
WinDrawChars( string, len, bounds->topLeft.x, bounds->topLeft.y );
// CH.8 We're done
return;
}
最后,在清除屏幕上以前的内容后,将字符串写入。这样定制函数就完成了。它很容易编写且有极高的灵活性。
调试
当在第一次运行时,最好是单步执行drawTable()和drawCell()函数。如果不关闭表中不用的行,由于drawCell()将试图访问不存在的记录,系统有可能会崩溃。记住在Detail 窗体和表之间不断的切换测试,并且使用下拉框使用不同排序标准进行排序。
Contact List窗体看起来如图8-3所示。
三种滚动条
在Palm OS中普遍使用的有三种滚动条。第一种是滚动按钮,它是一对向上和向下重复按钮,在Enter Time窗体中我们已经使用过,它们可以在所有的Palm OS版本中使用;第二种是滚动条;除了不能在Piolt1000或Piolt5000使用外,在其它Palm OS版本中都可以使用;第三种是PAGE UP和PAGE DOWN键。
下面,我们就将加入资源和代码来支持Contact List窗体中的这三种滚动条。但这样做通常并不是个好主意。
滚动条属性
表8-3是滚动条的属性:
名称 描述
Object Identifier 在资源头文件中,构造器用之代表资源ID
Scrollbar ID 滚动条的ID号。
Left Origin 水平方向上控件的最左端位置
Top Origin 垂直方向上控件的最顶端位置
Width 滚动条的宽度值。
Height 滚动条的高度值。
Usable 定义滚动条是否可见。
Value 滚动条的最初值。
Minimum Value 滚动条的最小值。
Maximum Value 滚动条的最小值。
Page Size 滚动条所关联的行或记事行的每一页的大小,这个用来设置滚动条中Box的大小。
Orientation 定义滚动条是水平方向还是垂直方向
添加滚动按钮和滚动条资源
添加两个滚动按钮和一个滚动条来支持三种滚动条类型其中的两种。
1.运行Metrowerks 构造器;
2.打开资源文件Contacts.rsrc,它位于你的项目文件夹中的Src文件夹中;
3.双击打开Contact List窗体;
4.在菜单中选择Window | Catalog,打开Catalog。
5.拖动一个滚动条到窗体中;
6.修改滚动条的属性:Object Identifier=Scrollbar,Left Origin=153,Top Origin=15,Width=7,Height=130。这样滚动条正好在表的最右边,紧靠窗体的右边界。
7.添加滚动按钮。你可以从Enter Time窗体中将滚动按钮拷贝过来,打开Enter Time窗体。从Enter Time窗体中把滚动按钮拖到Contact List窗体中。把向上的箭头的Left Origin设为149,Top Origin为145,将Object Identifier改为RecordUp;把向下的箭头Left Origin设为149,Top Origin为152,将Object Identifier改为RecordDown。
8.Contact List窗体看起来如图8-4。
让滚动按钮工作起来
在例子中,所要做的首要工作是要使游标(cursor)变量与表的顶部位置相等。并且使向上箭头在到达记录的顶部时要变灰,向下按钮在到达记录的底部时变灰。首先在contactListHandleEvent()中加入代码:
// CH.8 Respond to arrows
case ctlRepeatEvent:
{
switch( event->data.ctlRepeat.controlID )
{
// CH.8 Up arrow
case ContactListRecordUpRepeating:
if( cursor > 0 )
cursor--;
break;
// CH.8 Down arrow
case ContactListRecordDownRepeating:
if( (numRecords > TABLE_NUM_ROWS) &&
(cursor < numRecords - TABLE_NUM_ROWS) )
cursor++;
break;
}
// CH.8 Now refresh the table
drawTable();
}
return( true );
这些代码十分简单。注意由于响应重复按钮事件,所以需在ctlRepeatEvent事件中添加代码。对于向上的箭头,每按一次游标中减一;对于向下的箭头,没按一次游标中加一。
为了保证安全,需要检查游标到底能移到什么地方。在绘制表的过程中,我们会重新绘制按纽,或在需要的地方使按钮变得不可用。
为了完成这个操作,在drawTable()的按钮响应事件中添加以下代码:
// CH.8 Get pointers to the arrow buttons
upArrow = getObject( form, ContactListRecordUpRepeating );
downArrow = getObject( form, ContactListRecordDownRepeating );
// CH.8 Update the arrow buttons and scrollbars
if( numRecords > TABLE_NUM_ROWS )
{
// CH.8 Show the up arrow
if( cursor > 0 )
{
CtlSetLabel( upArrow, BLACK_UP_ARROW );
CtlSetEnabled( upArrow, true );
}
else
{
CtlSetLabel( upArrow, GRAY_UP_ARROW );
CtlSetEnabled( upArrow, false );
}
CtlShowControl( upArrow );
// CH.8 Show the down arrow
if( cursor >= numRecords - TABLE_NUM_ROWS )
{
CtlSetLabel( downArrow, GRAY_DOWN_ARROW );
CtlSetEnabled( downArrow, false );
}
else
{
CtlSetLabel( downArrow, BLACK_DOWN_ARROW );
CtlSetEnabled( downArrow, true );
}
CtlShowControl( downArrow );
// CH.8 Show the scrollbar
FrmShowObject( form, FrmGetObjectIndex( form,
ContactListScrollbarScrollBar ) );
SclSetScrollBar( getObject( form,
ContactListScrollbarScrollBar ), cursor, 0,
numRecords - TABLE_NUM_ROWS, TABLE_NUM_ROWS );
}
else
{
// CH.8 Hide the arrows
CtlHideControl( upArrow );
CtlHideControl( downArrow );
// CH.8 Hide the scrollbar
FrmHideObject( form, FrmGetObjectIndex( form,
ContactListScrollbarScrollBar ) );
}
// CH.8 We're done
return;
}
如果表的位置在开头或末尾,我们将重复按钮打上灰晕使之为不可用。这样就防止了游标被置到一个不存在值。
这样工作就完成了,重复按纽实现了象表的滚动条箭头一样的功能。
对PAGE UP和PAGE DOWN键的支持
为了捕捉PAGE UP和PAGE DOWN键,首先必须在keyDownEvent里添加代码。数学上的知识可以给我们一些提示。在当向上翻页或向下翻页,最好能在页面上留下一条常识的线。移动记录时不应移动到TABLE_NUM_ROWS,而应移动到TABLE_NUM_ROWS-1。由于不能使上下翻页键为不可用,就必须保证在按下它们时不会超出游标的移出范围。此外,游标和numRecords都是无符号的,所以必须在做数学运算前进行检查,避免它们变为负数而指向了不存在的值。这需要对contactListHandleEvent()作一些修改:
// CH.8 Respond to up and down arrow hard keys
case keyDownEvent:
{
switch( event->data.keyDown.chr )
{
// CH.8 Up arrow hard key
case pageUpChr:
if( cursor > TABLE_NUM_ROWS - 1 )
cursor -= TABLE_NUM_ROWS - 1;
else
cursor = 0;
break;
对向上翻页来说,运算相当简单。如果向上翻页没有使记录游标小于零,向上翻一整页;否则,就翻到零记录为止。
// CH.8 Down arrow hard key
case pageDownChr:
if( (numRecords > 2 * TABLE_NUM_ROWS - 1) &&
(cursor < numRecords -
2 * TABLE_NUM_ROWS - 1) )
cursor += TABLE_NUM_ROWS - 1;
else
cursor = numRecords - TABLE_NUM_ROWS;
break;
}
// CH.8 Now refresh the table
drawTable();
}
break;
对向下翻页来说,必须注意,当游标是numRecords减去TABLE_NUM_ROWS后(切记游标是基于零的),表是否已经到了最后的一条记录。所以首要的是检查翻页是否超出了最后一个记录。首先,保证表中有足够的记录在从numRecords中减去它后仍是一个正数。然后再检查游标是否到了最后一条记录。如果没有,向下翻一个整页。如果已超过了最后一条记录,翻到最后一条记录为止。
在程序的最后,和滚动按钮程序一样重新绘制表。完成这些后就可以支持翻页键了。
设计滚动条
滚动条需要在事件处理和订制程序中都添加一小段代码,首先来看一下事件处理中的代码:
// CH.8 Respond to scrollbar events
case sclRepeatEvent:
cursor = event->data.sclExit.newValue;
drawTable();
break;
使游标和新的滚动条值相等,就可以响应滚动条滚动事件。如果正确地设置了滚动条滚动的范围,就能保证不会使游标得到错误的值。在为游标赋值后,和其它的滚动条类型一样,需要刷新表和滚动条。
下面,看看添加在drawTable()中的代码。代码添加在滚动按钮代码中的if(numRecords>TABLE_NUM_ROWS)声明后面:
// CH.8 Show the scrollbar
FrmShowObject( form, FrmGetObjectIndex( form,
ContactListScrollbarScrollBar ) );
SclSetScrollBar( getObject( form,
ContactListScrollbarScrollBar ), cursor, 0,
numRecords - TABLE_NUM_ROWS, TABLE_NUM_ROWS );
在这里显示了滚动条,并将其设置了精确的值。因为我们已知道表中存在的记录比可见的行数多,所以numRecords-TABLE_NUM_ROWS不会产生一个错误的结果。
但如果不是这样,而是存在的记录比可见的行数要少,就要隐藏滚动条:
// CH.8 Hide the scrollbar
FrmHideObject( form, FrmGetObjectIndex( form,
ContactListScrollbarScrollBar ) );
}
支持滚动条的代码修改就完成了。
调试
和以前一样,首先调试刚刚添加的代码。另外,将所有的滚动条值移动到第一个和最后一个记录上,次数不要太少(一些记录不会显示)或太多(系统会崩溃的)。
Contact List窗体看起来如图8-5。
下一步做什么
在下一章中,我们将通过在Contacts中添加其他的一些很出色的函数,如系统查找、分类、保密记录等,来结束本书的基础知识部分。
清单
这是经过这一章修改后的Contacts.c:
// CH.2 The super-include for the Palm OS
#include <Pilot.h>
// CH.5 Added for the call to GrfSetState()
#include <Graffiti.h>
// CH.3 Our resource file
#include "Contacts_res.h"
// CH.4 Prototypes for our event handler functions
static Boolean contactDetailHandleEvent( EventPtr event );
static Boolean aboutHandleEvent( EventPtr event );
static Boolean enterTimeHandleEvent( EventPtr event );
static Boolean contactListHandleEvent( EventPtr event );
static Boolean menuEventHandler( EventPtr event );
// CH.4 Constants for ROM revision
#define ROM_VERSION_2 0x02003000
#define ROM_VERSION_MIN ROM_VERSION_2
// CH.5 Prototypes for utility functions
static void newRecord( void );
static VoidPtr getObject( FormPtr, Word );
static void setFields( void );
static void getFields( void );
static void setText( FieldPtr, CharPtr );
static void getText( FieldPtr, VoidPtr, Word );
static void setDateTrigger( void );
static void setTimeTrigger( void );
static void setTimeControls( void );
static Int sortFunc( CharPtr, CharPtr, Int );
static void drawTable( void );
static void drawCell( VoidPtr table, Word row, Word column,
RectanglePtr bounds );
// CH.5 Our open database reference
static DmOpenRef contactsDB;
static ULong numRecords;
static UInt cursor;
static Boolean isDirty;
static VoidHand hrecord;
// CH.5 Constants that define the database record
#define DB_ID_START 0
#define DB_ID_SIZE (sizeof( ULong ))
#define DB_DATE_TIME_START (DB_ID_START +\
DB_ID_SIZE)
#define DB_DATE_TIME_SIZE (sizeof( DateTimeType ))
#define DB_FIRST_NAME_START (DB_DATE_TIME_START +\
DB_DATE_TIME_SIZE)
#define DB_FIRST_NAME_SIZE 16
#define DB_LAST_NAME_START (DB_FIRST_NAME_START +\
DB_FIRST_NAME_SIZE)
#define DB_LAST_NAME_SIZE 16
#define DB_PHONE_NUMBER_START (DB_LAST_NAME_START +\
DB_LAST_NAME_SIZE)
#define DB_PHONE_NUMBER_SIZE 16
#define DB_RECORD_SIZE (DB_PHONE_NUMBER_START +\
DB_PHONE_NUMBER_SIZE)
// CH.6 Storage for the record's date and time in expanded form
static DateTimeType dateTime;
static Word timeSelect;
#define NO_DATE 0
#define NO_TIME 0x7fff
// CH.7 The error exit macro
#define errorExit(alert) { ErrThrow( alert ); }
// CH.7 The sort order variable and constants
static Int sortBy;
// CH.7 NOTE: These items match the popup list entries!
#define SORTBY_DATE_TIME 0
#define SORTBY_FIRST_NAME 1
#define SORTBY_LAST_NAME 2
// CH.8 Table constants
#define TABLE_NUM_COLUMNS 3
#define TABLE_NUM_ROWS 11
#define TABLE_COLUMN_DATE 0
#define TABLE_COLUMN_TIME 1
#define TABLE_COLUMN_NAME 2
#define BLACK_UP_ARROW "\x01"
#define BLACK_DOWN_ARROW "\x02"
#define GRAY_UP_ARROW "\x03"
#define GRAY_DOWN_ARROW "\x04"
// CH.2 The main entry point
DWord PilotMain( Word cmd, Ptr, Word )
{
DWord romVersion; // CH.4 ROM version
FormPtr form; // CH.2 A pointer to our form structure
EventType event; // CH.2 Our event structure
Word error; // CH.3 Error word
// CH.4 Get the ROM version
romVersion = 0;
FtrGet( sysFtrCreator, sysFtrNumROMVersion, &romVersion );
// CH.4 If we are below our minimum acceptable ROM revision
if( romVersion < ROM_VERSION_MIN )
{
// CH.4 Display the alert
FrmAlert( LowROMVersionErrorAlert );
// CH.4 PalmOS 1.0 will continuously re-launch this app
// unless we switch to another safe one
if( romVersion < ROM_VERSION_2 )
{
AppLaunchWithCommand( sysFileCDefaultApp,
sysAppLaunchCmdNormalLaunch, NULL );
}
return( 0 );
}
// CH.2 If this is not a normal launch, don't launch
if( cmd != sysAppLaunchCmdNormalLaunch )
return( 0 );
// CH.5 Create a new database in case there isn't one
if( ((error = DmCreateDatabase( 0, "ContactsDB-PPGU", 'PPGU', 'ctct',
false )) != dmErrAlreadyExists) && (error != 0) )
{
// CH.5 Handle db creation error
FrmAlert( DBCreationErrorAlert );
return( 0 );
}
// CH.5 Open the database
contactsDB = DmOpenDatabaseByTypeCreator( 'ctct', 'PPGU',
dmModeReadWrite );
// CH.5 Get the number of records in the database
numRecords = DmNumRecords( contactsDB );
// CH.5 Initialize the record number
cursor = 0;
// CH.7 Choose our starting page
// CH.5 If there are no records, create one
if( numRecords == 0 )
{
newRecord();
FrmGotoForm( ContactDetailForm );
}
else
FrmGotoForm( ContactListForm );
// CH.7 Begin the try block
ErrTry {
// CH.2 Our event loop
do
{
// CH.2 Get the next event
EvtGetEvent( &event, -1 );
// CH.2 Handle system events
if( SysHandleEvent( &event ) )
continue;
// CH.3 Handle menu events
if( MenuHandleEvent( NULL, &event, &error ) )
continue;
// CH.4 Handle form load events
if( event.eType == frmLoadEvent )
{
// CH.4 Initialize our form
switch( event.data.frmLoad.formID )
{
// CH.4 Contact Detail form
case ContactDetailForm:
form = FrmInitForm( ContactDetailForm );
FrmSetEventHandler( form, contactDetailHandleEvent );
break;
// CH.4 About form
case AboutForm:
form = FrmInitForm( AboutForm );
FrmSetEventHandler( form, aboutHandleEvent );
break;
// CH.6 Enter Time form
case EnterTimeForm:
form = FrmInitForm( EnterTimeForm );
FrmSetEventHandler( form, enterTimeHandleEvent );
break;
// CH.7 Contact List form
case ContactListForm:
form = FrmInitForm( ContactListForm );
FrmSetEventHandler( form, contactListHandleEvent );
break;
}
FrmSetActiveForm( form );
}
// CH.2 Handle form events
FrmDispatchEvent( &event );
// CH.2 If it's a stop event, exit
} while( event.eType != appStopEvent );
// CH.7 End the try block and do the catch block
}
ErrCatch( errorAlert )
{
// CH.7 Display the appropriate alert
FrmAlert( errorAlert );
} ErrEndCatch
// CH.5 Close all open forms
FrmCloseAllForms();
// CH.5 Close the database
DmCloseDatabase( contactsDB );
// CH.2 We're done
return( 0 );
}
// CH.4 Our Contact Detail form handler function
static Boolean contactDetailHandleEvent( EventPtr event )
{
FormPtr form; // CH.3 A pointer to our form structure
VoidPtr precord; // CH.6 Points to a database record
// CH.3 Get our form pointer
form = FrmGetActiveForm();
// CH.4 Parse events
switch( event->eType )
{
// CH.4 Form open event
case frmOpenEvent:
{
// CH.2 Draw the form
FrmDrawForm( form );
// CH.5 Draw the database fields
setFields();
}
break;
// CH.5 Form close event
case frmCloseEvent:
{
// CH.5 Store away any modified fields
getFields();
}
break;
// CH.5 Parse the button events
case ctlSelectEvent:
{
// CH.5 Store any field changes
getFields();
switch( event->data.ctlSelect.controlID )
{
// CH.5 First button
case ContactDetailFirstButton:
{
// CH.5 Set the cursor to the first record
if( cursor > 0 )
cursor = 0;
}
break;
// CH.5 Previous button
case ContactDetailPrevButton:
{
// CH.5 Move the cursor back one record
if( cursor > 0 )
cursor--;
}
break;
// CH.5 Next button
case ContactDetailNextButton:
{
// CH.5 Move the cursor up one record
if( cursor < (numRecords - 1) )
cursor++;
}
break;
// CH.5 Last button
case ContactDetailLastButton:
{
// CH.5 Move the cursor to the last record
if( cursor < (numRecords - 1) )
cursor = numRecords - 1;
}
break;
// CH.5 Delete button
case ContactDetailDeleteButton:
{
// CH.5 Remove the record from the database
DmRemoveRecord( contactsDB, cursor );
// CH.5 Decrease the number of records
numRecords--;
// CH.5 Place the cursor at the first record
cursor = 0;
// CH.5 If there are no records left, create one
if( numRecords == 0 )
newRecord();
}
break;
// CH.5 New button
case ContactDetailNewButton:
{
// CH.5 Create a new record
newRecord();
}
break;
// CH.7 Done button
case ContactDetailDoneButton:
{
// CH.7 Load the contact list
FrmGotoForm( ContactListForm );
}
break;
// CH.6 Date selector trigger
case ContactDetailDateSelTrigger:
{
// CH.6 Initialize the date if necessary
if( dateTime.year == NO_DATE )
{
DateTimeType currentDate;
// CH.6 Get the current date
TimSecondsToDateTime( TimGetSeconds(),
¤tDate );
// CH.6 Copy it
dateTime.year = currentDate.year;
dateTime.month = currentDate.month;
dateTime.day = currentDate.day;
}
// CH.6 Pop up the system date selection form
SelectDay( selectDayByDay, &(dateTime.month),
&(dateTime.day), &(dateTime.year),
"Enter Date" );
// CH.6 Get the record
hrecord = DmQueryRecord( contactsDB, cursor );
// CH.6 Lock it down
precord = MemHandleLock( hrecord );
// CH.6 Write the date time field
DmWrite( precord, DB_DATE_TIME_START, &dateTime,
sizeof( DateTimeType ) );
// CH.6 Unlock the record
MemHandleUnlock( hrecord );
// CH.6 Mark the record dirty
isDirty = true;
}
break;
// CH.6 Time selector trigger
case ContactDetailTimeSelTrigger:
{
// CH.6 Pop up our selection form
FrmPopupForm( EnterTimeForm );
}
break;
}
// CH.5 Sync the current record to the fields
setFields();
}
break;
// CH.5 Respond to field tap
case fldEnterEvent:
isDirty = true;
break;
// CH.3 Parse menu events
case menuEvent:
return( menuEventHandler( event ) );
break;
}
// CH.2 We're done
return( false );
}
// CH.4 Our About form event handler function
static Boolean aboutHandleEvent( EventPtr event )
{
FormPtr form; // CH.4 A pointer to our form structure
// CH.4 Get our form pointer
form = FrmGetActiveForm();
// CH.4 Respond to the Open event
if( event->eType == frmOpenEvent )
{
// CH.4 Draw the form
FrmDrawForm( form );
}
// CH.4 Return to the calling form
if( event->eType == ctlSelectEvent )
{
FrmReturnToForm( 0 );
// CH.4 Always return true in this case
return( true );
}
// CH.4 We're done
return( false );
}
// CH.6 Our Enter Time form event handler function
static Boolean enterTimeHandleEvent( EventPtr event )
{
FormPtr form; // CH.6 A form structure pointer
static DateTimeType oldTime; // CH.6 The original time
// CH.6 Get our form pointer
form = FrmGetActiveForm();
// CH.6 Switch on the event
switch( event->eType )
{
// CH.6 Initialize the form
case frmOpenEvent:
{
// CH.6 Store the time value
oldTime = dateTime;
// CH.6 Draw it
FrmDrawForm( form );
// CH.6 Set the time controls
setTimeControls();
}
break;
// CH.6 If a button was repeated
case ctlRepeatEvent:
// CH.6 If a button was pushed
case ctlSelectEvent:
{
Word buttonID; // CH.6 The ID of the button
// CH.6 Set the ID
buttonID = event->data.ctlSelect.controlID;
// CH.6 Switch on button ID
switch( buttonID )
{
// CH.6 Hours button
case EnterTimeHoursPushButton:
// CH.6 Minute Tens button
case EnterTimeMinuteTensPushButton:
// CH.6 Minute Ones button
case EnterTimeMinuteOnesPushButton:
{
// CH.6 If no time was set
if( dateTime.hour == NO_TIME )
{
// CH.6 Set the time to 12 PM
dateTime.hour = 12;
dateTime.minute = 0;
// CH.6 Set the controls
setTimeControls();
}
// CH.6 Clear the old selection if any
if( timeSelect )
CtlSetValue( getObject( form, timeSelect ),
false );
// CH.6 Set the new selection
CtlSetValue( getObject( form, buttonID ), true );
timeSelect = buttonID;
}
break;
// CH.6 Up button
case EnterTimeTimeUpRepeating:
{
// CH.6 If there's no time, do nothing
if( dateTime.hour == NO_TIME )
break;
// CH.6 Based on what push button is selected
switch( timeSelect )
{
// CH.6 Increase hours
case EnterTimeHoursPushButton:
{
// CH.6 Increment hours
dateTime.hour++;
// CH.6 If it was 11 AM, make it 12 AM
if( dateTime.hour == 12 )
dateTime.hour = 0;
// CH.6 If it was 11 PM, make it 12 PM
if( dateTime.hour == 24 )
dateTime.hour = 12;
}
break;
// CH.6 Increase tens of minutes
case EnterTimeMinuteTensPushButton:
{
// CH.6 Increment minutes
dateTime.minute += 10;
// CH.6 If it was 5X, roll over
if( dateTime.minute > 59 )
dateTime.minute -= 60;
}
break;
// CH.6 Increase minutes
case EnterTimeMinuteOnesPushButton:
{
// CH.6 Increment minutes
dateTime.minute++;
// CH.6 If it is zero, subtract ten
if( (dateTime.minute % 10) == 0 )
dateTime.minute -= 10;
}
break;
}
// Revise the controls
setTimeControls();
}
break;
// CH.6 Down button
case EnterTimeTimeDownRepeating:
{
// CH.6 If there's no time, do nothing
if( dateTime.hour == NO_TIME )
break;
// CH.6 Based on what push button is selected
switch( timeSelect )
{
// CH.6 Decrease hours
case EnterTimeHoursPushButton:
{
// CH.6 Decrement hours
dateTime.hour--;
// CH.6 If it was 12 AM, make it 11 AM
if( dateTime.hour == -1 )
dateTime.hour = 11;
// CH.6 If it was 12 PM, make it 11 PM
if( dateTime.hour == 11 )
dateTime.hour = 23;
}
break;
// CH.6 Decrease tens of minutes
case EnterTimeMinuteTensPushButton:
{
// CH.6 Decrement minutes
dateTime.minute -= 10;
// CH.6 If it was 0X, roll over
if( dateTime.minute < 0 )
dateTime.minute += 60;
}
break;
// CH.6 Decrease minutes
case EnterTimeMinuteOnesPushButton:
{
// CH.6 Decrement minutes
dateTime.minute--;
// CH.6 If it is 9, add ten
if( (dateTime.minute % 10) == 9 )
dateTime.minute += 10;
// CH.6 If less than zero, make it 9
if( dateTime.minute < 0 )
dateTime.minute = 9;
}
break;
}
// CH.6 Revise the controls
setTimeControls();
}
break;
// CH.6 AM button
case EnterTimeAMPushButton:
{
// CH.6 If no time was set
if( dateTime.hour == NO_TIME )
{
// CH.6 Set the time to 12 AM
dateTime.hour = 0;
dateTime.minute = 0;
// CH.6 Set the controls
setTimeControls();
}
// CH.6 If it is PM
if( dateTime.hour > 11 )
{
// CH.6 Change to AM
dateTime.hour -= 12;
// CH.6 Set the controls
setTimeControls();
}
}
break;
// CH.6 PM button
case EnterTimePMPushButton:
{
// CH.6 If no time was set
if( dateTime.hour == NO_TIME )
{
// CH.6 Set the time to 12 PM
dateTime.hour = 12;
dateTime.minute = 0;
// CH.6 Set the controls
setTimeControls();
}
// CH.6 If it is AM
if( dateTime.hour < 12 )
{
// CH.6 Change to PM
dateTime.hour += 12;
// CH.6 Set the controls
setTimeControls();
}
}
break;
// CH.6 No Time checkbox
case EnterTimeNoTimeCheckbox:
{
// CH.6 If we are unchecking the box
if( dateTime.hour == NO_TIME )
{
// CH.6 Set the time to 12 PM
dateTime.hour = 12;
dateTime.minute = 0;
// CH.6 Set the controls
setTimeControls();
// CH.6 Set the new selection
timeSelect = EnterTimeHoursPushButton;
CtlSetValue( getObject( form, timeSelect ),
true );
}
else
// CH.6 If we are checking the box
dateTime.hour = NO_TIME;
// CH.6 Set the controls
setTimeControls();
}
break;
// CH.6 Cancel button
case EnterTimeCancelButton:
{
// CH.6 Restore time
dateTime = oldTime;
// CH.6 Return to calling form
FrmReturnToForm( 0 );
}
// CH.6 Always return true
return( true );
// CH.6 OK button
case EnterTimeOKButton:
{
VoidPtr precord; // CH.6 Points to the record
// CH.6 Lock it down
precord = MemHandleLock( hrecord );
// CH.6 Write the date time field
DmWrite( precord, DB_DATE_TIME_START, &dateTime,
sizeof( DateTimeType ) );
// CH.6 Unlock the record
MemHandleUnlock( hrecord );
// CH.6 Mark the record dirty
isDirty = true;
// CH.6 Return to the Contact Details form
FrmReturnToForm( 0 );
// CH.6 Update the field
setTimeTrigger();
}
// CH.6 Always return true
return( true );
}
}
break;
}
// CH.6 We're done
return( false );
}
// CH.7 Our Contact List form event handler function
static Boolean contactListHandleEvent( EventPtr event )
{
FormPtr form; // CH.7 A form structure pointer
// CH.7 Get our form pointer
form = FrmGetActiveForm();
// CH.7 Parse events
switch( event->eType )
{
// CH.7 Form open event
case frmOpenEvent:
{
// CH.7 Draw the form
FrmDrawForm( form );
// CH.8 Populate and draw the table
drawTable();
}
break;
// CH.7 Respond to a list selection
case tblSelectEvent:
{
// CH.7 Set the database cursor to the selected contact
cursor += event->data.tblSelect.row;
// CH.7 Go to contact details
FrmGotoForm( ContactDetailForm );
}
break;
// CH.7 Respond to a menu event
case menuEvent:
return( menuEventHandler( event ) );
// CH.7 Respond to the popup trigger
case popSelectEvent:
{
// CH.7 If there is no change, we're done
if( sortBy == event->data.popSelect.selection )
return( true );
// CH.7 Modify sort order variable
sortBy = event->data.popSelect.selection;
// CH.7 Sort the contact database by the new criteria
DmQuickSort( contactsDB, (DmComparF*)sortFunc, sortBy );
// CH.8 Rebuild the table
drawTable();
}
break;
// CH.8 Respond to arrows
case ctlRepeatEvent:
{
switch( event->data.ctlRepeat.controlID )
{
// CH.8 Up arrow
case ContactListRecordUpRepeating:
if( cursor > 0 )
cursor--;
break;
// CH.8 Down arrow
case ContactListRecordDownRepeating:
if( (numRecords > TABLE_NUM_ROWS) &&
(cursor < numRecords - TABLE_NUM_ROWS) )
cursor++;
break;
}
// CH.8 Now refresh the table
drawTable();
}
return( true );
// CH.8 Respond to up and down arrow hard keys
case keyDownEvent:
{
switch( event->data.keyDown.chr )
{
// CH.8 Up arrow hard key
case pageUpChr:
if( cursor > TABLE_NUM_ROWS - 1 )
cursor -= TABLE_NUM_ROWS - 1;
else
cursor = 0;
break;
// CH.8 Down arrow hard key
case pageDownChr:
if( (numRecords > 2 * TABLE_NUM_ROWS - 1) &&
(cursor < numRecords -
2 * TABLE_NUM_ROWS - 1) )
cursor += TABLE_NUM_ROWS - 1;
else
cursor = numRecords - TABLE_NUM_ROWS;
break;
}
// CH.8 Now refresh the table
drawTable();
}
break;
// CH.8 Respond to scrollbar events
case sclRepeatEvent:
cursor = event->data.sclExit.newValue;
drawTable();
break;
} // CH.7 End of the event switch statement
// CH.7 We're done
return( false );
}
// CH.3 Handle menu events
Boolean menuEventHandler( EventPtr event )
{
FormPtr form; // CH.3 A pointer to our form structure
Word index; // CH.3 A general purpose control index
FieldPtr field; // CH.3 Used for manipulating fields
// CH.3 Get our form pointer
form = FrmGetActiveForm();
// CH.3 Erase the menu status from the display
MenuEraseStatus( NULL );
// CH.4 Handle options menu
if( event->data.menu.itemID == OptionsAboutContacts )
{
// CH.4 Pop up the About form as a Dialog
FrmPopupForm( AboutForm );
return( true );
}
// CH.3 Handle graffiti help
if( event->data.menu.itemID == EditGraffitiHelp )
{
// CH.3 Pop up the graffiti reference based on
// the graffiti state
SysGraffitiReferenceDialog( referenceDefault );
return( true );
}
// CH.3 Get the index of our field
index = FrmGetFocus( form );
// CH.3 If there is no field selected, we're done
if( index == noFocus )
return( false );
// CH.3 Get the pointer of our field
field = FrmGetObjectPtr( form, index );
// CH.3 Do the edit command
switch( event->data.menu.itemID )
{
// CH.3 Undo
case EditUndo:
FldUndo( field );
break;
// CH.3 Cut
case EditCut:
FldCut( field );
break;
// CH.3 Copy
case EditCopy:
FldCopy( field );
break;
// CH.3 Paste
case EditPaste:
FldPaste( field );
break;
// CH.3 Select All
case EditSelectAll:
{
// CH.3 Get the length of the string in the field
Word length = FldGetTextLength( field );
// CH.3 Sound an error if appropriate
if( length == 0 )
{
SndPlaySystemSound( sndError );
return( false );
}
// CH.3 Select the whole string
FldSetSelection( field, 0, length );
}
break;
// CH.3 Bring up the keyboard tool
case EditKeyboard:
SysKeyboardDialogV10();
break;
}
// CH.3 We're done
return( true );
}
// CH.5 This function creates and initializes a new record
static void newRecord( void )
{
VoidPtr precord; // CH.5 Pointer to the record
// CH.7 Create the database record and get a handle to it
if( (hrecord = DmNewRecord( contactsDB, &cursor,
DB_RECORD_SIZE )) == NULL )
errorExit( MemoryErrorAlert );
// CH.5 Lock down the record to modify it
precord = MemHandleLock( hrecord );
// CH.5 Clear the record
DmSet( precord, 0, DB_RECORD_SIZE, 0 );
// CH.6 Initialize the date and time
MemSet( &dateTime, sizeof( dateTime ), 0 );
dateTime.year = NO_DATE;
dateTime.hour = NO_TIME;
DmWrite( precord, DB_DATE_TIME_START, &dateTime,
sizeof( DateTimeType ) );
// CH.5 Unlock the record
MemHandleUnlock( hrecord );
// CH.5 Clear the busy bit and set the dirty bit
DmReleaseRecord( contactsDB, cursor, true );
// CH.5 Increment the total record count
numRecords++;
// CH.5 Set the dirty bit
isDirty = true;
// CH.5 We're done
return;
}
// CH.5 A time saver: Gets object pointers based on their ID
static VoidPtr getObject( FormPtr form, Word objectID )
{
Word index; // CH.5 The object index
// CH.5 Get the index
index = FrmGetObjectIndex( form, objectID );
// CH.5 Return the pointer
return( FrmGetObjectPtr( form, index ) );
}
// CH.5 Gets the current database record and displays it
// in the detail fields
static void setFields( void )
{
FormPtr form; // CH.5 The contact detail form
CharPtr precord; // CH.5 A record pointer
Word index; // CH.5 The object index
// CH.5 Get the contact detail form pointer
form = FrmGetActiveForm();
// CH.5 Get the current record
hrecord = DmQueryRecord( contactsDB, cursor );
// CH.6 Initialize the date and time variable
precord = MemHandleLock( hrecord );
MemMove( &dateTime, precord + DB_DATE_TIME_START,
sizeof( dateTime ) );
// CH.6 Initialize the date control
setDateTrigger();
// CH.6 Initialize the time control
setTimeTrigger();
// CH.5 Set the text for the First Name field
setText( getObject( form, ContactDetailFirstNameField ),
precord + DB_FIRST_NAME_START );
// CH.5 Set the text for the Last Name field
setText( getObject( form, ContactDetailLastNameField ),
precord + DB_LAST_NAME_START );
// CH.5 Set the text for the Phone Number field
setText( getObject( form, ContactDetailPhoneNumberField ),
precord + DB_PHONE_NUMBER_START );
MemHandleUnlock( hrecord );
// CH.5 If the record is already dirty, it's new, so set focus
if( isDirty )
{
// CH.3 Get the index of our field
index = FrmGetObjectIndex( form, ContactDetailFirstNameField );
// CH.3 Set the focus to the First Name field
FrmSetFocus( form, index );
// CH.5 Set upper shift on
GrfSetState( false, false, true );
}
// CH.5 We're done
return;
}
// CH.5 Puts any field changes in the record
static void getFields( void )
{
FormPtr form; // CH.5 The contact detail form
// CH.5 Get the contact detail form pointer
form = FrmGetActiveForm();
// CH.5 Turn off focus
FrmSetFocus( form, -1 );
// CH.5 If the record has been modified
if( isDirty )
{
CharPtr precord; // CH.5 Points to the DB record
// CH.7 Detach the record from the database
DmDetachRecord( contactsDB, cursor, &hrecord );
// CH.5 Lock the record
precord = MemHandleLock( hrecord );
// CH.5 Get the text for the First Name field
getText( getObject( form, ContactDetailFirstNameField ),
precord, DB_FIRST_NAME_START );
// CH.5 Get the text for the Last Name field
getText( getObject( form, ContactDetailLastNameField ),
precord, DB_LAST_NAME_START );
// CH.5 Get the text for the Phone Number field
getText( getObject( form, ContactDetailPhoneNumberField ),
precord, DB_PHONE_NUMBER_START );
// CH.7 Find the proper position
cursor = DmFindSortPosition( contactsDB, precord, NULL,
(DmComparF*)sortFunc, sortBy );
// CH.5 Unlock the record
MemHandleUnlock( hrecord );
// CH.7 Reattach the record
DmAttachRecord( contactsDB, &cursor, hrecord, NULL );
}
// CH.5 Reset the dirty bit
isDirty = false;
// CH.5 We're done
return;
}
// CH.5 Set the text in a field
static void setText( FieldPtr field, CharPtr text )
{
VoidHand hfield; // CH.5 Handle of field text
CharPtr pfield; // CH.5 Pointer to field text
// CH.5 Get the current field handle
hfield = FldGetTextHandle( field );
// CH.5 If we have a handle
if( hfield != NULL )
{
// CH.5 Resize it
if( MemHandleResize( hfield, StrLen( text ) + 1 ) != 0 )
errorExit( MemoryErrorAlert );
}
else
// CH.5 Allocate a handle for the string
{
hfield = MemHandleNew( StrLen( text ) + 1 );
if( hfield == NULL )
errorExit( MemoryErrorAlert );
}
// CH.5 Lock it
pfield = MemHandleLock( hfield );
// CH.5 Copy the string
StrCopy( pfield, text );
// CH.5 Unlock it
MemHandleUnlock( hfield );
// CH.5 Give it to the field
FldSetTextHandle( field, hfield );
// CH.5 Draw the field
FldDrawField( field );
// CH.5 We're done
return;
}
// CH.5 Get the text from a field
static void getText( FieldPtr field, VoidPtr precord, Word offset )
{
CharPtr pfield; // CH.5 Pointer to field text
// CH.5 Get the text pointer
pfield = FldGetTextPtr( field );
// CH.5 Copy it
DmWrite( precord, offset, pfield, StrLen( pfield ) );
// CH.5 We're done
return;
}
// CH.6 Set the Contact Detail date selector trigger
static void setDateTrigger( void )
{
FormPtr form; // CH.5 The contact detail form
// CH.6 Get the contact detail form pointer
form = FrmGetActiveForm();
// CH.6 If there is no date
if( dateTime.year == NO_DATE )
{
CtlSetLabel( getObject( form, ContactDetailDateSelTrigger ),
" " );
}
else
// CH.6 If there is a date
{
Char dateString[dateStringLength];
// CH.6 Get the date string
DateToAscii( dateTime.month, dateTime.day, dateTime.year,
(DateFormatType)PrefGetPreference( prefDateFormat ), dateString );
// CH.6 Set the selector trigger label
CtlSetLabel( getObject( form, ContactDetailDateSelTrigger ),
dateString );
}
// CH.6 We're done
return;
}
// CH.6 Set the Contact Detail time selector trigger
static void setTimeTrigger( void )
{
FormPtr form; // CH.5 The contact detail form
// CH.6 Get the contact detail form pointer
form = FrmGetActiveForm();
// CH.6 If there's no time
if( dateTime.hour == NO_TIME )
{
CtlSetLabel( getObject( form, ContactDetailTimeSelTrigger ),
" " );
}
else
// CH.6 If there is a time
{
Char timeString[timeStringLength];
// CH.6 Get the time string
TimeToAscii( dateTime.hour, dateTime.minute,
(TimeFormatType)PrefGetPreference( prefTimeFormat ), timeString );
// CH.6 Set the selector trigger label
CtlSetLabel( getObject( form, ContactDetailTimeSelTrigger ),
timeString );
}
// CH.6 We're done
return;
}
// CH.6 Set the controls in the Enter Time form based on dateTime
static void setTimeControls( void )
{
FormPtr form;
ControlPtr hourButton;
ControlPtr minuteTensButton;
ControlPtr minuteOnesButton;
ControlPtr amButton;
ControlPtr pmButton;
ControlPtr noTimeCheckbox;
Char labelString[3];
SWord hour;
// CH.6 Get the form
form = FrmGetActiveForm();
// CH.6 Get the control pointers
hourButton = getObject( form, EnterTimeHoursPushButton );
minuteTensButton = getObject( form,
EnterTimeMinuteTensPushButton );
minuteOnesButton = getObject( form,
EnterTimeMinuteOnesPushButton );
amButton = getObject( form, EnterTimeAMPushButton );
pmButton = getObject( form, EnterTimePMPushButton );
noTimeCheckbox = getObject( form, EnterTimeNoTimeCheckbox );
// CH.6 If there is a time
if( dateTime.hour != NO_TIME )
{
// CH.6 Update the hour
hour = dateTime.hour % 12;
if( hour == 0 )
hour = 12;
CtlSetLabel( hourButton,
StrIToA( labelString, hour ) );
// CH.6 Update the minute tens
CtlSetLabel( minuteTensButton,
StrIToA( labelString, dateTime.minute / 10 ) );
// CH.6 Update the minute ones
CtlSetLabel( minuteOnesButton,
StrIToA( labelString, dateTime.minute % 10 ) );
// CH.6 Update AM
CtlSetValue( amButton, (dateTime.hour < 12) );
// CH.6 Update PM
CtlSetValue( pmButton, (dateTime.hour > 11) );
// CH.6 Uncheck the no time checkbox
CtlSetValue( noTimeCheckbox, false );
}
else
// If there is no time
{
// CH.6 Update the hour
CtlSetValue( hourButton, false );
CtlSetLabel( hourButton, "" );
// CH.6 Update the minute tens
CtlSetValue( minuteTensButton, false );
CtlSetLabel( minuteTensButton, "" );
// CH.6 Update the minute ones
CtlSetValue( minuteOnesButton, false );
CtlSetLabel( minuteOnesButton, "" );
// CH.6 Update AM
CtlSetValue( amButton, false );
// CH.6 Update PM
CtlSetValue( pmButton, false );
// CH.6 Uncheck the no time checkbox
CtlSetValue( noTimeCheckbox, true );
}
// CH.6 We're done
return;
}
// CH.7 This function is called by Palm OS to sort records
static Int sortFunc( CharPtr precord1, CharPtr precord2, Int sortBy )
{
Int sortResult;
// CH.7 Switch based on sort criteria
switch( sortBy )
{
// CH.7 Sort by date and time
case SORTBY_DATE_TIME:
{
DateTimePtr pdateTime1;
DateTimePtr pdateTime2;
Long lDiff;
pdateTime1 = (DateTimePtr)(precord1 + DB_DATE_TIME_START);
pdateTime2 = (DateTimePtr)(precord2 + DB_DATE_TIME_START);
// CH.7 Compare the dates and times
lDiff = (Long)(TimDateTimeToSeconds( pdateTime1 ) / 60 ) -
(Long)(TimDateTimeToSeconds( pdateTime2 ) / 60 );
// CH.7 Date/time #1 is later
if( lDiff > 0 )
sortResult = 1;
else
// CH.7 Date/time #2 is later
if( lDiff < 0 )
sortResult = -1;
else
// CH.7 They are equal
sortResult = 0;
}
break;
// CH.7 Sort by first name
case SORTBY_FIRST_NAME:
{
sortResult = StrCompare( precord1 + DB_FIRST_NAME_START,
precord2 + DB_FIRST_NAME_START );
}
break;
// CH.7 Sort by last name
case SORTBY_LAST_NAME:
{
sortResult = StrCompare( precord1 + DB_LAST_NAME_START,
precord2 + DB_LAST_NAME_START );
}
break;
}
// CH.7 We're done
return( sortResult );
}
// CH.8 Draw our list of choices using a table object
static void drawTable( void )
{
FormPtr form;
TablePtr table;
Int column;
Int count;
ControlPtr upArrow;
ControlPtr downArrow;
// CH.8 Get the form pointer
form = FrmGetActiveForm();
// CH.8 Get the table pointer
table = getObject( form, ContactListTableTable );
// CH.8 For all columns
for( column = 0; column < TABLE_NUM_COLUMNS; column++ )
{
// CH.8 Set the draw routine
TblSetCustomDrawProcedure( table, column, drawCell );
// CH.8 Make the column visible
TblSetColumnUsable( table, column, true );
}
// CH.8 Initialize the table styles
for( count = 0; count < TABLE_NUM_ROWS; count++ )
{
// CH.8 If there is data
if( count < numRecords )
{
// CH.8 Show the row
TblSetRowUsable( table, count, true );
// CH.8 Set the cell styles
for( column = 0; column < TABLE_NUM_COLUMNS; column++ )
TblSetItemStyle( table, count, column, customTableItem );
}
else
// CH.8 Hide unused rows if any
TblSetRowUsable( table, count, false );
}
// CH.8 Draw the table
TblDrawTable( table );
// CH.8 Get pointers to the arrow buttons
upArrow = getObject( form, ContactListRecordUpRepeating );
downArrow = getObject( form, ContactListRecordDownRepeating );
// CH.8 Update the arrow buttons and scrollbars
if( numRecords > TABLE_NUM_ROWS )
{
// CH.8 Show the up arrow
if( cursor > 0 )
{
CtlSetLabel( upArrow, BLACK_UP_ARROW );
CtlSetEnabled( upArrow, true );
}
else
{
CtlSetLabel( upArrow, GRAY_UP_ARROW );
CtlSetEnabled( upArrow, false );
}
CtlShowControl( upArrow );
// CH.8 Show the down arrow
if( cursor >= numRecords - TABLE_NUM_ROWS )
{
CtlSetLabel( downArrow, GRAY_DOWN_ARROW );
CtlSetEnabled( downArrow, false );
}
else
{
CtlSetLabel( downArrow, BLACK_DOWN_ARROW );
CtlSetEnabled( downArrow, true );
}
CtlShowControl( downArrow );
// CH.8 Show the scrollbar
FrmShowObject( form, FrmGetObjectIndex( form,
ContactListScrollbarScrollBar ) );
SclSetScrollBar( getObject( form,
ContactListScrollbarScrollBar ), cursor, 0,
numRecords - TABLE_NUM_ROWS, TABLE_NUM_ROWS );
}
else
{
// CH.8 Hide the arrows
CtlHideControl( upArrow );
CtlHideControl( downArrow );
// CH.8 Hide the scrollbar
FrmHideObject( form, FrmGetObjectIndex( form,
ContactListScrollbarScrollBar ) );
}
// CH.8 We're done
return;
}
// CH.8 The custom drawing routine for a table cell
static void drawCell( VoidPtr table, Word row, Word column,
RectanglePtr bounds )
{
Int record;
CharPtr precord;
Char string[DB_FIRST_NAME_SIZE + DB_LAST_NAME_SIZE];
SWord width;
SWord len;
Boolean noFit;
// CH.8 Calculate our record
record = cursor + row;
// CH.8 Get our record
hrecord = DmQueryRecord( contactsDB, record );
precord = MemHandleLock( hrecord );
// CH.8 Get the date and time
MemMove( &dateTime, precord + DB_DATE_TIME_START,
sizeof( dateTime ) );
// CH.8 Switch on the column
switch( column )
{
// CH.8 Handle dates
case TABLE_COLUMN_DATE:
{
if( dateTime.year != NO_DATE )
{
DateToAscii( dateTime.month, dateTime.day,
dateTime.year,
(DateFormatType)PrefGetPreference(
prefDateFormat ), string );
}
else
StrCopy( string, "-" );
}
break;
// CH.8 Handle times
case TABLE_COLUMN_TIME:
{
if( dateTime.hour != NO_TIME )
{
TimeToAscii( dateTime.hour, dateTime.minute,
(TimeFormatType)PrefGetPreference(
prefTimeFormat ), string );
}
else
StrCopy( string, "-" );
}
break;
// CH.8 Handle names
case TABLE_COLUMN_NAME:
{
StrCopy( string, precord + DB_FIRST_NAME_START );
StrCat( string, " " );
StrCat( string, precord + DB_LAST_NAME_START );
}
break;
}
// CH.8 Unlock the record
MemHandleUnlock( hrecord );
// CH.8 Set the text mode
WinSetUnderlineMode( noUnderline );
FntSetFont( stdFont );
// CH.8 Truncate the string if necessary
width = bounds->extent.x;
len = StrLen( string );
noFit = false;
FntCharsInWidth( string, &width, &len, &noFit );
// CH.8 Draw the cell
WinEraseRectangle( bounds, 0 );
WinDrawChars( string, len, bounds->topLeft.x, bounds->topLeft.y );
// CH.8 We're done
return;
}