开发PalmOS教程介绍
第五章 数据库 |
Palm OS的所有内容在其存储器中都表现为数据库形式,下面我们就开始学习创建和使用数据库。我们将继续编写Contacts程序,把它写入一个数据库。 删除工作 为准备向Contacts添加一个数据库,首先应删除以前的示范语句。 备份Contacts程序 首先应备份当前的Contacts程序。我将它命名为Contacts CH.4。 步骤如下: 1. 运行Windows浏览器; 2. 找到并选中Contacts工程文件夹; 3. 按下CTRL-C复制文件夹; 4. 单击你想备份到的文件夹; 5. 按下CTRL-V将Contacts工程文件夹粘贴; 6. 单击Contacts的名字,将其改名为Contact CH.4。 从资源文件将原来的资源删除 把我们不再用到的一些资源从工程中删除。步骤如下: 1. 打开资源构造器; 2. 打开Contacts工程src文件夹中的Contacts.rsrc文件; 3. 选中FieldInit字符串资源,按Delect删除; 4. 关闭并保存Contacts.rsrc。 删除代码 在删掉资源后,我们要删除和重新组织代码使程序正常运行。 步骤如下: 1. 运行Code Warrior集成开发环境; 2. 从Contacts工程文件夹打开Contacts.mcp; 3. 从PilotMain()的刚开始处中删除下列代码: // CH.3 Our field memory handle static Handle htext; // CH.3 Handle to the text in our edit field #define HTEXT_SIZE 81 // CH.3 Size of our edit field 4. 从PilotMain()接近顶部的地方删除下列代码: // CH.3 Get the initialization string resource handle hsrc = DmGetResource( strRsc, FieldInitString ); // CH.3 Lock the resource, get the pointer psrc = MemHandleLock( hsrc ); // CH.3 Allocate our field chunk htext = MemHandleNew( HTEXT_SIZE ); if( htext == NULL ) return( 0 ); // CH.3 Lock the memory, get the pointer ptext = MemHandleLock( htext ); // CH.3 Initialize it StrCopy( ptext, psrc ); // CH.3 Unlock the field's memory MemHandleUnlock( htext ); // CH.3 Unlock the resource's memory MemHandleUnlock( hsrc ); // CH.3 Release the string resource DmReleaseResource( hsrc ); 5. 从contactDetailEventHandler()中将frmOPenEvent事件处理部分删掉下列代码: // CH.3 Get the index of our field index = FrmGetObjectIndex( form, ContactDetailFirstNameField ); // CH.3 Get the pointer to our field field = FrmGetObjectPtr( form, index ); // CH.3 Set the editable text FldSetTextHandle( field, htext ); // CH.2 Draw the form FrmDrawForm( form ); // CH.3 Set the focus to our field FrmSetFocus( form, index ); 6. 从contactDetailEventHandler()中删除frmCloseEvent; 7. 从PilotMain()删除MemHandleFree()的函数调用。 添加数据库 现在开始添加一个数据库。首先,利用资源构造器向Contact Detail窗体添加一些按钮用来浏览数据库记录;再添加一个帮助信息和新的警告;然后添加可创建和修改数据库的程序代码。 数据库技术和术语 数据库有很多种类型。也就存在与它们相关联的容易混淆的术语。我将在这一部分探讨一下基本术语并解释我们将要接触的其它术语。 数据库中最基本的单元叫记录,有的地方也叫“行”(raw)。一个记录通常有一些数据组成:例如,一个人的姓名、地址和电话号码等。每个数据可叫字段,也被称为“表元”或“列”。通常“列”是指在所有记录中提供相似信息的数据,例如:数据库中所有的“姓”。 你可以把数据库看成是由行和列组成的信息表。每一行代表一个单独的条目。每一行代表和所有条目相关联的一种特殊类型的数据集合。例如:你可以用数据库的每一行代表一个人。在这种情况下,列可能为所有人的姓和名。如果行代表约会,那么列可代表约会时间、约会时间等。 一般情况下,你一次只能查看数据库的一条记录。正在被你查看的行一般叫“游标”(cursor)。Palm OS称之为“索引”(index)。在数据库中一行行的移动被叫做“浏览”(navigation)。 数据库一般分为平面(flat-file)数据库和关系数据库两种。平面数据库是由一个单独的表组成。Palm OS就使用这种简单的数据库类型。关系数据库是由许多不同的表组成,并且它们之间可通过不同的方式相联系。现在绝大部分的数据库为关系数据库。在一些Palm OS程序中,你可以通过建立一些平面数据库,让它们像关系数据库一样的工作。 数据库可根据你提供的限制查询语句为你提供它的一个子集。这就像你问数据库一个问题然后数据库给你想要的答案一样。这种方式在数据库中叫做“查询”(query)。通过查询得到的行叫结果集(result set)或答案集(solution set)。 Palm OS中的数据库要比一般的平面数据库要灵活的多,因为它的记录就是内存中的存储块。你可根据自己的需要任意解释它们。结果,你就有了这样一个数据库:它里面的记录有着不同的格式和长度。 Contacts.rsrc文件内容的添加 现在我们就通过编制程序来看看数据库如何工作。首先,为Contact Detail窗体创建添加(add)和删除(delete)记录以及浏览(navigate)数据库的按钮: 1. 打开资源构造器; 2. 打开Contacts工程中src文件夹中的Contacts.rsrc文件; 3. 选中Contact Detail窗体双击打开; 4. 选中Window | Catalog生成Catalog窗口; 5. 拖动三个标签到Contact Detail窗体上。根据下表设置它们的属性: Left Origin Top Origin Text 20 15 First Name 21 30 Last Name 2 45 Phone Number 注意:我产生Left Origin Numbers的方法是:首先按下shift同时单击左键,然后从构造器的菜单上再选择Arrange | Align Right Edge。 6.拖动至少两个以上的输入框到Contact Detail窗体上。根据下表设置它们的属性: Object Identifier LeftOrigin TopOrigin Width MaxCharacters Auto Shift FirstName 80 15 79 15 Yes LastName 80 30 79 15 Yes PhoneNumer 80 45 79 15 Yes 7.拖动六个按钮到Contact Detail窗体上。根据下表设置它们的属性: ObjectIdentifier LeftOrigin TopOrigin Width MaxCharacters First 1 130 28 First Prev 45 130 28 Prev Next 88 130 28 Next Last 131 130 28 Last Delete 103 146 36 Delete New 53 146 36 New 注意:我利用了这样的小技巧使按钮水平对齐:按下shift,同时选中First 、Prev、Next和Last按钮,然后在菜单上选择Arrange | Spread Horizontally。 当你按上述步骤做完后,得到的Contact Detail窗体应如图5-1所示。 图5-1:添加了所有元素后的Contact Detail 窗体 创建警告信息 当你的Palm OS版本低于2.0(Pilot 1000,Pilot 5000)时,就需要添加警告信息。步骤如下: 1. 选中Alert资源,并按下CTRL-K创建一个新的警告信息; 2. 单击其名称,将其改为LowROMVersionError; 3. 双击打开新的警告信息; 4. 将属性Alert Type置为Error; 5. 将属性Title置为Fatal Error; 6. 将属性Message置为“The version of Palm device you have can’t run this software. Please upgrade you Palm device.”你的警告信息应如图5-2所示。 当新的数据库不能创建时,你也应添加一个警告信息来处理。步骤如下: 1. 选中Alert资源,并按下CTRL-K创建一个新的警告信息; 2. 单击其名称,将其改为DBCreationError; 3. 双击打开新的警告信息; 4. 将属性Alert Type置为Error; 5. 将属性Title置为Fatal Error; 6. 将属性Message置为“The Contacts database could not be created. Please free up some memory.”你的警告信息应如图5-2所示; 图5-2:”低版本的ROM错误”警告框 7. 关闭并保存Contacts.rsrc文件。 图5-3: “数据库创建时的错误”警告框 Contacts. c文件内容的添加 我将逐步的详细讲解Contacts.c文件增加的内容和改变的内容,在这一部分的最后将有程序的一个详细列表。 首先我们应对在PilotMain()中使用的ROM版本号进行定义。加入的新代码将保证Palm OS运行的更加稳定。 // CH.4 Constants for ROM revision #define ROM_VERSION_2 0x02003000 #define ROM_VERSION_MIN ROM_VERSION_2 注意: 有关ROM_VERSION_2使用的数字是从Palm OS SDK Reference(CodeWarrior Docoumentation文件夹里的Reference.pdf)中找到的,此信息在1012页。本书的碰到的所有问题几乎都可以拿这些文档来做参考。 在文件的头部定义了六个新的实用函数原型。NewRecord()函数为数据库产生一个新的记录;getRecord()函数连接函数FrmGetObjectIndex()和函数FrmGetObjectPtr();函数 setFields()将数据库的内容复制到上三个文本框中去。函数getFields()将窗体上文本框的内容填充到数据库中去。函数setText()设置文本框内容;函数文本框从文本框中获取文本并写入一被锁定的(locked)数据库中。我们将先浏览一下这些函数,然后把它们完全掌握。 // 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 ); 数据库中将使用五个新的变量。当数据库一旦打开,变量contactsDB允许我们可对其进行操作;变量numRecords定义了数据库中当前记录数;变量cursor代表Contact Detail窗体上显示的记录;变量isDirty定义当前记录是否已被修改,这就允许我们不需标识没有改变的记录而保持同步;变量hrecord表示当前记录的句柄。 // CH.5 Our open database reference static DmOpenRef contactsDB; static ULong numRecords; static UInt cursor; static Boolean isDirty; static VoidHand hrecord; 我们的数据库的记录长度是固定的(但是在Palm OS中并不一定非要这样)。为使记录能准确的保存,每条记录的开始点都应被仔细的定义。容易起见,大量的常量被定义: // 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) 如上所示,数据库中每一条记录的开始点和长度都已定义。顺便说一下,记录长度是为整条记录而定义的。 在以前的章节中我们在PilotMain()的头部定义了内存块,而现在我们创建并初始化了一个数据库。在编写正式代码前,首先让我们检查Palm装置的操作系统版本。 由于一些数据库函数特别是排序(sorting)函数自从Palm OS 1.0后有了改变,所以我们需要先加入一些代码以保证程序工作在Palm OS2.0或更高版本上。 // 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 ); 深入DmCreateDatabase()函数所带的两个参数具有其特殊的意义。它们把你的应用程序的数据库同其它应用程序区分开来,以防止你对它们进行意外的访问或修改。主ID号是第一个‘RPGU’,称为Creator ID。它表示创建数据库的一个或多个应用程序。第二个ID号你可以任意选龋我选取了‘ctct’代表‘Contacts’。请注意常量两边的“单引号”。这对于Mac程序员应该不会陌生,但对Windows编程人员来说不是很熟悉。Mac资源一般采用四个字符的常量,Palm OS继承了这一点。这四个字符将由编译器转替换成一个32比特的数字。为了保证你所用的Creator ID的唯一性,你必须到Palming Computing的网站http://www.palm.com/devzone/crid/cridsub.html去注册Creator ID。我已经为本书所用的例子注册了Creator ID‘PPGU’,你可以放心的使用它。 这个函数将调用刚创建的或已存在的数据库。由于已经保证了数据库的存在,所以程序定能正常运行。为在以后调用此数据库,我们把数据库保存在contactsDB中。 // CH.5 Get the number of records in the database numRecords = DmNumRecords( contactsDB ); // CH.5 Initialize the record number cursor = 0; // CH.5 If there are no records, create one if( numRecords == 0 ) newRecord(); 如果访问不存在的记录将导致Palm OS的崩溃,因此你必须清楚各条记录并保证在程序中访问的必须是合法的记录。你可以按上述代码去做。首先变量numRecords被初始化。它给出了你可以编址记录的范围。一般情况下,数据库的第一条记录号是零,所以我们初始cursor为零保证在即使数据库中只有一条记录时,也可以访问到一条合法记录。如果数据库中没有记录,我们将调用函数newRecord()创建一条记录。 // CH.5 Close all open forms FrmCloseAllForms(); // CH.5 Close the database DmCloseDatabase( contactsDB ); 在程序结束时,所有记录应释放内存并关闭数据库。为释放最后一条记录,我们再次调用Contact Detail 事件句柄函数中的frmCloseEvent。无论什么时候改变窗体,我们通常都要调用这个事件。为保证能关闭所有打开的窗体,在程序末尾调用FrmCloseAllForms()是个不错的办法。 调用此函数后,我们就可保证所有记录已被关闭。然后调用函数DmCloseDatabase()关闭数据库。 // CH.5 This function creates and initializes a new record static void newRecord( void ) { VoidPtr precord; // CH.5 Pointer to the record // CH.5 Create the database record and get a handle to it hrecord = DmNewRecord( contactsDB, &cursor, DB_RECORD_SIZE ); // 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.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; } 函数newRecord() 现在我们结束事件句柄的讨论,接着我们开始分析刚添加的实用函数。函数newRecord()将在由变量cursor定义的数据库的当前索引位置添加一条新的记录。 函数开始时调用了DmNewRecord()创建一条新的记录。但是这个记录里只是些垃圾信息。所以我们必须初始化记录,用一空白记录来代替它。向第二章定位数据块一样,我们先锁住记录,然后添加初始化信息。在此例中,我们初始化记录信息全部为零。请注意,在写零时,我们使用了函数DmSet(),而不是直接写零或调用StrCopy()。这是因为数据库所在的存储块被特殊保护免于讹误,而只能利用函数DmSet()才能将其内容改变。如果想写入其它的非零字符需调用函数DmWrite()。 对于DmNewRecord()的返回值应检查其是否为零,虽然在上例我们没有检查。如果返回值为零则表明记录已溢出内存。我将在第七章修正此疏忽,在那一章将全面介绍致命错误解决策略。 和前面的frmCloseEvent一样,我们调用DmReleaseRecord()来释放DmGetRecord()和DmQueryRecord()结果记录占用的内存。因为增添了一条新记录,变量numRecords加一。IsDirty位置“真”是为了在下面的attachFields()函数中可以立即访问数据。 函数getObject() 函数getObject()可通过关联FrmGetObjectIndex()和FrmGetObjectPtr()为程序节省大量的时间。由于我们经常用到字段指针及其它控件,此函数可使我们现在和将来的代码更加简洁,因此更具可读性。 // 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 ) ); } 字段和数据库记录 大多数的计算机系统都有一个RAM和硬盘。RAM用来保存临时数据,硬盘用来永久的保存数据。由于RAM的读写速度要比硬盘快的多,因此程序员做的很多工作是从硬盘上读取数据或写入新的及修改硬盘上的数据。 因为Palm中的数据都是在内存中,所以访问速度很快,把访问时间缩减到了最校从理论上讲,在Palm内存中的任何数据被读取的速度时一样的,因此完全没有必要像其它计算机一样,把数据再移动到其它特别的可更快访问的地方。 在PC上,我们访问数据库时,首先在RAM中为一个新记录分配内存,然后再将其写入到硬盘的数据库中。而在Palm中此过程将变的更快,因为在Palm中的永久性存储器为记录分配内存,然后可直接的向存储器中写入。Palm OS通过调用函数FldSetText()使过程可视化,即可使你在一个编辑区域直接和一个数据库记录联系。这个区域就叫做编辑域(Edit in place)。不幸的是,最近的Palm OS版本中,编辑域有两个主要的缺陷。一个是每一次对于每一条记录只能访问它的一个字段。如果你访问的多于一个的记录,程序可能运行一段时间,但是最终程序将被锁定。第二个是在编辑域编辑字段后面的的字段有时会变成零字段。这在代码中很难跟踪知道这是怎么回事,所以当你编辑数据库记录时,记住把要编辑的字段放到记录的最后。 由于编辑域的这种缺陷,我们不能不能将其应用到Contacts程序中实现必要的功能。然而也要记住它(特别是它的缺陷),因为当你编辑一个大的单编辑字段的数据库如内嵌程序Memo Pad时,或许会用到它。 函数setFields() 在函数setFields()中,数据库中的记录被拷贝到Contacts Detail窗体的三个编辑框中。 // 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 ); precord = MemHandleLock( hrecord ); // 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; } 首先,我们先激活窗体和记录。对于每一个编辑框的内容,都调用了函数setText()将数据库中的记录的相应部分拷到里面。 小技巧: 如何获得一个记录有两种办法可以获取一条已存在的记录。一个是函数DmGetRecord()另一个是函数DmQueryRecord()。对于DmGetRecord()来说,有一点要注意,就是当对记录操作完成后,必须要调用函数DmReleaseRecord()释放记录。如果你忘记了这一点,那么当下一次获取这一记录的句柄时,你得到将是0。 新记录我们可以通过设置“脏”位来识别。当出现新记录这种特殊情况时,我们把游标设置在First Name 这个字段上。这里的设置焦点和第三章讲到的一样。为设置upper shift on须调用函数setGrfState()。这是因为当将这一字段设置焦点后,即使我们设置了Auto Shift属性后,系统也不会设置upper shift on。在调用函数SetGrfState()前,在头文件Pilot.h下面写入它的头文件Graffti.h。 // CH.5 Added for the call to GrfSetState() #include 函数getFields() 函数getFields()将在编辑框中的字段写入到数据库当前记录中。 // CH.5 Wipes out field references to the record and releases it // static void detachFields( 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.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.5 Unlock the record MemHandleUnlock( hrecord ); } // CH.5 Reset the dirty bit isDirty = false; // CH.5 We're done return; } 首先把焦点移动到所指记录上。这意味着在此之前应指定一记录并设置此记录为“脏”。 第二步,从窗体上收集数据并输入到要修改的记录中。 最后,我们必须清除“脏”位。不然的话,第一个记录被修改后,所有的记录都将被置“脏”。只有当用户指定修改记录或调用函数newRecord()后,“脏”位才被重新设置。 函数setText() 和第三章中将字符串资源传递到字段中一样,函数setFields()将一个字符串和字段指针传递到指定的记录中。 // 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 MemHandleResize( hfield, StrLen( text ) + 1 ); } else // CH.5 Allocate a handle for the string hfield = MemHandleNew( StrLen( text ) + 1 ); // 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; } 首先我们获取字段的句柄。如果字段有一个句柄,那么我们调整它以适合字符串。如果字段还没有句柄,我们以合适的大小为其分配一个。不对调整和分配句柄判断是否成功是十分危险的。像处理newRecord()那样,我们将在第七章介绍错误处理时补充这些代码。 当获得正确的句柄后,下面所做的你就很熟悉了。锁定句柄、拷贝字符串、解锁字符串然后写入字段。最后我们绘制(draw)编辑框以反映其变化。 函数getText() getText()将字段的内容拷贝到数据库的记录中。 // 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; } 首先我们获取字段字符串的指针。因为这是字段内部的指针,所以我们没必要解锁它!我们调用函数DmWrite()依据给出的偏移量将字段的字符串拷贝到相应的数据库记录中。由于我们限制了能写入字段的字符的个数并且规定了相应记录的大小,所以可保证此拷贝是有效的。这也是为什么没有添加错误检查的原因。 函数contactDetailHandleEvent()内容的添加 在第一个case语句,即frmOpenEvent语句里,在函数FrmDrawForm()后调用函数setFields(),用来拷贝当前的记录到数据库的各字段中。放在FrmDrawForm()后的原因是在绘制完窗体的其它部分前,不能绘制编辑框。否则会造成程序运行错误。 // CH.4 Form open event case frmOpenEvent: { // CH.2 Draw the form FrmDrawForm( form ); // CH.5 Draw the database fields setFields(); } break; 另外,有一些新的事件需要被处理。首先是frmCloseEvent。 // CH.5 Form close event case frmCloseEvent: { // CH.5 Store away any modified fields getFields(); } break; 这个case语句的作用是,在关闭窗体前,应获得最后一次被修改的字段内容。 下一步我们象以前处理按钮事件ctlSelectEvent一样处理其它的按钮事件。这一次我们应该知道哪一个按钮被按下了。为此,我们利用switch...case 语句,以controlID作为标识符进行判断。 // 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; 我们处理的第一个按钮即First按钮,这个按钮的作用是将定位到数据库的第一个记录上。在处理所有按钮事件以前,先调用函数getFields()获取当前记录以保证在移动新记录前能够保存所有的变化。在这个按钮处理事件中,我们把游标移动到第一个记录上。在处理完所有按钮事件以后,我们调用setFields()来获得新的当前记录并拷贝到各字段中。 下一个按钮事件为Previous按钮,它将记录移动到前一个。 // CH.5 Previous button case ContactDetailPrevButton: { // CH.5 Move the cursor back one record if( cursor > 0 ) cursor--; } break; 在检查是否到达第一个记录后(到达第一个记录后,不能在向前移),我们将记录前移一个并将新记录和各字段相关联。 下面处理Next按钮。 // CH.5 Next button case ContactDetailNextButton: { // CH.5 Move the cursor up one record if( cursor < (numRecords - 1) ) cursor++; } break; 这个代码块和处理向前的代码块十分相似。在判断完是否到达最后一个记录后,将记录后移一个。 为什么我们对变量cursor的处理如此的小心呢?这是因为如果我们调用函数DmQueryRecord()时访问了一个不存在的记录,程序会立即崩溃。因此,必须保证cursor必须是一个有效的值。 现在我们看一下按钮Last的处理,此按钮将移动记录到最后一个。 // CH.5 Last button case ContactDetailLastButton: { // CH.5 Move the cursor to the last record if( cursor < (numRecords - 1) ) cursor = numRecords - 1; } break; 这段代码和Previous按钮事件处理代码有些相似。注意因为第一个记录编号为0所以最后一个记录应为总记录数减一。 下面是Delete按钮的处理: // 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; 为保证函数setFields()中的DmQueryRecord()函数的成功调用,必须确保至少剩余一条记录。因此当检查到已经没有记录时,调用newRecord()创建一条新记录。 做一个一条记录也没有的数据库应用程序也是可能的,但须保证不能调用函数DmQueryRecord()。 最后,我们处理New按钮。 // CH.5 New button case ContactDetailNewButton: { // CH.5 Create a new record newRecord(); } break; } // CH.5 Sync the current record to the fields setFields(); } break; 以上代码也和其它浏览按钮代码差不多。只不过它没有将游标移动到一个已存在的记录上,而是游标指到一个我们刚刚创建的记录之上。结果该记录被作为当前记录并且其后的记录编号自动的加一。 在处理完所有按钮事件后,调用setFields()。它的作用是获取并拷贝当前记录(或许是条新记录,或许是并没有修改的记录)到各字段中。 另外,还有一个事件需要处理。那就是如果记录被修改后,其“脏”位应被标识。其中有一种方法就是判断编辑域是否被调用来识别记录是否被修改。 // CH.5 Respond to field tap case fldEnterEvent: isDirty = true; break; 这里我们通过判断编辑域是否被选中来表示“脏”位。 调试 一旦你做完了上面所有对Contacts.c程序的修改后,你就可以开始使用Debug工具来调试程序了。 1.打开Code Warrior IDE 集成开发环境。 2.打开Contacts目录下的Contacts.mcp工程文件 3.选择 Project | Make 菜单项。如果你的程序不能成功编译连接的话,就回过头检查一下最后修改的代码。如果你的问题是出在某些资源的命名上,记得去检查Contacts_res.h文件,以确保.h文件和.c文件中的资源命名相匹配。 4.使Palm与PC同步,这样做主要是为了保护Palm中的其它数据即便使你被迫重启设备也不会丢失数据。 5.退出HotSync同步软件。 6.一旦你的代码通过了编译和连接,你就可以选点 Project | Debug 来打开调试器了。 你可以通过不断按下调试器窗口顶部的单步调试按钮(样子就像一个向右的箭头下面有个大括号),来实现对程序的逐行运行调试。当你第一次运行程序的时候,ContactsDB数据库将被建立,此时DmCreateDatabase()函数将返回0。而等到后来再次运行时,DmCreateDatabase()函数的返回值将是537,那表示,数据库已经存在(dmErrAlreadyExists)。 在每一个按钮处理事件的进入case语句的第一行设置断点,这样做后,有断点的语句的左边将出现一个小红点。点击调试器顶部向右箭头的按钮,使程序自由的运行。 Contact Detail 窗体将出现,在窗体上的文本框中输入一些数据,然后点击“New”按钮来添加一条新的记录。我想你有必要在这里加断点观察一下程序的执行情况。 为了进入函数newRecord()观察,我们可以先在函数语句的前一条语句加断点,当程序运行到这个断点时,点击调试器顶部的“jump into”按钮(样子像一个向下的箭头,外面有一个打括号)进入函数体内部运行。然后再连续按单步调试按钮逐行的运行函数中的语句,以确保函数运行是正常的。函数内运行结束后再按下调试器顶部的”run”按钮。 运行程序,为你的数据库创建最少4条记录。然后你就可以开始测试你的“navigation”按钮了。调试步骤与上面介绍的相同,当你调试完相应函数,确保他们正常工作后,你可以在有断点的语句左边的小红点上点击,以取消那里的断点设置,这样你的程序就不会在已经调试过的代码上停住了。 当你确保“First”、“Previous”、“Next”和“Last”等按钮都正常运行后,就可以着手调试“Delete”按钮。单步跟踪”Delete”按钮的代码,删除所有的记录,最后再创建一条记录,以确保数据库中至少存在一条记录。 下一步是什么? 在第六章我们将继续完善Contacts 应用程序。我们将为Contacts Detail窗体添加时间和日期输入框,将使用到更多的控件。 源程序清单 下面是经过本章修改的Contacts.c的源代码。本章的源代码在随书所带的光盘的CH.5目录中有一份完整的拷贝。 // CH.2 The super-include for the Palm OS #include // CH.5 Added for the call to GrfSetState() #include // 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 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 ); // 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.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.5 If there are no records, create one if( numRecords == 0 ) newRecord(); // CH.4 Go to our starting page FrmGotoForm( ContactDetailForm ); // 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; } FrmSetActiveForm( form ); } // CH.2 Handle form events FrmDispatchEvent( &event ); // CH.2 If it's a stop event, exit } while( event.eType != appStopEvent ); // 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 // 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.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.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.5 Create the database record and get a handle to it hrecord = DmNewRecord( contactsDB, &cursor, DB_RECORD_SIZE ); // 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.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 ); precord = MemHandleLock( hrecord ); // 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 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.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.5 Unlock the record MemHandleUnlock( hrecord ); } // 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 MemHandleResize( hfield, StrLen( text ) + 1 ); } else // CH.5 Allocate a handle for the string hfield = MemHandleNew( StrLen( text ) + 1 ); // 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; } |
本文地址:http://www.45fan.com/dnjc/72070.html