PalmOS开发教程

Palm OS开发教程

http://www.5ivb.net/Info/Info39To1/
http://bingle.pku.edu.cn

译者序 1
第一章 关于Palm 1
第二章 建立一个应用程序 8
第三章 文本框中的乐趣 20
第四章 窗体的编程 37
第五章 数据库 54
第六章 控件 84
第七章 列表框和排序 123
第八章 表和滚动条 168
第九章 分类和查找 216
第十章 创建高效的有效界面 264
第十一章 工具和方法简介 271
第十二章 专业编程技巧 278


译者序
后PC时代是嵌入式和网络化的时代,包括掌上电脑在内的PDA产品是后PC时代最重要的产品之一。在PDA市场激烈竞争的今天,Palm这个PDA界的龙头老大,将被国内越来越多的人所接受,有关Palm OS 的研究、开发和应用工作也将随之推广。Palm OS programming from the ground up作为一本Palm开发的入门书籍较为全面的向读者介绍了使用Code Warrior开发工具开发一个完整Palm OS应用程序的全过程。

本书面向各个层次的C程序员读者和PDA爱好者。对于初学者来说,这是一本很好的入门教程,对于有经验的程序员来说,这是专业的参考书,对于PDA爱好者来说,这又是一本方便的手册。通过本书的学习,不仅可以快速学会开发基本的Palm OS应用程序,还可以学到调试程序、使用数据库,优化用户界面等高级PDA开发技术。

需要说明的是由于书中出现的一些新的专业词汇,国内尚无统一标准译法,只好按实际含义译出。由于译者水平有限,错误和不妥之处敬请广大读者指正。

本书由傅曦、张昌利和瞿芹翻译,书中所有例子在Palm IIIc 和 POSE 模拟器上调试通过。如有疑问可联系:fxqq@sina.com
衷心期望通过本书的学习你能成为Palm OS高级开发人员中的一员。
第一章 关于Palm
本章将对Palm这个新奇的东西进行一些简单的介绍。对于有一定的嵌入式软件开发基础,又想尽快开始编程的读者,我建议你跳过本章,直接从第二章开始阅读,等闲暇时间再回到本章浏览。

书中的一些章节提到了如何为头带式、腕带式设备编写Palm OS应用程序。而在第一章中讲述的更多的则是有关Palm产品的一些背景知识:它从何而来?为什么别的手持设备纷纷落马时,它却取得了巨大的成功?不同版本操作系统之间有什么细微差别?以及对流行的P alm硬件及其特性的一些讨论。作为程序员,是否掌握这方面的知识也许并不是很重要,但作为一个专业的Palm开发者,这些又都是非常有参考价值的。

除了直接的应用程序开发外,还有三个章节涉及到其它方面知识。如第十章讲的是应用程序的设计问题,这对于在Palm程序开发的各个阶段改善程序用户界面有着极大的帮助。第十一章是一些为开发者提供的硬件和软件资料,告诉你如何与资源提供者联系。第十二章是有关如何组织和存档你的P alm OS 的 C代码,使之更具可读性和重用性。

Palm Computing的成功
出于各种原因,90年代初期的笔式计算装置(pen computing device)还没有很好的发展就趋向了没落,正当人们想最后放弃由笔式计算装置带来的巨大商业效益时,Palm Computing推出了Pilot, 开创了一个新的历史。Pilot的市场增长速度比以往任何计算装置都快,甚至超过了从前电视和录像机的增长速度。那么,在以前的手持设备纷纷被淘汰出局的情况下,P alm Computing为何却能异军突起,创造一个又一个辉煌呢?

有丰富想象力的你,一定会对此做出各种推测吧。但我的观点是这样的:Palm Computing的成功很大程度上依赖于Jeff Hawkins(Pilot和Palm OS的发明者)所说的“用户经验”。见到Jeff Hawkins时, 我不由得联想起了过去时代的北方匠人(Yankee craftsman)。他是一个善于思考,精确缜密,重视设计简单化的人。他能设身处地的为用户着想,他能把自己当作一个新用户来把玩产品,这实在是难能可贵的一项技能。我曾听过一些传闻说在他开发G raffiti那段时期,当开会时,他老是拿东西在纸片上戳来戳去。人们认为他有些古怪,可实际上他是在想象着以一个真实用户的体验来测试G raffiti:“我能否用这玩意在会议上作纪录?”
另一个关于他的故事,是传说他有一段时间总是随身带着块木头,在上面按着些并不存在的按钮,对着它说话等等。人们又一次认为他有点疯了,而那一段木头在他眼里是P ilot。

我认为 Palm的成功就在于Jeff Hawkins完全根据“用户经验”来设计它。他在不影响用户使用的情况下尽可能使界面简单,从而让任何人都能快速而有效地学会使用,同时他也努力降低它的价格。

Jeff 过去和现在在Handspring做的第二件大事就是继续从开发者的角度从事开发。我曾在很多平台上开发过,我能肯定地说Palm OS是个人计算机发明以来最有趣并且最容易在上面从事开发的操作系统。这种容易和有趣也来自于设计,并带来无数免费和富有创新的Pa lm应用软件。

Palm Computing的历史
Palm的概念并非一夜而成。它的成就和成功道路不仅对Palm开发者,而且对从事任何新技术设计的人员有启发意义。
Jeff Hawkins和Donna Dubinsky创建了Palm Computing。Jeff过去在Grid工作。Grid以那从二楼摔下而不坏的laptop而出名。也许大家还记得,那时大多数PC是非常容易坏的。如果你的P C稍被碰撞,它上面的数据就会被损坏,因为在当时硬盘是很娇气的东西。因此我们对Grid的印象非常深刻。

Palm Computing从一个软件公司开始。最初他们开发Graffiti, 一种在所有的Palm OS设备上都找得到的速记方式的数据输入器。这种输入器被作为Newton和其它一些早期掌上机的附加产品出售。

除了Graffiti自身的成功之外,整个pen computing市场一度也很兴旺。Palm Computing在Casio Zoomer的开发上花了很大力气,但这个很有前景的设备不久也走了其它无数掌上机的老路。

但Jeff认定拯救pen computing市场的唯一出路是设计对路的硬件。为了成功的做到这一点,需要两个前提条件:Palm开发者必须设计出对路的硬件,同时还必须有人能提供P alm持续生产和销售设备所需的资金。

为了解决这些问题,Jeff带着他的木头四处奔走。早期的Palm OS的外观和感觉在HyperCard的基础上被开发出来了。一些重要的设计也先后产生了。更令人侧目的是,在基于输入笔的计算机市场剧烈崩溃的那段时间里,J eff却说服投资者投资了第一个Pilot。

在1996年3月,Pilot 1000开始在零售店出售。由在Pilot开发过程中与Palm computing熟悉起来的美国Robotics公司出资,无比精简又在基本功能上几乎没有缺漏的第一个模型问世了,当时售价是299美元。

在1997年,美国Robotic公司被3Com公司买下,于是Palm computing并入了3Com公司并搬到了3Com的大本营Santa Clara。在1998年春天,非常成功的Palm III登台亮相。在1999下半年,Palm computing又重新独立出来,又称为Palm computing 公司 。

Palm computing公司的整体结构
Palm computing已不再是3Com的一部分了。作为一个开发者你有必要知道它的整体结构。Palm computing由几个机构组成,其中有两个机构开发者是有必要知道,它们在历史上曾被叫做平台组和硬件组。

平台组,给你带来了Palm OS系统。他们要干的事就是开发Palm OS并颁发许可证。他们不关心硬件组在做什么。如果你想知道有关Palm OS的软件或工具的信息,就去问平台组吧。

硬件组,也是发放Palm OS的许可证的机构,但他们发放的是硬件许可证,他们直接与其它硬件许可证的领取者竞争。如果你对Palm computing公司特定的硬 件有疑问,就去问硬件组。

Palm 操作系统
在这部分里,将简单介绍Palm OS 的各个版本以及他们在使用过程中各自支持哪些特性。

版本1.0
1996年,支持版本1.0的Pilot 1000面市,接下来的是Pilot 5000。1.0版本后的其它版本在大部分功能上没有大的改变,这说明版本1.0在其最初设计上是很成功的。这一点就很了不起。

版本2.0
1997年二月,支持版本2.0的个人Palm Pilot和专业Palm Pilot面市。不久IBM的OEM产品也随即面市。专业Palm Pilot有一个TCP/IP内嵌堆栈,如果能正确的连接,它就能使Palm Pilot在TCP/IP网络上通信。
版本2.0在版本1.0基础上的改进十分保守,主要改进如下:
l 增加了TCP/IP支持
2 增加了滚动条
3 更容易对数据库进行分类和查找
4 更容易对文本框(fields)操作
5 更容易对类(categories)操作
6 更容易对事件操作
7 增加了许多标准字段操作功能

版本3.0
1998年3月,作为加盟3COM后的第一个Palm产品,支持版本3.0的Palm III 问世。推出此版本的主要动机是为了扩展Palm的是红外线发送(Infrared beaming)功能,由此添加了一组有关红外线的指令集。Symbol SPT1500 的3.0.2和版本3.0功能基本相同,其主要改进如下:
l 增加了红外线接口(Infrared beaming)及其完全帮助文件
2 灰度增加到四种:黑色、深灰、淡灰和白色
3 可调用的动态内存堆(dynamic memory heap)(用于程序运行)增加到了96K
4 增加了更先进的声音功能
5 更容易产生和使用自定义字体
6 为那些喜欢使用文件系统的人增加了文件系统
7 更容易的产生动态的窗体和控件
8 增加了进度条显示对话框(progress dialogs)
9 每个装置都有其唯一的序列号,只有在3.x装置中才支持此项功能。
l0 应用程序加载器(launcher)日渐成熟

版本3.1
1999年3月,支持版本3.1的Palm IIIx和Palm V问世。不久IBM把这些产品冠以自己的商标,但仍由Palm Computing生产。版本3.1有以下改进:
l1 对应用于0x88上的ASCII有一些微小的改变。
l2 支持Dragon Ball EZ处理器

版本3.2
1999年5月,支持版本3.2的Palm VII问世。最初,Palm VII只在纽约出售,在年末被逐渐推广。Symbol公司的最新元件也是基于版本3.2的。
版本3.2主要是支持无线和网络功能。同时也增加了改进的串行通信软件。你应该检查配有3.2的产品是否具有这些功能,因为这些功能不一定是有效的,例如不支持无线接入。

版本3.3
1999年秋,支持版本3.3的一种新的Palm OS面市,其中包括Visor和TRG Pro。3.3版本的Palm OS对操作系统内部做了很大的删减,以便于更好的为第三方所使用。

版本3.5
2000年2月,支持版本3.5的第一个彩色Palm 问世。版本3.5的最大特点就是支持彩色。其主要改进如下:
l 支持256色
2 增加了图形按钮,可用位图显示其被选中或是没有
3 增加了滑动控件
4 增加了动态菜单
5 更容易编制配件(Gadgets)和用户自定义控件
产品
表1-1列出了本书提到的各种Palm OS产品。

表1-1略
Pilot 1000和Pilot 5000
这是基于版本1.0的早期产品,不带背光(backlight)

个人Palm Pilot和专业Palm Pilot
这两个产品在物理外观上很像5000。它们支持2.0版本的Palm OS并带背光(backlight)

Palm III
Palm III是第一个使用锥形塑料外壳,第一个带红外端口的Palm 。3.0版本的Palm OS最初是为Palm III设计的。

Palm IIIx
Palm IIIx除有更好的显示屏和更多的内存(由2M增加到4M)以外,和Palm III基本一样。

Palm V
Palm V在外观上有了彻底的改变。它具有圆角结构,并可再充电。它的另一个特点是能够调节对比度。按Palm V边上的按钮,你就可以通过拉动显示屏上的滑动条来调节。
2000年2月,支持版本3.5 的新式Palm V将面市。

Palm VII
Palm VII是第一个采用无线集成的产品。在外观上,它很像Palm IIIx――有一个笨重的顶端。不同的是,它有一个天线,当拉出天线并打开Palm VII时,屏幕上将显示Palm.Net应用程序目录。它有2M内存。

Palm Vx
最初,它支持版本3.3。2000年2月,它将支持版本3.5。

Palm VIIxe
Palm VIIxe相对于Palm VII来讲,也是把内存扩大到8M。支持版本3.5。

Palm IIIc
Palm IIIc是第一例支持彩色的产品。它具有256色。除顶端比Palm IIIx稍厚些外,其外观和Palm IIIx基本相同。Palm IIIc和标准Palm III基本兼容,但它不能使用Palm III的支架来充电。而Palm III却能完全的使用Palm IIIc的支架来同步和充电。因彩色需更大空间,Palm IIIc有8M 空间。

IBM产品
IBM的产品和它的竞争对手在功能上很相似。如:早期的IBM WorkPad很像专业Palm Pilot。较新的WorkPad和Palm IIIx十分相似,和Palm V基本相同。
还没有确切消息说WorkPad C3将支持OS 3.5。但由于C3和Palm V的 ROM都不能再升级,把OS 3.5加载到C3是很有可能的。

Symbol产品
Symbol是Palm Computing外的第一家获Palm OS许可的企业。由于它支持条形码识别器使之感觉很有趣。SPT1500在外观上很像Palm VII。在显示屏上有一定的空间用来放置条形码识别器。SPT1700和SPT1740十分经久耐用。并且SPT1740带有无线网络接口。

SPT1700和SPT1740都比标准的Palm产品体积上大的多。

Handspring Visor
这是Jeff Hawkins和Donna Dubinsky自己刚创办的公司,已获得Palm OS许可证。在外观上,Visor很像专业Palm Pilot。它的显示屏和早期的差不多,不能在强烈的阳光下阅读。与标准的Palm OS相比,Visor修改了Datebook应用程序。Visor的主要优点是增加了一个专用扩展槽――Springboard槽。现在已经有很多扩展卡适用于此槽,这在很多方面增强了V isor的功能。

TRG pro
TRG pro在外观上和Palm III十分相似。不同之处是它有一个用来插Compact Flash卡的扩展槽。Compact Flash卡是数字相机及其它一些小的电子设备的标准卡,所以TRG pro的功能在广度和多样性上得到了扩展。

Palm OS的开发软件
开发Palm OS可以有很多方法。在这一部分将逐一介绍。具体关于各个不同工具和有用的服务方式将在第十一章探讨。这一部分将做简单的论述。

语言的选择
Palm OS是由C API开发的,所以最有效和灵活的工具就应该是C了。C++用于开发Palm应用软件还存在一个普遍问题就是C++需要一块不被重新分配的内存。由于P alm的内存使用受到动态堆(dynamic heap)的很大限制,而C++应用软件又必需足够的内存才更有效的工作,所以使用C++开发Palm应用程序是不适宜的。

使用680x0汇编语言是开发Palm应用程序的另一个选择。即便是你花很长的时间也不一定能够打造出一个较好的编译器,尤其像是运行Mot orola 68K系列之上的GCC编译器。当你十分渴望程序运行的飞快的时候,我建议你先用C编好代码,然后用一个好的性能评测工具(profiling tool)来找出程序中出效率最低的函数。再用汇编重写这个函数的代码,直至达到要求的性能指标。

一些工具例如Puma Satellite Forms可使你快速编写出复杂的应用程序。但它不如C灵活和性能好。在历史上,用这种方法开发程序的不足之处是程序许可受到限制。
另外,还有一种截然不同的方法,就是从网络上下载有关Palm VII的应用程序。Palm Computing会免费的提供各种工具使编程更加容易。

Palm设备的硬件
我们将更深入的探讨Palm OS硬件的一些细节问题。在编写程序时你或许并不一定用到,但是,知道了这些会让你更有效的使用Palm。

RAM和ROM
理论上讲,Palm OS能支持有很大的内存。今天Palm OS的最大内存为8M。TRGpro设备的Compact Flash卡可支持40M的扩展闪存。Palm内存具有快速且非易失的特点。它的非易失性是因为当Palm关闭后也有微小电流支持。这就是为什么不论你的P alm使用与否电池最后还是被消耗完的原因。这一点使得你不得不经常的更换电池。

Palm OS定义了一小块内存区域作为动态内存堆(dynamic heap)使用。程序中的栈(stack)、堆(heap)及其它动态部分都会用到这部分内存。在表1-1中列出了不同设备的动态堆的大小。对于支持T CP/IP的设备,TCP/IP将占用32K的动态堆栈。

Compact Flash卡或其它扩展卡上的闪存(flash memory),要比一般的内存(memory)慢的多。 到现在为止,只有Visor和TRGpro支持这种闪存。记住,闪存一定是在卡1而不是卡0上。在Palm OS的函数调用上你必须知道所用的是哪一号卡。

大多数的Palm OS使用flash ROM。这就意味着它们的操作系统在理论上是可以升级到较新版本的。而在Palm III、Palm IIIe、Visor和pdQ前的Palm不能升级它们的ROM。

Motorola 68328 “Dragon Ball”
Motorola 68328 “Dragon Ball”以及后来的68EZ328“Dragon Ball EZ”是Palm设备的处理器。和早期的Macintosh和Amiga计算机一样,它们是由68K处理器组成。但除了一般的基本元件外,它们还有定时器、并行口、可发出声音的脉宽调制电路(p ulse width modulation circuit)和LCD控制器等等。你完全不必要去直接的接触这些元件,否则,你就会陷入想知道操作系统到底怎样工作的困境,甚至会使你写的代码和P alm OS新版本或新的Palm硬件不相兼容的。

状态

Palm有三种操作状态:休眠状态、半休眠状态和工作状态。
当Palm处于休眠状态时,看起来就像被关闭了,如表现为黑屏等现象。实际上,此时的Palm确实和关闭差不多,它只使用很少的电量。一般情况下,可以按下绿色按钮或等着定时器将其转换为这种状态。

当Palm已经上电但没有事件发生时(如没有按钮事件或用笔触发事件),Palm就处在半休眠状态。你可以编写程序使Palm不能处于此状态,例如你可以不停调用E vtGetEvent()使之没有休息时间。但这样会用去你更多的电量。有时你必须编制这样的程序等待不确定的事件,但为节省电源,应尽量少用这种方法。

接口
所有的Palm 设备在底部都有一个串行口。当插入同步电缆后可使Palm与电缆另一端的设备同步。你也可插入Modem或其它硬件。

另外,新式的Palm都有一个红外线接口。大部分设备的红外线接口都在顶部。但是有一些Palm(如Visor)的红外线接口在机身的一侧。接口支持I rDA标准,因此在你进行适当的编程后,就可以同打印机、电视机、录像机或其它东西进行红外线通信。

Palm VII有一个连接Internet的无线接口,可以连接适当的无线设备。我极力推荐你使用Internet库和Socket服务。这样,你的代码就能和将来的无线方案及现在的诸如N ovatel的无线硬件设备兼容。

Symbol设备有一个集成的条码扫描仪。这对你开发使用条形码的商务应用程序很
有用。Symbol有一个免费并且组织得很好的开发工具使你在需要时能容易地使用扫描仪。虽然我听说欧洲的一个超市连锁店在使用Symbol 设备使售货更简单, 但该设备到零售阶段还有一定距离。如今条形码到处都是。谁知道Symbol的前景如何呢?

SPT1740,一种Symbol设备,连接到了Symbol的无线局域网中。依靠这种连接,

你能通过socket服务让Palm设备上网。我想将来你会看到更多的使用Palm设备的解决方案。

Visor和TRGpro拥有可连接任何设备的扩展槽。Visor扩展槽是一个名叫Springboard的特殊设计。它是一个设计得很好的扩展槽,可惜目前还未形成标准。TRGpro是一个Compact Flash扩展槽。这个体系结构也存在一些局限之处,大多数使用Compact Flash扩展槽的其它设备都需要较大内存。Handspring和TRGpro都为它们的扩展槽提供免费的开发工具。

时钟和定时器
在Dragon Ball的内部有一个实时的时钟,它在设备为睡眠模式时仍能维持当前时钟。有一种时钟能每百分之一秒跳一次。它被操作系统用来完成询问屏幕,看时钟是否在走或记下输入笔的轨迹等各种任务。当你调用E vtGetEvent()时,系统需要等待一段时间之后再把控制交还给你。系统完成等待任务所用的时钟就是这个时钟。

屏幕
Palm设备的屏幕是160x160象素。所有的设备都能显示黑白两色(或用某些阴影对来表示黑色和白色)。带有Palm OS 3.0或更高版本的所有设备都能运行4种灰色的阴影。带有基于热度等等的对比度变化之后,除了图像和游戏之外,中间的两种过渡色变得难以区分。

Palm IIIc带有一个支持256色、具有革命性突破的火星矩阵TFT屏幕。这些颜色明亮而细腻。

声音
Palm设备使用置于Dragon Ball之内的脉宽调制(PWM)电路来发出声音。PWM电路本质上是一个相当快速的开关。使用这种电路非常快地开、关扬声器的电源能从扬声器中发出某种模糊的声波。

这种扬声器本身非常小,高音符的声音比低音符的声音大。从一个Palm设备得不到太多的低音效果。

置位
Palm设备上有三种置位:软置位、中置位、硬置位。
你可以按下位于设备背后的小孔内的小按钮实现软置位。如果你使用Palm III之类设备随机附带的输入笔,只要你旋下输入笔顶端的塑料部分,你将发现其上有一个小塑料棒正好可以插入置位孔。当然使用回形针也是可以的。软置位将清除动态堆。在调试本书的示例中我们将大量用到软置位。
同时按下Reset和上箭头按钮将使Palm中置位。在Palm文档中这类置位被称为“轮班置位”(shift reset)。如果应用程序对软置位的应答崩溃了,设备无法通过软置位真正实现置位。这时就使用中置位试试。由于中置位不通知应用程序置位正在进行,所以当一个应用程序占有了P alm操作系统并且在置位时崩溃了,你就可以提交一个中置位来删除这个讨厌的应用程序。

通过按下reset 按钮并保持按下状态,同时按下 on/off按钮并保持按下状态,然后放开reset按钮,再放开on/off按钮,这几步来实现一个硬置位操作。硬置位通常在产品被第一次使用的时候做,这样可以清除所有的客户数据。作为开发者,你也许会被问及“置位操作是否可以删除所有的数据”这样的问题。如果你的回答是:不能。那么你的所指的一定是一个软置位。如果你的回答是:可以。那么你的P alms设备一定是完全的不正常,需要做一次彻底清除数据的硬置位了。

如果你的Palm设备在硬置位后依然不正常的话,那么就去掉所有的电池,让Palm保持至少5分钟的掉电状态,然后再把电池放回去。这样做的效果同一次硬置位的操作一样。顺便说一句,掉电对P alm是无害的。
第二章 建立一个应用程序
无论你是一位编程专家还是初学者,通过本章的学习,你都将很快的学会Palm OS的编程。在这个过程中你首先要创建一个能稳定工作的开发环境。值得一提的是,开发环境并不仅仅是一些编程工具的简单组合,它更是一个能协助开发者更好完成工作的系统结构。一旦源代码能够在开发环境中正常的编译并运行,这就意味着你迈出了整个程序编写过程中最重要的一步。在程序以后的修改过程中,就是一些其它的尝试和寻找B UG的工作了,当然完成这些工作也都是要在开发环境中反复调试的。

在简单复习一下Palm OS应用程序的基本结构后,我们就将创建一个这样的开发环境。然后我们将仔细研究资源编程的实质和Palm OS的代码。我们最后得到的将是一个简单而又实用的Palm OS应用程序。

编程环境
我们几乎全部使用C语言来编写应用程序代码,因为用C编写的Palm OS应用程序开销是最小的。书中的大多数例子都利用了Metrowerks Code Warrior编程环境。一般情况下,我们假定你使用的计算机操作系统是Windows。在所附光盘中,包含了一个免费的Metrowerks Code Warrior Lite版本,可以在Windows 9x和NT下运行。

用Mac作为开发平台

如果你使用的是Mac操作系统,那么你就需要做更多的工作,毕竟例子都是为Windows操作系统编写的。当然,在所附光盘中也提供了Mac版本的M etrowerks Code Warrior Lite。事实上,Mac版本和Windows版本的Code Warrior是很相似的。

对C语言应知道多少
在开始做本书的练习以前,假设你已能用C语言编写简单的程序。我建议你至少拥有一本有关C语言的好书。如果还没有的话,Herbert Schildt编写的《Teach Yourself C》(Osborne/McGraw-Hill,1997)是一个很不错的选择。

Palm OS应用程序的剖析
Palm OS的C语言编程和普通的C语言编程非常的相似。源代码被编译,并和一些控制Palm设备内部运行机制的库文件连接到一起。图2-1展示了一个P alm OS可执行程序是如何通过各种不同文件编译和连接后得到的。

另外,资源文件是被单独编译和连接的。它们用来表示程序中的图形元件,诸如窗体、按钮、编辑框等等。很多现代的图形应用程序环境如M ac OS和Windows也是用资源文件来描述它们的用户界面的。

每一个窗体都包含大量的按钮或其它控件。在资源文件中包含了应用程序所有窗体的所有信息。你可以通过Metrowerks Constructor(构造器)编辑资源文件――添加、改变或删除窗体和按钮等。
y深入什么是控件?
控件就是用来接受用户信息的屏幕上的特殊区域。它可以从简单的如按钮(通过手指或输入笔激活)到复杂的如窗体(其中包含了各种不同的其它控件)。你可以通过资源编辑器创建和定义控件。

Palm OS的存储器
传统的存储器一般分两种:一种是快速但掉电后丢失数据的RAM,另一种是低速但永久保存数据的硬盘存储器。大多数计算机在将数据从诸如硬盘、C D等存储设备转移到RAM时花费了大量的时间。

在这方面Palm的存储器较之传统计算机设备有着很大的区别,它使用的是另一种形式的存储器:快速且可永久保存的存储器。这使得它操作数据的速度有了极大的提高。同时P alm是没有文件系统的,它的工作原理实际上就是把存储器组织成数据库的形式。从基本上来说,存储器中的所有东西都被保存成数据库形式。甚至应用程序也是以这种形式保存的。应用程序所在的数据库就是由可执行的代码以及其它在资源编辑器编辑过的资源来填充的。

对于传统的计算机应用程序,为使程序更快的运行,不可避免的存在着多个数据块的拷贝,如一个拷贝在RAM中,一个拷贝在硬盘上。但在P alm OS中,你如果移动数据或制作拷贝却完全没有必要,因为不管你的数据块在什么地方,Palm OS都能直接的访问到它,这是把存储器组织成数据库形式的好处之一。

图2-1:一个Palm OS 应用程序是怎样产生的源文件(Source File *.C) 文本编辑器(Text Editor) 头文件(Income File *.H) 编译器(Compiler)库(Librarian)对象文件(Object Files .OBJ 或.O) 资源编辑器(Resource Editor)库文件(Library Files *.LIB) 连接器(Linker) 资源文件(Resource Files *.RSRC) 可执行文件(Executable File *.PRC)

Palm OS应用程序的执行和调试
Palm一次只能执行一个程序。你可通过选择主菜单中的应用程序图表或按下在Palm底部的四个按钮的其中一个来运行程序。一旦一个程序开始后,它只在另一个程序开始时才会中止。当你关闭P alm后,程序停止,但再次打开后,程序将在断点处继续执行。

当你测试程序时,Code Warrior开发环境会运行一个叫Console的程序来初始化调试信息。Code Warrior将把代码及其它调试信息传给Console,然后Code Warrior利用Console来执行程序、单步测试、设置断点、检查数据等等。当你调试完程序后,最好重启一下你的Palm,即按下调试面板上的X 按钮或用push-pin(或其它尖的东西)插入Palm背后的洞来重启。这样,你就可以把保存在存储器工作区域的数据清除掉。

Console文件是作为隐藏文件保存在Palm中,你并不需要为调入Console而做任何事情。
深入什么是调试器呢?调试器就是用来寻找和修改程序中存在问题的计算机应用程序。就Code Warrior和Palm OS来讲,调试器内嵌在PC中并与Console文件相关联用来检查程序运行时是否有什么问题。调试器可让你一步一步的观测代码和变量。你还可以设置断点来观测当程序运行到此处时会出现什么问题。

开发环境的安装
根据随书光盘所指出的步骤安装 Code Warrior。Code Warrior Lite和Code Warrior几乎一样,但为了督促某些程序员使用正版软件,它在程序的大小和复杂程度上有一定的限制。如果要成为一个真正的开发人员的话,我认为还是购买一套完全版的C ode Warrior为好。即便你不想立即购买,你在读完本书后也该拥有一套。

在Windows计算机上安装完Code Warrior或Code Warrior Lite 后,你或许会发现你的构造器(constructor)还存在问题,那就是你所建窗体上的字体太大了。你须进行以下步骤以使你的构造器正常运行:
1. 找到包含操作系统的文件夹,一般情况下在C:盘上的Windows目录下;
2. 打开Windows文件夹;
3. 打开Fonts文件夹;
4. 关闭Fonts文件夹;
5. 关闭Windows目录;
现在你的构造器就可正常工作了。

IDEs和Code Warrior
当你需要把程序思想转变为现实时,一个好的编程环境显得尤为重要。很早以前(在打孔卡过时后和Turbo Pascal出现之前),你要用一个文字编辑器、一个编译器、一个连接器来构成你的编程环境,这或许就得用到三个不同公司的产品。Palm OS的免费编译器――gcc就是这样的一个例子。首先用文字编辑器产生代码文件和资源文件,然后用gcc工具单独的编译、连接和调试代码。

90年代初,出现了一种新的编程环境――IDE(集成开发环境),它是一种集编辑、编译、连接、调试于一体的开发环境。Code Warrior就是这样一种编程环境,这样就为你节省很多时间和精力,以免你为在各个环节间不停的转换;并且,你也不必再为建立自己的各个开发环节而浪费时间,因为所有的工具都被集成在一起了。

检查你的开发环境

如果你能够保证硬件和软件衔接正常,以后就能节省很多时间,特别是对于Palm OS这样的内嵌环境――编译、连接和程序的运行不在同一个机器上。我想再没有比遇到问题时不知道问题出在开发环境、硬件还是自己的代码上更恼人的事了。经过多年的编程实践,我尽量使自己避免这些不定因素的影响。下面就讲讲如何一步步检查你的开发环境:

1. 如果你还什么都没有做的话,那么第一步就是在你的PC上安装能和Palm通讯的软件Hotsync。
2. 使你的Palm设备与PC保持同步;

小技巧:

如果你的设备不能同步,可尝试下面的几种方法:确保你的串行通讯接口所插的PC端口与HotSync软件中选择的串行口是同一个;确保你所选的端口在开始通讯时不被其它的进程占用(这与使用M odem是同样的道理); 根据帮助中的提示,检查你的计算机是否存在硬件冲突,这也会使你的串口无法正常工作; 尝试其它的串行端口,如果有的话; 按Palm设备后面的重启按钮,重启Palm设备。

注意: 如果你的Palm设备不仅仅是用于开发,那么在调试之前,一定要确保Palm与PC的数据同步,否则有可能导致Palm原有的数据丢失。

3. 开始调试之前,在PC上HotSync图标上单击右键,选择弹出菜单上的Exit退出HotSync。由于HotSync在运行时独占了Palm设备的连接通道,不关闭H otSync就不能下载代码或调试,所以这一步是完全必要的。但如果你忘记了,情况也不会太严重,Code Warrior将弹出一个漂亮的消息框提示你关闭HotSync。

4. 运行Code Warrior集成开发环境(IDE);

5. 选择File | New;

6. 从Project选项卡上选择Palm OS 3.1;

7. 通过Set按钮设置目录路径;

8. 键入工程名称Starter;

注意:把你的例程及将来的程序放入独立于Code Warrior目录树的文件夹中是个很好的办法。 因为这样可以使你轻松的备份和查找。而且,当你需重装Code Warrior时就不会由于疏忽而把你辛苦编写的程序删掉了。别问我为什么想起来告诉你这些,那是令人不快的经历。

9. 在PC上,选择Project | Enable Debugger;

10. 在PC上,选择Project | Debugger或按F5编译连接工程;

11. 把Palm设备设置为Console模式。最简单的方法是利用find,删除find输入框中原有的文字并输入快捷符号。快捷符号很像连写的小写字母“l ”,你在输入区上画一个连写的“l”以产生快捷符号。然后写一个句号(在输入区轻击两下)和数字“2”。在你这样做后,你会发现Pa lm设备会发出声音并且所画的内容将消失。图2-2显示了把Palm设备设置为Console模式所要画的图形;

图2-2 为了把Palm设置到Console模式所要画的内容Cursive L: 草体的英文小写字母 “l”Two Taps: 两次点击Number Two: 数字2

12. 在PC上,单击OK把应用程序下载到Palm上。在Palm上将出现如图2-3的对话框;

13. 单击Palm上的OK。现在你就可以从函数PilotMain()的第一行开始调试程序了;

14. 在PC上,单击“ “或按下Ctrl-R运行应用程序;

15. 在Palm上,运行应用程序,屏幕上会出现一个空白窗体。按下“菜单图标”将产生菜单栏。在菜单栏上有一个可选条目,当选择后会出现另一个带有O K按钮的窗体。如果你按下OK按钮,将回到原始的窗体;

16. 在PC上,按下调试窗口的X按钮重启Palm 。(见图2-4。)如果你已关闭了Palm的电源,请重新打开再按下X按钮。这样做将重启Palm并使之处在console关闭的正常状态。也可以通过使用图钉或其它尖的东西按下P alm背面的Reset按钮。

如果你能正确走到这一步,就应该知道你的开发工具和下载环境已经能使得一个叫Starter的应用程序正常运行了。你可以把这个程序常备身边,以便当古怪离奇的东西攻击你的计算机时(相信我,即使你是我们中最幸运的,“厄运”也是有可能光顾你的),再用此程序检查你的开发环境。
当你查看Palm上的应用程序区时,就会发现多了一个叫Starter的应用程序。如果你用的是Palmed或更高版本,可选其中的Apps | Delete将下传的应用程序删掉。

生成工程
现在到了着手生成自己的工程的时候了。在这个工程中,有一个按钮上面写有“Hello”。我们可以像建立Starter程序那样生成这个工程,删去S tarter程序原有的代码和资源而用自己的新的代码和资源将之代替。你可以通过改变工程的设置使之自动生成。很多设置比较有趣,它们有直接的答案,况且在S tarter应用程序中已经设置好了。所以,这并不存在很多困难:

1. 运行Code Warrior集成开发环境;
2. 选择File | New;
3. 在Project选项卡中选择Palm OS 3.1;
4. 按下Set按钮选择目录;
5. 单击OK;
6. 给工程起名叫Hello;

注意:
你可以把有用的文献放在顶级目录或一个特殊的文献目录,以便自己和别人能很容易的找到。我在保留应用程序以备后用时,就经常放在一个叫i deas的目录中。当想添加这样的文件时,可选择“Project | Add Files”。注意应把文件类型设为All Files以便能看见文献文件。

7. 单击AppSource树使之打开;

8. 右击Starter.c并选Delete。这是原来Starter的代码,你应该用你自己的代码将之代替;

9. 单击AppResources树使之打开;

10. 右击Starter.rsrc并选Delete。这是包含原来Starter程序中有关窗体和控件的规划信息。你也应该用你自己的资源文件将之代替;

11. 进入Windows浏览器,找到新建的工程文件夹,它应该叫Hello。在这个文件夹里面,还有一个文件夹叫Src。打开此文件夹,里面是原来S tarter程序中的源文件,全选并删除。在下一部分中,你将为Hello程序建立新的源文件,它们就保存在这里。

生成资源
Palm OS资源在本质上和为Mac和Windows生成的资源是相同的,但更加简单。由于资源是来自于RezEdit的资源格式,所以它更接近Mac的资源。P alm OS程序的界面叫做窗体。为产生这个简单的“Hello”程序,你应该建立一个上面有一个按钮的单一窗体。

步骤如下:

1. 运行Palm上构造器,这是Metrowerks的资源编辑器。在Code Warrior中也会发现相同的菜单;

2. 选择File | New Project File。这样将产生一个空的资源文件。现在的工程属性取缺省就行了,在下面的章节会详细讲到它们还能做什么;

3. 改变Application Icon Name为工程名称“Hello”;

4. 在Resource Type and Name列表箱里选中Forms行;

5. 选择Edit New Form Resource或者按CTRL-K创建一个新的窗体。新窗体将出现在Forms行并被命名为“untitled”;

6. 单击name,出现编辑框后,取名字为“Hello”;

7. 双击窗体打开,在下面的章节中将详细的讲解这些属性的意义并论述它们如何使用;

8. 选择Window | Catalog或按CTRL-Y打开控件工具箱;

9. 拖动一个按钮至窗体的中心。现在按钮的属性将代替窗体的属性出现在窗体对话框的左边。这些属性缺省就可以了,以后将详细讨论它们。你如果十分渴望改变按钮的位置的话,你可以改变L eft Origin 和Top Origin属性;

10. 单击Label属性,将其名称由OK改为Hello;

11. 单击右侧的面板,重新得到窗体属性;

12. 选择Layout | Hide Object Ids。这将使你确实看到写有“Hello”的按钮。如果“Hello”看起来字体比较大,这是因为你遇到了我在安装部分讲到的古怪字体问题。为解决这个问题,关闭构造器,打开W indows文件夹中的Fonts文件夹,然后重新打开构造器。你的“Hello”窗体将如图2-5所示;

13. 选择Fiel | Save保存工程资源。将其保存在Hello工程文件夹的Src子文件夹下面。并命名为hello.rsrc。

14. 当你完成你的新工程后,下一步该涉及到总工程。资源工程是总工程的一部分,总工程将包括所有有关用户界面的图形描述的各个方面,例如:大小、布局及窗体和控件的外观等;

15. 再次打开Code Warrior集成开发环境,选择File | Open Recent | hello.mcp;

16. 选择Project | Add Files。置文件类型为“All Files”以便能看见的资源文件。找到hello.rsrc打开;

17. 这时资源文件会出现在工程的顶部。为简洁起见,把它拖到应用程序(AppResource)资源组的下边。为此,你可以按住鼠标把文件图标拖到应用程序(A ppResource)资源组的下部,当出现在资源组下面出现一直线时,松开鼠标;

18. 到此为止,你已成功的创建和添加了新的资源。

生成代码
为使你的程序能够运行,必须添加C代码,步骤如下:

1. 建立可以写入代码的新文件,你可以选择File | New Text File创建之;
2. 选择File | Save As保存文件为hello.c;
3. 选择Project窗口,选中Project | Add Files。找到hello.c添加之;
4. 如果它不在AppSource文件夹,可把它拖到该文件夹中。当你拖动文件的时候,你会发现一条直线,它允许你把文件插入;
将下面的代码输入到hello.c文件中。在下一部分将逐行进行详细的讲解。

/* The super-include for Palm OS */
#include

/* Our resource file */
#include "hello_res.h"

/* A prototype for our form handler function */
static Boolean myHandleEvent( EventType* event );

/* The main entry point */
DWord PilotMain( Word cmd, Ptr, Word )
{
FormPtr form; /* A pointer to our form structure */
EventType event; /* Our event structure */

/* If this is not a normal launch, don't launch */
if( cmd != sysAppLaunchCmdNormalLaunch )
return( 0 );

/* Initialize our form */
form = FrmInitForm( HelloForm );
FrmSetEventHandler( form, myHandleEvent );
FrmSetActiveForm( form );
FrmDrawForm( form );

/* Our event loop */
do
{
/* Get the next event */
EvtGetEvent( &event, -1 );

/* Handle system events */
if( SysHandleEvent( &event ) )
continue;

/* Handle form events */
FrmDispatchEvent( &event );
/* If it's a stop event, exit */
} while( event.eType != appStopEvent );

/* We're done */
return( 0 );
}

/* Our form handler function */
static Boolean myHandleEvent( EventType* event )
{
/* Parse the event */
if( event->eType == ctlSelectEvent )
SndPlaySystemSound( sndAlarm );

/* We're done */
return( false );
}


代码分析
源代码由两个函数组成。它创建了一个窗体和可以退出应用程序的按钮。当然,在程序为解决实际问题而变的更复杂时,我们必须增加更深层的程序结构。
下面是代码的逐行解释。

/* The super-include for Palm OS */
#include

Pilot.h包括了所有Palm OS相关的头文件。

/* Our resource file */
#include "hello_res.h"

深入(IN DEPTH)注意到Pilot.h包含在<>中而hello_res.h包含在引号中了吗?这就是说,编译器将在Palm OS所指定的系统文件夹中寻找Pilot.h,而对于hello_res.h,则先在HelloSrc中寻找,若找不到再到所有的文件夹中寻找。
当你创建hello.rsrc时,hello_res.h随之也建立了。hello_res.h中包含了窗体和控件的ID标识符常量。

注意:在运行之前,应把hello_res.h添加到你的工程中,它允许你方便的查看和编辑。添加文件时,首先选择Project | Add Files。找到hello_res.h将之拖到AppSource文件夹中。

/* A prototype for our form handler function */
static Boolean myHandleEvent( EventType* event );

以上就是处理窗体事件和控件事件的回调函数的原型,所谓回调函数就是在应用程序中的一个系统调用,它一般用来响应系统或用户自定义的事件,譬如你用输入笔点击窗体中的按钮所产生的事件。

函数PilotMain()

/* The main entry point */
DWord PilotMain( Word cmd, Ptr, Word )
{

PilotMain()就是包含了被称为启动代码的函数。变量cmd 提供了激活应用程序的入口。接下来我将告诉你开始运行启动代码后,其它参数将起到什么样的作用。

如下定义了指向窗体的指针。
FormPtr form; /* A pointer to our form structure */

如下定义了指向当前事件的指针。EventType是一个数据结构,包含了当前事件的所有信息。
EventType event; /* Our event structure */

小技巧:
一般我们会先查看一下PilotMain()函数头部是否存在一些识别Palm OS版本的代码,以确保程序在发现操作系统存在任何不兼容的现象后,安全而体面的退出。为了达到这个简单的目的,我先把这部分代码分离出来如下。需要事先说明的是,现在提供的这个应用程序确实能够运行在任何P alm 设备上,因为它只用到了很少的系统资源,但是在运行其它复杂的程序时就存在一些不可预料的问题了。让我们从下面的代码看起。


Dword ROMVersion;
//Get the ROM version
ROMVersion = 0;
FtrGet ( sysFtrCreator ,sysFtrNumROMVersion, &ROMVersion );

//Alert and bail if the ROM version is too low
if ( ROMVersion < ROM_VERSION_MIN )
{
// Do something here that reports an error

//Palm OS 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 );
}
通过函数Ftret()的调用可以得到ROM中Palm OS的版本。从而得知当前运行的Palm OS版本是否达到了你应用程序的最低运行配置。一般Palm OS 1.0版本的处理方法是不停的重新加载应用程序(如果上一次加载不成功的话),直到你主动切换到其它的应用程序。这样就有可能使你陷入一种近乎死循环的尴尬局面,所以,在这样的情况发生前你应该先调用A ppLaunchWithCommand()函数,使你得以从容跳出当前的应用。
下面是环境检查代码,你可以藉此查验当前的运行环境是否是一个正常的运行环境(normal cicumstances)。到下一章,我们还将看到其它的运行环境。

/* If this is not a normal launch, don't launch */
if( cmd != sysAppLaunchCmdNormalLaunch )
return( 0 );

下面是一些关于Palm OS内嵌窗体管理器的函数调用。首先,调用FrmInitForm()函数,使我们的窗体进入备用状态。通过使用FrmSetEventHandler()函数为窗体绑定事件处理函数(e vent handling function)。然后,使用FrmSetActiveForm()函数来激活窗体。最后,调用FrmDrawForm()显示窗体。

/* Initialize our form */
form = FrmInitForm( HelloForm );
FrmSetEventHandler( form, myHandleEvent );
FrmSetActiveForm( form );
FrmDrawForm( form );

下面是事件循环的开始部分。

/* Our event loop */
do
{

深入: 什么是事件循环?事件循环是用来处理应用与用户,及应用之间交互的一种程序循环,譬如处理输入笔运动、按钮被按下等用户事件。许多现代图形用户界面(G UI),如Mac OS、Windows等都是事件驱动的操作系统。事件是一种操作系统和你的应用程序精确交互的手段,从而可以得知用户正在对应用程序做一些什么样的操作。一般,如P alm OS这样简单的操作系统,通常按发生的顺序来保存事件,用户可以请求事件和处理事件。

下面这个函数从ROM中调用事件处理器(Event Manager),并把老的事件放到队列中,以操作系统接收的先后顺序来处理事件。

/* Get the next event */
EvtGetEvent( &event, -1 );

下面的函数调用了系统管理器用来处理系统事件。

/* Handle system events */
if( SysHandleEvent( &event ) )
continue;

下面这个函数不但调用了回调函数处理事件,而且还执行了很多其它的任务。譬如,使用penUpEvent 和penDownEvent 两个原始事件来产生新的事件,这就像ctlSelectEvent事件,我们使用在窗体事件处理函数中去决定按钮是否被按下了。这就是我们使用系统回调函数处理事件而不是把所有事件都放到一个巨大的事件循环中去的原因。

/* Handle form events */
FrmDispatchEvent( &event );

一旦另一个应用程序被用户选择,就交由下面的代码来处理程序退出前的一切善后工作。 在这个例子程序中并没有什么需要清除的,所以我们就可以通过结束循环来退出执行中的应用程序。这些代码往往出现在循环的底部,因为我们在退出时总是需要往窗体事件处理器(或者其它的处理函数)中添点什么代码的,所以让这个退出事件接收循环贯穿整个程序结构是很有好处的。

/* If it's a stop event, exit */
} while( event.eType != appStopEvent );

/* We're done */
return( 0 );
}

helloHandleEvent() 函数
还记得吗?我们调用FrmSetEventHandler()函数的时候就是通过传递helloHandleEvent()函数的入口地址给Palm OS来实现的。这个函数通过调用FrmDispatchEvent()函数来处理窗体事件。

/* Our form handler function */
static Boolean myHandleEvent( EventType* event )
{

这个事件发生在窗体上的”Hello”按钮被按下后,在例子中,我同时让系统发出了警报音。
/* Parse the event */
if( event->eType == ctlSelectEvent )
SndPlaySystemSound( sndAlarm );

/* We're done */
return( false );
}

我的编程风格是通过超过500,000行以上代码,16年的程序生涯养成的。然而,看了我的程序并不意味着就学到了我的风格。我认为,不应该全部照搬书中的内容,而是要采用自己的方法来编程,并且不为尝试自己独特的编程风格而感到惭愧不安。用自己的方法写自己的样列程序,往往能使人更好的学习和理解那些内在的原理,所以我是非常赞同程序员编写自己风格的代码的。

调试:使它工作起来
在这一部分你就可以开始编译和链接这个项目工程了。首先选择运行Project | Make。如果你像我那样干的话,就可能会搞出一些BUG的(当然了并不是所有的方面都是完美的嘛)。祝你们好运吧。

Okay, Okay,我们已经取得不少的进步了。你现在可以通过Compare files操作比较快速的除掉一些编译时间错误了。如果你在创建工程的时候意外的删除了库文件,那么就会产生连接错误。你最好的处理办法就是尽早做好这些文件的备份工作。

发现运行时间错误的最好方法就是逐行的调试程序。你可以在Code Warrior IDE集成环境中执行下面的一些调试步骤:
1.在PC上选择Project | Debug 或者 点击F5来编译和连接工程,然后调出调试器(debugger)。
2.在Palm上,把设备先置位到Console模式。最简单的方法就是利用find框,删除find输入框中原有的文字并输入快捷符号。快捷符号很像连写的小写字母“l ”,你在输入区上画一个连写的“l”以产生快捷符号。然后写一个句号(在输入区轻击两下)和数字“2”。在你这样做后,你会发现Pa lm设备会发出声音并且所画的内容将消失。图2-2显示了把Palm设备设置为Console模式所要画的图形;

3.在PC上,点击OK用来下载应用程序到Palm设备。再点击OK,来去掉Palm上出现的Code Warrior lite警告框,现在你就应该停在Code Warrior 调试器中,PilotMain()函数的第一行了。

4.在PC上,点击调试器窗口顶端看起来象一个向右箭头的按钮。这将使你以单步调试的方式运行程序,进入函数后,箭头每次都将停留在下一步将要执行的语句上。点击另一个像向上箭头的按钮,将使你跳出任何一个执行中的函数。

5.在PC上,按下调试器窗口顶端的“X”按钮可以重置Palm设备。如果你的Palm设备已经关掉,那就再打开,然后按下“X”按钮,则设备将被置于一种良好的C onsole模式关闭的状态。当然,也可以通过使用图钉或其它尖的东西按下Palm背面的Reset按钮,来达到同样的目的。

值得庆幸的是,我早期的一些源代码评论将对你有用,特别是像你这样正努力推测程序中每条代码含义的时候。当然,如果你的窗体根本不显示的话,那么你就得首先确保你已经正确连接了当前资源文件(* .rsrc),以及通过在FrmDrawForm()函数入口设置断点和单步跟踪来确保它是否被调用了。

你也可以通过点击每行左边的页空白处来设置断点。如果设置了断点,一个小红点将出现在该行左边的页空白处。去掉断点只要点击该行左边页空白处显示的小红点就可以了。
图2-6显示了本示例程序运行的样子

图2-6显示了本示例程序运行的样子

当你的Hello示例程序被完全调试好之后,你将会发现:

1 在Palm的应用程序区将出现“Hello”的图标
2 一旦程序被加载,将显示它的窗体和“Hello”按钮
3 当“Hello”按钮被按下,程序将退出
4 当按下Palm上的4个真正的按钮或任何一个输入板(graffiti pad)边上的“Silkscreen bottom”,程序都将退出

祝贺你。你已经写完了你的第一个Palm OS 应用程序了。这是一个很好的程序,可以用来测试你当前使用的代码中是否存在一些不易发现的不安全的设置。

当你运行你的程序时发生了什么?

现在你已经拥有一个能工作的Palm OS程序了,让我们设置一个断点,再一次单步调试,以便于更加深入的了解它是如何工作的。

1.在PC上,开始运行调试器。选择Project | Debug 或 按下 F5 开始编译和连接工程以及弹出调试器。

2.在Palm上,设置你的设备到Console状态。使用find框,删除find输入框中原有的文字并输入快捷符号。快捷符号很像连写的小写字母“l ”,你在输入区上画一个连写的“l”以产生快捷符号。然后写一个句号(在输入区轻击两下)和数字“2”。在你这样做后,你会发现Pa lm设备会发出声音并且所画的内容将消失。图2-2显示了把Palm设备设置为Console模式所要画的图形;

3.在PC上,单击OK后,下载应用程序到Palm。现在你需要在PilotMain()函数的第一行设置断点。

4.在PC上,从调试器窗口中找到,如下代码:
/* Parse the event */
if( event->eType == ctlSelectEvent )
SndPlaySystemSound( sndAlarm );

5.在PC上,在if语句前设置断点。

6.通过反复的按下执行一行代码的按钮,来单步逐行调试整个程序。注意到,FrmDispatchEvent()函数的调用经常停留在窗体事件处理器上(m yHandleEvent()),但由于发生的并不是ctlSelectEvent,所以SndPlaySystemSound()就没有运行。跳过SndPlaySystemSound()函数后,程序又进入了G etEvent()状态,等待其它事件的发生了。

7.点击Palm上的“Hello”按钮,注意到,GetEvent()函数响应了,窗体事件处理器被反复调用了许多次,这次终于发生了ctlSelectEve nt,于是SndPlaySystemSound()就被调用了。你现在已经亲眼目睹了你的代码分配和处理一些事件的全过程了。

8.在PC上,按下调试器窗口顶端的“X”按钮可以重置Palm设备。如果你的Palm设备已经关掉,那就再打开,然后按下“X”按钮,则设备将被置于一种良好的C onsole模式关闭的状态。当然,也可以通过使用图钉或其它尖的东西按下Palm背面的Reset按钮,来达到同样的目的。

把你的工程存放到固定的目录中
在第四章中,我们将使用本章做好的应用程序作为整个程序外观的基础。在Code Warrior中,提供了一种叫做“Stationary”的建立详细工程项目的模板,现在就请把当前项目做成一个模板。在你的 Code Warrior下的Stationary子目录中保存一个应用程序模板的拷贝,这样当你下一次选择New Project选项的时候它就会出现。你也可以按照以下步骤在Windows资源管理器中复制应用程序模板:

1.打开Windows资源管理器,找到你的Hello工程的目录。
2.选中整个Hello工程目录。
3.按下CTRL-C复制目录
4.在Code Warrior目录下找到Stationary子目录,进入。
5.按下CTRL-V复制Hello目录。

接下来讲什么?
在下一章中,我们将开始研究文本框,用以输入文本。
第三章 文本框中的乐趣
在本章节中,我们将通过讨论第二章中Hello应用程序的副本,来研究文本框的属性和事件。涉及的内容包括文本框如何调用Palm OS存储器和处理它们之间的关系等。我们也将花些时间讨论与文本框相关的其它一些内容:

◆手写输入转换指示器(Graffiti shift indicators)
◆字符串资源(String resources)
◆编辑菜单的特殊之处和普通菜单
◆Palm OS 版本可兼容代码
◆错误信息和警告

文本框究竟是什么?
文本框其实就是一个编辑框(单行可编辑的),利用它你可以让你的用户输入文字或数据。让我们来创建一个文本框并感受学习的乐趣吧。

先在Code Warrior集成开发环境中创建一个应用程序:

1.运行Code Warrior集成开发环境。

2.选中菜单File | New Project来创建一个新的项目。

3.从项目选项中选出Hello应用程序,并重命名为你的新应用程序的名称。

4.从项目中移走原来的资源文件。你可以这样操作:在Src文件夹中用右键点击Hello.rsrc,选中Remove Selected Items。

5.到你的项目文件夹的Src目录下删除Hello.rsc。

6.运行资源构造器(Constructor)来创建一个资源文件。

7.在资源列表中选取窗体并按Ctrl+K,创建一个名为Contact Detail的窗体。点击默认名称并重新命名窗体。

8.双击窗体以打开该窗体进入编辑状态。

9.向窗体中拖放一个文本框。你可以选中菜单WindowCatalog来打开目录窗口。然后从目录窗口中拖动一个文本框控件放到窗体中。

10.命名文本框名为FirstName。你可以这样操作:点击该文本框来显示它的属性,在窗体的左边方框会出现一个窗口,点击Object Identifier属性,然后输入FirstName。

11.在这一步,最好把手写输入转换指示器(Graffiti shift indicators)放到你的窗体中。从目录窗口中拖动一个手写输入转换指示器控件放到窗体中,这个转换器的标准位置应放在窗体的右下角。

12.按以上操作后,窗体显示如图3-1所示。

13.回到Code Warrior集成开发环境中,在项目中加入一个新的资源文件。在Code Warrior集成开发环境中选中Project | Add Files命令。

14.选中Project | Make命令编译连接项目。

15.到调试器中调试程序。首先通过菜单Project |
Enable Debugger来击活调试器。

16.打开你的Palm,确认它固定在支架上。然后关掉PC的HotSync软件。

17.在PC上,选中Project | Debug命令。

18.在你的装置中,输入l连写的字母“1”,两个点号“..”,和一个数字“2”(1..2),来启动控制台程序(console)。

19.在PC上,点击OK启动调试器。

20.在调试器中点击前进箭头,执行应用程序。
图3-1 程序中窗体的外观

在Palm上运行应用程序,点中文本框,你就可以输入文字或数字了。如果你在大字状态时向右上一挑或者在符号中一点,你放在窗体上的手写输入转换指示器会分别露出箭头或点。应用程序显示如图:


需要结束应用程序时,请切记点击调试器窗口中的X来重启你的装置或停止控制台程序的运行。让控制台继续运行会引起Palm的一些问题。

如果你的手写输入转换指示器在你刚才的程序中运行顺利的话,你就可以开始进一步了解它了。但如果它没能正常工作,那么你就有必要去判定程序中是否存在阻止文本框事件被操作系统处理的代码了。正确的做法是,可以把它们放在任何窗体中,那么当你手写输入时就可以看到它的用途了。它通常被摆在窗体右下角,因为人们习惯这样做。没有程序一定需要它,但把它放到窗体中它就能工作。

从上面的例子中你可以看到许多与文本框相关的东西在工作。我们运行的窗体中的代码(hello.c)其实并没有对窗体做什么,然而我们却可以修改,输入,查找和替换文本,甚至做其它我们想做的事情。剪切和粘贴在这里不起作用。至于菜单和快捷键,我们将在以后的章节中接触到。

注意:如果你的Palm在调试器运作后出现异常,可能是因为它没有被重启或控制台依然在运行。请把你的Palm放好,用推针或回形针的尾部去顶开P alm背面小洞中的重启按钮。

属性
你可以在资源构造器中改变很多有关文本框如何工作的内容。在构造器中调出Contact Detail窗口,选中项目中的.rsrc文件,然后在Contact Detail窗体中双击文本框,它的属性会显示在旁边。所有的Palm OS 用户界面控件都有一些公共的属性,像Left Origin和Top Origin。表3-1是所有的文本框属性和描述的列表。熟悉这些属性后,重新编译并调试程序,以试验每一个属性是如何影响文本框行为的。

表3-1

名称 描述
Object Identifier 你选定的文本框名称。

Field ID 这个数值是Palm OS 用来定义特殊的用户界面对象的。

Left Origin 定义文本框左边界的位置,整个屏幕跨越160象素。

Top Origin 定义文本框上边界的位置,整个屏幕从顶到底共160象素。

Width 文本框宽度象素。

Height 文本框高度象素,你必需增加这个数值以避免砍掉一些大字体的顶部。

Usable 定义文本框是否在窗体中显示。那些没有被标记为可用的文本框是不可见或不可选的,直到它们被某个功能命令激活。

Editable 定义文本框是否可选或接受涂写输入。对大部分文本框来说,该项已被选中。

Underline 在文本框下面画上圆点线来显示它的位置。否则,在屏幕上不能指出已存在的空文本框。

Single Line 限制输入一行文本。该文本框不会垂直滚动或接受回车或Tab键的输入。

Dynamic Size 使该文本框可以根据需要扩大,以显示所有输入的文本。你必需增加代码来实现,不用担心在改变属性时没有看到它的作用。

Left Justified 使文字左对齐排列。不用检查对数字显示的有用性,所以小数排成行放置。

Max Characters 规定的最大字符数,在Palm OS禁止输入和发声报警前可以输入的字符数。

Font 文本中的字体在文本框中显示。请注意你最好根据你所选的字体手动改变文本框的高度。

Auto Shift 把输入的第一个字母变为大写。

Has Scroll Bar 这个选项使文本框在字符太多时自动加上滚动条。这会引起Palm OS发送事件,用户可以在该事件中更新滚动条显示。

Numeric 允许输入的数字转变为字符保存。

再谈事件
从第二章可以了解到,Palm OS程序,类似于Mac OS程序和Windows程序,都是以事件驱动为基础的。程序不用做任何事情(大部分是这样),直到用户的输入到来,比如按下了按钮,用输入笔或手指接触了屏幕等。

当用到文本框的时候,我们关心以下四个事件。第一,penDownEvent和penUpEvent。每一次装置的屏幕被接触时会有PenDownEvent发生。同样地,每一次输入笔离开屏幕时会有P enUpEvent发生。

对Palm OS的用户来说,界面元素事件,包括文本框的事件,被转换成为对FrmDispatchEvent()函数的调用。对于文本框事件FrmDispatchEvent() 函数其实就是转化成了其它三个事件:fldEnterEvent,fldChangedEvent和keyDownEvent。只要文本框被敲击选中,就发送FldEnterEven t。只要文本框调整它的外观,例如,水平滚轴,就发送FldChangedEvent。只要手写输入特征被识别并发送到文本框中,就发送keyDownE vent。

如果你不处理事件的话,就应该在事件处理器中返回false值。这样的话,我刚才描述的极好的自动产生事件的特性就没用了。如果事件处理器中返回t rue值,FrmDispatchEvent()处理下一个事件;如果返回false值, DispatchEvent()就调用FrmHandleEvent(),FrmHandleEvent()函数实质是把多个简单事件转化成若干复杂事件来处理的函数。以处理文本框事件为例,F rmHandleEvent()函数调用FldHandleEvent()就产生了我们刚才讨论的文本框事件。除非是在非正常的环境中,我不推荐调用FrmHandleEv ent()函数或FldHandleEvent()函数。当你想进一步地处理特殊的事件时,请留意从你的事件处理器中返回的false值。

现在让我们来修改Contacts项目中的代码,调用标注了日期的刚创建的新资源。使用资源管理器,复制另一个Hello.c副本到Contacts项目的S rc文件夹中。把它重命名为Contacts.c,然后通过选中Project | Add Files命令把它重新加入到项目中。使用新的Contacts_res.h头文件来替代hello_res.h,在Contacts.c文件中包含它。做完这些后,为了使代码能成功编译并连接,你还必需将引用从H elloForm改到ContactDetailForm。到现在,虽然我们还在使用错误的资源文件,但程序已经可以工作了,因为表示HelloForm窗体的ID与表示C ontactDetailForm窗体的ID是相同的。构造器经常从1000开始赋窗体的ID值的。
其它的需要修改的内容是case语句,在程序中找到处理ctlSelectEvent事件的case语句。下面就是要找的,它表示按钮被按下的事件:

//CH.2 The botton was pressed
case ctlSelectEvent;
SndPlaySystemSound(sndAlarm);
return(false);

到现在为止,Contacts程序中的case语句还没有做完。由于窗体中并没有按钮,所以根本没有机会收到ctlSelectEvent事件。那么就让我们通过改变c ase语句来捕获penDownEvent事件吧,它会使你更有信心的。用下面的代码替换上面的case ctlSelectEvent代码:

//CH.3 The pen touched down
case penDownEvent;
SndPlaySystemSound(sndAlarm);
return(false);

选中Project | Make(在Project | Debug下)来调试已修改的代码。在调试窗口点击运行按钮运行已修改的应用程序,当你每次点击文本框时,它可能发出只有紧急事件产生时才有的警报声。事实上,无论你点击窗体的任何地方都会听到警报声,因为都有p enDownEvent事件被触发产生。你也可以试着把penDownEvent替换为penUpEvent,make,debug,然后运行程序。现在警报声就在输入笔点击后离开屏幕时发出了。

再试着把penUpEvent替换为fldEnterEvent,make,debug,并运行程序。可以发现直到输入笔碰到窗体的文本框部分时,警报声才响起。现在我们可以很容易地可以用精确事件去来替换未明了的事件了,像p enDownEvent和penUpEvent事件。这样做不但相当的简单,而且它还会给人一种用户界面的感觉,这与其它Palm OS应用程序的风格是一致的。所以,知道有关penDownEvent和penUpEvent事件很有益,但是我的建议只是在很多精确的事件不可用的情况下才使用它们。

试着把fldEnterEvent替换为fldChangedEvent。make,debug,并运行应用程序。这次如果你在文本框中输入了相当多的字符的话,文本框将出现水平滚动条,警报就会消失。

试着把fldEnterEvent替换为keyDownEvent。make,debug,并运行应用程序。现在只要在输入区域产生一个正确的字形,警报就会响起了。如果在输入板引擎还未决定你是否已经输入正确的内容前,你就已经抬起了笔,那么就可以看到一些很有趣地东西。这并不容易做到(至少对我来说)。

注意:在事件产生时播放声音通常是一个较好的方法,这样可以正确地看到在什么环境下什么时候一个特殊的事件会发生。这是一个很好的实验方法,用来帮助你采集操作系统的状态。

焦点
有50种以上的Palm OS函数,允许你以不同的方法操作文本框和你输入的数据。在下一部分,我们将处理大多数基本功能调用,你可能经常用到(至少对文本框来说,并不是直接连接数据库中的数据)。文本框功能中剪切,复制,删除,粘贴,和撤消最受欢迎,我们在用到菜单和快捷菜单时,将处理这些问题。

你需要先点击文本框来才能够向文本框中写入字符。换句话说,文本框需要先获得焦点才能够接收输入。
让我们调用FrmSetFocus()函数来满足点击文本框后获得焦点的需要吧。这个函数必须紧跟着画窗体的语句出现,因为在窗体出现前获得焦点函数是不起作用的。所以,对它来说最合适的位置应该是在P ilotMain()中的FrmDrawForm()后。为了用使用FrmSetFocus()函数,你必须首先调用FrmGetObjectIndex()函数得到目标对象索引。这样,在你m ake,debug,并且运行代码后,在你点击输入之前,就可以看到在文本框中有一个闪动的指针。你加入的新代码应该如下:

//CH.3 Initialize our form
form=FrmInitForm(ContactDetailForm);
FrmSetEventHandler(form,myHandleEvent);
FrmSetActiveForm(form);
FrmDrawForm(form);

//CH.3 Get the index of our field
index=FrmGetObjectIndex(form,ContactDetailFirstNameField);

//CH.3 Set the focus to our field
FrmSetFocus(form,index);

//CH.2 Our event loop
do
{

向文本框输入字符

现在我们该向文本框中输入一些字符了。在这样做之前,你有必要学习一点有关文本框使用Palm OS存储器方面的知识。一旦对文本框进行编辑后,就会相应的引起数据库内存被直接编辑,与文本框有关的一切操作都被对应到一块可以进行操作和自由改变大小的存储片。在P alm OS的术语中,这些存储片被叫做块(chunk)。

文本框需要用到这些特殊的数据结构来保存用户输入的数据,因为这种数据结构具有根据输入数据改变大小的特性。现在假定我们已经分配了两个块的内存给两个文本框了。每个块的初始大小是8 0个字节:

XXXXXXXXYYYYYYYY

这时就存在这样一个问题,如果需要扩大第一个文本框的容量到100个字节,但因为第二个文本框分配的块紧挨着它的下面分布,所以就没有空间可以用来扩展第一个内存块了。此时如果我们不准备放弃,不限制文本框小一些并且不改变字符总数的话,文本框中就会丢失一些内容。
为了解决这个问题,人们扩展了内存处理(memory handle)的概念。采用把指针放在内存指针列表中的方法代替了直接返回指针给每一个被分配的存储片的方法。操作系统可以利用空闲的时间来调整块的位置,这样就可以允许块的扩展了,

XXXXXXXXXXXXYYYYYYYY

当内存块正在使用的时候,我们通过锁定内存块来阻止操作系统试图移动内存块的操作。在我们还没有完成操作之前,内存块一直处于锁定的状态。

在先前的简单文本框例子中,当我们开始向文本框加入字符时,Palm OS就自动分配了一个内存块给文本框,并实现了内存块和文本框的自动绑定。如果想在程序一开始就得到一个带字符的文本框,我们就必须先给文本框分配一个内存块,向内存输入文本,并且把内存与文本框绑定。让我们在C ontacts.c中加入C代码,使我们能够在Contacts.c的开头部分分配并初始化内存块:

//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

//CH.2 The main entry point
Dword PilotMain(Word cmd,Ptr,Word)
{
FormPtr form; //CH.2 A pointer to our form structure
CharPtr ptext; //CH.3 Points to the text in the edit field
Word index //CH.3 A general purpose index
FieldPtr field; //CH.3 Used for manipulating fields
EventType event; //CH.2 Our event structure

//CH.2 If this is not a normal launch don’t lauch
if(cmd!=sysAppLaunchCmdNormalLaunch)
return(0);
//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,”hello”);
//CH.3 Unlock the field’s memory
MemHandleUnlock(htext);

在上面的代码中可以看到四个Palm OS的新功能。

MemHandleNew()分配一个内存块。MemHandleLock()锁定内存块,防止Palm OS移动存储器。StrCopy()就像C语言函数strcpy(),它从存储器的一个地方复制一个零界限字符串到另一个地方――这样,就可以复制一个常量的值到我们的内存块了。M emHandleUnlock()告诉操作系统,我们已经解锁了一个内存块。这条语句后,任何对ptext指针的调用都会变得危险,因为内存块在任何时候都可能被移动的。

若发生Palm设备内存溢出这样的悲惨事件的话,MemHandleNew()将返回NULL。以上的代码可以使应用程序安静的退出,但是这还远不是一种理想的解决方案。在下一章中你将会学到如何发送带有警告的错误信息给你的用户。
剩下要做的就是把我们的内存块丢给文本框了。可以在画窗体前做以下这些工作:

//CH.3 Initialize our form
form=FrmInitForm(ContactDetailForm);
FrmSetEventHandler(form,myHandleEvent);
FrmSetActiveForm(form);

//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);

这里稍有一些技巧的函数是FldSetTextHandle()。为了调用这个函数,你必须有一个指针指向文本框。你可以调用FrmGetObjectPtr()来取得这个指针。可以看到F ldSetTextHandle()必须在FrmGetObjectPtr()前被调用,否则你输入到内存块的文本就不能与窗体部分一起画上。调用后面的FldDrawFie ld()来画出整个窗体。

你把经过上面修改的代码加入到Contacts.c后,再一次make,debug,并运行你的项目。初始值”hello”就将会显示在文本框中了。

使用字符串资源

当前,我们初始化了程序中的文本框,它带有不容易编码的字符串。对于这么一个小型的应用程序来说,字符串编码也许不成问题,但对于大型应用程序来说,它就可能引起不愉快的问题了,当你准备将你的软件向海外销售时,你会发现你必须无止尽的把时间花费在代码的改变问题上,为的只是把它的显示部分翻译成其它语言。我知道,唯一一个能帮助你解脱出来的办法就是使用字符串资源。

字符串资源就是与其它界面元素一起存储在你的资源文件中的字符串。许多翻译公司都有能力把你的应用程序翻译成其它外语,只要你给他们资源文件就可以了。所以这是把你的应用程序翻译成另一种语言花代价最小的一种方法。


在资源构造器(Constructor)中创建一个字符串资源:
1.启动资源构造器并打开Contacts.rsrc文件。
2.从资源列表中选中字符串资源类型。
3.按Ctrl-K来创建一个新的字符串资源。并命名为FieldInit。
4.双击打开新的字符串资源。输入hello。
要在你的代码中使用字符串资源,必须获得它,锁定它,在想用的时候用到它,最后才释放。代码实现如下:

//CH.2 If this is not a normal launch,don’t launch
if(cmd!=sysAppLaunchCmdNormalLaunch)
//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);

在Contacts.c中修改以上代码。Make,debug,并运行结果。程序看起来和原来并没有什么不同,但现在你的字符已经是从字符串资源中得到了的。

菜单
现在是给Contacts程序增加一个Edit菜单的时候了。Palm OS用户界面方针定义了一个标准的Edit菜单,只要你提供一个文本框,它就可以被输入和编辑。这个菜单显示如下:

首先构造菜单的资源部分:

1.启动资源构造器并打开Contacts.rsrc文件。

2.从资源列表中点击菜单条资源类型。按Ctrl-K来创建一个菜单条。并命名为Contact Detail。

3.双击打开Contact Detail菜单条资源。按Ctrl-M来创建一个新菜单。

4.在菜单条上改变名称,把Untitled改为Edit。

5.按Ctrl-K来创建一个新的菜单条目。输入Undo。按TAB键到达菜单条目快捷区域。输入U。继续这个过程,创建每一个新的菜单条目,就像前面例子中显示的那样。你可以使用C trl-连接号(-)来创建Select All和Keyboard之间的分隔条。

6.当你创建完菜单,双击打开Contact Detail窗体。在Menu Bar ID属性条中输入菜单的ID号(可能1000)。

7.以上工作完成了菜单条资源的创建,它看起来像前面例子中的一个标准的Edit菜单。
为了使用你刚才创建的菜单,必须在Contacts.c中增加一些代码。为了处理菜单事件,你必须增加一个叫MemuHandleEvent()的函数调用,在事件循环中处理菜单事件。

//CH.2 Handle system events
if (SysHandleEvent(&event))
continue;
//CH.3 Handle menu events
if(MenuHandleEvent(NULL,&event,&error))
contiue;
//CH.2 Handle form events
FrmDispatchEvent(&event);

在你事件处理器的循环中,以menuEvent函数调用替换ctlSelectEvent调用。使用它再去调用一个名为menuEventHandler()的函数。现在你新的事件处理器看起来像下面这样:

//CH.2 Our form handler function
static Boolean myHandlerEvent(EventType* event)
{
//CH.3 Parse menu events
if(event->eType= =menuEvent)
return(menuEventHandler(event));
//CH.2 We’re done
return(false);
}

现在来写menuEventHandler()函数。首先,你必须发信号给用户界面,报告菜单事件已经被接收:

//CH.3 Handle menu events
Boolean menuEventHandler(EventPtr event)
{
FormPtr form //CH.3 A pointer to our form structure
Word index //CH.2 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.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);
}

下一步,你得到了文本框指针后就可以调用那些非常好的编辑命令了。下面的例子显示了一个用最普通的方法来得到文本框指针,这个方法我们在先前的c ase frmOpenEvent:中用到过。无论窗体上有多少文本框,它都可以工作。

//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);

现在我们可以执行edit命令了。调用这些编辑函数非常简单并且它们处理每一件事都比较恰当。如执行Select All命令,你只要把整个字符串传递给FldSetSelection()函数就可以了。

//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 EditSelect All;
{
//CH.3 Get the length of the string in the field
Word length=FldGetTextLength(field);

//CH.3 Select the whole string
FldSetSelection(field,0,length);
}
break;
//CH.3 Bring up the kdyboard tool
case EditKeyBoard;
SysKeyBoardDialog (kbdDefault);
break;
}
//CH.3 We’re done
rerurn(true);
}

经过以上修改,make,debug,并运行你的应用程序。你就可以使用已创建的菜单和快捷方式了。

支持各种不同版本的Palm OS
事实上,上面的代码如果拿到Pilot 1000或者Pilot 1500这些使用Palm OS1.0版本的系统上运行的话就会使系统崩溃。这是因为在1.0版本中,SysKeyboardDialog()是一个于现在不同的函数调用。但也有好几种方法可以解决这个问题。首选就是换成调用函数S ysKeyboardDialogV10(),这是一个向后兼容的函数。除了最新的系统命令外,如果你还想了解更多的话,你就有必要检查一下OS的版本,这样可以基于O S版本来正确的调用函数。

如果我们多次遭遇由于OS版本不同带来的严重后果的话,我们在编程时将变得更加老练。现在就用SysKeyboardDialogV10()替换这个SysKe yboardDialog()函数调用。

//CH.3 Bring up the keyboard tool
case EditKeyboard;
SysKeyboardDialogV10();
break;

错误和警告
Contacts程序已经扩展了它的使用范围,用户对它做的某些操作可能导致错误发生。现在是一个极好的时机来讨论出错处理。

如果你执行编辑命令,就可以发现如果你做一些无意义的事,譬如在没有选中任何文本的时候复制,编辑函数也会产生一个警告声。不只是S elect All如此,因为你已经调用了一个普通函数,即便没有错误,它也会发出声音的。为了使Select All符合其它函数的发音标准,你可以加入如下的一个SndPlaySystemSound()函数:

//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(legth= =0)
{
SndPlaySystemSound(sndError);
return(false);
}
//CH.3 Select the whole string
FldSetSelection(field,0,length);
}

当用户的输入超越了被调函数的职能时,就应该提示用户他们可能做错了什么。对此,有一个很好的解决方法,就是使用Alerts。Alerts `是由操作系统控制的袖珍型窗体,创建和使用都非常的方便。

作一个试验:创建一个Alert资源,来显示Select All命令的错误信息:
1.启动资源构造器。
2.从资源类型列表中选中Alert。
3.按Ctrl-K来创建一个新的警告。
4.双击打开警告。
5.改变消息属性像“There was no text to select。”
6.改变Error的标题属性。
7.改变Error的警告类型属性。
为了调用这些Alert,我们加入FrmAlert()函数。你可以从Contacts_res.h文件中得到警告ID的变量名。

//CH.3 Pop up an error if appropriate
if(length= =0)
{
SndPlaySystemSound(sndError);
FrmAlert(SelectAllErrorAlert);
return(false);
}

加入上面的代码后,make,debug,并运行应用程序。可以发现,自从我们给错误设了警告类型,就有两次嘟嘟声:一个来自SndPlaySyst emSound(),一个来自警告。

另一个我们要放置Alert,至少是一次系统嘟嘟声的地方,就是当我们检查焦点的时候。事实上,当休眠状态的应用程序被唤醒的时候,文本框是获得焦点的首选控件。在一般情况下我们就想要在此时发一个经典的系统嘟嘟声来通知用户。
由于Select All的错误提示并不是经常会出现的,就让我们移走这个Alert资源。在以后的章节中,我们再使用Alert通知用户各种不同的错误信号吧。

下一步是什么?
在下一章中,我们将研究如何在你的应用程序中拥有多个窗体。

程序清单
这里是完整的Contacts.c程序清单。从hello.c改变的行都用//CH.3注解。
//CH.2 The super-include for the Palm OS
#include

//CH.3 Our resource file
#include “Contacts_res.h”

// CH.2 prototypes for our event handler functions
static Boolean myHandleEvent(EventPtr enent);
static Boolean menuEventHandler(EventPtr event);

//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

//CH.2 The main entry point
Dword PilotMain (Word emd,Ptr,Word)
{
FormPtr form; //CH.2 A pointer to our form structure*/
Handle hsrc; //CH.3 Handle to the string resource
Charptr psrc; //CH.3 Points to the text in the resource
Charptr ptext; //CH.3 Points to the text in the edit field
Word index; //CH.3 A general purpose index
FieldPtr field; //CH.3 Used for manipulating fields
EventType event; //CH.2 Our eent structure
Word error; //CH.3 Error word for menu event handler

//CH.2 If this in not a normal launch,don’t launch
if (cmd!=sysAppLaunchCmdNormalLaunch)
return(0);

//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 resource, 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);

//CH.2 Initialize our form
form=FrmInitForm(ContactDetailForm);
FrmSetEventHandler(form,myHandleEvent);
FrmSetActiveForm(form);

//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);

//CH.2 Our event loop
do
{
//CH.2 Get the next event
EvtGetEvent(&envet,-1);

//CH.2 Handle system events
if(SysHandleEvent(&event))
continue;

//CH.3 Handle menu events
if (MenuHandleEvent(NULL,&event,&error))
continue;

//CH.2 Handle form events
FrmDispatchEvent(&event);

//CH.2 If it’s a stop event,exit
}while(event,eType!=appStopEvent);

//CH.2 We’re done
return(0);
}
//CH.2 Our form handler function
static Boolean myHandleEvent(EvenType* event)
{
//CH.3 Parse menu events
if (event->eType= =menuEvent)
return(menuEventHandler(event));

//CH.2 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.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 fiedl
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 EditSelect All;
{
//CH.3 Get the length of the string in the field
Word length=FldGetTextLength(field);
//CH.3 Pop up 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 kdyboard tool
case EditKeyBoard;
SysKeyBoardDialogv10():
Break;
}
//CH.3 We’re done
rerurn(true);
}
第四章 窗体的编程
在本章中,我将通过为上一章的Contacts程序添加一个About对话框,继续向读者展示Palm OS简单有效的程序风格。你将学会如何创建含有位图、文本和按钮的窗体。你可在Contacts程序中加入菜单和代码使你可以访问About对话框并重新回到Contacts主窗体。

在此过程中,我们将先了解应用程序的基本设置,将为Contacts程序创建一个大的和一个小的应用程序图标。

程序设置
我们在这部分中将改变一些对整个程序都有影响的设置。同时还将为Contacts创建图标。

首先,为你的程序做一个备份:
1. 打开Windows文件浏览器;
2. 找到Contacts文件夹;
3. 复制并粘贴Contacts;
4. 命名新文件夹为Contacts CH.3。这就是你的备份文件夹。

创建大小应用程序图标
先查看一下资源构造器的工程设置(Project Settings)。

注意:
Code Warrior6所带的构造器版本有一个bug――不允许创建和编辑多位(multibit)图标。我们找到了一个解决的办法,在本书附带的CD里面找到文件icon.txt,里面写有如何解决此问题的步骤。
1. 运行资源构造器,打开Contacts.rsrc;

2.在Contacts.rsrc窗口的底部找到工程设置图标。如果在窗口的底部只有一个指向右方箭头,单击则工程设置就会弹出。在工程设置中一般要改变的有:应用程序图标的名字、版本字符串和应用程序图标。在上一章节中你已经改变了应用程序图标的名称。表4-1列出了所有设置极其功能:

名称 描述
Generate App Resources 为应用程序产生版本和图标名称的资源,我建议你大多情况下选中次复选框。

Application Icon Name 应用程序的名称

Version String 应用程序的版本号。当对你的应用程序做出有意义修改后,你应该提高版本号表示不同版本。

Application Icon 定义了黑白的程序大图标,这是为Palm OS 2.0和更早版本准备的,在这里不用。

Auto Generate Header File 资源构造器自动生成头文件。选中。

Include Details in Header 资源构造器向头文件中添加
内容。选中。

Keep Ids in Sync 使资源构造器在控件ID改变时自动更改ID。如果你在程序中不大改变ID,你可以不选;但在一般情况下,选中会更好。

表4-1:资源构造器中工程设置的选项。
3. 创建应用程序大图标。从资源列表中选择Multibit Icon并按CTRL-K。一定要保证资源ID为1000。还要注意如果按下了应用程序图标属性边上的创建(Create)按钮,那将生成标准图标而不是多位图标。虽然这样也能产生大图标,但不够理想。

4.双击新图标,弹出一个编辑窗口,你现在就可以在里面画图了。通过选择编辑窗口右端的两个图标,确定颜色为黑白(black and white)还是2位的灰度级。编辑窗口内的编辑控件和Windows下的画图(Paint)及其它作图程序都很相似。当完成后,按窗口顶部右端的X即可关闭窗口。图4-1是本人所画的大图标;

图4-1 一个Contacts程序中的大图标

5. 接下来创建一个应用程序小图标。它只能在Palm OS 3.0或更高版本显示。打开Contactas.rsrc窗口,在Resource Type 和Name面板上选择Multibit Icons;

6. 按下CTRL-K创建一个新图标;

7. 点击ID,将其改为1001,这一步十分重要;

8. 点击新图标。弹出图标编辑窗口,由于Palm OS只显示上部9象素和最左端的15象素,在设计时要注意。本人所画的小图标如图4-2所示;
图4-2 一个Contacts程序中的小图标


CodeWarrior集成开发环境中的工程设置

这部分所讲的是如何在使用应用程序的设置:
1. 行CodeWarrior集成开发环境,打开Contacts工程;
2. 选择Edit|Stater Settings。程序设置还叫Stater Settings的原因是自从我们建立Starter工程后还没有将它改名。在Stater Settings会发现大量的设置选项,你千万不要把它们搞混;
3. Stater Setting 对话栏的左边是设置结构树。选择Target子树下的Target Setting,将Target Name中的名字由Starter改为Contacts;保存,现在CodeWarrior集成开发环境将会调用工程Contacts;
4. 在Target子树下选中68K Target,将文件名从Starter.tmp改为Contacts.tmp;
5. Linker子树下选中PalmRez Post Linker。将Mac Resource Files 设置由Starter.tmp改为Contacts.tmp;将Output Files设置由Starter.prc改为Contacts.prc,将Database Name设置由空白添加Contacts-PPGU;

注意: 如前所述,在Palm设备中所有东西都是以数据库的形式保存的,应用程序也不例外。每个数据库都必须有唯一的名称,否则就会出现问题。Contacts-PPGU就是和它的唯一原始(creator)ID相关联的文件名,这样命名可以保证唯一性。有关更多原始ID的内容,请参看下一章。

在Contact Settings子树下还有很多其他的设置属性,但是这时候还没必要去设置它们。如果你感兴趣的话,可以参考CodeWarrior集成开发环境指南(Guide)的有关内容。


多窗体界面
在这部分里我们将再为Contacts程序添加一个窗体:About窗体。我们也将修改Contacts.c中的代码使窗体之间可相互切换。

Contacts.rsrc文件中内容的添加
我们将为资源文件添加一个About窗体:
1. 打开资源构造器,打开文件Contacts.rsrc;
2. 建About窗体。从资源文件列表中选择Forms并按下CTRL-K;
3. 命名窗体。单击名字Untitled改为About;
4. 击打开About窗体;
5. 找到Form对话面板左边的窗体属性。表4-2列出了窗体的各个属性极其用途;
6. 修改窗体属性。复选Save Behind,写入About Contacts。

表4-2 窗体属性

Left Origin 窗体的左侧位置的象素数,整个屏幕为160象素。

Top Origin 窗体的顶部位置的象素数,整个屏幕从上到下为160象素。

Width 窗体的宽度,窗体不一定占满整个屏幕。

Height 窗体的高度。
Usable 决定窗体是否可见,如果对象没有标识为Usable,则它是不可见的。窗体一般情况下标识为Usable。
Modal 如果窗体为工作窗体且选取Modal,则在窗体外的笔击事件不起任何作用。Modal窗体的名字在窗体顶部的中央。Modal窗体不会被诸如警告之类的系统对话框中断,所以使用时要慎重。

Save Behind 如果选中,窗体关闭后,对窗体操作前的屏幕上窗体后的内容将被保存。

Form ID 窗体的ID号,ID唯一标识窗体。

Help ID 如果窗体为Modal,可填入代表帮助信息资源文件的ID字符串,在窗体的右上角会出现一个“i”图标。如果按下,则帮助信息就会被调出。在Date Book应用程序中的Details窗体有一个这样的例子可供参考。

Menu Bar ID 窗体菜单栏的ID号,在最后一章节中,我们将利用这一属性添加一个菜单拦。

Default Button ID 如果提供了此ID号,当用户切换的其它应用程序时,Palm OS在退出前会自动按下此按钮。这对Modal窗体来说尤其方便,经常使用的缺省按钮为Cancel。
Form Title 如果为窗体提供了标题,Palm OS会创建写有你设定标题的一个标题栏。

添加位图
为About窗体添加一个位图,步骤如下:
1. 选择Resource Type and Name列表下的Bitmaps;
2. 按CTRL-K创建新位图;
3. 在构造器中,选择Options | Set Image Size。可让你调整位图的大小;
4. 调整位图大小为42x42;
5. 绘制位图。我做的图如图4-3所示。用构造器的编辑器做出如此大的图是很困难的,我是先使用了画笔(Paint)绘好后然剪切过来的;
6. 绘完后。单击右上角的X关闭位图编辑器;
7. 将位图添加到Contacts窗体。选中Window | Catalog打开Catalog窗口。从其中拖动一位图到About窗体;
8. 改变位图的属性。位图的属性意义十分直观。请参考表4-3;
9. 将Bitmap Resource ID置为前面部分创建的位图的资源ID号。它的ID号应为1000。此时,你应在位图出现的一般位置看到自己的位图;
10. 设定位图在窗体中的Left Origin和Top Origin。置位图左侧位置为59(让它处于窗体中间)顶部位置为20(让它在标题栏下)。

Object ID 在窗体中标识位图唯一的ID号。此号不能和应用程序中其它任何的ID号相同。

Object Identifier 位图的名称。

Left Origin 位图左边位置的象素数,全屏幕左右长度为160象素。

Top Origin 位图顶部位置的象素数,全屏幕上下长度为160象素。

Bitmap Resource ID 决定位图的资源文件的ID号。

Usable 决定位图的可见与否。只有当标识为Usable时,位图才可见。

添加标签

现在创建一个包含Contacts信息的标签。你可以从Catalog窗口把一个标签拖到窗体上。表4-4列出了标签对象的属性。

Object Identifier 标签名。构造器在Contacts_res.h文件中创建的变量就来源于此名。

Label ID Palm OS用来识别该对象的唯一ID号。

Left Origin 标签左边位置的象素数。全屏幕左右长度为160象素。标签的长度由它包含的文字数决定。

Top Origin 标签顶端位置的象素数。全屏幕上下长度为160象素。标签包含文字的行数和字体的大小将决定标签的高度。

Usable 决定标签是否在窗体上可见。如果未选中的话,标签为不可见。当你想不改变窗体而使控件可见或不可见时,就要用到此设置。

Font 标签内文字字体的大小。你可以通过Windows Key

Caps 实用工具查找你所需字体。

Text 标签内显示的文字。

在标签中的内容一般为应用程序的名字、你或你的公司的名称、版权信息和程序的版本号等。我创建了两个标签是为了使Contacts的字体比剩下的字体大一些。
由于位图和标签在代码中没被提到,因此再没有比资源构造器更能满足你对不同位图和标签的要求了。我通过Windows Key Caps 实用工具从Pilot基本字体中找到了版本符号,并且复制字符到了资源构造器中。

添加按钮
在About窗体的底部创建一个按钮。你可把它命名为OK或任何其它的名字。
当Contacts.c修改后,此按钮的功能是关闭About窗体并返回到Contacts窗体。因为我们除了在初始的例子接触到按钮外,还没有正式介绍过,所以下面详细介绍一下按钮的各个属性。

Object Identifier 按钮的名称。构造器在

Contacts_res.h文件中创建的变量就来源于此名。

Button ID Palm OS用来识别该对象的唯一ID号。

Left Origin 按钮左边位置的象素数。全屏幕左右长度为160象素。由于按钮的左侧边到窗体的边框至少需一个象素,所以按钮的最左端也应置1。对bold按钮来说,要需二个象素。

Width 按钮的宽度。而实际的宽度,对普通按钮来说应加2,对bold按钮来讲应加2。

Height 按钮的高度。而实际的宽度,对普通按钮来说应加2,对bold按钮来讲应加2。
Usable 决定按钮是否在窗体上可见。如果未选中的话,按钮为不可见。当你想不改变窗体而使控件可见或不可见时,就要用到此设置。

Anchor Left 决定当应用程序运行时,在改变标签长度时,按钮的大小怎样作自我调整。当被选中时,则按钮的右端会做相应的伸长或缩短;否则,按钮的左侧作相应的伸长或缩短。

Frame 如果被选中,则按钮有一边框。

Non-Bold Frame 如果选中,则按钮边框占一象素;否则,边框占二象素宽。

Font 按钮上文字字体的大小。你可以通过Windows Key

Caps 实用工具查找你需要的字体。

Label 按钮上显示的文字。
把按钮命名为OK。当设置完所有属性后,你将看到一个图4-4所示的图形界面。

添加菜单
通过对Option菜单条目的选择可以访问About窗体。创建Option菜单并加入Contacts菜单栏。步骤如下:
2. 在Resource Type and Name List中选择Menus,按CTRL-K创建一新菜单;
3. 单击name区,将其命名为Options;
4. 双击Options打开;
5. 在菜单栏中将标题改为Options;
6. 按下CTRL-K创建一个新菜单。命名为About Contacts;
7. 关闭Options菜单;
8. 双击打开Contact Detail菜单栏;
9. 将Options菜单从Resource Type and Name List中拖到Contact Detail菜单栏Edit菜单的后面。当你做完后,你的Contacts菜单应如图4-5所示;
10. 关闭并保存Contacts.rsrc。
图4-4:当前窗体外观

向Contracts.c添加代码
如要使多个窗体运转,我们需要在Contracts.c中添加代码和其他的一些部分。首先添加的是一个Option菜单,同时我们也要为这个菜单添加一些代码。

Form-Loading代码更广的应用
因为我们要加载多个窗体,所以我们现在修改form-loading代码以便更加容易操作。
在文件的头部,将myHandleEvent()函数名改为contactDetailHandleEvent(),因为这个事件仅用于Contact Detail 窗体。修改之后的函数的原型如下:

// Prototypes for our event handler functions
static Boolean contactDetailHandleEvent( EventPtr *event );

myHandleEvent()函数的头部应修改如下:


// Our Contact DETAIL form event handler functions
static Boolean contactDetailHandleEvent( EventPtr event )
{

下一步,我们处理一个名为frmOpenEvent的事件。当从一个窗体切换到另一个窗体时,该事件被发送。在修改事件处理器以切换窗体之后,我要说明怎样修改窗体。

Word index; // CH.3 A general purpose index
FieldPtr field; // CH.3 Used for manipulating fields

// CH.3 Get our form pointer
form = FrmGetActiveForm();

// CH.4 Parse events
switch( event->eType )
{
// CH.4 Form open event
case 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 );
}
break;

frmOpenEvent块中,剪切从PilotMain()至事件循环的代码,把代码放到这里。这样,当我们在窗体之间切换时,每当窗体被显示时这段代码将被执行。在执行应用程序时,这
段代码将被执行若干次。

在我们切换窗体之前,最好先获取源窗体文本框中的内容。这是因为当我们切换到一个新的窗体时,与原窗体相关的所有内存都将被释放。这样一旦我们返回原窗体时就会碰到大麻烦。

我们通过对frmCloseEvent的处理来获取文本句柄。在切换到另一个窗体时,当前窗体被注销前,frmCloseEvent事件被触发。

// CH.4 Form close event
case frmCloseEvent:
{
// 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.4 Unlink our handle from the field
FldSetTextHandle( field, NULL );
}
break;

我们在这里所做的一切是获取指向域的指针,将它的文本句柄设为NULL。

我们需要用一个将导致frmOpenEvent被发送的调用来替换我们从PilotMain()中剪切下来的代码。如果我们不这样做,什么事情都不会发生。我们将永远盯着一个空白屏幕,因为第一个窗体永远不会被加载。你用来发送frmLoadForm和frmOpenForm事件来切换窗体的函数是FrmGotoForm()。在PilotMain()中FrmGotoForm()之外的代码如下:

//CH.3 Release the string resource
DmReleaseResource(hsrc);

//CH.4 Go to our starting page
FrmGotoForm(ContactDetailTorm);

//CH.2 Our event loop
do
{

用于推广窗体加载的最后一点代码是将事件处理函数从一个窗体切换至另一个窗体。每一个窗体有它自己的事件处理。一个好的方法是应答frmLoadForm事件,即使是在主事件循环中。代码如下:

// 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 );


对每一个我们要加载的窗体,我们在switch语句中增加case语句来初始化窗体并设置对应的事件处理。

现在是为我们在上一章中分配的内存在做一些工作的时候了。尽管当应用进程从内存卸下时我们的文本句柄将被释放,我们最好还是在PilotMain()末尾显式卸载它,因为关闭窗体不再释放其内的文本句柄。

// CH.4 Deallocate memory
MemHandleFree( htext );

// CH.2 We're done
return( 0 );

程序的整洁性是与应用进程是否崩溃的相关的重要因素。当你的程序变得越来越复杂,你的程序代码到处搬动时,有序地分配和释放内存将为你免去应用程序崩溃之灾,为你节约大量时间。

处理Options菜单
我们将按惯例选择菜单项来获得About窗体。因为所有的菜单ID都是唯一的,你仅需在现有的菜单时间处理中为Contacts添加一些代码:

// 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 );
}

在调用MenuEraseStatus()之后在MenuEventHandler()中增加的代码将正常运转。你检查新选择项的ID,然后调用FrmPopusForm()。FrmPopusForm()与FrmGotoForm()相似,除了旧的窗体从不关闭。这在About窗体中是恰当的,因为我们知道我们将从About窗体中返回到Contact Detail窗体中。除此之外没有别的路径。

这意味着我们不需要作我们在上一章所做的通用的窗体变换工作。尽管如此,我们最好加上窗体变换代码,这样我们不必担心在加入窗体时使Contact Detail窗体的行为变乱。

注意:在这里我们在处理代码中原本为了处理edit菜单的options菜单。这是怎么回事?原来,Contructor(构造器)给所有菜单的所有菜单项赋予唯一的ID号。因此,没必要单独处理某一组菜单项。

为About窗体加入一个事件处理

每一个窗体应有它自己的时间处理,因此我们为新的About窗体添加一个事件处理。首先,为About窗体的事件处理函数添加一个函数原型,称之为aboutHandleEvent()。代码如下:

// CH.3 Prototypes for our event handler functions
static Boolean contactDetailHandleEvent( EventPtr event );
static Boolean aboutHandleEvent( EventPtr event );
static Boolean menuEventHandler( EventPtr event );

然后,在处理frmLoadEvent的事件循环中加入代码。这段代码将对About窗体初始化并以FrmDispatchEvent()为目标加入事件处理函数。

// 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;
}

剩下的所有的事就是加入时间处理函数aboutHandleEvent()。

// 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 );
}

事件处理函数对frmOpenEvent应答以唯一的显示窗体基本调用:FrmDrawForm()。它也像我们的第一个Hello应用程序一样应答ctlSelectEvent,除了在这一次,它不发送告警,而是调用FrmReturnToForm()。FrmReturnToForm()函数使About窗体被消灭,该窗体的调用窗体(在这里是Contact Detail窗体)被重激活。

注意:在返回调用窗体时返回true是十分重要的。原因是否则Palm OS试图全面处理cltSelectEvent事件,处理工作包括处理在调用FrmReturnToForm中被你消灭的结构。为了避免这些内存恶作剧,在FrmReturnToForm之后总是返回true, 这样窗体会立即消失。

调试
又回到调试了。你的Contact窗体看起来和工作起来和以前一样。当你从Options菜单选择对应项时,你的About窗体会弹出。当你按下它的OK按钮时,窗体返回Contact Detail窗体。Contact Detail窗体含有一个包含原有的Edit菜单和新的Options菜单。About窗体没有菜单条因为我们没有定义它。

如同以前一样,程序在PilotMain()开始执行,按照它通常的路径走下去。当frmLoadEvent
事件被处理时,它初始化Contact Detail窗体的事件处理函数。之后,frmOpenEvent事件被发送到Contact Detail窗体的事件处理函数。在那里,事件被处理,窗体被画出,各个域如以前一样被设置。然后程序等在那里等待事件的发生。
Contact Detail 窗体处于等待输入状态

接下来讲什么?
在下一章中,我们将继续考察更多的基于Palm设备的控件。

源代码
下面是新版本的Contacts.c的所有代码。

// CH.2 The super-include for PalmOS
#include

// CH.3 Our resource file
#include "Contacts_res.h"

// CH.3 Prototypes for our event handler functions
static Boolean contactDetailHandleEvent( EventPtr event );
static Boolean aboutHandleEvent( EventPtr event );
static Boolean menuEventHandler( EventPtr event );

// 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

// CH.4 Constants for ROM revision
#define ROM_VERSION_2 0x02003000
#define ROM_VERSION_MIN ROM_VERSION_2

// 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
Handle hsrc; // CH.3 Handle to the string resource
CharPtr psrc; // CH.3 Points to the text in the resource
CharPtr ptext; // CH.3 Points to the text in the edit field
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.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 );

// 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.4 Deallocate memory
MemHandleFree( htext );

// CH.2 We're done
return( 0 );
}

// CH.4 Our Contacts form handler function
static Boolean contactDetailHandleEvent( EventPtr event )
{
FormPtr form; // CH.3 A pointer to our form structure
Word index; // CH.3 A general purpose index
FieldPtr field; // CH.3 Used for manipulating fields

// CH.3 Get our form pointer
form = FrmGetActiveForm();

// CH.4 Parse events
switch( event->eType )
{
// CH.4 Form open event
case 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 );
}
break;

// CH.4 Form close event
case frmCloseEvent:
{
// 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.4 Unlink our handle from the field
FldSetTextHandle( field, NULL );
}
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 );
}
第五章 数据库
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;
}
第六章 控件
在这一章中,我们将继续研究Contact Detail 程序,让它具有显示并设置日期时间的功能。我们要添加一个窗体来设置时间。这个时间和日期是可选的以供下次联系使用。

为实现此功能,我们将使用新的资源:选择触发器(selector triggers),开关按钮(push buttons),重复按钮(repeating buttons)。连同前面的按钮,它们都是Palm OS的控件。它们具有类似的属性,并且在触发时发出相同的事件。它们都可拥有自己的标签,它们都是被单击触发的。在单击后它们的形状都有所改变,不过有的只是瞬间改变就恢复了而已。

保存你的工程

当在修改工程之前,最好先制作一个它的副本。这样当出现问题时,你就可以拿出的工程的副本重新开始。步骤如下:

1. 打开Windows 资源管理器;
2. 找到工程所在的文件夹;
3. 选中工程,按下CTRL+C拷贝文件夹;
4. 选择想要保存到的文件夹;
5. 按下CTRL+V保存;
6. 将工程重新命名,以便你能清楚记忆。我将其命名为Contacts CH.5。

对Contatcs.rsrc文件内容的添加

这一部分我们为Contact Detail添加日期和时间的控件。我们还将创建一个用来改变时间的窗体。对改变日期,我们将调用Palm OS的标准对话框。

添加日期时间选择触发控件

向Contact Detail窗体添加两个标签和两个选择触发器(selector triggers)。我们将使用选择触发来显示下一次调用此contact的日期和时间。选择触发控件处理事件和按钮很相似,只是外形有很大不同。它被一个点壮矩形所环绕。和按钮相比,这个矩形在宽度和高度上都占有一个象素的额外空间,这一点在放置此控件时要考虑。有关选择触发器(selector triggers)的属性见表6-1。


Object Identifier 构造器用来代表资源头文件ID的常量

Selector Trigger ID 选择触发控件的资源ID;

Left Origin 水平方向上控件的最左端位置;

Top Origin 垂直方向上控件的最顶端位置;

Width 控件的最大宽度。此属性很少使用,因为控件的右
边界会随着标签文本的长度改变而改变;

Height 控件的高度;

Usable 决定次控件是否可见能用。如果没有选中,也可在通过函数调用来实现其可见;

Anchor Left 决定当文本长度改变时,控件的左侧或右侧是否做相应的伸缩;

Font 标签使用的字体;

Label 标签的缺省文本;


以下是添加控件的步骤:
1. 打开资源构造器;

2. 打开文件Contacts.rsrc。它在Src文件夹中;

3. 双击Contacts Detail窗体;

4. 选择Window | Catalog,产生控件模板;

5. 将一个标签拖到窗体上。置标签文本为Next Call Date。将它放在Phone Number 标签底下。设置Left Origin为0、Top Origin为60,文本字体为粗体;

6. 将一个选择触发器(selector triggers)拖到窗体上。设置Object Identifier为Date,Left Origin为81,Top Origin 为60,Width为78。向标签输入10个空格,这样可保证在缺省的情况下,当被finger按下时有充足的空间;

7. 向窗体上再拖一个标签。置标签文本为Next Call Time。将它放在Next Call Date 标签底下。设置Left Origin为12、Top Origin为80,文本字体为粗体;

8. 向窗体上再拖一个选择触发器(selector triggers)。设置Object Identifier为Time,Left Origin为81,Top Origin 为80,Width为78。也向标签输入10个空格。

9. 添加控件后的窗体如图6-1所示。按下右上角的X按钮,关闭窗体。

图6-1:Contact Detail 窗体
创建一个新的设置时间窗体

现在创建一个窗体:

1. 点击资源(resource)中的窗体(Forms)选项,按下CTRL-K创建一个新的窗体;

2. 点击name框并重命名为Enter Time;

3. 双击打开窗体进行编辑;

4. 首先设置窗体属性。复选中属性Modal和Save Behind。我们将此窗体作为对话框的形式出现;此窗体是我们接触到的第一个不是全屏显示的窗体;我们修改其宽度(Width)为156,其高度(Height)为53;

5. 为实现modal边框可见,我们需要在窗体和屏幕边界留出2个象素的宽度。所以宽度设为156,而左初始边界应为2,顶端初始边界应为105,这样就保证了两个象素的余度;

6. 修改窗体的名字(Name)属性为Enter Time。

添加开关按钮(push buttons)

开关按钮(push buttons)在表现为按下的状态时,不同于通常淡的底色和黑的文字,而是黑的底色和淡的文字。我们使用开关按钮(push buttons)显示小时、分钟、和上午/下午(AM/PM)。在放置开关按钮(push buttons)时,我们必须考虑它的边界所占用的一个象素的宽度。开关按钮(push buttons)的属性如表6-2所示:
Object Identifier 资源构造器用来代表资源头文件ID的常量

Push Button ID 开关按钮(push buttons)的资源ID

Left Origin 水平方向上按钮的最左端位置;

Top Origin 垂直方向上按钮的最顶端位置;

Width 按钮的宽度

Height 按钮的高度

Usable 用来定义控件是否可见及可用,如果没有选中,也可在通过函数调用来实现其可见

Group ID 表示当按钮按下时是否突出 显示。如果此数字为0,则按钮当被按下是将在“按下”和“没有按下”两个状态间切换。当此数字不为0时,按钮被按下是保持原来的状态。在每一组中所用的组(Group)ID应是唯一的,因为在后面的代码中将使用到组ID

Font 标签显示文字的字体

Label 标签本身的文字

添加步骤:
1. 将一个开关按钮(push buttons)拖到Enter Time 窗体上;

2. 既然此按钮显示时间,可将Object Identifier设置为Hours。设置属性:Left Origin=5,Top Origin=17,Width为18,Group ID为1。Font属性为Bold,清除标签内的文字;

3. 复制Hours开关按钮(push buttons)。可选中Hours开关按钮(push buttons)后,按下CTRL-D进行复制。修改Object Identifier为MinutesTens。设置属性:Left Origin=34,Top Origin=17,Width为12,因为它只包含一个数字;

4. 复制MinutesTens按钮。可选中MinutesTens按钮后,按下CTRL-D进行复制。修改Object Identifier设置为MinutesOnes。设置属性:Left Origin=50,Top Origin=17;

5. 再拖动一个开关按钮(push buttons)到窗体上。修改Object Identifier设置为AM。设置属性:Left Origin=109,Top Origin=17,Width为20,Group ID为2。设置标签内的文字为AM;

6. 复制AM开关按钮(push buttons)。修改Object Identifier为PM。设置属性:Left Origin=130,Top Origin=17,注意AM 按钮和PM按钮重叠是为了使他们之间只有一个象素的间隔,这就是相关的开关按钮(push buttons)如何归为一组的方法。设置标签内的文字为PM;

名字 描述
Object Identifier 构造器用来代表资源头文件ID的常量。

Push Button ID 开关按钮(push buttons)标志号。

Left Origin 开关按钮(push buttons)的左边缘的水平起始位置。

Top Origin 开关按钮(push buttons)的顶边缘的垂直起始位置。

Width 开关按钮(push buttons)的宽度。

Height 开关按钮(push buttons)的高度。
Usable 该参数定义该控件是否可视和被激活。如该参数未被选择,你可通过一函数调用使该控件可视和被激活。

Group ID 该参数影响开关按钮(push buttons)被选时是否仍保持加亮状态。如该参数为0,开关按钮(push buttons)被按下时,其状态在“on”和“off”之间切换。如果你要在程序中使用组号,则该组号应为唯一。

Font 标签显示文字的字体

Label 标签本身的文字

向设置时间窗体添加重复按钮(repeating buttons)
如果输入笔按在Repeating按钮上的时间超过半秒,该按钮将连续发ctlRepeatEvent事件。头半秒之后,ctlRepeatEvent事件每十分之一秒发一次。我们使用重复按钮(repeating buttons)来构造Up和down箭头以调整时间。如同一般的按钮一样,重复按钮(repeating buttons)可有一边框。在我们的事例中,这些按钮不使用边框,因此,它们像标签和文本框一样排列。

重复按钮(repeating buttons)的属性如下表:

名字 描述

Object Identifier 构造器用来代表资源头文件ID的常量。

Button ID 重复按钮(repeating buttons)标志号。

Left Origin 按钮的左边缘的水平起始位置。

Top Origin 按钮的顶边缘的垂直起始位置。

Width 按钮的宽度。

Height 按钮的高度。

Usable 该参数定义该控件是否可视和被激活。如该参数未被选择,你可通过一函数调用使该控件可视和被激活。

Anchor Left 如该特性被选择,按钮被程序重定大小时向右扩张。

Frame 如该特性被选择,按钮将有一边框。

Non-Bold Frame 如该特性被选择,按钮边框粗细为1像素。

Font 标签显示文字的字体

Label 标签本身的文字

向设置时间窗体添加一个Repeating按钮
1. 将一个Repeating按钮拖放至设置时间窗体。将Object Identifier改为TimeUp。参考位置为Left Origion = 109, Top Origion = 17。参考大小为Width=20,Height =8。不选Frame特性。

2. 选字体为Symble 7,在label上选Hex box。键入01。这样,你在form上看到的是空白标签,而运行代码后将显示一个向上的箭头。不要被hex 21所迷惑。当你运行代码时,hex 21显示为一个复选框。

3. 通过选择Repeating按钮并按CTL-D来复制该按钮。将将Object Identifier改为TimeDown。此按钮的位置应为Left Origion = 69, Top Origion = 25。将label设置为hex 02,则程序运行时显示一个向下的箭头。

向设置时间窗体添加一个复选框
复选框的左端有一个小框可被选择以指示某些事。在设置时间窗体中,该小框可让用户选择不输入时间。复选框没有边框,所以它们的排列如同一个域一样。复选框的属性如下表所示:

名字 描述

Object Identifier 资源构造器用来代表资源头文件ID的常量。

Checkbox ID 复选框标志号。

Left Origin 复选框的左边缘的水平起始位置。

Top Origin 复选框的顶边缘的垂直起始位置。

Width 复选框的宽度。

Height 复选框的高度。

Usable 该参数定义该控件是否可视和被激活。如该参数未被选中,你可通过一函数调用使该控件可视和被激活。

Selected 如该参数被选择,则复选框被画时缺省为被选。

Group ID 该参数影响复选框被选时是否仍保持加亮状态。如该参数为0,复选框被按下时,其状态在被选和未选之间切换。如果你要在程序中使用组号,则该组号应为唯一。

Font 标签显示文字的字体。

Label 标签本身的文字

现在向设置时间窗体添加一个复选框:
1. 从Catalog窗拖放一个复选框到设置时间窗体。

2. 将Object Identifier属性设置为NoTime。参考位置为Left Origion = 53, Top Origion = 37。设Width为50。选Selected。设Group ID为0因为并未成组。设Label为NoTime。

完善设置时间窗体
现在让我们来添加一些熟悉的控件:
1. 小时和分钟之间用冒号隔开会更好看。从Catalog窗体拖放一个label控件到设置时间窗体中。位置为Left Origion = 27, Top Origion = 17。设字体为Bold。加一个冒号。

2. 每一个对话框都需要一个OK按钮。从Catalog窗体拖放一个button控件到设置时间窗体中。位置为Left Origion = 5, Top Origion = 37。注意Left Origion设为5使按钮与其上的push按钮对齐,并在左边缘留出4象素的空间。

3. 有一个Cancel按钮也会很好。拖放一个button控件到窗体中。改设Object Identifier为Cancel。其位置为Left Origion = 115, Top Origion = 37。改设Label为Cancel。

4. 使Cancel按钮为此窗体的缺省按钮。记下Cancel按钮的Button ID。点击窗体背景的任意处以显示设置时间窗体的属性表。将窗体缺省按钮的ID设为Cancel按钮的Button ID。

你已经完成了设置时间窗体的构建。你的窗体应该如下图所示。点击右上角的X来关闭窗口。选File | Save来保存你的改变。

向Contacts.c添加代码

现在,为了在数据库、控件以及我们才添加的新窗体中支持日期和时间,我们向Contacts.c添加代码。

在数据库中初始化和保存日期和时间

为了在内部和数据库中保存和定义日期和时间,你需要一些变量和常量的定义。

// 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

变量dateTime保存目前正被处理的记录的日期和时间。设置时间窗体使用timeSelect变量完成同样功能。常量NO_TIME和NO_DATAE同样使用dataTime来表示没有日期或没有时间或二者兼备。

在newRecord()功能中,加入代码设date和time的初始状态为没有日期和没有时间。

// 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 ) );

注意我们使用MemSet()将整个记录清零。如果我们不这样做,记录中的域将有垃圾并且域功能将崩溃,因为我们并没有如域功能希望的那样发送以零定界的字符串。然后,我们把变量dateTime作为临时变量来初始化该记录。
图6-2:EnterTime 窗体

在setFields()功能中,从记录中载入dateTime变量的值。
// CH.6 Initialize the date and time variable
precord = MemHandleLock( hrecord );
MemMove( &dateTime, precord + DB_DATE_TIME_START,
sizeof( dateTime ) );
并且,设置日期和时间(selector triggers)的外观。我们将在讨论选择触发器时详细研究这些功能。

// CH.6 Initialize the date control
setDateTrigger();

// CH.6 Initialize the time control
setTimeTrigger();


支持日期和时间选择按钮

加入的第一行代码应是Contact Detail窗体事件处理句柄,对日期选择按钮来说,然后再添加向处理普通按钮那样事件处理过程。

// 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(),
&currentDate );

// 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;

如果以前没有填入时间,把时间设为当前时间。获取当前时间是通过调用函数TimGetSeconds()并通过函数
TimSeconsToDateTime()将其输出转换为日期。这些函数是时间处理器的一部分,在Palm OS的Reference.pdf文件中有详细的描述。

时间初始化完成后,我们调用函数SelectDay()以产生Palm OS内置控件来选择日期。当选定时间后,我们锁定数据库当前记录,将新的时间值写入。

选择时间有些不一样。因为Palm OS中没有可以直接选择时间的控件,那么我们弹出Enter Time 窗体来和用户交互。

// CH.6 Time selector trigger
case ContactDetailTimeSelTrigger:
{
// CH.6 Pop up our selection form
FrmPopupForm( EnterTimeForm );
}
break;

在Contact Detail事件处理过程中我们所做的就是调出Enter Time 窗体。修改数据库的工作就由后者来处理。
函数setDateTrigger()用来更新日期选择按钮的外观,代码如下:

// 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;
}

如果没有时间,我们控件中写入10个空格。由于选择触发按钮能够根据标签自动调整大小,这样就可保证触发按钮足够大可用手指来选择。记住一定要保证控件标签(包括触发按钮)中的文本大小不要超过最初在构造器中定义的数量。

如果已有时间,那我们返回时间。系统内的优先权将决定我们使用什么格式来显示时间。我们调用带参数preDateFormat的函数PrefGetPreference()来获取日期的优先权。我们选择类型DateFormat的原因是preGetPreference()能够返回许多不同的优先权,我们选最普遍的一种。

函数DateToAscii()用来将日期变量转换为我们定义的短时间格式,这是由我们从系统中获得的。当我们获得时间后,我们就将其写入触发按钮标签内。

函数SetTimeTrigger()用来设置时间触发按钮。它和setDateTrigger()很相似,除了用它自己相应的函数外。
支持开关按钮(push buttons)

像在Contact Details所做的那样,我们也将建立switch语句来处理ctlSelectEvent。Case语句建立在不同button ID值上,它可用真正的控件ID来表示。

首先来处理小时和分钟开关按钮(push buttons):

// 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;

按钮将在它们的标签上显示各自的值。哪一个值和上或下箭头相关联是根据哪一个被选中决定的。如果当按钮被选中后但没有时间显示,12PM将被设置并显示。如果先前已选择了一个按钮,那么我们先清除这个选择,然后选中我们刚刚点击的那个按钮。

在函数setTimeControl()中,我们看它是如何设置标签文本的。实际上,它和其他任何控件处理标签一样。

// 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 ) );

时间是以24小时格式显示,我们把它转换为12小时格式并在按钮上显示。

在函数enterTimeHandleEvent()中也有两个Push按钮,用来进行AM/PM设置。它们的代码如下:

// 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;

在AM case语句中,如果没有时间,那么我们设置时间为12 AM;如果时间是PM,那么我们从24小时格式中减去12而变为AM。

PM按钮处理和AM差不多。
在setTimeControls()中显示AM和PM,代码如下:

// CH.6 Update AM
CtlSetValue( amButton, (dateTime.hour < 12) );

// CH.6 Update PM
CtlSetValue( pmButton, (dateTime.hour > 11) );

在需要布尔值的地方根据其逻辑变换布尔值是个很有意思的事。

支持重复按钮(repeating buttons)

Repeating按钮允许时间被增加或减少。为了使其有效,我们必须像处理ctlSelectEvent那样处理ctlRepeatEvent。在函数enterTimeHandleEvent()我们必须也要从事件句柄那里返回false,否则就不能产生repeating 事件。

// 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;


如果没有时间显示,上下箭头不做任何事情。有时间显示时,根据所选择的开关按钮(push buttons),重复按钮(repeating buttons)将增加小时、分钟的十位、分钟的个位。其中也处理了必要的循环情况。

减少按钮和增加按钮基本相似。

支持复选框

复选框的处理和其它按钮一样。当被触发时,产生一个ctlSelectEvent。

// 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;

为方便起见,如果没选复选框,我们将选中小时Push按钮,然后其它的事由函数setTimeControl()来处理。当选中复选框时将清空所有的控件内容。

结束Enter Time窗体
还有一些其它的窗口处理事件需要讨论。在enterTimeHandleEvent()中处理frmOpenEvent。

// 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;


当打开窗体时,我们用函数setTimeControls()初始化窗体。我们也保存了当前时间以便按下了cancel能够恢复。

// 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 );

Cancel按钮事件储存了老的时间,并重新返回给Contact Detail窗体.

Ok按钮有些复杂。我们必须根据新时间相应的更新数据库和Contact Detail窗体。

在这里有几个重点。这个代码块表明了为什么变量hrecord在函数中是公用的。在这里由于hrecord是有效的,所有我们就用它来将新的时间值写入数据库中。

注意到在函数FrmReturnToForm()后调用的setTimeTrigger(),它用来触发Contact Details窗体的时间选择触发按钮。能够实现触发的原因是因为当执行FrmReturnToForm()后,活动窗体就变为了Contact Details窗体。这就使弹出窗体的数据能够顺利的传递到调用窗体上。

通常情况下,在调用FrmReturnToForm()后返回true,这是因为调用后老的窗体结构已经消失。然而如果返回了false,Palm OS就试图访问按钮结构以做更多的素材。由于窗口已经不在了,这样就会使程序崩溃。

调试

首先,你应该保证数据库记录能被正确的创建和修改。如果顺利的话,你所加的显示函数能够正确的显示所得结果。你的已有记录表现为no date,在界面上会显示12 AM,但不会造成危险。

下一步操作Contact Detail窗体上的日期触发按钮,看是否能按设计程序正常工作。对日期内置控件来说,这应比较容易。

对于时间控制,你需要调试Enter Time窗体。仔细的调试各个控件直到都能够可靠的工作。当窗体能够顺利运行后,你可验证数据库记录和时间选择触发按钮是否被正确的修改。

下一步
下一步,我们将向Contacts应用程序添加一个列表框窗体。然后修改代码使我们能够根据first name、last name、date 和time进行排序。


程序清单

下面是完整的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 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 );

// 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.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;

// CH.6 Enter Time form
case EnterTimeForm:
form = FrmInitForm( EnterTimeForm );
FrmSetEventHandler( form, enterTimeHandleEvent );
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
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.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(),
&currentDate );

// 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.3 Handle menu events
static 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.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
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;
}

// 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;
}
第七章 列表框和排序
在这一章中,将接触到一些新的控件和数据库操作技巧。我们先生成一个窗体来显示contact数据库中的所有记录,然后创建一个下拉框供选择排序标准,最后添加代码进行排序,并使新创建或修改过的记录也能够在列表中正确排列。

保存工程

按我们的习惯先保存工程,步骤如下:

1. 运行Windows浏览器;
2. 找到保存现有工程的文件夹;
3. 按下CTRL-C复制;
4. 选择目的文件夹;
5. 按下CTRL-V将现有工程粘贴到目的文件夹;
6. 将其重命名为容易记忆的名字,我将其命名为Contacts CH.6。

列表框
列表框能够显示许多文本条目并允许从中选择,我们可以对每个条目进行浏览或选择。我们还可以通过调用函数浏览列表或选择条目。当然,我们可以画出自己的列表框,在缺省情况下,列表框由Palm系统绘制。

Contacts.rsrc的内容添加
在这一部分中,我们将向应用程序添加一个新的窗体。此窗体将以列表的形式显示数据库记录,并允许我们浏览或选择。如果选中了其中的一条记录,记录内容将从Contact Detail 窗体上显示出来。

Contact Detail 窗体的内容添加
由于Contact Detail 窗体被调用来返回一条记录内容,所以我们需要添加一个Done按钮。步骤如下:

1. 运行构造器;
2. 打开资源文件Contact.rsrc,它位于工程文件夹的src文件夹里面;
3. 双击打开;
4. 选择Windows | Catalog打开控件模板;
5. 向Contact Detail 窗体拖动一个按钮;
6. 设置按钮属性Object Identifier为Done,Left Origin=1,Top Origin=147,Label为Done;
7. 添加完成后,所得按钮如下图所示,选择右上角的X按钮关闭编辑器。



在Contact List窗体上创建菜单栏

在创建Contact List窗体前,我们先创建一个菜单;这样当设置窗体的属性时,就可以直接写入此菜单栏的ID了。

1. 在Resource Type and Name 列表框中选择Menu Bars并按下CTRL-K,于是一个新的菜单就产生了,将其重命名为Contact List;
2. 双击打开;
3. 拖动Option菜单到Contact List菜单栏上;
4. 完成后应如下图所示,点击右上角的X按钮关闭菜单栏。


创建Contact List窗体

现在我们来创建Contact List 窗体,步骤如下:
1. 点击Forms,并按下CTRL-K创建一个新的窗体;
2. 点击窗体名称将其命名为Contact List;
3. 在窗体编辑器中双击打开Contact List 窗体;
4. 为Contact List 添加标题,并使菜单栏的ID和Contact List 窗体的菜单栏的ID相匹配。
创建列表框(List)
1. 从Catalog窗口中拖动一个列表框(List)到窗体上。它的属性如表7-1所示;
Object Identifier 在资源头文件中,构造器用之代表资源ID
List ID 列表框的资源ID
Left Origin 水平方向上控件的最左端位置
Top Origin 垂直方向上控件的最顶端位置
Width 列表的宽度
Usable 决定此控件是否可见能用,如果没有选中,也可在通过函数调用来实现其可见
Font 列表项的字体
Visible Items 列表框可显示的行数。注意,实际的列表条目数比此值可多可少
List Items 动态的初始化列表条目,如果想添加条目,按CTRL-K
2. 设置属性Object Identifier 为List,Left Origin=0,Top Origin=12,Width=160,Visible Items=12;
3. 完成后窗体如下图所示,按下右上角的X按钮关闭窗体,并选File | Save保存所做修改。



添加内存错误警告

在这一章里,我们将开始添加错误处理代码。在内存溢出时,系统将发出警告信息:
1. 点击Resource Type and Name列表中的Alerts 并按下CTRL-K创建一个新的警报;
2. 将其命名为MemoryError;
3. 双击,在编辑器中打开;
4. 设置属性Alert Type 为Error,标题(title)为Fatal Error,信息(message)为“I have run out of memory.”
5. 完成后,如下图所示:




Contact.c的内容添加

现在添加代码以完成新窗体的功能。首先,先在前几章中异常处理的忽略处添加代码进行异常处理。
异常处理
在这一章中,有很多地方可能会造成内存溢出。因此,我们必须添加严密的异常处理程序。下面我们将利用异常处理器(Error Manager)来完成,在The Palm OS SDK Reference 中对异常处理器有详细的论述。
首先,在PilotMain()前,定义异常退出(Exit)宏:

// CH.7 The error exit macro
#define errorExit(alert) { ErrThrow( alert ); }

异常处理函数ErrThrow()有些与众不同,它与宏ErrTry和ErrCatch()配合作用,类似C++和Java的形式给出异常处理。如果在ErrTry模块中的任何代码调用了ErrThrow()函数,控件就会立即调用ErrCatch()函数。下面我们看看在PilotMain()中做了哪些修改。
在事件loop 产生前先添加宏ErrTry:
// CH.7 Begin the try block
ErrTry {

// CH.2 Our event loop
do
{

结束宏ErrTry并添加ErrCatch() 函数:

// CH.7 End the try block and do the catch block
}
ErrCatch( errorAlert )
{
// CH.7 Display the appropriate alert
FrmAlert( errorAlert );
} ErrEndCatch

此函数将根据ErrThrow()传给ErrCatch()的ID号显示其相应的异常警报。然后,应用程序将在catch模块后正常退出,正常执行关闭数据库诸如此类的操作。最后,PilotMain()将控制权归还给Palm OS。

Contact List 窗体的切换
下面,我们添加代码来实现Contact List 窗体和Contact Detail窗体之间的切换。在PilotMain()中,修改初始化Contact Detail窗体的代码如下:

// CH.7 Choose our starting page
// CH.5 If there are no records, create one
if( numRecords == 0 )
{
newRecord();
FrmGotoForm( ContactDetailForm );
}
else
FrmGotoForm( ContactListForm );
在切换到Contact List窗体时,判断如果数据库中没有记录,就要创建一条新记录;一般情况下,数据库中都是有记录的,我们可以直接调用Contact List窗体。

为使Contact List窗体能正确的初始化,在loop事件中的frmLoadEvent处理代码中再添加case项:

// CH.7 Contact List form
case ContactListForm:
form = FrmInitForm( ContactListForm );
FrmSetEventHandler( form, contactListHandleEvent );
break;

下面的代码是如何处理Contact Detail 窗体中的Done按钮事件:

// CH.7 Done button
case ContactDetailDoneButton:
{
// CH.7 Load the contact list
FrmGotoForm( ContactListForm );
}
break;

为了在窗体间切换,而不是弹出窗体,我们使用了函数FrmGotoform()来初始化。这个函数首先调用frmCoseEvent关闭前一个窗体,然后调用frmLoadEvent和frmOpenEvent打开新的窗体。

Contact List窗体事件处理函数

下面我们为Contact List 窗体来添加事件处理函数。首先,在文件的开头加入原型:

static Boolean contactListHandleEvent( EventPtr event );

另外,还需要两个变量表示给列表框分配内存的句柄:

// CH.7 Contact list variables
static VoidHand hchoices; // CH.7 Handle to packed choices
static VoidHand hpchoices; // CH.7 Handle to pointers

下面是事件处理函数:

// 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.7 Build the list
buildList();
}
break;

// CH.7 Form close event
case frmCloseEvent:
{
// CH.7 Unlock and free things here
MemHandleUnlock( hpchoices );
MemHandleFree( hpchoices );
MemHandleUnlock( hchoices );
MemHandleFree( hchoices );
hchoices = 0;
}
break;

// 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;

// 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.7 Rebuild the list
buildList();
}
break;

} // CH.7 End of the event switch statement

// CH.7 We're done
return( false );
}

事件处理函数以十分标准的形式开始:首先建立了一个窗体指针变量,它将在调用frmOpenEvent时被调用;然后窗体被绘制并调用了一个新的函数buildList(),这个函数在下一部分将详细讨论。

我们调用frmCloseEvent释放用来储存列表框内容的内存。之所以在关闭(close)事件中释放内存,是因为只要列表框可见、窗体在使用的时候,这一部分内存就一直被Palm OS使用。在这一点上,列表控件和其它控件有所不同,一般的控件都有自己的内存,而对于列表控件来说,它需要另外分配内存。

下面,讲述一个新的事件处理函数――lstSelectEvent。当列表的条目被选中时,此事件将被触发。变量selection标明哪一个条目被选中,列表得记录从0开始。这样我们使用起来就十分方便了,只要将当前记录等于列表变量selection的值,然后调用Contact Detail窗体,窗体就会使用前面所设置的变量cursor产生正确的记录。

最后,对本程序菜单事件的处理和对Contact Detail窗体的菜单处理几乎相同。这样,对Contact List窗体的处理程序就算完成了。

函数buildList()

下面我们来讨论这个实用函数buildList(),这个函数通过浏览数据库,为每条记录创建一文本字符串,然后使用这些字符串填充List对象的各个条目。

static void buildList( void )
{
FormPtr form; // CH.6 A form structure pointer
Int choice; // CH.7 The list choice we're doing
CharPtr precord; // CH.7 Pointer to a record
Char listChoice[dateStringLength + 1 + // CH.7 We
timeStringLength + 1 + // build
DB_FIRST_NAME_SIZE + // list
DB_LAST_NAME_SIZE]; // choices here
// CH.7 The current list choice
CharPtr pchoices; // CH.7 Pointer to packed choices
UInt offset; // CH.7 Offset into packed strings
VoidPtr ppchoices; // CH.7 Pointer to pointers to choices

// CH.6 Get our form pointer
form = FrmGetActiveForm();
在声明变量后,函数象往常一样获取窗体指针。这里面最有趣的变量是char array ,它将保证列表字符串的连续性。为使阵列(array)有足够的大小来保存任何Palm OS的系统时间和日期,可以使用Palm OS常量dateStringLength和timeStringLength。

// CH.7 Put the list choices in a packed string
for( choice = 0; choice < numRecords; choice++ )
{
// CH.7 Get the record
hrecord = DmQueryRecord( contactsDB, choice );
precord = MemHandleLock( hrecord );

// CH.7 Get the date and time
MemMove( &dateTime, precord + DB_DATE_TIME_START,
sizeof( dateTime ) );

首先,浏览数据库的所有记录,然后获取记录将变量dateTime设置为在所得记录中的时间和日期。注意到我们使用了DmQueryRecord()函数来获取记录句柄。此函数只给我们提供一个只读的记录副本。

下面,我们创建表示记录的字符串:

// CH.7 Clear the list choice string
*listChoice = '';

// CH.7 Add the date string if any
if( dateTime.year != NO_DATE )
{
DateToAscii( dateTime.month, dateTime.day,
dateTime.year,
(DateFormatType)PrefGetPreference(
prefDateFormat ), listChoice );
StrCat( listChoice, " " );
}

// CH.7 Add the time string if any
if( dateTime.hour != NO_TIME )
{
TimeToAscii( dateTime.hour, dateTime.minute,
(TimeFormatType)PrefGetPreference(
prefTimeFormat ), listChoice +
StrLen( listChoice ) );
StrCat( listChoice, " " );
}

// CH.7 Append the first name
StrCat( listChoice, precord + DB_FIRST_NAME_START );
StrCat( listChoice, " " );

// CH.7 Append the last name
StrCat( listChoice, precord + DB_LAST_NAME_START );

在这里我们将date,time,first name和last name都写入了字符串。注意到函数DateToAscii()和函数TimeToAscii()运行需要花很多时间。对本程序来说调用这两个函数还算可以,但是如果要提高处理记录的速度,就必须考虑将时间和日期保存在单个记录来提高速度。

下一步,我们将为这些字符串分配内存,这样做是因为列表框在填充新条目时需要一定的格式。

// CH.7 Allocate memory for the list entry string
// CH.7 If this is the first choice
if( hchoices == 0 )
{
// CH.7 Allocate the storage for the choice
if( (hchoices = MemHandleNew(
StrLen( listChoice ) + 1 )) == 0 )
errorExit( MemoryErrorAlert );

// CH.7 Initial offset points to the start
offset = 0;
}


上面就是字符串没有创建时的代码,在给字符串分配内存时,如果失败了,就调用新的错误处理函数来处理。然后我们初始化偏移(offset)说明在哪里将字符串置零。
下面是字符串已经创建好的情况:

else
// CH.7 If this is a subsequent choice
{
// CH.7 Unlock
MemHandleUnlock( hchoices );

// CH.7 Resize
if( MemHandleResize( hchoices, offset +
StrLen( listChoice ) + 1 ) )
errorExit( MemoryErrorAlert );

}

在这里我们将锁定的内存解锁(Unlock),使之能储存下一个字符串。

接着我们将字符串写入包列表(Packed List):

// CH.7 Lock
pchoices = MemHandleLock( hchoices );

// CH.7 Copy the string into the memory
StrCopy( pchoices + offset, listChoice );
offset += StrLen( listChoice ) + 1;

// CH.7 Unlock the record
MemHandleUnlock( hrecord );
}


首先我们锁定主块(chunk),然后将新建字符串拷入。做完这些工作后,解锁我们所使用的数据库记录句柄。注意,我们没有调用函数DmReleaseRecord()是因为我们用函数DmQueryRecord()代替了DmGetRecord()。

循环操作完成后,我们已有了一个包含每个数据库记录的列表字符串包。现在我们将这些选项发送到列表对象显示:
// CH.7 Create a pointer array from the packed string list
if( (hpchoices = SysFormPointerArrayToStrings( pchoices,
numRecords )) == 0 )
errorExit( MemoryErrorAlert );
ppchoices = MemHandleLock( hpchoices );

// CH.7 Set the list choices
LstSetListChoices( getObject( form, ContactListListList ),
ppchoices, numRecords );
// CH.7 Draw the list
LstDrawList( getObject( form, ContactListListList ) );

// CH.7 We're done
return;
}

我们使用函数SysFormPointerArrayToStrings()建立了一个指向所建包列表的指针。这样包列表的指针就确定了。

函数LstSetListChoice()利用包列表的指针来填充每个条目。注意,此函数将清空所有已存在的条目,所以在向此函数发送列表项时,必须是完全的包列表条目。

记住在程序的最后,通过窗体关闭(Form Close)事件来释放所有的内存。

在这一部分的最后,我们绘出了列表框。
调试

在这一类程序的调试中,最好是使用单步调试来检查程序是否能够正常运行。首先调试刚创建的新函数builList()。在函数的开始处设置一个断点,单步运行,看程序能否顺利运行,特别注意一下内存在最后是否得到释放。

从列表中选中一条记录,看是否能从Contact Detail窗体正确显示,再按下Contact Detail窗体上的Done按钮返回。在Contact Detail窗体上新添加一条记录,再从列表中选择此记录,看能否在Contact Detail窗体上正确显示。

排序

这一部分除了讲述排序外,还将接触到两个新控件,即弹出触发按钮(pop-up triggers)和弹出列表框(pop-up lists)。把弹出列表框加入Contact Detail窗体中可允许我们从其中选择不同的排序选项作为排序标准。最后,我们将添加代码创建或修改在Contact Detail窗体的相应记录,使之能够根据排序标准插入到正确的位置。

弹出触发按钮(pop-up triggers)

弹出触发按钮和普通按钮很相似。它们都有一个标签并响应触发事件。它的特殊之处在于它能够和一个弹出列表框相关联。当按下弹出触发按钮时,弹出列表框就会显示。如果选中列表框上的条目,就会产生事件popSelectEvent。当然,我们还可以利用弹出触发按钮和列表框相关联做一些其它的工作。

对Contacts.rsrc内容的添加

现在向Contact List窗体添加弹出列表框。它将允许我们依据三个排序标准进行排序:时间日期、姓(first name)、名(last name)。

1. 运行构造器;
2. 打开资源文件Contact.rsrc,它位于工程文件夹的Src文件夹中;
3. 双击打开Contact List窗体;
4. 选择Windows | Catalog打开控件面板;
5. 从Catalog窗口中拖动一个标签到窗体上。修改属性为:Left Origin=0,Top origin=149,Label为Sort By:;
6. 拖动一个弹出列表框到窗体上。修改属性维:Object Identifier 为SortList,Left Origin=40,Top Origin=125,Visible Items=3。在下面我们会看到它的位置和新添加的弹出触发按钮正好对齐;
7. 单击选中List Items,然后按下CTRL-K产生第一个条目。修改条目文本为Date and Time;
8. 单击选中List Items,然后按下CTRL-K产生第二个条目。修改条目文本为First Name;
9. 单击选中List Items,然后按下CTRL-K产生第三个条目。修改条目文本为Last Name。

创建弹出触发按钮
1. 拖动一个弹出触发按钮到Contact List 窗体上,其属性如表7-2所示;
Object Identifier 在资源头文件中,构造器用之代表资源ID
Popup ID 弹出触发按钮的资源ID
Left Origin 水平方向上控件的最左端位置
Top Origin 垂直方向上控件的最顶端位置
Width 按钮的宽度
Height 按钮的高度
Usable 用来定义控件是否可见及可用,如果不设置,也可在通过函数调用来实现其可见
Anchor Left 决定当文本长度改变时,按钮文本是以左侧还是右侧为锚点扩展,选中时,按钮文本将向右侧扩展。
Font 标签文本的字体
Label 标签上的文本内容
List ID 和弹出触发按钮相关联的弹出列表框的ID

2.修改属性为:Object Identifier为Trigger,Left Origin=40,Top Origin=149,Width=80,Label 为Date and Time,将List ID设置为刚创建的弹出列表框的ID;
3. 在对Contact List窗体修改完毕,其图如下所示。单击窗体编辑器右上角的X按钮关闭,选择File | Save 保存所做修改。





对Contacts.c内容的添加
首先,在文件头添加排序所必需的变量和常量:

// 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

变量sortBy表示在三个排序标准中的当前值。三个常量代表了弹出列表框中的三个排序标准,注意使它们和列表框的每个选项相对应。

排序的初始化
为了建立排序标准,必须添加代码来处理popSelectEvent事件,此事件在弹出列表框的列表选项被选中时触发。下面的代码是如何根据所选项进行排序:

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.7 Rebuild the list
buildList();
}
break;

首先,在排序标准没有改变时,避免重复排序。然后保存排序标准,因为在Contact Detail窗体中当有新记录或修改记录后,排序发生了改变,这时又用到了排序标准。最后调用函数DmQuickSort(),此函数将排序标准sortBy传递给函数sortFunc()。在数据库排序完成后,绘制列表框显示。
下面我们研究一下函数sortFunc()。这个函数可以比较前后两个条目的大小,在将数据库中的两条记录比较后,函数返回一个整数。如果此数大于零,则第一个记录排在前面;如果小于零则第二条记录排在前面;如果为零则说明两记录相同。

// 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;

代码中首先将时间和日期从所选记录中抽出,然后将其转换成秒进行比较。在这个算法中会将无日期的记录放在列表框的顶部,而将无时间的记录放在相同日期的最底部。由于所转换的秒值有可能超过16位整值,所以我们采用32位长整数来进行比较,相应的设置变量sortResult为16位。

// 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;


调用函数StrCompare()为first name和last name 排序,此函数定义在Developing Palm OS 3.0 Applications Part II:Sytem Management中。它和ANSI C的函数strcmp()十分相似,直接给Palm OS返回一个整值。

排序的记录写入列表框
在移动到另一个记录或退出Contact Detail窗体时,函数getField()都要被调用,因此应该在此函数中添加代码,来保证在添加新记录或修改记录后重新排序。
为达到要求,这些代码应添加在记录内存已被释放但“脏(dirty)”位还没有清除的位置。

// 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 );
}

既然只是在记录被改变时重新排序,因此可以先使用isDirty位来判断记录是否已被修改。然后将“脏”的记录从数据库中临时分离出来并将之锁定,最后调用函数DmFindSortPosition()决定应该在列表框的什么地方插入。此函数的前提示列表已经有了正确的排序,由于我们一次只改变或添加一条记录,因此可以满足要求。最后解锁该记录并将之插入到数据库的新位置。

调试
分别在popSelectEvent事件的顶部、函数sortBy()的顶部、函数getField()的if语句的开始处设置断点。检查程序是否能正常运行。下面是一些功能的测试:
l 用三个排序标准分别排序,看数据库是否能根据各个标准正确的排序;
l 向数据库中添加一个记录,看其是否能排在正确的位置;
l 修改一个现存记录,看其是否能排在正确的位置。


下一步做什么
下一章我们将添加更多新的控件,例如表和浏览栏。这一章我们使用列表框显示程序的记录信息已是个不错的方法,下一章我们将修改程序,用表控件来做到这一点。

程序列表
// 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 void buildList( void );
static Int sortFunc( CharPtr, CharPtr, Int );

// 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 Contact list variables
static VoidHand hchoices; // CH.7 Handle to packed choices
static VoidHand hpchoices; // CH.7 Handle to pointers

// 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.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(),
&currentDate );

// 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.7 Build the list
buildList();
}
break;

// CH.7 Form close event
case frmCloseEvent:
{
// CH.7 Unlock and free things here
MemHandleUnlock( hpchoices );
MemHandleFree( hpchoices );
MemHandleUnlock( hchoices );
MemHandleFree( hchoices );
hchoices = 0;
}
break;

// 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;

// 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.7 Rebuild the list
buildList();
}
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
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 Builds the contact list
static void buildList( void )
{
FormPtr form; // CH.6 A form structure pointer
Int choice; // CH.7 The list choice we're doing
CharPtr precord; // CH.7 Pointer to a record
Char listChoice[dateStringLength + 1 + // CH.7 We
timeStringLength + 1 + // build
DB_FIRST_NAME_SIZE + // list
DB_LAST_NAME_SIZE]; // choices here
// CH.7 The current list choice
CharPtr pchoices; // CH.7 Pointer to packed choices
UInt offset; // CH.7 Offset into packed strings
VoidPtr ppchoices; // CH.7 Pointer to pointers to choices

// CH.6 Get our form pointer
form = FrmGetActiveForm();

// CH.7 Put the list choices in a packed string
for( choice = 0; choice < numRecords; choice++ )
{
// CH.7 Get the record
hrecord = DmQueryRecord( contactsDB, choice );
precord = MemHandleLock( hrecord );

// CH.7 Get the date and time
MemMove( &dateTime, precord + DB_DATE_TIME_START,
sizeof( dateTime ) );

// CH.7 Clear the list choice string
*listChoice = '';

// CH.7 Add the date string if any
if( dateTime.year != NO_DATE )
{
DateToAscii( dateTime.month, dateTime.day,
dateTime.year,
(DateFormatType)PrefGetPreference(
prefDateFormat ), listChoice );
StrCat( listChoice, " " );
}

// CH.7 Add the time string if any
if( dateTime.hour != NO_TIME )
{
TimeToAscii( dateTime.hour, dateTime.minute,
(TimeFormatType)PrefGetPreference(
prefTimeFormat ), listChoice +
StrLen( listChoice ) );
StrCat( listChoice, " " );
}

// CH.7 Append the first name
StrCat( listChoice, precord + DB_FIRST_NAME_START );
StrCat( listChoice, " " );

// CH.7 Append the last name
StrCat( listChoice, precord + DB_LAST_NAME_START );

// CH.7 Allocate memory for the list entry string
// CH.7 If this is the first choice
if( hchoices == 0 )
{
// CH.7 Allocate the storage for the choice
if( (hchoices = MemHandleNew(
StrLen( listChoice ) + 1 )) == 0 )
errorExit( MemoryErrorAlert );

// CH.7 Initial offset points to the start
offset = 0;
}

else
// CH.7 If this is a subsequent choice
{
// CH.7 Unlock
MemHandleUnlock( hchoices );

// CH.7 Resize
if( MemHandleResize( hchoices, offset +
StrLen( listChoice ) + 1 ) )
errorExit( MemoryErrorAlert );

}

// CH.7 Lock
pchoices = MemHandleLock( hchoices );

// CH.7 Copy the string into the memory
StrCopy( pchoices + offset, listChoice );
offset += StrLen( listChoice ) + 1;

// CH.7 Unlock the record
MemHandleUnlock( hrecord );
}

// CH.7 Create a pointer array from the packed string list
if( (hpchoices = SysFormPointerArrayToStrings( pchoices,
numRecords )) == 0 )
errorExit( MemoryErrorAlert );
ppchoices = MemHandleLock( hpchoices );

// CH.7 Set the list choices
LstSetListChoices( getObject( form, ContactListListList ),
ppchoices, numRecords );

// CH.7 Draw the list
LstDrawList( getObject( form, ContactListListList ) );

// CH.7 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 );
}
第八章 表和滚动条
在这一章中,我们将讨论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(),
&currentDate );

// 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;
}
第九章 分类和查找
在这一章中,我们在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 = '';

// 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, N
第十章 创建高效的有效界面
在这一章中,我们开始讲述用户界面的设计和测试。首先讨论一下Palm OS自带的一般用户界面,然后介绍由Palm Computing 提出的具体设计原则及相关方法,它们使界面更加有效和实用,最后我们将讲述用户界面和程序设计的整个流程和测试,因为用户界面不是凭空想象出来,它是一个高效过程的结果。

设计考虑

从哪里开始呢?对于Palm装置来说,设计角度应集中在实用上,即怎样才能确实符合用户的当时需要。我常常想象如果我的用户是在交通拥挤的街道上使用Palm OS(当然我不是鼓励去在交通拥挤时使用Palm,只是一个假设而已)。在这样一个时候,用户没有时间来观察到底怎样才能获取信息,只能快速的看一下屏幕上有什么东西而已。所以我想如果在这样环境下,应用程序如能很简便的使用,那么在其他环境下也应不成问题。

我们再来想象一下交通阻塞的情景。有些时候,某些需要可能是即时的。例如我想查看一下别人告诉我下一步到底向那边转的消息,这时我想赶快的找到它而别错过了我转弯;还有时或许我想找一个电话号码给某人打电话,这些时候我最好是尽快找到,否则他们就不知我想现在告诉他们什么事了。

交通阻塞中使用Palm的例子说明了Palm OS的在程序设计的一定要易于使用。为做到这一点,应用程序必须遵循下面将要讲到的一系列简单易用的原则。程序尽量象用户希望的那样去工作,换句话讲,程序看起来的确是一个“Palm OS”的应用程序。设计界面时,获取信息应使用户尽量少触击屏幕、当信息出现时应尽量清晰。如果用户必须要应答信息或输入数据,也要把这个过程做的尽量简单和直观。

站在用户的角度

对于任何的软件设计,都应该站在用户的角度上去考虑。在我们这里,也应该站在“Palm OS”用户的角度上,而不是其它如PC或Mac或其它别的装置用户的角度上去考虑。换句话说,既然用户选择了使用Palm,或许也知道了Palm OS程序的标准运行方式。因此,用户有权利希望其它的程序也要使用相同的模式。在这样一个概念下,我们虽然能够编制无数新鲜有趣的程序,但是注意有一点,就是不要否定那些已是既定的东西。如果你的“创造性”使得用户找不到本来由Palm OS已定义的简单的变量,那肯定是错误的。

站在用户的角度上,首先要从自己的模式中走出来:不要为使程序更简单而设计界面;不要使用没有用户基础的任何复杂的界面,不管它有多酷;要站在尽量客观的角度上来考虑问题。

把Palm 程序想象成用在交通阻塞的汽车上,就可以理解为什么其界面要简单易用,实际上还有其他的类比。谁是Palm OS的典型用户呢?通常情况下是很忙的人。他们还有许多其他的事要做,他们一直在忙。我们知道也只有这些人可以从Palm OS的使用中获取很大的益处。这些人中,很大一部分还会有自己的个人电脑,所以可以相信,他们对电脑比较熟悉。就市场调查来看,Palm的用户都比较喜欢技术或有技术的小玩意,因此他们会花很多时间用在考虑技术问题上。所以有可能他们会想那些应用程序如何修改,才能变为他们自己想要的那样。

你可以从一些流行Palm OS应用程序的编写中获得很好的设计经验。不过你会发现在本书中的例子中,界面都比较差。这是因为我的目的是讲述Palm OS的内部技术,而不是为某个用户而设计。当你看一些其他讲述设计思想的程序时,注意程序是否普及。如果用户在那样的程序中不喜欢使用那些看起来很酷的小玩意,那他们也不会使用你的程序中的这些东西。另外,你也注意一下他们是怎样处理你所想处理的用户问题,这样,你就可以借用他们的处理方法来解决你自己的问题。

简单易用就是一切

每一个被交通堵塞里的人都希望获得最新最快的的道路信息,或许你会有例外的想法,但是,那除了是尽快离开这个旋涡之外还能是什么呢?简单的重要性可见一斑, 我以为,高的效率就是用户的最高利益。但是,效率究竟是什么?对我来说,效率意味着你能毫不费力地获得手头所最需要的一切,具体而言,我对于效率的体会就是要把很多小的事情规划纳入到你整个的设计方案中去,这些琐碎的事情大致有:

◆ 有逻辑的、简易的程序设计流程
◆ 细致的界面,使得信息以非常直观的方式显现出来
◆ 对于可视化设计组件适时而不烦琐的运用,可以程序更易使用


点击(tap)因素

我在上面曾经提到过“点击数越少越好”,但是,究竟怎样理解这句话呢?如果你分析过标准的 Palm OS应用,你就会发现,它们需要如下点击次数来实现的标准化的操作。

◆ 0到3 次点击来观看信息,具体取决于使用的频率
◆ 0到1次点击来开始进入新记录
◆ 0到3 次点击来编辑已经存在的记录
◆ 3次点击来删除记录

好了,现在似乎已经可以考虑明确一下我们的目标了,显然,除了那些最不明显的信息外,我们几乎都已可以通过三次点击就实现了,当然要是比三次还少就更好了。删除使用三次点击是可以接受的。添加记录应该做到尽可能的快,如果可能的话,不用点击也未尝不可。同时,正因为点击次数因素重要性,所以在以后的界面设计中都必须考虑到所谓的点击次数因素。

显然,一个方面越是经常用到,我们就应该要设法给它越少的点击次数,通常来讲是0次或者1次。对于绝大部分应用来说,“观察屏幕明细”和“删除记录”操作会比“浏览记录”和“添加记录”用得要少,所以,前者就可以拥有比后者使用稍多的点击次数。当然,这条原则并不普遍使用,尤其是对于同样用得比较少,但却是那些隐藏在菜单背后一些操作如果有可能,一般的操作的点击次数都应在三次之内。

“七“的原则

现在我们可以来定量地分析一下,要使得界面看起来不产生混乱,一个屏幕上能处理多少控件。大量心理学的研究认为,人一般容易接受的是在一个屏幕上出现七个左右的控件。“左右”的含义就是要视所用控件或工程的复杂程度而定,所以,或许我们只考虑考虑使用三个控件,但如果工程很复杂的话。控件数目还可以增至九个。

屏幕的考虑

Palm的屏幕设备对于160*160像素的显示界面是很友好的,但也要谨慎地使用。如果你还是习惯使用更大的PC机的屏幕,那就要改变一下习惯,来适应使用小一点的了,改掉你喜欢把所有要用到的东西都写道屏幕上的习惯。
大部分情况下,你可能只会看到黑白的屏幕。对于Palm Ⅲ及以上版本只有四种灰度。这就意味着你的用户界面设计不能再寄希望于精确的灰度级别,也不能指望通过高的亮度值来获取信息。

在给定的有限且有较好的可读性的空间里设计界面,这就需要预先安排好窗体的布局。
在把文本和控件置于屏幕上前,要考虑以下几点:

◆ 要添加的内容对用户是否有用?
◆ 要添加的内容用户是否容易接受?
◆ 要添加的内容用户是否能够很容易地看到?
◆ 对简单的应用而言,屏幕是否过于混乱?换句话说,各内容是否都被安置在屏幕的合适位置上?
◆ 要添加的内容常用还是不常用?(如果不常用,就可以考虑使之成为较多点击次数中的一部分,这样就能保证那些常用的界面不至于过分混乱而影响视觉效果。)
你可以把PC机的应用设计想象成为是往一间空房子里堆放杂物,而Palm应用就好比是把杂物堆放到一个小的船舱里。当你规划这个小船舱时,必须先要考虑好即将到来的航行,关键是检查准备携带的物品是否都是必须的,同时还要仔细考虑往哪儿放置这些物品才能使之更易于发现和使用。类似地,在Palm应用中,也没有足够大的空间来放置你可能要用的用的大量信息,所以,就很有必要仔细考虑好哪些信息是最为常用的,那些则是可以完全抛弃的。下一步,各种信息的装载必须符合逻辑以及最少点击次数的要求,这样才能便于应用。

空间的重要性

在重要信息周围安置尽可能多的空白空间是个好主意。这能够使重要部分更加显眼从而容易发现。在Palm屏幕上,这一点确实是很重要的,它能使你的程序可用性大大增强。你可能会十分希望在所有可能的地方放置信息,而使用户能容易地找到它们,但是注意到,Palm屏幕并非很大,特别是早期低对比度模式的那种,界面是很模糊的。我们希望通过适当的放置给用户带来他们所需要的一切,而不是让他们去在视所能及的范围内来寻找他们所需要的东西。

尽管人的眼睛能够发觉到非常微小的细节。但为了保护你的眼睛,还是最好把控件等归整到一条线上,并且均匀地分布它们。同时,注意保持你设计的数据输入字段归整为左对齐,或右对齐,而不要使两者都有,那样看起来会显得很别扭。这些看起来都是很小的事情,但是它们对应用程序的视觉的影响就不小了。如果你如上面所说的那样去避免在屏幕上堆放过多的内容,那么就可以用余下的空间来填充用各项目之间的空处,从而结束整个工程了,这样应用程序对于用户可读性就好多了。

有时过分精确地规划控件的位置也不容易做到,在这种情况下,就要相信你和你朋友的眼睛,用眼睛直觉告诉你答案,到底怎样安排控件的位置。

输入笔的考虑

输入笔也是一个输入的重要方法,但是如果设计的程序经常使用键盘,并且要求用户大量输入文字的话,那就要改改习惯了。在Palm OS中,输入文本是最后一种方式。我们所希望用户做的最后一件事就是在拥挤的路况中挤出一条通道来,在本来可以使用选中某个列表选项或选择按钮的地方输入文本。在一些必须需要输入文本的地方,可以使用明细窗体来显示其详细内容,这样用户才就可以通过一两次点击就查看到了信息。

输入笔的触击和PC/Mac的鼠标点击事件相类似,区别仅在于你不能再要求用户通过多余三次点击来观察他们所需要的内容了。随时记住这样一句话,点击绝非键盘输入,点击次数越少越好。

双击是一个不错的主意,然而,如果你考虑使用弹出式菜单来选择操作,点击次数相同,但弹出菜单看起来更直观。

理想的情况是不使用输入笔。毕竟,在拥挤的路况下经常发生就是在接近冲突地点和出口处的事,输入笔掉在地上并滚到座位底下去再也找不到了。那怎么办呢?你的应用程序的控件就应有一定的大小,允许人们手指来点击它。注意使大部分内置的应用程序能够在不用输入笔情况下也能够使用。

处理器的考虑

Palm系统并非电站的电脑而设计的。不要搞错了,它所能做到的远比其他系统要快,这主要要归功于Palm OS的效率,但它也不是为高负荷操作而设计的。然而我可以再增加一个界面,来使系统更好的运行。在你要进行某些计算之前,考虑好你的用户是否真的需要这些计算;至于屏幕显示和鼠标点击次数,在PC机中这些几乎不成问题,但是在“小船舱“里面,任何计算都会使之减速。

我能想到的利用这种方法的一个好的例子就是花费的应用;明智的设计者不会考虑总花费,尽管这样很容易做到。只有到达终点后,他们才会考虑途中的总花费的,和旅行者的路费相比,PC的总耗费也是这样。设计人员因此避免了应用程序地运行减速。

造成计算降速普遍地一个原因,象我们的Contacts程序中那样,可能是由于在Palm OS内部日期转换或者从时间格式向文本格式转换引起的。克服类似问题的一个办法是增加存储格式,例如:在记录中存储文本格式的时间或者日期的同时存储它们的数字格式。

设计原则

我们已经讨论了Palm系统设计的一般性原则,下面就可以进一步讲述它的具体原则了。在这部分里我们首先讨论当今Palm OS应用普遍使用的设计原则,我们分两部分主要内容来讲,一是原则(Palm Computing资格证明必须遵照的部分),二是一些建议。

编辑框、手写区、和剪贴板

仔细检查允许用户输入数据的方式非常重要。另外,与其他应用的连贯性和兼容性也很重要,举个例子,不要因为学习手写输入使部分用户花费大量的时间而失去这部分用户
原则

◆ 对于可编辑的编辑框来说,确保标准的编辑菜单(剪切,复制,粘贴等等)以及相应的快捷方式都可以获得。
◆ 允许用户从你的应用当中复制或者剪切数据。如果用户没有进行下一次复制或者剪切,你要确保以前放置在剪切板中的内容是完全的。
◆ 无论哪儿用到手写输入,确保可以通过菜单或快捷方式获得手写输入的帮助。
◆ 如果手写输入能用在具体的窗体上,在窗体的右下角安排一个切换指示器。在这些窗体上,不要再把右下角兼作它用。
◆ 如果手写输入用在窗体上,需支持手写输入浏览(即,向上划或向下划等同于移动到下一记录)
◆ 对于可写的编辑框要保持手写输入,不要仅仅是应用键盘。
◆ 如果一个编辑框具有焦点,你应该始终能够做到通过菜单或快捷方式来调用键盘操作。
◆ 在用户没有知道的情况下,不要把任何东西放到剪切板上,用不到剪切板的时候也不要破坏它上面的内容。
◆ 确保编辑框被选中的时候能够发声提示。
◆ 确保有当菜单选择或快捷方式发生错误的时有错误提示声音。比如,在没有选中任何内容的时候而按下了复制选项。
◆ 确保不要输入残缺的、不符合要求的数据。
建议
◆ 通常,在以前没有要求的情况下,把焦点设置在左上角的编辑框上。
◆ 不要弹出菜单出错或快捷方式出错警告,用出错声音提示就够了。
◆ 用手写输入来在用户需要的地方增强你的应用程序的流畅程度。

菜单

如果你在应用程序中大量使用菜单,最好采用Palm OS推荐的标准的设计原则。菜单因为其精巧的结构和连贯性的外观使得它在应用程序中显得尤为重要。如果换成其他的外观可能会使用户大吃一惊。
原则
◆ 不要使不用的菜单项或者其UI控件变灰。把他们直接去除掉或者隐藏起来。
◆ 多使用菜单快捷方式,少使用“关于”。
◆ 确保标准的菜单――编辑,选项,记录――等菜单项如图10-1所示。




建议
◆ 为每一个菜单选项设置快捷键。
◆ 除非菜单操作失败的原因不明显,对菜单的错误都要给出警告提示。
◆ Palm3.0及更高的版本上支持闪烁功能,注意在菜单增加一个闪烁选项及快捷键。

按钮和控件

遵循以下的原则,会为你在设计窗体的时候减少很多麻烦。
原则
◆ 不要改变网状图标的形状如Home、Menu、Find等;
◆ 不要用一个和计算器功能无关的应用程序覆盖计算器的快捷按钮。
◆ 如果你的系统中要用到数据簿、地址簿、待做事项和备忘录按钮,注意在退出后要把他们恢复到原来的排列顺序。
◆ 在窗体中把按钮尽量排在屏幕的底部。
◆ 在文字和按钮之间至少要留下两像素的空间。
建议
◆ 如果窗体上有多于一个的按钮,把最重要的按钮放在窗体的左下角。

一般情况

下面是一些你在设计的时候容易忘记的细节,如果你在设计应用程序的时候,仔细阅读一下以下的事项,你也许就不会忘记了。
原则
◆ 当应用程序的日期、时间、数字和星期的格式改变的时候,系统应该做出相应的改变。
◆ 如果你要使用分类,你最使用户最多只能建立15个分类。
◆ 如果你的应有程序支持私人信息,那么在一些Security应用程序中,如果隐藏被选中,应该隐藏这些私人纪录。
◆ 编制相应的代码,使你的应用程序支持全局查询。
◆ 提供自己的About。
◆ 如果有可能的话,当应用程序重新启动的时候,应该让时回到先前的界面。
◆ 使你的应用程序和所有的数据库有相同和唯一的ID号。
◆ 最好使你的应用程序有备份和重新排列的功能。
◆ 在打开、切换和查找纪录时,你的应用程序不应该比内置的应用程序慢很多。
◆ 为你的应用程序创建一个小图标。
建议
◆ 在进行正常的操作的时候,不要老是停留在模式(modal)窗体上。因为当用户停留在模式窗体上的时候,Palm是不能弹出警告或则电池没电的报警的。在用户想进去修改一些东西,然后就退出来的情况下,可以考虑使用模式窗体。
◆ 不要嵌套使用窗体三层以上。
◆ 尽量在界面使用用户熟悉的的元件。
◆ 在用户没有特殊的要求或控件能够在速度和清晰程度上都很好的情况下尽量使用一些传统的控件。
◆ 在模式程序对话框中提供帮助。

窗体的布局

遵循以下一些简单的原则,就会使你的窗体界面符合标准窗体界面要求。
原则
◆ 所有主窗体应该充满整个屏幕。
◆ 每一个窗体都应该有一个标题栏,标题栏中应包含窗体名称或者这个窗体的主要信息。
◆ 确使所有的窗体用手指就能按到,而不要求助于输入笔。
◆ 模式窗体应该占据整个的宽度,并且要和屏幕的底部对齐。
◆ 非模式窗体上控件的布局如图10-2所示。





◆ 模式窗体上控件的布局如图10-3所示。





◆ 对于非模式窗体,应该使之占据整个屏幕,不要在控件和屏幕边缘之间留下空隙。
◆ 对于模式窗体,在控件和屏幕边缘之间应该至少留下3个像素的间隔。
◆ 在窗体上不同组的标签和控件应该按行列形式排放,不要设计委一个表格形式,并且使标签加粗和右对齐。
建议
◆ 把经常使用的控件放在窗体的左上角,不经常使用的控件放在右下角。但是对于按钮不能这样安排,应该使按钮在窗体的底部对齐。原则上应该让用户觉得是在看一页书。如果你是为不习惯于从左到右从上到下的用户设计界面,那你因该重新考虑界面的设计。
◆ 在屏幕上只显示用户需要的信息。不要把用户并不十分想有的信息杂乱的堆积在一起。而是把用户想得到的一些相关信息,集中起来放在另一个窗体中。这样,用户如果想看的话,就可以按一个按钮去阅读,这比杂乱的放在同一个窗体上要好得多。
◆ 要尽量多用标签,这样更便于用户操作。

操作流程
在这一节里,我们来讨论Palm的一种好的设计流程。Palm的操作流程和前面讨论的按钮的设计密切相关。设计的好的界面,应该让用户在许多应用场合不用按很多按钮就能完成想要的操作。对于一些比较匆忙的用户来说,他只需一面点击按钮,一面想着程序的操作流程就可以熟练使用应用程序了。如图10-4所示:





分类

如果你的系统需要处理数据库记录,把这些记录分为15个或则少于15个组是很有必要的,你可以考虑使用内置的数据库分类来完成这个工作,这样的话,正如我们在前面的章节中所看到的,对Palm用户来说不但很方便,而且也十分快捷和简单。

如果在你的系统中,你需要支持分类,你应该显示弹出式菜单以便你的用户显示分类信息。这种弹出式菜单最好放置在屏幕的右上角,紧靠着窗体的标题。
当系统中某一窗体处在激活状态时,如果你按下Hard Case按纽时内置的程序会在分类中自动滚动。如果你要使用分类的话,建议也使用这种方法。当某一窗体正常打开,运行标志位(lunch flags)也提示你,应用程序已经处在激活状态,那么应该跳到列表中的下一个分类中去。
滚动

在Palm操作系统中,通常有三种形式的滚动:有上下箭头的重复按钮、滚动条、上升和下降按钮(up and down buttons)。

总的来说,屏幕上的向上和向下按钮应该向上和向下移动一行,而硬件按纽应向上或者向下卷动一页。如果想用笔来定位的话,滚动条就显得更易使用。
对象的优先顺序

在设计一个Palm系统时,你必须仔细考虑用户想要这个系统来做什么事,也就是说用户在大多数时间内在这个系统上做什么。这儿有一个例子,当一个在Palm Computing工作的人来创建一个电话列表的应用程序的时候,他就会主要考虑用户想对电话列表进行什么样的操作,比如增加一个新的纪录、编辑纪录、删除纪录和查询纪录这些数据库常用的功能。然后他就会更深一层的考虑,是那种功能用户用得更多,很显然,在电话列表应用程序中,查询电话列表相对于其他几个功能来说,用户用得更多,同时他也会考虑到,删除电话纪录是一个用得比较少的功能,所以他把删除纪录的功能隐藏在编辑屏幕后面的子屏幕上。这样的话,我们就得到一个如图10-4所示的设计方案,图中的箭头表示屏幕之间的切换转移,方框表示屏幕。请注意,这个流程和电脑上一些设计的比较好的应用程序的流程不一样,在电脑上,浏览主要是通过菜单实现的,Palm上的菜单主要是用来完成一些不常用的功能(比如在系统中处处都用到的键盘、剪切、复制、粘贴、参数选择、关于等等),而不是主要为了浏览的目的。Palm中的浏览功能主要是通过屏幕上按钮来实现的,从Palm用户的角度来看,这种方式更快(只需按很少的按钮)和更清楚(按钮总是在屏幕上)。
主窗体

当开始设计一个应用程序的时候,你必须首先考虑的问题是这个应用程序的主要功能,因为这个功能是你的用户在大多数时间里使用的。如果你的应用程序比较简单的话,主窗体可能是应用程序的唯一窗体。仔细考虑你的Palm OS应用程序的结构,在主窗体外,最好再增加一个显示详细信息的窗体、一个添加新记录的窗体和一个修改记录的窗体,这样在屏幕上实现各个功能时会比较方便。

如果你的应用程序有多个主要部分,这时可以把过大的功能模块分成几个功能小块,每个小块组成一个窗体。如果一个应用程序所需达到功能不尽相同,并且不是很多,那么Palm是最好的工具。举个例子来说,如果要实现保存文档、显示日历和显示待做事项这三种功能,为这三种功能分别建立一个应用程序比把他们堆积在一起好的多。在PC平台上,开发者有可能更致力于把他们集合起来。从这个典型的例子中,我们也可以看到,在不同的开发平台上,对于同一个问题,解决的最好方法是不一样的。

如果你的应用程序中有的功能需要按三次或则三次以上的按钮才能实现,很显然,这就表明你的应用程序比较臃肿,如果是这样的话,你可以考虑将这个应用程序分成一些内部相关联的小应用程序。

WHAT’S NEXT

在这一章里,我们仔细讨论了Palm操作系统的外部界面。在下一章里,我们讨论Palm系统所有现成可用的工具;接着,我们以设计一个计算器为例,来探讨在代码编写、应用程序组织以及用户界面设计方面,怎样才能利用这些方法来设计一个好的应用程序.
第十一章 工具和方法简介
我们已知道了系统臃肿是很危险的,现在就来讨论所有现成的工具和方法,或许可以防止使你成为一个更危险系统的提供者。或许你有一个具体的Palm OS应用程序,而没有所需的硬件资源,或许你的解决方法必须和Linux工具相联系,下面将介绍有关这些方面的信息。

因为在World Wide Web上,我们的知识变化得太快,所以先给出我的网址:

www.mykland.com/palmosbook/

在我的主页上提供了许多的信息,包括书的附录、其他我以前写的文档、例程和应用程序等等。更重要的是,我会及时更新在本书中所讲的内容。

Palm操作系统的开发工具

这一节将涉及到免费和非免费的主要开发工具,利用这些工具可以来开发Palm OS应用程序。表11介绍了这些工具的纲要,在后面我们将仔细讨论这些工具的使用。

CoderWarrior

名字 描述
CodeWarrior 在Mac OS和Win32平台上最流行的Palm OS系统集成开发环境

GCC/PRC工具 Palm OS的一个免费的编译连接器,高效但是难于使用。是UNIX用户的唯一选择。

Palm OS Emulator 一个对各个Palm OS版本提供广泛支持的全方位模拟器,能在Win32、Mac Os、Unix环境下模拟Palm OS,是调试应用程序的必备工具

Palm OS SDK 来自Palm Computing的一份包含最新支持代码和技术文档的免费工具套件

Pendragen Forms 一个非C语言的Palm OS开发环境。于

Satellite Forms相比由于使用的较差的用户接口,使得应用程序变得庞大臃肿,它的技术文档和,用户指南也不是高质量的。

PiLRC Palm OS一个免费的资源编译器。有许多人认为它比CoderWarrior资源构造器更加优秀

Satellite Forms 对非编程人来说,这是一个完美的开发工具,非常有利于快速开发。如果你想成为一个开发者,必须对它十分熟悉。但须为每个你所开发的Palm应用程序购买一个许可证,这使得开发变得比较昂贵。

CodeWarrior
对Palm操作系统来说,它是一个主要开发工具,用C语言可以非常方便和快捷写程序和调试程序。如果你是一个正规的Palm开发者,即使你在大多的时间并没有用到,你也应该拥有这个工具。CoderWarrior有Windows和MacOS两个版本。在本书中,用的就是这个工具。

开发者 Metrowerks,Inc
网址: www.metrowerks.com/
价格: 完全专业版$269左右
试用版: 在本书的背面的光盘上有CoderWarrior6 Lite,也可以从www.palm.com/devzone/tools/cw/免费下载

GCC/PRC-TOOLS
GCC/PRC-TOOLS首先是由Free Software Foundation开发,现在由EGCS Steering Committee管理。GCC被大多数人认为包含了最好的68K连接器,与其他非免费的工具相比,他编译连接生成的代码更快更少。

Pendragon Forms
这是一个用于Palm设备的非C开发环境,我曾有机会看到它在ACCESS 97 下的免费版本。

我相信程序员不难发现这个开发环境与Satellite Forms相比,使用起来比较困难;与Code Warrior 相比,它又缺乏个性和可用性,而且其与用户的接口也很难处理。并且它的文档和索引的质量也很差。

开发者 Pendragon Software Corporation
URL www.pendragon-software..com/
价格 $149
测试版 可以从www.pendragon-software.com/forms3/downloads.html免费下载

y
PilRC
PilRC是把程序文本转化为一个二进制图像的工具。该图像和GCC/PRC_Tools组件一起使用可做一个完整的Palm OS应用程序。这是个不错的工具,所以有人说它比Constructor还好。它不是画图工具,却包含了一个称为PilRCUI的浏览器,可以让你看到所编制的画面。如果在UNIX上运行或你有一个不能很好处理二进制文件的类似于SCCS的资源控制系统(source Control System),PilRC是必需的.
开发者 Aaron Ardiri
URL Www.hig.se/~ardiri/development/palmIII/pilrc/
价格 免费
测试版 N/A


Palm OS Emulator
在过去相当长的一段时间内,曾经只有在Mac OS平台下用户才能够运行Palm OS模拟器(一般简称为POSE),但是现在不同了,POSE的Windows版本也同样表现出了极好的运行特性,而且据说,POSE的UNIX版本也正在逐步的改进中。这个优秀的程序在桌面上几乎完全模拟了Palm设备内部的工作环境。你只需要一个Palm 的ROM文件就可以使它正常的运行,你可以从自己的Palm中得到一个ROM文件的拷贝。当然了,如果你签署了Palm Computing的一些协议,那么你就可以从他们的网站下载各种机型Palm的ROM文件了,这其中也包括了debug版本的ROM文件,这是你从任何一台Palm上都下载不到的。

作为调试工具,模拟器要优于一台真实的Palm设备,CodeWarrior的调试器(debugger)运行速度很快,即使从头到尾的遍历所有代码也用不了很长的时间,在模拟器中带有一个灵巧的调试特性叫做Gremlin,通过Gremlin你能够向Palm应用程序发送许多随机事件,它知道如何充分的控制和操作这些事件。Gremlin也可以用于测试用户自定义控件(Gadget)。它确实是可以发现许多你想不到的可以令你的机器崩溃的问题。本书所带的例子程序就是经过了1,000,000个以上的Gremlin事件测试的。但这并不是说Gremlins就能够完全代替功能性测试。

你或许能够找到其它的一些调试应用程序的方法,但是我敢打赌你在正式使用你的程序之前是一定会用模拟器来测试它的。

在Code Warrior中是这样使用POSE的
1. 在Code Warrior中选中Edit | Preferences菜单项

2. 在左边的目录树中选择Palm Connection S under Debugger

3. 在右边窗口的Target组何框中选中:Palm OS Emulator

4. 选中Always Launch Emulator 选择框

5. 点击Choose按钮,找到Emulator.exe

6. 点击Save按钮

7. 在确保模拟器没有运行的状态下,关闭IDE

8. 再次打开IDE时,模拟器就会跟着打开了。

开发者 Palm Computing, Inc.
URL www.palm.com/devzone/pose/pose.html
价格 免费
测试版 N/A


Palm OS SDK
这是Palm Computing为所有Palm开发者提供的基本开发文档和工具。只要加入Palm Computing的解决方案提供计划,你就可以免费的得到这个有用的工具套件。

开发者 Palm Computing, Inc.
URL www.palm.com/devzone/tools/
价格 免费
测试版 N/A

Satellite Forms

对于非程序员,这是一个很好的工具。相当多的非技术人员可以通过这个工具开发出一个合理的应用程序。它很容易使用,且相关文档也写得非常好。它是一个极快的控件组装的开发环境。

但它有两个缺点:对于超越直接和标准控件的开发,就只有专业的开发人员才能做到了;更不幸的是,对Palm OS 的每一个应用程序,都必须花钱去买许可证。这些限制条件就使得它不能应用于更复杂的应用程序和更广泛的应用。
开发者 Puma Technology
URL Www.pumatech.com/
价格 标准版$795,企业版$995,外加一人一个许可证费
测试版 从www.pumatech.com/trial-sf.html免费下载
Window Conduit 开发工具
这是Windows 95,98和NT开发管道(conduit)的相关工具的回顾。下表是这些工具的纲要:
名字 描述
CDK 从Palm Computing 可得到免费的标准管道开发包
CDK Java 版 从Palm Computing 可得到标准的管道开发包的Java版
Insider 帮助你检查Palm设备的数据库的便利的软件包
VisualCafe Professional Palm Computing CDK使用的Symantec公司开发的Java IDE
Visual C/C++ Palm Computing CDK使用的Microsofe公司开发的C++ IDE

CDK
这是Palm Computing提供的免费的管道(Conduit)软件包。它使用Microsoft的Visual C/C++,特别是MFC类库.
你可以完全不使用MFC,但若想做到这一点,你必须很谨慎操作。
开发者 Palm Computing,Inc.
URL www.palm.com/devzone/cdkjwin/cdkwin.html
价格 免费
测试版 N/A

CDK JAVA Edition
这是 Palm Computing 推出的JAVA版的免费标准管道开发软件包。它以Symantec公司的Visual Cafe Pro作为开发工具。我没用过这个开发包,虽然我希望你能顺利的使用这个工具,但文档上说它还没有在Sun公司的免费Java下测试过。
开发者 Palm Computing,Inc.
URL www.palm.com/devzone/cdkjava/cdkjava.html
价格 免费
测试版 N/A
Insider
这是一个便利的小工具,允许你检查Palm的数据库。通过这个工具,你甚至不必再需要Palm上的应用软件去确定你生成的数据库是否正确。
开发者 Sylvain Beaulis
URL www.iro.umontreal.ca/~beaulis/pilot.html
价格 $10
测试版 从www.iro.umontreal.ca/~beaulis/pilot.html免费下载
Visual Cafe Professional
这是一个好的Java 集成开发环境,使用它会感到十分方便。
开发者 Symantec.
URL Www.symantec.com/domain/cafe/index.html
价格 专家版是$95.95
测试版 无

Visual C/C++
这是Windows下的工业标准开发环境,虽然它并不是IDE中做的最好的,但很多人都懂得如何使用。最令人疯狂的是MFC类库,当错误很多时不要使用它。如果你想大量使用MFC,你应考虑使用一个内存调试工具,如NuMega BoundsChecker。
开发者 Microsoft
URL Msdn.microsoft.com/visualc/
价格 专家版是$95.95
测试版 无
Macintosh Conduit 开发工具
以下是在Macintosh下开发管道的相关工具的综述.
名字 描述
CDK Palm Computing公司的在MacOS下的开发管道的免费软件包
用于MacOS的Code Warrior MacOS开发的最流行的集成开发环境

CDK
这是MacOS开发管道的标准开发包。
开发者 Palm Computing,Inc.
URL www.palm.com/devzone/cdkmac/cdkmac.html
价格 免费
测试版 N/A

用于MacOS的CodeWarrior:
这是一个很受人喜欢的MacOS开发软件。
开发者 Metrowerks
URL www.metrowerks.com/desktop/mac_os/
价格 程序员喜欢的专家版是$404.95
测试版 无





外围设备

有很多Palm OS外围设备正在开发中。下面的产品都是已经开发出来的。

TRGpro卡

浏览www.trgpro.com/support/cf_compatible.html 网址可得到一张关于CompactFlash卡与TRGpro一起使用使用清单。这里有许多内存卡和一些其他设备,包括Pretec Compact Modem 56k、Socket通信串口I/O CF+卡以及Communications Bar Code Wand CF+卡等。

Visor 卡

浏览www.handspring.com/products/modules.asp可得到有关新的跳板模块的信息。目前有8M闪存模板、备份模板和一些游戏的信息。

一般的外围设备

Palm Computing 自己有售各种外围设备、键盘和Modem等。另外你也可从LandWare(www.landware.com/)和iBiz(www.ibizcorp.com/)得到用于Palm设备的键盘。
TRG出售用于Palm IIIx 和一些其它Palm设备的存储器扩展器(www.superpilot.com/)。

Corex Technologies生产CardScan卡,它是能够用于Palm 设备的一个扫描器设备(www.cardscan.com/)。
DeLorme生产可插到Palm设备的modem口的GPS接受器。它被称作Earthmate,你可以从www.delorme.com/gps.htm找到有关它的信息。

最后可能也是最有趣的外围设备是Novatel Wireless,它是用于Palm设备的无线modem(www.novatelwireless.com/)。这个无线modem包括了所有标准Palm设备,包括IIIc,
并得到许多的最好的无线网络商的支持,如AT&T。

网址

以下的网址可使你获得相关主题的最新信息。
EScribe
在www.escribe.com/computing/上,目前有4个关于Palm OS 各个方面和 conduit开发开发者论坛:

1 Palm Computing Platform Conduit Developer Forum

2 Palm Computing Platform Emulator Forum

3 Development Questions About the Palm Computing Platform

4 Palm Computing Platform PQA Developers Forum

Handspring
这是生产Visor Palm OS设备公司的网址。它为开发者能充分利用他们的Springboard插槽提供了有用的信息。
J.Marshall
(www.Homepages.enterprise.net/jamarshall/palmos/)是我所找到的使用GCC编写Palm OS 应用程序最好网站。

Massena.com
在www.massena.com/darrin/pilot/index.html网站上有很多有用的其它链接和一些开发工具。

Palm Computing
正如你想象的一样,这个网址上(www.palm.com)有很多有用的链接和信息。记得一定要查找Developer Zone。如果你是真的要为Palm编程,加入Palm的免费Solutions Provider程序来获得访问Provider Pavilion的权利。在那里有Palm Computing的各种各样的原型和测试的硬件和软件.

Qualcomm/Kyocera
Qualcomm(www.qualcomm.com)最近将负责pdQ电话的分部售于了Kyocera(www.kyocera.com/)。在Qualcomm网址上还有一些pdQ的信息。到现在为止,在Kyocera网址上还没有相关信息。

Quality Partners
在www.qpqa.com.com/palm/index.html上有很多文献,它将告诉你怎样彻底调试你的Palm OS应用程序和如何获得资格认证。

RoadCoders
www.developer.com/roadcoders/上有很多与Palm OS主题有关的有趣的文章和例程。

Symbol
它是在Palm设备嵌有条形码扫描器的的硬件制造商(www.symbol.com/products/mobile_computers/mobile_palm.html)。SPT1740也有无线LAN接口。你能这个URL上找有关与这个硬件接口的信息。

TRG
它是为Palm Computing设备制造TRGpro设备和存储器扩展单元的制造商。在www.trgpro.com/ 上有如何使用CompactFlash slot的信息。
第十二章 专业编程技巧
随着程序变得越来越大,我们会发现很多问题。这些问题或许在编制几千行代码时不会出现,但是当编到上万行或更多时并且程序由不同的程序员来编写,在运行时问题就出现了。

这就要求程序有经深思熟虑的结构和详细的注释。在编制大程序时主要要考虑两点:

1 怎样使程序容易维护
2 怎样使程序可重用(reuse)

当然,我们想使程序可以被任何一个人都看得懂,容易维护并重用。特别是对Palm OS来说,由于它的很多应用窗体都十分相似,采取这样的策略将带来很大的效率。虽然PC的处理器速度和内存都在飞速的发展,但是就Palm OS现在的情况来看,必须要聪明的使用处理器和内存。但也不能忽视可维护性,只有具备好的可维护性,才能使代码更容易被优化。

这一章中,我们将接触到很多专业的编程策略。在学习的过程中,我们还将根据这些策略建立一些可重复使用的模块,希望你能在这些编程策略中得到益处。

通过这一章的学习,可为你创建Palm OS应用程序打下一个坚实的基础。

类型保护变量和可移植性

可移植性是代码可重复使用的关键因素之一。可移植性是指代码可以做很少的修改就能被不同的编译器所编译;并且只有将界面和代码分离(因为界面一般都是基于系统的,而代码要工作在不同的平台上)才能提高可移植性。

下几章中我们将以计算器的例子来证明下面讲到的内容。计算器的用户界面和程序代码被分离开来,这样就可以很容易将计算器程序应用到Windows 或Macintosh平台上。

为使程序工作在不同的编译器和系统下,使用类型保护变量是主要途径之一。或许你还不知道什么是类型保护变量,但是在本书中我们一直在使用类型保护变量――至少是使用Palm OS版本的类型保护变量。类型保护变量就是指:不是使用标准C中的数据类型如int或char,而是使用命令typedef来自定义的数据类型。
z
为什么类型保护是如此重要呢?这是因为在标准C中,如int在CodeWarrior C编译器中是16位,而在其它的一些常用编译器中包括GCC,它的长度都是32位。如果你在很多地方都使用了数据类型int而又想从CodeWarrior转换到GCC,那么由于类型int的长度问题就会引起很多的bug,数据结构要改变,数据库记录的大小也要改变。情况严重的话,程序将根本就不能运行。

不仅仅是类型int可以造成可移植性的问题。还有一些其它的类型存在此类问题,例如数据类型char,在日本,char是16位,而不是一般所指的8位。因此,如果要使所编制的程序国际化,那就需要重新做大量的工作。

有关类型保护变量和Palm OS,我的意见是如果你在写基于Palm OS的用户界面代码,就要使用Palm OS的类型保护变量。在Palm OS以后的发展中,其开发人员会小心的处理这些类型保护变量。这样你会发现只要使用了正确的类型保护变量,就可以很容易的将现有的版本升级到新版本。

如果你写的代码也将工作在其它的操作系统平台上,也应该使用自己定义的数据类型。当然,也不能再直接调用Palm OS的函数,利用自己的定义的函数名调用这些函数,或者在头文件中使用#define语句修改函数名来调用这些函数。

使用匈牙利符号(Hungarian Notation)

在看一个大块的代码时,很容易将在函数头部定义的变量的数据类型忘掉。使用匈牙利符号是解决这个问题的一个好方法,因为它可以使别人很容易看懂。在匈牙利符号中,它使用了一些字母在变量的开头,可以使你能记住变量的数据类型。例如,“c”代表char,“p”代表指针,所以cpBuffer代表指向一个缓冲区的char*变量。

匈牙利符号有很多样式。在表12-1中,是我的两个版本,我发现它们可以很好的应用在C、C++和Mac、Windows、Unix中,当然还包括Palm OS。在接下来的部分中,我将全部使用这些符号来定义变量。

字母 数据类型 描述和例子
a [] 数组符号:Char caBuffer[20]
b Byte 8字节的数字变量:Byte bFlags
c Char 字节:char c
d Dword 32字节的数字变量:Dword dCounter
e enum 枚举类型的变量:spEvent->eType
f float 32字节浮点数:float fResult
g global 全局性变量,即可以在所有的模块中使

用:app_t gsApp
h handle 在一些系统中是void*,其它的为Int:VoidHand hRecord
i int 整型变量:Int iCounter
j
k const 在C++中为常量定义或返回值定义
l long 长整型:long Ivalue
m member 结构或类的成员变量:char mcaBuffer[20]
n double 64位的浮点数:double nBigNum
o Boolean 布尔值,真或假:Boolean oFirstPass
p pointer 指针:void* vpPointer
q
r raw 二进制数据类(C++)
s struct 结构或类:sEvent
t text 文本型类(C++)
u unsigned 无符号数:unsigned bong ulNumber
v void 空数据类型:void* vpPointer
w Word 字,一般为16字节:Word wNumber
x
y
z

可重复使用的主模块

我们将重新从头开始,虽然有些烦但是你将有很大的收获。第一步我们先创建一个象在第二章中的Hello程序的例程。但是我们的新程序将更容易被重用并扩展至更大的程序。学习如何创建一个这样的主程序框架并不是什么困难的事。

首先打开CodeWarrior IDE创建一个叫计算器(Caculator)的程序。将源文件和资源文件夹中的文件全部删除,并清除Src文件夹中的所有文件。

从构造器中创建一个新的资源工程,将其保存为Calculator.rsrc。按下CTRL-K新建一个窗体。打开窗体并放置一个按钮到窗体的中间。使用这个按钮将可以测试到我们的程序是不是在运行。然后我们将为计算器创建一个真正的用户界面。程序的界面如图12-1所示:





我们也添加一个叫做LowROMVersionError的警告。当在我们使用的 Palm设备版本太老不能支持程序中使用的函数时,就弹出此警告框,如图-12所示:




现在向工程中添加一些源代码。

1. 打开CodeWarrior IDE并选中Project | Create New Group;

2. 将其命名为AppIncludes。这里面将放头文件。工程将缺省的创建一个叫Caculator_res.h的头文件放到文件夹中;

3. 将Caculator_res.h添加到这个新组中。
main.c模块

现在创建一个叫main.c的新文件,将其放在AppSource组中。这个模块包含着应用程序及事件循环(event loop)的入口以便能重复使用。详细内容如下:

//
// main.c
// Main entry point and event loop.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

注释程序十个很好的习惯,实际上这也是程序可维护性的关键。作为一个专业的程序员,如果写了大量的代码但是没有注释,那么这个程序员的水平就不会再提高并且会变得筋疲力尽。水平不能提高是因为没人能看懂他的程序而不能提出程序的缺陷;变得筋疲力尽是因为剩下的时间他不得不都用在这段烦人的程序上。记住,要注释程序,不要搞成这样。

有时我觉得很幸运,因为在我刚学编程时主要用的是汇编语言。如果不注释程序的话,就不得不花大量的时间来搞清在几周前到底写了些什么东西。使用C也是这样,只是程度有所不同。如果和其它12个人一起编制一个几十万行的代码,就十分有必要将当时是怎么想的以注释的形式写到程序里面。

过去我常常想不起在注释中写些什么内容,现在我基本有了一个大体的格式。例如:在每个模块的开头我都会写如上所示的注释内容,有些人喜欢将版本号也写到里面。把将版本号写到源程序控制系统(source control system)中是必要的,不然,我认为写入版本号没有什么用处。

注意:

什么是源程序控制系统(source control system)呢?这是一个用来存储和返回程序不同的版本号的工具。当发现一个大的bug时,就可以将能正常运行的程序和崩溃的程序相对照,看到底做了哪些改变。你还可以将代码分为不同的版本作不同的用途。如果你和其它的程序员共同编制一个大的程序,那么源程序控制系统将保证你和他们对程序的修改保持同步而防止了程序不统一。总起来说,对于专业程序员来说,源程序控制系统是个很重要的工具。

//
// Includes //
//

#include "app.h" // The definitions for this application

你或许会问:Pilot.h跑到哪里去了?头文件app.h是我们将要自定义的文件,其中将包括我们程序中所用到的所有函数的定义,在这里面是以Pilot.h为头文件的。从某种意义来讲,这是一个自定义的Pilot.h,里面增添了我们自己需要的内容。我想甚至对很大的程序来讲,这也是一个很好的方法。如果你的头文件很大,由此造成了在编译时要花费大量的时间,那么就可以先让CodeWarrior 预编译这个文件,这样会使编译速度大大加快。

///
// Global Prototypes //
///

DWord PilotMain( Word, Ptr, Word ); // Main entry point.
Boolean processEvent( Long ); // Processes the next event.

这些是这个模块中的函数定义的原型。我们一直在使用函数原型,但是它到底有什么作用和好处呢?函数原型可以增强函数的可维护性。当定义了函数原型后,就可以保证不会将不匹配的参数传递到函数中。如果发生此类错误的话,将会造成一些很难理解的错误,并且也不容易找到这些错误。不过,在C中是允许不使用函数原型而调用函数的。为保证这些函数的正确使用,我们将在头文件app.h中定义另一个版本。本模块头部的函数原型定义就可以保证参数的匹配和定义。

/
// Local Variables //
/

// The event handler list
EVENT_HANDLER_LIST

在这里定义了局部变量。这个变量乍看起来像是个语法错误。它定义在app.h中。在后面我们将详细的讲解这个变量。它定义了程序中窗体的事件处理函数。实际上,这个#define语句定义了一个函数,我们还可以用很多其它的普通的方法来定义这个事件处理函数。我想在这里应该用大写字母或其它醒目的格式来代表它,这样可以让别人(还有自己在一段时间后看代码时)能更清楚的看出这个头文件中的定义,以便弄懂它到底有什么作用。

//
// Global Functions //
//

//----------------------------------------------------------------------------
DWord PilotMain(
//----------------------------------------------------------------------------
// The main entry point for this application.
// Always returns zero.
//----------------------------------------------------------------------------
Word wCmd, // The launch code
Ptr, // The launch parameter block
Word ) // The launch flags
//----------------------------------------------------------------------------
{

这是我经常使用的注释形式之一。我觉得这样做很是不错,因为它很清晰,很容易在一个很长的函数模块中找到这个函数的头部。首先写出了函数的内容以及其返回值,后面注释了各个变量和参数的含义。有些人还喜欢将版本号加入到里面,但是我不想这样做,因为这样做并没有什么实际意义。

DWord dROMVersion;

// Get the ROM version
dROMVersion = 0;
FtrGet( sysFtrCreator, sysFtrNumROMVersion, &dROMVersion );

// Alert and bail if the ROM version is too low
if( dROMVersion < ROM_VERSION_MIN )
{
FrmAlert( LowROMVersionErrorAlert );

// Palm OS 1.0 will continuously re-launch this app unless we switch
// to another safe one
if( dROMVersion < ROM_VERSION_2 )
{
AppLaunchWithCommand( sysFileCDefaultApp,
sysAppLaunchCmdNormalLaunch, NULL );
}
return( 0 );
}

在以前也看到过这几行代码,它的作用就是可以防止用户的代码在不支持所用函数定义的Palm设备上运行。在我们的caculator例子中,由于使用的浮点库(floating-point libraries)只存在于Palm OS 2.0以及更高的版本,所以我们将常量ROM_VERSION_MIN定义为ROM_VERSION_2。这样只要我们使用不同的app.h就可以使main.c工作在不同的平台上,将各个模块连接在一起的“枢纽”就写在app.h里面。

// If this is not a normal launch, don't launch
if( wCmd != sysAppLaunchCmdNormalLaunch )
return( 0 );

// Initialize all parts of the application
appInit();

上面的这些代码看起来也很熟悉吧。首先确定是正常运行后,调用函数appInit()初始化程序,它要把应用程序中所有待运行的数据初始化。例如:在Contacts程序中,我们使用appInit()将数据库打开,在下面的具体定义中还有详细的论述。

// Go to the starting form
FrmGotoForm( StartForm );

这里引发了事件来装载StartForm。但我们的并没有一个叫StartForm的窗体,它在app.h中被定义为CalcForm。这就是我们使main.c可重用的另一个技巧。当我们需要改变开始窗体时,我们只需改变app.h,而不需在main.c中做大量的改动。

// Wait indefinitely for events
ErrTry
{
while( true )
processEvent( -1 );
}

// Stop the application
ErrCatch( lError )
{
} ErrEndCatch

// Clean up before exit
appStop();

// We're done
return( 0 );
}

这段代码和我们前面接触到的事件循环代码基本相同,只不过循环中的函数调用是在一个单独的函数processEvent()里面。这样我们就可以控制事件在程序的其它部分是怎样进行的,在后面的章节中,你会发现它是如何做到这样的。

在ErrCatch()后,我们调用了appStop()。象appInit()一样,它是定义于app.h连接不同程序模块的一个纽带,在退出程序前清除程序参数。正如你看到的,甚至正常的程序也在这个catch模块中停止,所以appStop()应该在模块的出口处调用。
下面是我们的事件处理函数:

//----------------------------------------------------------------------------
Boolean processEvent(
//----------------------------------------------------------------------------
// Waits for and processes the next event.
// Returns false if the queue is empty.
//----------------------------------------------------------------------------
Long lTimeout ) // Time to wait in 100ths of a second, -1 = forever
//----------------------------------------------------------------------------
{
注意引入事件中的时间控制参数,这在控制动画制作和声音的程序中很有用。由于Palm OS是一个单任务操作系统,为使程序的效果更好,我们就需要控制程序的运行时间。你也许想问为什么PilotMain()没有返回值而processEvent()却返回了一个布尔量。在以后的程序中(如在动画制作中),我们或许会想知道事件是怎样运行的以便于使动画更加平滑。这时我们就会调用processEvent()来处理或刷新其它的函数。
EventType sEvent; // Our event
Word wError; // The error word for the menu event handler

// Get the next event
EvtGetEvent( &sEvent, lTimeout );

这里定义了一些熟悉的变量,后面调用了函数EvtGetEvent()。注意,这个函数将我们的时间参数传递给了系统。

// If it's a stop event, exit
if( sEvent.eType == appStopEvent )
{
// Exit
ErrThrow( 0 );
}

停止事件处理和以前相比有了些变化。它不只是向main()返回一个值,而是向mian.c中的ErrCatch()模块回馈了函数ErrThrow()。为什么我们不能象从前那样做了呢?如果我们在程序的其它地方调用了函数processEvent()并收到一个appStopEvent,它就不是向main()返回值,而是向其它的地方返回了一个值。所以我们就需要一个机制,不论在哪里收到了appStopEvent,错误处理器都会象C中那样安全。

// If it's a nil event, return queue empty
if( sEvent.eType == nilEvent )
return( false );
这是循环中一个新的检查措施。当事件队列为空时,Palm OS就会发出一个nilEvent。由于以前的程序中,我们让Palm OS一直的等待,我们就从来没收到过此事件。现在收到nilEvent后,我们就知道了事件队列是空的。在做一些耗时的工作时,如果调用processEvent来周期性的刷新事件队列,它就会派上用场。因此让它返回一个具体值是很重要的。
// Handle system events
if( SysHandleEvent( &sEvent ) )
return( true );

// Handle menu events
if( MenuHandleEvent( NULL, &sEvent, &wError ) )
return( true );

以上是一些常用函数,我们用它们处理系统事件和菜单事件。

// Load a form
if( sEvent.eType == frmLoadEvent )
{
Word wFormID; // The form ID
FormPtr spForm; // Points to the form

// Get the ID
wFormID = sEvent.data.frmLoad.formID;

// Initialize the form
spForm = FrmInitForm( wFormID );

// Establish the event handler
FrmSetEventHandler( spForm, getEventHandler( wFormID ) );

// Point events to our form
FrmSetActiveForm( spForm );


这里是普通的窗体加载代码。你看起来肯定很熟悉吧,首先从事件结构(event structure)中获得窗体的ID,然后初始化并绘制窗体。然而要注意函数FrmSetEventHandler()中的getEventHandler()。这个函数定义在app.h中,可以根据支持不同窗体的模块,以相应的事件句柄获得相匹配的窗体ID,这就允许我们以相同的方式处理main.c中所有的窗体。因此这段代码是我们不必知道程序中到底有什么样的窗体,就可以重用这些代码来进行处理。

// Handle form events
FrmDispatchEvent( &sEvent );
// We're done
return( true );
}

这是processEvent()的最后一点。我们将余下的事件分配到活动的窗体中。如果的确还有事件没有处理的话,函数将返回true。
Main.h模块
头文件main.h中有支持main.c的定义,并且有为了自定义main.c而在app.h中添加的函数定义。创建一个main.h文件放入源文件中,将之加入Calculator工程并保存在AppIncludes中。下面是它的详细代码:

#ifndef MAIN_H
#define MAIN_H
//
// main.h
// Definitions for the main entry point.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

///
// Global Prototypes //
///

DWord PilotMain( Word, Ptr, Word ); // Main entry point.
Boolean processEvent( Long ); // Processes the next event.

///
// Constants //
///

// The different versions of PalmOS
// Pilot 1000 and Pilot 5000
#define ROM_VERSION_1 0x01003001
// PalmPilot and PalmPilot Professional
#define ROM_VERSION_2 0x02003000
// Palm III, IIIx, and V
#define ROM_VERSION_3 0x03003000

#endif // MAIN_H

在程序的开始是两个预处理指令:#ifndef MAIN_H和#define MAIN_H,相应的在程序的结尾,也有一个预处理指令:#endif。如果MAIN_H没有定义的话,#ifndef和#endif这对预处理指令就会跳过中间所有的代码,MAIN_H将在它们的内部开始定义。预处理指令的作用就是把编译器已经编译过的内容忽略以防重复编译。

在一些比较大的程序中,为方便起见,在同一个文件中头文件不止被引用一次。这时候就可以将代码分开编译,而不必担心文件中函数到底有没有定义。这些预处理指令就可以避免让编译器花费一些不必要的时间来进行一些重复的编译工作。

在标准的头模块的内容下面是main.c中的函数原型,由于它们和源文件中的函数定义完全相同,所以如果两者间有一点点不同,编译器就会给出错误警告。

在函数的最后是对Palm OS的版本号的常量定义。为了使将来的程序少有版本不符的情况,将版本号在头文件中进行定义是有必要的(如果系统中没有定义的话)。这是因为代表版本号的十六进制数很难引用和理解,如果不定义的话,在将来会造成很多麻烦。

Fcalc.c模块

在fcalc.c中只有一个事件处理函数。创建一个叫fcalc.c的新文件并保存在工程的Src文件夹中。下面是fcalc.c的详细代码。在我们熟悉的头文件声明的代码后,是事件处理函数的函数原型。

//
// fcalc.c
// Code for the "calc" form.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include "app.h" // The definitions for this application

///
// Global Prototypes //
///

Boolean calcFormEventHandler( EventPtr spEvent );

//
// Global Functions //
//

//----------------------------------------------------------------------------
Boolean calcFormEventHandler(
//----------------------------------------------------------------------------
// Handles events for this form.
// Returns true if it fully handled the event.
//----------------------------------------------------------------------------
EventPtr spEvent )
//----------------------------------------------------------------------------
{
// Handle the event
switch( spEvent->eType )
{
// A control was selected
case ctlSelectEvent:

// Sound an alarm
SndPlaySystemSound( sndAlarm );
return( false );

// A menu item was selected
case menuEvent:

// Handle the menu event
calcFormMenuEventHandler( spEvent );
return( true );
}

// We're done
return( false );
}

当有按钮按下时,函数发出提示声音 ,当菜单选项被选中时,我们调用函数calcFormMenuEventHandler()来处理,它实际上是定义在app.h中的一个宏,允许我们在其它的程序中选择菜单时也可以调用这个函数。

注意到,当我们按下按钮时函数的返回值是false。这是因为Palm OS还需要进一步的处理按钮按下的图形。记住,一般情况下,当函数返回false时是希望系统忽略这个事件,但是在菜单事件中(memuEvent),如果在处理一个菜单事件后返回false,Palm OS就会发出一声警告声,好像出现了什么错误似的。

当编制这段程序后,我们就可以天衣无缝的将这个程序添加到其它的应用程序中。例如:在一个财政信息软件中,我们可以把它做成一个弹出窗口,再根据主窗口的需要进行计算。

Fcalc.h模块

Fcalc.h是fcalc.c的头文件。创建一个新文件,将其命名为fcalc.h,并保存在Caculator工程的Src文件夹中。

象main.h一样,fcalc.h也有预处理指令#ifdef、#define、#endif等以防止编译器作无用功。Fcalc.h中的代码定义了fcalc.c中函数原型,以保证在调用此函数时,这个函数已定义过并且参数传送正确。

#ifndef FCALC_H
#define FCALC_H
//
// fcalc.h
// Definitions for the "calc" form.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

///
// Global Prototypes //
///

Boolean calcFormEventHandler( EventPtr spEvent );

#endif // FCALC_H

你或许会问,这样一个头文件中怎么就只定义了一个函数呢?这个问题关键在于我们现在只有一个函数。不过要十分注意的是,我们首先要有一个完整的开始框架,为将来的扩展着想,为以后的代码留出空间。如果永远也不扩展的话也没关系,这样作可以更容易的找到函数。
App.h模块

头文件app.h将前面所有模块的头文件汇总在了一起。创建一个新文件,将其命名为app.h并存放在Src文件夹中。将其添加在工程中,放在AppIncludes中。

下面让我们一步一步的来分析代码:

#ifndef APP_H
#define APP_H
//
// app.h
// Definitions for the application.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include <Pilot.h> // All the Palm includes
#include "Calculator_res.h" // Resource definitions
#include "fcalc.h" // Definitions for the "calc" form
#include "main.h" // Definitions for the main entry point

app.h看起来和我们以前编辑的头文件十分相似,首先是#ifndef和#define预处理指令,然后是一些头文件的定义。第一个是Pilot.h头文件的定义。由于我们将在所以的源文件中都使用app.h头文件,所以必须在此处定义Pilot.h。下一个是资源头文件。注意到因为Pilot.h是系统头文件,所以被包括在<>中,我们自定义的头文件被被包括在引号中。

下面是fcalc.c的头文件定义,app.h被分成了几个部分分别处理各个源文件,这样就使我们在将来可以很容易的从程序中摘取必要和充分的代码应用于其它的模块。

/
// Definitions for fcalc.c //
/

// The menu event handler macro for the "calc" form
#define calcFormMenuEventHandler(spEvent)

这里是菜单处理函数的定义。现在的定义没有什么实际意义,在代码中也没有内容,这是因为calc窗体还没有菜单。我特意在函数调用的括号中留出了一个空格,宏会在宏名称后的第一个空格后开始运行,所以你向宏添加参数时,为是编译正常进行,注意不要留出空格。
注意:

只有在必要的时候才使用宏,因为使用宏是十分危险的。一般的函数都是可以利用原型来检查函数的参数,而对于宏来说没有函数原型。如果你向宏中传递了错误的参数,你将很难检查出来。我们在这里使用宏是想创建一个快速简易的“函数”,除了在源文件中被引用,它们不会占有代码空间。我们不想在头文件中添加函数,因为那样会使它们在每次被在源文件调用时都会被执行。

再下面是main.c的定义,首先是函数getEventHandler()的原型,在这里有一个我们以前没有见过的定义:EVENT_HANDLER_LIST。


// Definitions for main.c //


// The prototype for getEventHandler()
static FormEventHandlerPtr getEventHandler( Word );

// This creates the function getEventHandler,
// which returns the event handler for a given form
// in this application.
#define EVENT_HANDLER_LIST
static FormEventHandlerPtr getEventHandler( Word wFormID )
{
switch( wFormID )
{
case AboutForm:
return( aboutFormEventHandler );
case CalcForm:
return( calcFormEventHandler );
case PrefsForm:
return( prefsFormEventHandler );
}
return( NULL );
}

EVENT_HANDLER_LIST实际上就是函数getEventHandler()的定义。在main.c中预处理器用这个定义代替了此函数。我们可以在任何地方使用#define语句,它实际上就象文本编译器中的查找替换命令。在每一行后面的反斜杠可以使预处理器忽略每一行后的行中断符,当遇到行中断符时,#define语句就会停止,所以我们在getEventHandler()的最后一行没有反斜杠。

现在,getEventHandler()只是关联了窗体CalcForm中的calcFormEventHandler()。如果想再添加窗体以及事件处理函数,我们可以在头文件中直接扩展。

// This defines the macro that initializes the app
#define appInit()

// This defines the macro that cleans up the app
#define appStop()

// This application works on PalmOS 2.0 and above
#define ROM_VERSION_MIN ROM_VERSION_2

// Define the starting form
#define StartForm CalcForm


// Definitions for moptions.c //


// Menu ID name conversions
#define OptionsAbout OptionsAboutCalculator

#endif // APP_H

这里是appInit()和appStop()的宏定义。但它们现在还没有实在意义。文件的最后定义了运行此程序的最低版本为version 2.0,其中变量使用了main.c中的定义。我们还定义了开始窗体是CalcForm。最后是#endif语句,千万不要忘记这一结束语句,否则在编译时会出现很奇怪的错误。

建立一个向app.h这样的文件的主要目的是节省运行时间,并做到尽量在头文件中修改代码,而不要在源文件中修改。这样做有很多好处因为只要你修改代码,就有可能引入bug,但是一般来说,修改程序中的常量可以减少引入bug的机会。如果将代码的修改集中在一些头文件中,就可以减少本来运行正常的程序经修改后崩溃的比率。

如果一个程序员一般不会向程序中引入bug,这样的程序结构也是有好处的,因为即使程序中有良好的注释,也需要花费一些时间来看懂程序并作修改。如果将改变集中在有着好的文件结构的头文件中,就会比重新学习各个模块的细节花费的时间少的多。
调试


所有模块均已完成,现在开始调试。如果不出意外的话,运行后的程序界面如下图所示:




程序从函数PilotMain()开始执行。初始化过程如下:检查ROM的版本并检查运行代码,然后调用了函数FrmGotoForm(),它将产生两个待处理的事件:frmLoadEvent和frmOpenEvent,接着就进入了无限的事件循环。
FrmLoadEvent引发了processEvent(),它将从事件结构中获得窗体的ID并初始化窗体,然后就等待其它事件的发生。
如果你按下了OK按钮,就会发出一个提示声音。
程序列表:
下面是app.h全部的源代码:
#ifndef APP_H
#define APP_H
//
// app.h
// Definitions for the application.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include <Pilot.h> // All the Palm includes
#include "Calculator_res.h" // Resource definitions
#include "fabout.h" // Definitions for the "about" form
#include "fcalc.h" // Definitions for the "calc" form
#include "fprefs.h" // Definitions for the "prefs" form
#include "main.h" // Definitions for the main entry point
#include "moptions.h" // Definitions for the "options" menu

//
// Definitions for fabout.c //
//

// The menu event handler macro for the "about" form
#define aboutFormMenuEventHandler(spEvent)

/
// Definitions for fcalc.c //
/

// The menu event handler macro for the "calc" form
#define calcFormMenuEventHandler(spEvent)
{
optionsMenuEventHandler( spEvent );
}

//
// Definitions for fprefs.c //
//

// The menu event handler macro for the "prefs" form
#define prefsFormMenuEventHandler(spEvent)


// Definitions for main.c //


// The prototype for getEventHandler()
static FormEventHandlerPtr getEventHandler( Word );

// This creates the function getEventHandler,
// which returns the event handler for a given form
// in this application.
#define EVENT_HANDLER_LIST
static FormEventHandlerPtr getEventHandler( Word wFormID )
{
switch( wFormID )
{
case AboutForm:
return( aboutFormEventHandler );
case CalcForm:
return( calcFormEventHandler );
case PrefsForm:
return( prefsFormEventHandler );
}
return( NULL );
}

// This defines the macro that initializes the app
#define appInit()

// This defines the macro that cleans up the app
#define appStop()

// This application works on PalmOS 2.0 and above
#define ROM_VERSION_MIN ROM_VERSION_2

// Define the starting form
#define StartForm CalcForm


// Definitions for moptions.c //


// Menu ID name conversions
#define OptionsAbout OptionsAboutCalculator

#endif // APP_H

下面是main.c全部的源代码:

//
// main.c
// Main entry point and event loop.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include "app.h" // The definitions for this application

///
// Global Prototypes //
///

DWord PilotMain( Word, Ptr, Word ); // Main entry point.
Boolean processEvent( Long ); // Processes the next event.

/
// Local Variables //
/

// The event handler list
EVENT_HANDLER_LIST

//
// Global Functions //
//

//----------------------------------------------------------------------------
DWord PilotMain(
//----------------------------------------------------------------------------
// The main entry point for this application.
// Always returns zero.
//----------------------------------------------------------------------------
Word wCmd, // The launch code
Ptr, // The launch parameter block
Word ) // The launch flags
//----------------------------------------------------------------------------
{
DWord dROMVersion;

// Get the ROM version
dROMVersion = 0;
FtrGet( sysFtrCreator, sysFtrNumROMVersion, &dROMVersion );

// Alert and bail if the ROM version is too low
if( dROMVersion < ROM_VERSION_MIN )
{
FrmAlert( LowROMVersionErrorAlert );

// Palm OS 1.0 will continuously re-launch this app unless we switch
// to another safe one
if( dROMVersion < ROM_VERSION_2 )
{
AppLaunchWithCommand( sysFileCDefaultApp,
sysAppLaunchCmdNormalLaunch, NULL );
}
return( 0 );
}

// If this is not a normal launch, don't launch
if( wCmd != sysAppLaunchCmdNormalLaunch )
return( 0 );

// Initialize all parts of the application
appInit();

// Go to the starting form
FrmGotoForm( StartForm );

// Wait indefinitely for events
ErrTry
{
while( true )
processEvent( -1 );
}

// Stop the application
ErrCatch( lError )
{
} ErrEndCatch

// Clean up before exit
appStop();

// We're done
return( 0 );
}

//----------------------------------------------------------------------------
Boolean processEvent(
//----------------------------------------------------------------------------
// Waits for and processes the next event.
// Returns false if the queue is empty.
//----------------------------------------------------------------------------
Long lTimeout ) // Time to wait in 100ths of a second, -1 = forever
//----------------------------------------------------------------------------
{
EventType sEvent; // Our event
Word wError; // The error word for the menu event handler

// Get the next event
EvtGetEvent( &sEvent, lTimeout );

// If it's a nil event, return queue empty
if( sEvent.eType == nilEvent )
return( false );

// Handle system events
if( SysHandleEvent( &sEvent ) )
return( true );

// Handle menu events
if( MenuHandleEvent( NULL, &sEvent, &wError ) )
return( true );

// Load a form
if( sEvent.eType == frmLoadEvent )
{
Word wFormID; // The form ID
FormPtr spForm; // Points to the form

// Get the ID
wFormID = sEvent.data.frmLoad.formID;

// Initialize the form
spForm = FrmInitForm( wFormID );

// Establish the event handler
FrmSetEventHandler( spForm, getEventHandler( wFormID ) );

// Point events to our form
FrmSetActiveForm( spForm );

// Draw the form
FrmDrawForm( spForm );
}

// Handle form events
FrmDispatchEvent( &sEvent );

// If it's a stop event, exit
if( sEvent.eType == appStopEvent )
{
// Exit
ErrThrow( 0 );
}

// We're done
return( true );
}

可重用的About窗体
在这一部分中,我们将向工程中添加两个窗体和它们相应的代码:About窗体,本程序的关于对话框;prefs窗体,一个用来显示优先权的对话框。在这一章我们只是在这两个窗体上添加一个OK按钮以示它们的存在。
文件Calculator.rsrc的内容添加
我们将向窗体添加一个about窗体、一个prefs窗体和一个菜单项。
1. 运行构造器并打开文件Caculator.rsrc;
2. 创建about窗体的框架,我们将拷贝calc窗体然后作一些修改;
3. 选中calc窗体;
4. 选择Edit | Copy并按下CTRL-C拷贝窗体;
5. 选择Edit | Paste并按下CTRL-V将窗体粘贴到构造器上,此时会出现一个对话框显示输入该窗体的ID;
6. 选中Unique ID;
7. 将窗体的名字改为“about”;
8. 双击打开about窗体,进入窗体属性选项;
9. 选中Save Behind,设置title为Caculator;
10. 将按钮向下移动20个象素,避免和calc窗体的按钮重合,这样就可以容易的和calc窗体进行切换。当一切完成后,about窗体应如图12-3所示:

现在创建prefs窗体。拷贝粘贴About窗体并重命名为“prefs”。将title属性改为“Caculator Preference”
将按钮位置在下移20个象素,完成后的窗体应如图12-4所示:





然后添加一个菜单栏使我们可以在两个新建窗体间切换。首先从菜单栏资源类型和列表中创建一个菜单栏并按下CTRL-K,将其命名为“calc”。选择Resource type创建一个新的菜单并按下CTRL-K,并将其命名为“options”。双击打开菜单栏,将一个菜单拖动到菜单编辑框中。将菜单命名为Options,然后按下CTRL-K添加菜单项,并命名为Preferences...,快捷键为“R”。另外一个菜单项命名为About Calculator。设计完成后,菜单栏如下图所示:

fabout.c模块
现在开始为我们新建的窗体和菜单添加代码。下面所示的是fabout.c的源代码。它包括了about窗体的事件处理函数以及这些函数的原型。因为在结构上它和fcalc.c十分相似,所以我建议将fcalc.c拷贝,在这样的基础上进行修改。最后别忘了将fabout.c保存在AppSource下面。

//
// fabout.c
// Code for the "about" form.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include "app.h" // The definitions for this application

///
// Global Prototypes //
///

Boolean aboutFormEventHandler( EventPtr spEvent );

//
// Global Functions //
//

//----------------------------------------------------------------------------
Boolean aboutFormEventHandler(
//----------------------------------------------------------------------------
// Handles events for this form.
// Returns true if it fully handled the event.
//----------------------------------------------------------------------------
EventPtr spEvent )
//----------------------------------------------------------------------------
{
// Handle the event
switch( spEvent->eType )
{
// A control was selected
case ctlSelectEvent:

// Return to the calling form
FrmReturnToForm( 0 );
return( false );

// A menu item was selected
case menuEvent:

// Handle the menu event
aboutFormMenuEventHandler( spEvent );
return( true );
}

// We're done
return( false );
}

在aboutFormEventHandler()中,我们处理了两种类型的事件。如果一有按钮按下,那么就调用FrmReturnToForm()将控制权交给所要引用的窗体。利用这个函数,我们就可以建立一个弹出窗体,它只能由函数FrmPopupForm()引发而不能被FrmGotoForm()所调用。由于about窗体通常都是弹出窗体,所以这样做并不会影响代码的可重用性。
对于菜单事件,我们调用了函数aboutFormEventHandler()。它是我们在app.h中的一个宏定义。在头文件中给出此宏定义,就可以让我们在不同的程序中的菜单上重用此代码。
Fabout.h模块
此头文件和calc的头文件本质上是一致的,所以我们拷贝fcalc.h并将其重命名为fabout.h。将其添加到工程中并放置在AppIncludes下面。修改代码和以下所示一致:

#ifndef FABOUT_H
#define FABOUT_H
//
// fabout.h
// Definitions for the "about" form.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

///
// Global Prototypes //
///

Boolean aboutFormEventHandler( EventPtr spEvent );

#endif // FABOUT_H

fprefs.c模块
下面是fprefs.c的源文件。目前该文件除了将“about”改为“fpref”以外,其他的全部和fabout.c相同。将fabout.c拷贝并做相应修改,并添加到AppIncludes下的Calculator工程下面。

//
// fprefs.c
// Code for the "prefs" form.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include "app.h" // The definitions for this application

///
// Global Prototypes //
///

Boolean prefsFormEventHandler( EventPtr spEvent );

//
// Global Functions //
//

//----------------------------------------------------------------------------
Boolean prefsFormEventHandler(
//----------------------------------------------------------------------------
// Handles events for this form.
// Returns true if it fully handled the event.
//----------------------------------------------------------------------------
EventPtr spEvent )
//----------------------------------------------------------------------------
{
// Handle the event
switch( spEvent->eType )
{
// A control was selected
case ctlSelectEvent:

// Return to the calling form
FrmReturnToForm( 0 );
return( false );

// A menu item was selected
case menuEvent:

// Handle the menu event
prefsFormMenuEventHandler( spEvent );
return( true );
}

// We're done
return( false );
}

fprefs.h模块
此头文件和calc和about的头文件基本上是一致的,拷贝fabout.h并将其重命名为fprefs.h。将其添加到工程中并放置在AppIncludes下面。下面是所要做的修改:

#ifndef FPREFS_H
#define FPREFS_H
//
// fprefs.h
// Definitions for the "prefs" form.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

///
// Global Prototypes //
///

Boolean prefsFormEventHandler( EventPtr spEvent );

#endif // FPREFS_H

moptions.c模块
Options菜单的源代码也只有一个事件处理函数。从源文件中任意拷贝一个.c文件,并添加到AppSource下的工程中,根据下面代码列表做相应的修改:

//
// moptions.c
// Code for the "options" menu.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include "app.h" // The definitions for this application

///
// Global Prototypes //
///

Boolean optionsMenuEventHandler( EventPtr spEvent );

//
// Global Functions //
//

//----------------------------------------------------------------------------
Boolean optionsMenuEventHandler(
//----------------------------------------------------------------------------
// Handles events for this form.
// Returns true if it fully handled the event.
//----------------------------------------------------------------------------
EventPtr spEvent )
//----------------------------------------------------------------------------
{
switch( spEvent->data.menu.itemID )
{
// The about menu item was selected
case OptionsAbout:
FrmPopupForm( AboutForm );
return( true );

// The prefs menu item was selected
case OptionsPreferences:
FrmPopupForm( PrefsForm );
return( true );

}

// We're done
return( false );
}

当菜单项被选中后,我们调用FrmPopupForm()弹出相应的窗体。如果菜单项和窗体ID相符返回true,否则返回false。
Moptions.h模块
该头文件和以前的头文件也十分相似,只包含和一个事件处理函数的原型。将前面任意的一个头文件拷贝并重命名moptions.h,然后将其添加到AppIncludes下的工程中,相应的修改如下所示:

#ifndef MOPTIONS_H
#define MOPTIONS_H
//
// moptions.h
// Definitions for the "options" menu.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

///
// Global Prototypes //
///

Boolean optionsMenuEventHandler( EventPtr spEvent );

#endif // MOPTIONS_H

app.h的内容添加
为了将新建的模块包含在app.h里面,我们需要添加一些代码,首先是新的头文件:

//
// Includes //
//

#include <Pilot.h> // All the Palm includes
#include "Calculator_res.h" // Resource definitions
#include "fabout.h" // Definitions for the "about" form
#include "fcalc.h" // Definitions for the "calc" form
#include "fprefs.h" // Definitions for the "prefs" form
#include "main.h" // Definitions for the main entry point
#include "moptions.h" // Definitions for the "options" menu

将新建的头文件逐个添加到app.h中,我一般是按照字母的顺序来排列这些文件名,这样就可以很容易查找所需头文件。

//
// Definitions for fabout.c //
//

// The menu event handler macro for the "about" form
#define aboutFormMenuEventHandler(spEvent)

/
// Definitions for fcalc.c //
/

// The menu event handler macro for the "calc" form
#define calcFormMenuEventHandler(spEvent)
{
optionsMenuEventHandler( spEvent );
}

fabout.c和fpref.c所需要的程序入口和fcalc.c相似。然后为各自的菜单添加相应的宏。另外,由于我们添加了菜单,所以菜单处理宏也应该干点什么了。在宏中我们调用了函数optionMenuEventHandler()来处理该菜单的各个选项。
下面向getEventHandler()函数添加代码,以使我们新建的窗体能够被引发。

// This creates the function getEventHandler,
// which returns the event handler for a given form
// in this application.
#define EVENT_HANDLER_LIST
static FormEventHandlerPtr getEventHandler( Word wFormID )
{
switch( wFormID )
{
case AboutForm:
return( aboutFormEventHandler );
case CalcForm:
return( calcFormEventHandler );
case PrefsForm:
return( prefsFormEventHandler );
}
return( NULL );
}

about窗体和prefs窗体的入口和calc窗体的模式相同,在main.c中不需要做其它的修改。
为Option菜单添加定义:


// Definitions for moptions.c //


// Menu ID name conversions
#define OptionsAbout OptionsAboutCalculator

因为Calculator_res.h中的常量定义基于具体的菜单项,所以我们应该将其抽象为不含诸如“Calculator”等基于具体程序的文本的通用名称,使之能重复使用。
程序列表
下面是新的app.h的全部代码:

#ifndef APP_H
#define APP_H
//
// app.h
// Definitions for the application.
// Copyright (c) 1999, Robert Mykland. All rights reserved.
//

//
// Includes //
//

#include <Pilot.h> // All the Palm includes
#include "Calculator_res.h" // Resource definitions
#include "fabout.h" // Definitions for the "about" form
#include "fcalc.h" // Definitions for the "calc" form
#include "fprefs.h" // Definitions for the "prefs" form
#include "main.h" // Definitions for the main entry point
#include "moptions.h" // Definitions for the "options" menu

//
// Definitions for fabout.c //
//

// The menu event handler macro for the "about" form
#define aboutFormMenuEventHandler(spEvent)

/
// Definitions for fcalc.c //
/

// The menu event handler macro for the "calc" form
#define calcFormMenuEventHandler(spEvent)
{
optionsMenuEventHandler( spEvent );
}

//
// Definitions for fprefs.c //
//

// The menu event handler macro for the "prefs" form
#define prefsFormMenuEventHandler(spEvent)


// Definitions for main.c //


// The prototype for getEventHandler()
static FormEventHandlerPtr getEventHandler( Word );

// This creates the function getEventHandler,
// which returns the event handler for a given form
// in this application.
#define EVENT_HANDLER_LIST
static FormEventHandlerPtr getEventHandler( Word wFormID )
{
switch( wFormID )
{
case AboutForm:
return( aboutFormEventHandler );
case CalcForm:
return( calcFormEventHandler );
case PrefsForm:
return( prefsFormEventHandler );
}
return( NULL );
}

// This defines the macro that initializes the app
#define appInit()

// This defines the macro that cleans up the app
#define appStop()

// This application works on PalmOS 2.0 and above
#define ROM_VERSION_MIN ROM_VERSION_2

// Define the starting form
#define StartForm CalcForm


// Definitions for moptions.c //


// Menu ID name conversions
#define OptionsAbout OptionsAboutCalculator

#endif // APP_H

调试
又到了调试的时候了,界面应该和以前相同。当你按下菜单栏上的prefs或about选项时,相应窗体就会跳出。当按下各自的OK按钮后,返回到calc窗体。
和以前一样,程序从PilotMain()开始,按照常规的路线向下运行,processEvent()产生的frmLoadEvent事件初始化了窗体的新的菜单栏。然后程序就进入无限循环来等待事件的触发。
如果你按下OK按钮,系统将发出警告声音。
面向对象的编程
面向对象编程的目的就是使代码更加容易被维护和重复使用。基于其它代码中的一段或变量,使用封装和抽象的方法,使bugs变得很明显。只要你弄懂各个代码部分之间的关系,就可以找到并修正这些bugs。代码的重用性提高是因为各个代码部分间的依赖性减少了,这样就可以从很容易的从面向对象的程序中摘出有用的代码应用到其它的工程中去。根据上个世纪中人们对面向对象编程的看法,或许你从认为它不过是骗局转变到认为它是唯一的编程方法吧。或许你是对的,也就是说,不管有没有价值,我也对面向对象的编程谈一下或许并不谦虚的看法。
首先,面向对象的编程应该是一种编程的设计原则,并不和你所使用的编程语言有多大的关系。的确,象C++和JAVA很容易实现面向对象的方法,但应用的便利应是面向对象这种原则所提供的。经过近十年的大量使用面向对象的语言诸如Smalltalk、C++和JAVA的编程,我知道了一个人如果精通JAVA的语法,并不代表他真正知道了如何有效的、有价值的利用对象。
真正重要的应该是理解面向对象的理念和设计模式,知道为什么使用这些理念和设计原则就更有效和更有价值。面向对象的编程也是一种工具,这和别的并没有区别,如果你胡乱用的话,它和其它工具的滥用一样会造成损害。没有人会因为锤子是最新的产品就在房间里乱砸东西。但是,我看到许多人由于使用面向对象的编程方法不对,结果弊大于利。
C++、JAVA和Palm OS
一般来讲,我也很喜欢使用C++和JAVA。但我不提倡在Palm OS中使用的原因是,这些语言的类库在Palm OS中的使用并没有带来任何益处。我们得到的只是程序更臃肿、运行速度更慢。
当有了一个小的、简洁为Palm OS或C++和JAVA开发的类库,我会第一个使用它。或许我将写一个。
幸运的是,在Palm OS应用程序的开发中,我们可以从面向对象的原则中获得一些有意义的启示。如我前面所言,关键就是掌握面向对象的原则。下面,我就向你介绍一下面向对象编程的一些最流行的概念,并给出一些如何在C程序使用这些原则的技巧。
数据封装
面向对象的基本原则就是将数据结构及其应用放在一个叫做“对象”的实体中。数据封装的思想就是把数据结构保护起来,以免遭到对象外代码的破坏。如果你遵从了这个原则,当数据结构遭到破坏时,就可以确定问题一定出在对象的里面,除非存在所谓的“wild pointer”。
“wild pointer”是指指针并没有指到一个具体的位置上。例如,下面的代码就会造成“wild pointer”的问题。

Char * cpBuffer=2000;

在有“wild pointer”的情况下,或许就有不走运的对象被它指定了,所以就会引起一些问题。因此,最好要节俭的、小心的使用指针。
数据封装在C中很容易实现的。将各成对象的代码分成单独的文件保存,并把变量声明成静态变量(static)。这就可以防止它们被其它文件中的函数所调用。
如果对象想访问其它对象中的变量,你可以把变量声明为全局变量来实现,但不要把全局变量声明的太多。不要把数据结构的指针传递给对象外的函数,而是让函数传递一个数据结构供你填入或者创建一个原始数据的复件来代表函数。
数据抽象
数据抽象的原则是指一个对象中的数据结构不依赖于任何其它对象中的数据结构。因此,一个对象可以重写或完全改变它的基本变量,而不会影响代码或数据在其它对象中的有效性。
数据抽象的用处就是可以重建对象,使程序更易重用和维护而不影响系统的其他部分。
显然,不要对其他对象的数据结构中使用“内部消息”。要把它们看成黑箱。不要认为你知道了其他对象的数据结构的大小,而应使用sizeof();不要认为你知道了变量在数据的什么地方。
继承
继承的含义是指在一般的对象上添加具体的内容生成其子对象。这就避免了你花费大量的时间来产生和已有的对象很多地方都相同的对象。
在C中实现继承的一种方法是使用#define语句。假设你有一个简单的列表框,现在你想再产生一个列表框。在这个列表框中,除了增加了浏览功能外,其它的和简单的列表框完全相同。下面是简单列表框的函数原型:
VoidHand slCreateList(void);
Void slDestroy(VoidHand);
Void slAddToList(VoidHand,lidtitem_t*);
Void slSaveList(VoidHand,FILE*);
Void slLoadList(VoidHand,FILE*);
在可浏览列表的头文件中,你可以这样定义:
#define ilCreatList slCreatList;
#define ilDestroyList slDestroyList
#define ilAddtoList slAddToList
#define ilSaveList slSaveList;
#define ilLoadList slLoadList
void ilGetFirst(VoidHand,listitem_t*);
void ilGetNext(voidHand,listitem_t*);
多态性
多态性指可以统一的处理一组对象。例如,我们可以删除一个列表框条目,而不必管列表是简单列表框还是可浏览的列表框。
在C中可以通过使用指针来实现多态性。注意,在删除函数中保存在列表建立时代表对象的指针,当调用函数 DeleteListItem()时,使用这个变量将所指记录删除。
下一步做什么
在下面的两章中,我们将使用本章讲到的新的框架生成一个calculator程序。
在下一章中,我们将应用第十章中讲到的用户界面知识来设计计算器,我们还将用到公共库,特别是MathLib的应用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值