在这一章中,我们在Contacts程序中添加分类。分类允许把Contacts分成组,如Business和Personal。你可以分别或统一查看这些组。你还可以为Contacts应用程序添加、删除、或者改变分类名称。你可以把每一条记录分配到一个组。 我们也将在Contacts中添加代码使用Palm OS系统查找(Find)功能在Contacts数据库中查找相关的内容。你可以输入短日期或者人的全名,在存储有不同的字段和格式的数据库中找到匹配的记录。你也可以选中任何一个找到的条目,让其在Contact Detail窗体中显示。 保存工程 在做下一步之前,还是要提醒你这一点。操作步骤如下: 1.运行Windows浏览器; 2.找到工程存放的文件夹; 3.选中文件夹,按CTRL+C来将其复制; 4.选择一个文件夹用来保存副本; 5.按CTRL+V将副本粘贴到备份文件夹中; 6.把项目名重命名为容易记的名字,我把它命名为Contacts CH.8。 分类 如果你想把应用程序放在一个组中,分类正好可以实现。可以给一个应用程序定义15个组,这样多的组对于一般的应用程序已足够了。在我的Palm上,任何一个应用程序的组都不超过6个。 Palm OS做了大量分类的工作。一旦把应用程序信息创建完成,分类管理器(Categories Manager)就会将其保持在那个地方。并且创建出下拉框来管理,允许对分类的创建、 修改和删除。 我们的主要工作是把分类中的记录分离出来并浏览。在这之前,处理记录的方法和以前完全相同。在显示记录之前,检查这条检查是否属于当前的分类。这就会引发滚动条的一些问题。 Contacts.rsrc的内容添加 在Constructor中添加三个分类: ◆在Contact List窗体添加弹出列表框,用来过滤contacts窗体列表。它必须与分类管理器(Category Manager)的要求一致; ◆在Contact Detail窗体中添加弹出列表框,用来选择当前记录所在的分类; ◆创建一个App Info String List资源,定义应用程序中所在的初始分类; Contact List Form的内容添加 现在到了向Contact List窗体中加入资源时候了,让人们来看看我们具体的分类。 1.运行Metrowerks构造器; 2.打开资源文件Contacts.rsrc,它位于项目文件夹中的Src文件夹中; 3.双击打开Contact List窗体; 4.从菜单中选择Window | Catalog,打开Catalog; 5.拖动一个列表框(list)到窗体中; 6.设置列表框属性:Object Identifier=CategoryList,Left Origin=86,Top Origin=1,Width=72,不复选Usable项,因为我们不希望窗体显示的时候把列表也把窗体显示出来,Visible Items=0。作为分类服务的一部分,Palm OS将动态地建立列表(根据我们的要求)。 7.拖动一个弹出触发按纽(Pop-up trigger)到窗体上; 8.将触发器命名为CategoryPopup。Left Origin=160,Top Origin=0,Width=0。不复选Anchor Left,这样使触发器标签文本(从左端的160象素处)和屏幕右侧右对齐。我们将在程序中快速的删除或添加标签中的文字。 9.完成后,Contact List窗体看起来如图9-1。 注意: 你不需要和一般的弹出触发器一样来设置列表的ID,尽管做了也不会有什么不当。你将看到,我们不是调用触发器的ctlSelectEvent事件,而是调用了具体的分类函数。 Contact Detail Form的内容添加 现在我们修改Contact Detail Form,允许人们在分类具体的条目。 10.在Resource Type and Name列表中双击打开Contact Detail List窗体; 11.拖动一个标签到窗体中。设置标签属性:Text为Category,选择粗体。Top Origin=90,选中所有的(按住SHIFT点击)的标签,再选择Arrange | Align Right Edges,使它与其它右对齐的标签对齐; 12.拖动一个列表框控件到窗体中; 13.设置列表框属性:Object Identifier= CategoryList。Left Origin=80,Top Origin=90,Width=80。不复选Usable,因为我们不希望窗体显示的时候列表框就显示。设Visible Items为0。和其它的分类列表框一样,Palm OS将动态建立这个列表; 14.拖动弹出触发器到窗体中; 15.和Contact List窗体类似,把触发器命名为CategoryPopup。Left Origin=80,Top Origin=89,Width=80。和以前一样,你可以保留或者删除标签中的文字; 16.完成后,Contact Detail窗体看起来如图9-2; 17.最后,需要新建一个App Info String List资源来初始化分类名称。在Constructor的Resource Type and Name列表中点击App Info String Lists,并按CTRL-K 新建一个。注意不要创建一个旧的String List; 18.把App Info String List命名为Category Labels,双击打开; 19.在列表的前三个条目中输入:Unfiled,Business和Personal。 20.按12次ENTER键创建16个条目。这将初始化其它分类为空白而不是垃圾。完成后,列表看起来如图9-3。我重申一下:你必须初始化所有16个条目;否则,一些令人费解的事情就会出现。 这就把为支持分类对资源文件所需做的修改完成了。关闭Constuctor并及时保存资源文件。 Contacts.c的修改 为支持分类,在Contacts.c中需要添加四个任务: ◆创建数据结构,以符合Category Manager的要求; ◆支持Contact Detail窗体中分类弹出列表; ◆支持Contact List窗体中的分类弹出列表; ◆Contact List窗体在不同分类时,处理滚动条事件; 初始化分类 分类信息一般要保存在应用程序主数据库的信息模块上。为此,需要在PilotMain()中创建数据库后,新添加一些代码。在函数的顶部先添加一些新的变量: LocalID dbID; // CH.9 Local ID of the database UInt cardNum; // CH.9 Card number LocalID appInfoID; // CH.9 Local ID of the app info block VoidHand hAppInfo; // CH.9 Handle to the app info block AppInfoPtr pAppInfo; // CH.9 Points to the app info block 然后,看一下应用程序的信息块: // CH.9 Get the ID and card number DmOpenDatabaseInfo( contactsDB, &dbID, NULL, NULL, &cardNum, NULL); // CH.9 Get the app info pointer if any DmDatabaseInfo( cardNum, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &appInfoID, NULL, NULL, NULL ); 深入: 程序信息模块(App Info Block) 每个Palm OS数据库都有一个叫app info block特殊区域。你可以在这个区域中保存任何东西。如果已经使用它保存了分类,还可以在分类结构的末尾处插入具体的数据,Palm OS也会对此处理。我经常使用这个空间保存优先权(preferences)或数据库中全局变量。例如,可以使用它来存储数据库结构,由此可以使用代码处理不同类型的数据库。 小技巧 注意我们在这里细致定义了卡号,而不是只指定卡号为0。尽管卡号为0的卡在Palm Compting的设备上一般都能正常工作,但对一些第三方的硬件,特别是Handspring Visor和TRGpro,他们有一块以上的内存卡,为了支持更宽的Palm内存单元,正确的处理卡号在程序中也就变得很重要了。 如果我们没有找到已创建分类的应用程序信息块,就回到开头新建一个。 // CH.9 If there is no application info block, create one if( appInfoID == 0 ) { // CH.9 Allocate an application info block if( (hAppInfo = DmNewHandle( contactsDB, sizeof( AppInfoType ) )) == NULL ) errorExit( MemoryErrorAlert ); // CH.9 Translate the handle to a local ID appInfoID = MemHandleToLocalID( hAppInfo ); // CH.9 Set the application info block DmSetDatabaseInfo( cardNum, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &appInfoID, NULL, NULL, NULL ); // CH.9 Translate the local ID to a pointer pAppInfo = MemLocalIDToLockedPtr( appInfoID, cardNum ); // CH.9 Clear it DmSet( pAppInfo, 0, sizeof( AppInfoType ), 0 ); // CH.9 Initialize the categories CategoryInitialize( pAppInfo, CategoryLabelsAppInfoStr ); // CH.9 Unlock the application info block MemPtrUnlock( pAppInfo ); } 当调用Palm OS的函数CategoryInitialize()时,Palm OS自动地初始化分类。你需将在Constructor中创建的App Info String List ID号传给此函数。保证所使用的字符串就是上一部分中所定义的。如果不是,上面的分类就不会显示或者显示一些垃圾,但CategoryInitialize()的也不会返回错误。 小技巧 你可以在应用程序信息模块中保存任何东西。如果已保存了分类,你只需在应用程序信息模块扩展分类结构,然后添加你想保存的数据就可以了。不过应用程序信息模块中添加的数据必须符合分类结构形式。 对Contact Detail Form的支持 当我们浏览记录时,Contact Detail窗体应正确的显示每一条记录所在的分类,当我们翻动记录时。它也应该允许我们把分类设置为一个记录。 不像常规的弹出触发按纽(Pop-up Trigger),为了使用分类管理器(Category Manager)来处理事件,必须捕捉分类弹出触发按纽的初始值来触发ctlSelectEvent事件,然后使用Palm OS的函数CategorySelect()完成其他所有的工作。我们让事件返回true,来防止Palm OS在一般情况下对一个弹出触发按纽所做的操作(试图弹出一个列表框)。在这里,列表框的创建、弹出和关闭都是通过调用CategorySelect()来完成。 在程序的开始处,我们定义三个变量。变量detailCat代表在Contact Detail窗体中的显示当前记录的分类。ListCat代表Contact List窗体当前的分类。第三个变量talbeIndex,是为浏览特定分类中的记录而定义的,在下一部分中将做详细的解释。 // CH.9 Category variables static Word listCat = dmAllCategories; // CH.9 The current category ID static Word detailCat; // CH.9 Category ID for details static UInt tableIndex[TABLE_NUM_ROWS]; // CH.9 Record indexes for rows 在contactDetailHandleEvent()事件的顶部,需要分配内存来保存分类的名称: Char catName[dmCategoryLength]; // CH.9 Category name 下面是支持分类弹出触发按纽和列表框的代码,将之添加到ctlSelectEvent()事件中: // CH.9 Catch a tap on the category trigger case ContactDetailCategoryPopupPopTrigger: { UInt recAttrs; // CH.9 The record attribs // CH.9 Palm OS will present the popup list for us. CategorySelect( contactsDB, form, ContactDetailCategoryPopupPopTrigger, ContactDetailCategoryListList, false, &detailCat, catName, 1, 0 ); // CH.9 Get the record attributes DmRecordInfo( contactsDB, cursor, &recAttrs, NULL, NULL ); // CH.9 Put in the category bits recAttrs &= ~dmRecAttrCategoryMask; recAttrs |= detailCat; // CH.9 Set the record attributes DmSetRecordInfo( contactsDB, cursor, &recAttrs, NULL ); } // CH.9 Set fields and return true in this case setFields(); return( true ); } 在这里,CategorySelect()几乎完成了所有的困难工作,包括创建和管理列表框和可能要编辑的列表框条目。注意CategorySelect()中的第五个参数为False,这个参数提示CategorySelect()我们是使用列表框选择分类而不是对列表框的条目进行排序。这使CategorySelect()去掉了列表框中的All选项。 CategorySelect()中的第八个参数值(catName后面的一个)是1,提示CategorySelect()防止第一个分类Unfiled被修改。使用这种方式,就可以使很多分类不被修改。 你可以使用最后一个参数指定一个字符串作为分类编辑对话框的标题,来替代原来的Edit Categories。在所有例子中,迄今为止,我认为这个缺省的标题是最好的,所以我总是把它设为0。 注意: Palm OS1.0有它自己版本的CategorySelect()—— CategorySelectV10()。如果你对支持原来的Pilot1000到5000单元有兴趣,就需要使用这个命令。 在调用CategorySelect()后,我们在当前记录的属性位上设置分类的值。首先DmRecordInfo()获取属性位,在做一些位运算后,通过DmRecordInfo()为属性设置新值。在事件处理过程中要做的就是这些。 在newRecord()函数中,我们为创建的每一个记录初始化其分 类。在函数的顶部,需要定义一个变量临时保存记录属性。 UInt recAttrs; // CH.9 The record's attributes 在函数的末尾,和在事件处理函数中一样,我们获得并设置记录的属性。如果在Contact List中我们选择的分类是All,我们就不知道将这个记录到底该放在哪一个分类中,这时我们就把分类设置为Unfiled。如果Contact List上面有一个具体的分类,那么就把记录放在这个分类中。 // Ch.9 Get the record attribute bits DmRecordInfo( contactsDB, cursor, &recAttrs, NULL, NULL ); // CH.9 Clear the category bits recAttrs &= ~dmRecAttrCategoryMask; // CH.9 Set the category to the appropriate category if( listCat == dmAllCategories ) recAttrs |= dmUnfiledCategory; else recAttrs |= listCat; // CH.9 Set the record attributes DmSetRecordInfo( contactsDB, cursor, &recAttrs, NULL ); 在attachFields()中,我们需要设置弹出触发按纽的标签文本。首先,在函数的顶部再定义一些新的变量。 UInt recAttrs; // CH.9 The record attribute bits Char catName[dmCategoryLength]; // CH.6 The category name 在函数的末尾,我们使用一些专门的分类管理函数设置并重新得到标签文本。 // CH.9 Get the record attributes DmRecordInfo( contactsDB, cursor, &recAttrs, NULL, NULL ); // CH.9 Get the category detailCat = recAttrs & dmRecAttrCategoryMask; // CH.9 Set the category popup trigger label CategoryGetName( contactsDB, detailCat, catName ); CategorySetTriggerLabel( getObject( form, ContactDetailCategoryPopupPopTrigger ), catName ); 这样,使分类能在Contact List窗体上正常工作的代码修改工作就完成了,其中包括将具体的记录写入分类中。 对Contact List Form的支持 对Contact List窗体的修改要更多一些,因为我们需要将记录仅仅显示在一个给出的分类或者All分类中。问题是先前的代码很大程度上依赖于数据库中的每一个记录,例如,我们可以通过Cursor+1实现显示下一条记录。但是在分类中,我们就不能这样做了,因为里面或许只有一些甚至没有分类中所要显示的记录。 要知道当前分类到底有没有这条记录的一种方法是将记录用函数包装起来,这样我们就能象以前那样来浏览记录了。在程序的开头,先声明三个新函数的原型: static void initIndexes( void ); static void scrollIndexes( Int amount ); static UInt findIndex( UInt scrollValue ); 函数initIndexes()从当前分类中获得与之相关联的表的索引tableIndex。这样,我们就可以在指定的表中从头到尾的浏览,从而取代盲目地在所有记录间浏览。函数scrollIndexes()允许我们把具有特殊分类的窗口上移或下移。函数findIndex()返回给我们一个记录在分类中的游标位置。在以后的部分中将看到这些函数是如何工作的,现在我们只是在Contact List 窗体的事件处理函数及其它函数中调用它们来实现浏览的功能。 在Contact List窗体事件处理函数 contactListHandleEvent()应该怎样做呢?首先,在函数的开头保存分类的名称。 Char catName[dmCategoryLength]; // CH.9 Category name 修改窗体打开事件,设置弹出触发按纽的标签文本,并调用initIndexes()来初始化列表框。 // CH.7 Form open event case frmOpenEvent: { // CH.7 Draw the form FrmDrawForm( form ); // CH.9 Set the category popup trigger label CategoryGetName( contactsDB, listCat, catName ); CategorySetTriggerLabel( getObject( form, ContactListCategoryPopupPopTrigger ), catName ); // CH.8 The cursor starts at the beginning cursor = 0; // CH.9 Initialize the table indexes initIndexes(); // CH.8 Populate and draw the table drawTable(); } break; 当选中一个记录切换到Contact Detail窗体时,我们在tableIndex数组中查找所选中记录的游标位置。 // CH.7 Respond to a list selection case tblSelectEvent: { // CH.7 Set the database cursor to the selected contact cursor = tableIndex[event->data.tblSelect.row]; // CH.7 Go to contact details FrmGotoForm( ContactDetailForm ); } break; 在popSelectEvent事件中,由于把记录依据不同的标准进行了重新排序,所以必须根据排序的结果重新初始化tableIndex。 // CH.7 Sort the contact database by the new criteria DmQuickSort( contactsDB, (DmComparF*)sortFunc, sortBy ); // CH.8 Cursor starts at zero cursor = 0; // CH.9 Initialize the table indexes initIndexes(); // CH.8 Rebuild the table drawTable(); 我们调用scrollIndexes()代替向上、向下箭头重复按钮所做的数学运算。 // CH.8 Up arrow case ContactListRecordUpRepeating: scrollIndexes( -1 ); break; // CH.8 Down arrow case ContactListRecordDownRepeating: scrollIndexes( 1 ); break; 同样的,我们通过调用scrollIndexes()代替用按键事件的游标运算。 // CH.8 Up arrow hard key case pageUpChr: scrollIndexes( -(TABLE_NUM_ROWS - 1) ); break; // CH.8 Down arrow hard key case pageDownChr: scrollIndexes( TABLE_NUM_ROWS - 1 ); break; 为了处理滚动条,我们需要做两件事。首先,由于刷新基于滚动条的记录的时间变长了,那么我们通过响应sclExitEvent时间来代替响应sclRepeatEvent。第二,使用findIndex()在分类记录的子集中找到任意记录的游标值。我们必须仅仅处理基于当前分类的记录的滚动条。如果把表滚动到底的话,返回的数字是处在0和表顶端的记录之间。我们用findIndex()把这个数字转化为一个绝对的游标值。一旦知道表顶部记录的实际位置,我们调用initIndexes()把余下的值赋给tableIndex数组。 // CH.8 Respond to scrollbar events case sclExitEvent: { //CH.9 Find the record in our category cursor = findIndex( event->data.sclExit.newValue ); // CH.9 Initialize our index list initIndexes(); // CH.8 Draw the table drawTable(); } break; 下面是处理分类弹出触发按纽的代码。和Contact Detail窗体相比,我们为触发器捕捉ctlSelectEvent事件,并把控件传递给CategorySelect(),为了防止采用弹出触发器的缺省操作,函数向Palm OS返回True。请注意我们在CategorySelect()中使用了True参数,它将该在分类列表框中添加All选项。一旦新的分类被选中,我们就需要重新做索引并重新绘制表。 // CH.9 Catch a tap on the category trigger case ctlSelectEvent: { // CH.9 Palm OS will present the popup list for us. CategorySelect( contactsDB, form, ContactListCategoryPopupPopTrigger, ContactListCategoryListList, true, &listCat, catName, 1, 0 ); // CH.9 Cursor starts at zero cursor = 0; // CH.9 Initialize the indexes initIndexes(); // CH.9 Draw the table drawTable(); } // CH.9 Don't let the OS generate other events from this return( true ); drawTable()的变化包括修改了一行代码和截去了一整块代码。在initIndexes()和scrollIndex()中可以更方便地管理的重复按钮和滚动条,并且更容易知道下一信息是什么。同样的,我们也可以使用这两个函数处理表和记录的何时到达结尾的问题。 和第八章不同,记录到达最后一条后就使所有行变为不可用,检查talbeIndex数组,看看是否有其它记录与这一行相联系。 // CH.8 Initialize the table styles for( count = 0; count < TABLE_NUM_ROWS; count++ ) { // CH.9 If there is data if( tableIndex[count] != 0xffff ) { // CH.8 Show the row TblSetRowUsable( table, count, true ); 截去处理向上向下箭头按钮和滚动条的代码,而把它放在程序的底部。这些完成后,在调用TblDrawTable()后,drawTable()函数就会立即结束。 // CH.8 Draw the table TblDrawTable( table ); // CH.8 We're done return; } 在drawCell()中,我们只修改一行代码,这一行在函数的顶部,决定到底使用哪一条记录。 // CH.9 Calculate our record record = tableIndex[row]; initIndexes()函数 函数initIndexes()创建了在表中所要显示的记录。它创建了数组tableIndex,代表在当前分类中的记录。我们在函数的顶部定义了一些变量并获得当前窗体的指针: static void initIndexes( void ) { FormPtr form; Int count; UInt index = cursor; ControlPtr downArrow; ControlPtr upArrow; UInt numRecsInCategory; // CH.9 Get the current form form = FrmGetActiveForm(); 根据行建立循环,在当前分类中查找下一条记录并把它放入行中。如果我们到达了最后一条记录,就把数字0xffff(65,535)赋给数组表示没有所查记录。最后,把游标指向一个已知的有效记录,代替原来或许是在分类中根本就不存在的记录。因为如果分类中没有所查记录,就把游标设为了0xffff。由于表中所有的行已被关掉,这样做就可以了。 // CH.9 For each table row for( count = 0; count < TABLE_NUM_ROWS; count++ ) { // CH.9 Find the next matching record if( DmSeekRecordInCategory( contactsDB, &index, 0, dmSeekForward, listCat ) ) { // CH.9 No more records. Fill the rest of the array with // 0xffff for( ; count < TABLE_NUM_ROWS; count++ ) tableIndex[count] = 0xffff; break; } // CH.9 Put the index number in the array tableIndex[count] = index; index++; } // CH.9 Set the cursor to a known category record cursor = tableIndex[0]; 下面是箭头按扭和滚动条处理的代码。它与原来drawTable()函数相比,没有太大的改变。这次它使用DmNumRecordsInCategory()和DmPositionInCategory()来实现对去按钮和滚动条的操作。 // CH.8 Get pointers to the arrow buttons upArrow = getObject( form, ContactListRecordUpRepeating ); downArrow = getObject( form, ContactListRecordDownRepeating ); // CH.8 Update the arrow buttons and scrollbars numRecsInCategory = DmNumRecordsInCategory( contactsDB, listCat ); if( numRecsInCategory > TABLE_NUM_ROWS ) { UInt position = DmPositionInCategory( contactsDB, cursor, listCat ); // CH.8 Show the up arrow if( position > 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( position >= numRecsInCategory - TABLE_NUM_ROWS ) { CtlSetLabel( downArrow, GRAY_DOWN_ARROW ); CtlSetEnabled( downArrow, false ); } else { CtlSetLabel( downArrow, BLACK_DOWN_ARROW ); CtlSetEnabled( downArrow, true ); } CtlShowControl( downArrow ); // CH.9 Show the scrollbar SclSetScrollBar( getObject( form, ContactListScrollbarScrollBar ), position, 0, numRecsInCategory - TABLE_NUM_ROWS, TABLE_NUM_ROWS ); } else { // CH.8 Hide the arrows CtlHideControl( upArrow ); CtlHideControl( downArrow ); // CH.8 Hide the scrollbar SclSetScrollBar( getObject( form, ContactListScrollbarScrollBar ), 0, 0, 0, 0 ); } // CH.9 We're done return; } scrollIndexes()函数 函数scrollIndexes()修改了tableIndex()数组,要知道我们或许只浏览一小部分内容,因此表中的大部分值仍是好用的。我们先定义一些变量并获得当前窗体指针。我们使用窗体指针来获得向上向下重复按钮的指针。 static void scrollIndexes( Int amount ) { FormPtr form; UInt count; UInt index; ControlPtr downArrow; ControlPtr upArrow; UInt numRecsInCategory; // CH.9 Get the current form form = FrmGetActiveForm(); // CH.9 Get pointers to the arrow buttons upArrow = getObject( form, ContactListRecordUpRepeating ); downArrow = getObject( form, ContactListRecordDownRepeating ); 余下的函数分成两部分,一个是向上浏览,一个是向下浏览。首先是向下浏览,我们一直循环直到达到我们的要求。 // CH.9 If we're scrolling down if( amount > 0 ) { // CH.9 While there is still an amount to scroll while( amount-- ) { 在循环中,每次移动一条记录。如果我们一条记录也没有找到,使向下箭头呈灰晕。我们将列表中的索引加一,并且在程序的底部,得到最新的索引。 // CH.9 Get a new index after the last one index = tableIndex[TABLE_NUM_ROWS - 1]; if( DmSeekRecordInCategory( contactsDB, &index, 1, dmSeekForward, listCat ) ) { // CH.9 No more records. We're done scrolling CtlSetLabel( downArrow, GRAY_DOWN_ARROW ); CtlSetEnabled( downArrow, false ); return; } // CH.9 Move current indexes up one for( count = 0; count < TABLE_NUM_ROWS - 1; count++ ) tableIndex[count] = tableIndex[count + 1]; // CH.9 Put the index number in the array tableIndex[count] = index; } 为了确定我们是否使向下箭头有效,需进一步地去查找下一条记录。如果的确还有一条记录,就应该仍然使向下箭头有效。同样,对于向上箭头也是这样。 // CH.9 Disable the down arrow if needed if( DmSeekRecordInCategory( contactsDB, &index, 1, dmSeekForward, listCat ) ) { CtlSetLabel( downArrow, GRAY_DOWN_ARROW ); CtlSetEnabled( downArrow, false ); } // CH.9 Enable the up arrow CtlSetLabel( upArrow, BLACK_UP_ARROW ); CtlSetEnabled( upArrow, true ); } 向上箭头的代码也是一样,只是箭头方向不同而已。 else // CH.9 If we're scrolling up if( amount < 0 ) { // CH.9 While there is still an amount to scroll while( amount++ ) { // CH.9 Get a new index before the first one index = tableIndex[0]; if( DmSeekRecordInCategory( contactsDB, &index, 1, dmSeekBackward, listCat ) ) { // CH.9 No more records. We're done scrolling CtlSetLabel( upArrow, GRAY_UP_ARROW ); CtlSetEnabled( upArrow, false ); return; } // CH.9 Move current indexes down one for( count = TABLE_NUM_ROWS - 1; count > 0; count-- ) tableIndex[count] = tableIndex[count - 1]; // CH.9 Put the index number in the array tableIndex[count] = index; } // CH.9 Disable the up arrow if needed if( DmSeekRecordInCategory( contactsDB, &index, 1, dmSeekBackward, listCat ) ) { CtlSetLabel( upArrow, GRAY_UP_ARROW ); CtlSetEnabled( upArrow, false ); } // CH.9 Enable the down arrow CtlSetLabel( downArrow, BLACK_DOWN_ARROW ); CtlSetEnabled( downArrow, true ); } 在两个方向的浏览处理好后,我们把游标指向索引的顶部并更新滚动条。 // CH.9 Set the cursor cursor = tableIndex[0]; // CH.9 Set the scrollbar numRecsInCategory = DmNumRecordsInCategory( contactsDB, listCat ); SclSetScrollBar( getObject( form, ContactListScrollbarScrollBar ), DmPositionInCategory( contactsDB, cursor, listCat ), 0, numRecords - TABLE_NUM_ROWS, TABLE_NUM_ROWS ); // CH.9 We're done return; } findIndex()函数 函数findIndex()相当地简单,这应该归功于Palm OS的DmSeekRecordInCategory()函数。设置好参数后,这个函数与DmPositionInCategory()基本上是相对应的。 // CH.9 Find a particular index static UInt findIndex( UInt scrollValue ) { UInt index = 0; // CH.9 Seek from zero to the scrollvalue DmSeekRecordInCategory( contactsDB, &index, scrollValue, dmSeekForward, listCat ); // We're done return( index ); } 调试 主要应该记住的事就是检查分类管理器(Category Manager)函数的参数设置是否正确。这类函数大多不会返回错误信息,所以唯一能找出哪里有错的方法就是检查函数的输入和输出是否正确。相信这些函数在接到信息后可以工作,然后根据所发送东西查找错误。我经常犯的错误是没有为CategoryInitialize()正确的定义App Info String List资源,这会引起很多怪问题。如果你用String List代替App Info String List给CategoryInitialize(),它会可以不用做任何工作了。同样地,记住在这个列表中定义16个字符串,虽然它们中的大部分可能是空的。否则,你会得到许多“垃圾”分类名。如果在传递给CategoryInitialize()之前清除应用程序信息模块失败,请添加监视。 图9-4和9-5是Contacts应用程序的运行示意图。图9-4是Contact Detail窗体中打开分类列表的示意图。图9-5是Contact List窗体中的弹出触发按纽的外观示意图。 保密记录 保密记录的处理与分类类似。在每一条记录的属性中有一位可以定义它是否为保密。我们可以在Contact Detail窗体中加入一个复选框,来设置或清除这个位。然后,如果你从来不是使用数学方法来浏览记录,而是使用DmSeekRecordInCategory()或者类似的函数,保密记录就不会从里面出现。 你所需做的变动主要在Contacts窗体里面。所以必须在窗体加入选择框,并编写相应的代码设置记录中的保密位。然后使用在Contact List窗体实现浏览类似的函数来替换Contact Detail窗体中的所有的基于数学方法的浏览。否则,保密记录在使用Contact Detail窗体中的浏览按钮就会被看到。 查找 如果在你的应用程序中有很多文本,我高度推荐应支持查找(Find)。不管使用查找有多大意义,所有规范的Palm OS应用程序都应支持查找,。 在Contacts中,我们将整个记录合成为字符串形式,这样可以在数据库中更容易的查找日期,时间,或全名信息。 对Contacts.c的修改 为了支持Find,我们将加入一个简单的函数。在程序顶部声明其函数原型。 static UInt findIndex( UInt scrollValue ); 这个函数没有使用全局变量,就可以在我们的数据库中进行查找。 运行(Lunch)代码 以前,我们只关心应用程序被启动后的位置,因为我们就从这里开始。而现在我们的应用程序在调用查找时也被触发。同样的,如果有人选中了一个已建立的记录,我们就让窗体跳到那个记录上。这个可以通过调用运行代码(lunch Code)来实现。 对这些新代码,我们需要查看PilotMain()传递过来更多的信息。因此,定义变量params。 深入 未用的参数 我们仍然忽略了最后的一个参数类型Word。如果你定义一个函数参数但不使用它,一些编译器将会给出一个警告信息。它使我们知道当不使用这些参数将能做什么。 DWord PilotMain( Word cmd, Ptr params, Word ) { 我们响应运行的第一种类型是系统的缺省查找: switch( cmd ) { // CH.2 Normal launch case sysAppLaunchCmdNormalLaunch: break; 如果我们调用系统的查找,那么另一个应用程序就会占用应用程序区域。所以,全局变量对我们来说是无用的。那就只能在堆栈中保存或分配内存,这两种方法都不要使用的太多。因此,最好在查找的函数调用中封装查找操作。 // CH.9 System find case sysAppLaunchCmdFind: find( params ); return( 0 ); 如果用户选择了一个查找结果的条目,我们会收到运行标识。在这种情况下,应用程序一直被激活,这时可以使用全局变量。 // CH.9 Go to item from find case sysAppLaunchCmdGoTo: break; 如果我们不去处理查找的运行代码,返回就行了。 // CH.2 We don't handle what's being asked for default: return( 0 ); } 深入 除了我们处理的之外,还有一些运行代码,但是上面几个是最重要的。其它的运行代码和在The Palm OS SDK Reference中有详细的描述,包含在本书后面的CD中的CodeWarrior Lite版本中。 在PilotMain中,我们还需要考虑,当我们正在运行程序时产生查找和相关事件时,系统Find会重新使用以前的堆栈。如果它不能找到,就会在复制堆栈的末尾来调用系统查找。为了防止这一点,我们需要添加一个全局变量来确定当前的状态。在初始化查找时,我们无法访问全局变量,但是下面的Goto lunch语句中,就可以访问它了。 // CH.9 Goto variable static Boolean upStack; 首先检查我们的数据库是不是已打开,如果数据库已打开,最好不要再打开。此时,把变量upStack设置为true,来标识程序已开始运行。 // CH.9 Open the database if it isn't already open if( contactsDB == NULL ) { contactsDB = DmOpenDatabaseByTypeCreator( 'ctct', 'PPGU', dmModeReadWrite ); } else upStack = true; 当在查找结果中选中了一个具体的条目,将其切换到Contact Detail窗体中显示。为做到这一点,在窗体初始化的逻辑中可以再添加一种情况。如果我们已经得到运行的结果,就移到数据库中相应的记录上,并在Contact Detail窗体中将其显示。其中最重要的是要关掉所有已打开的窗体,以免在运行系统查找时应用程序是激活的。不然,当试图打开一个已经打开的窗体时,我们的应用程序就会死掉。同样的,如果我们刚开始运行程序,在设置游标和切换到Detail窗体后,需要返回原始的情况。这个代码写在两个FrmGotoForm()命令之间。 else // CH.9 We are going to a particular record if( cmd == sysAppLaunchCmdGoTo ) { // CH.9 In case our app was running before the find FrmCloseAllForms(); // CH.9 Point the cursor to the found item cursor = ((GoToParamsPtr)params)->recordNum; // CH.9 Go to the details page FrmGotoForm( ContactDetailForm ); // CH.9 If we are running on top of ourselves, // return to the original event loop if( upStack ) { upStack = false; return( 0 ); } } find()函数 在函数的顶部,find()有它自己单独的变量列表: static void find( Ptr params ) { FindParamsPtr findParams = (FindParamsPtr)params; // CH.9 Params DmOpenRef contactsDB; // CH.9 Our local database ptr UInt numRecords; // CH.9 Number of records in the db LocalID dbID; // CH.9 Local ID of the database UInt cardNum; // CH.9 Card number UInt cursor; // CH.9 The current record VoidHand hrecord; // CH.9 Handle to the record CharPtr precord; // CH.9 Pointer to the record DateTimeType dateTime; // CH.9 Date and time in this record Char textRecord[dateStringLength + 1 + // CH.9 We timeStringLength + 1 + // build DB_FIRST_NAME_SIZE + // text DB_LAST_NAME_SIZE + // record here DB_PHONE_NUMBER_SIZE]; Char lcText[dateStringLength + 1 + // CH.9 Copy timeStringLength + 1 + // lower DB_FIRST_NAME_SIZE + // case DB_LAST_NAME_SIZE + // text here DB_PHONE_NUMBER_SIZE]; Word offset; // CH.9 Offset of the match RectangleType bounds; // CH.9 Bounding rect for text SWord width; // CH.9 Width of the bounds rect SWord len; // CH.9 Text length Boolean noFit; // CH.9 Does it fit 为和其它的应用程序相区分,首先为Find添加一个标题。 // CH.9 Draw a title for our find items // CH.9 If there's no more room, return if( (FindDrawHeader( findParams, "Contacts" )) == true ) return; 接着我们打开数据库,获取一些稍后会用到有关查找的信息。由于在查找操作中我们不想改变数据库中的信息,所以最好以只读的模式打开数据库。 // CH.9 Open the database for reading if( (contactsDB = DmOpenDatabaseByTypeCreator( 'ctct', 'PPGU', dmModeReadOnly )) == NULL ) return; 我们通过循环查找匹配的记录。注意到我们不一定从记录0开始。开始处依赖于Find Manager是第一次查找还是在查找结果中再次查找。查找完成后,一屏只能显示一次查找的信息。 深入 如果支持了记录的保密,在查找时,就要保证保密记录不会被找到。在这个情况下,我们就不能一个个的查找记录,而应使用一个叫DmSeekRecordInCategory()的函数。这个函数会自动地把保密记录排除在外。 因为记录中的信息类型是多种多样的,这就需要把数据记录类型转化为与查找相匹配的类型。举个例子,我们是把姓和名字段合为一个字符串中去查找,如果用户输入John Smith作为查找字符串,就会在数据库中找出John Smith。如果我们没有把记录结合在一个大的字符串,而是一个记录一个记录地查找,即使数据库中有John Smith这个记录,我们也不会找到。因此就需要把日期,时间等等转换成文本,并把它们放在一个很大的字符串中。由于内嵌的应用程序是基于字段的,所以它不支持直接查找。 // CH.9 For each record for( cursor = findParams->recordNum; cursor < numRecords; cursor++ ) { // CH.9 Get the record hrecord = DmQueryRecord( contactsDB, cursor ); precord = MemHandleLock( hrecord ); // CH.9 Get the date and time MemMove( &dateTime, precord + DB_DATE_TIME_START, sizeof( dateTime ) ); // CH.9 Start over *textRecord = '/0'; // CH.9 Add the date string if any if( dateTime.year != NO_DATE ) { DateToAscii( dateTime.month, dateTime.day, dateTime.year, (DateFormatType)PrefGetPreference( prefDateFormat ), textRecord ); StrCat( textRecord, " " ); } // CH.9 Add the time string if any if( dateTime.hour != NO_TIME ) { TimeToAscii( dateTime.hour, dateTime.minute, (TimeFormatType)PrefGetPreference( prefTimeFormat ), textRecord + StrLen( textRecord ) ); StrCat( textRecord, " " ); } // CH.9 Append the first name StrCat( textRecord, precord + DB_FIRST_NAME_START ); StrCat( textRecord, " " ); // CH.9 Append the last name StrCat( textRecord, precord + DB_LAST_NAME_START ); StrCat( textRecord, " " ); // CH.9 Append the phone number StrCat( textRecord, precord + DB_PHONE_NUMBER_START ); // CH.9 Unlock the record MemHandleUnlock( hrecord ); 深入 取消查找 这个例子中,在查找过程中不能被取消,但添加这个功能也十分简单。可以调用函数EvtSysEventAvail()来实现,将其放在for()循环顶部。 在Find窗体中,我们需要指定字符串的格式,这里使用小写。然后使用函数FindStrInStr()来查找匹配的记录。从它的参数中可以看到,strToFind并不是一个小写的字符串。如果你想建立自己的函数来查找,也需要规定自己的字符串格式。 // CH.9 Copy and convert to lower case StrToLower( lcText, textRecord ); // CH.9 If there's no match, move on if( (FindStrInStr( lcText, findParams->strToFind, &offset )) == false ) continue; 如果找到了一条匹配记录并且屏幕上有空间,那么就在屏幕上显示,然后继续查找下一条记录。 // CH.9 Send it to find // CH.9 If there's no more room, return if( (FindSaveMatch( findParams, cursor, offset, 0, NULL, cardNum, dbID )) == true ) break; // CH.9 Get the rectangle for our line of text FindGetLineBounds( findParams, &bounds ); // CH.9 Truncate the string if necessary width = bounds.extent.x; len = StrLen( textRecord ); noFit = false; FntCharsInWidth( textRecord, &width, &len, &noFit ); // CH.9 Draw the text WinEraseRectangle( &bounds, 0 ); WinDrawChars( textRecord, len, bounds.topLeft.x, bounds.topLeft.y ); // We used a line in the find dialog (findParams->lineNumber)++; } // CH.9 Close the database DmCloseDatabase( contactsDB ); // CH.9 We're done return; } 当查找完所有的记录后,关闭数据库。 调试 我主要调试了查找函数中我所传递的参数,并看看能不能将其修改。例如,不要相信strToFind就是你要找的字符串。另外,注意Palm OS不允许将lineNumber值增加。图9-6所示是查找函数运行后的结果。 下一步做什么 这是介绍Palm OS基础知识的最后一章,在下面的章节中将介绍一些有关软件设计的内容。第十章介绍了如何设计Palm OS的用户操作界面,第十一章介绍了建立Palm OS应用程序所使用的一些工具,第十二章介绍了如何组织和修改代码来加强它的可重用性。 程序列表 下面是最新版本的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 ); static void initIndexes( void ); static void scrollIndexes( Int amount ); static UInt findIndex( UInt scrollValue ); static void find( Ptr params ); // 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.9 Category variables static Word listCat = dmAllCategories; // CH.9 The current category ID static Word detailCat; // CH.9 Category ID for details static UInt tableIndex[TABLE_NUM_ROWS]; // CH.9 Record indexes for rows // CH.9 Goto variable static Boolean upStack; // CH.2 The main entry point DWord PilotMain( Word cmd, Ptr params, Word ) { DWord romVersion; // CH.4 ROM version LocalID dbID; // CH.9 Local ID of the database UInt cardNum; // CH.9 Card number LocalID appInfoID; // CH.9 Local ID of the app info block VoidHand hAppInfo; // CH.9 Handle to the app info block AppInfoPtr pAppInfo; // CH.9 Points to the app info block 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.9 Respond to launches switch( cmd ) { // CH.2 Normal launch case sysAppLaunchCmdNormalLaunch: break; // CH.9 System find case sysAppLaunchCmdFind: find( params ); return( 0 ); // CH.9 Go to item from find case sysAppLaunchCmdGoTo: break; // CH.2 We don't handle what's being asked for default: 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.9 Open the database if it isn't already open if( contactsDB == NULL ) { contactsDB = DmOpenDatabaseByTypeCreator( 'ctct', 'PPGU', dmModeReadWrite ); } else upStack = true; // CH.9 Get the ID and card number DmOpenDatabaseInfo( contactsDB, &dbID, NULL, NULL, &cardNum, NULL); // CH.9 Get the app info pointer if any DmDatabaseInfo( cardNum, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &appInfoID, NULL, NULL, NULL ); // 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 // CH.9 We are going to a particular record if( cmd == sysAppLaunchCmdGoTo ) { // CH.9 In case our app was running before the find FrmCloseAllForms(); // CH.9 Point the cursor to the found item cursor = ((GoToParamsPtr)params)->recordNum; // CH.9 Go to the details page FrmGotoForm( ContactDetailForm ); // CH.9 If we are running on top of ourselves, // return to the original event loop if( upStack ) { upStack = false; return( 0 ); } } else // CH.7 Display the list 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 Char catName[dmCategoryLength]; // CH.9 Category name // 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 Char catName[dmCategoryLength]; // CH.9 Category name // 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.9 Set the category popup trigger label CategoryGetName( contactsDB, listCat, catName ); CategorySetTriggerLabel( getObject( form, ContactListCategoryPopupPopTrigger ), catName ); // CH.8 The cursor starts at the beginning cursor = 0; // CH.9 Initialize the table indexes initIndexes(); // 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 = tableIndex[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 Cursor starts at zero cursor = 0; // CH.9 Initialize the table indexes initIndexes(); // 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: scrollIndexes( -1 ); break; // CH.8 Down arrow case ContactListRecordDownRepeating: scrollIndexes( 1 ); break; } // CH.8 Now refresh the table drawTable(); } break; // 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: scrollIndexes( -(TABLE_NUM_ROWS - 1) ); break; // CH.8 Down arrow hard key case pageDownChr: scrollIndexes( TABLE_NUM_ROWS - 1 ); break; } // CH.8 Now refresh the table drawTable(); } break; // CH.8 Respond to scrollbar events case sclExitEvent: { //CH.9 Find the record in our category cursor = findIndex( event->data.sclExit.newValue ); // CH.9 Initialize our index list initIndexes(); // CH.8 Draw the table drawTable(); } break; // CH.9 Catch a tap on the category trigger case ctlSelectEvent: { // CH.9 Palm OS will present the popup list for us. CategorySelect( contactsDB, form, ContactListCategoryPopupPopTrigger, ContactListCategoryListList, true, &listCat, catName, 1, 0 ); // CH.9 Cursor starts at zero cursor = 0; // CH.9 Initialize the indexes initIndexes(); // CH.9 Draw the table drawTable(); } // CH.9 Don't let the OS generate other events from this return( true ); } // 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 UInt recAttrs; // CH.9 The record's attributes // 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.9 Get the record attribute bits DmRecordInfo( contactsDB, cursor, &recAttrs, NULL, NULL ); // CH.9 Clear the category bits recAttrs &= ~dmRecAttrCategoryMask; // CH.9 Set the category to the appropriate category if( listCat == dmAllCategories ) recAttrs |= dmUnfiledCategory; else recAttrs |= listCat; // CH.9 Set the record attributes DmSetRecordInfo( contactsDB, cursor, &recAttrs, NULL ); // 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.6 A record pointer Word index; // CH.5 The object index UInt recAttrs; // CH.9 The record attribute bits Char catName[dmCategoryLength]; // CH.6 The category name // 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.9 Get the record attributes DmRecordInfo( contactsDB, cursor, &recAttrs, NULL, NULL ); // CH.9 Get the category detailCat = recAttrs & dmRecAttrCategoryMask; // CH.9 Set the category popup trigger label CategoryGetName( contactsDB, detailCat, catName ); CategorySetTriggerLabel( getObject( form, ContactDetailCategoryPopupPopTrigger ), catName ); // CH.5 We're done return; } // CH.5 Puts any field changes in the record 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, pre |