第九章 导航菜单与收入记录

在上一章我们创建了导航菜单屏幕——MENU,该屏幕是整个应用的枢纽 ,从这里可以到达任何一个功能页面,同时,从任何一个功能页面都能返回到导航菜单页,本章将实现该屏幕的导航功能,然后创建应用中的所有屏幕,并实现收入记录功能。

第一节 导航菜单

一、数据模型

这是整个应用中最简单的一个屏幕,唯一的数据就是全局变量“屏幕列表”,如图9-1所示。其中包含8个键值对,前7个键值对的键分别为功能名称,值为该功能所对应的屏幕名称,最后一个键值对的键为“退出”,值为空字符。

图9-1 导航菜单页面中唯一的数据

二、界面设计

导航菜单屏幕中只有一个组件——列表显示框,用户选择其中的列表项,即可打开对应的屏幕。用户界面如图9-2所示,组件的属性设置如表9- 1所示。

图9-2 导航菜单屏幕的界面设计

表9- 1 导航菜单界面的组件设置

三、页面逻辑

  1. 屏幕初始化:为导航菜单设置列表属性,数据来源于全局变量“屏幕列表”中的键;
  2. 导航菜单的完成选择事件:当用户从导航菜单中选择了某个功能项时,转到相应的屏幕;如果用户选择了退出,则关闭应用。

四、编写程序

1、屏幕初始化程序

如图9-3所示,在MENU屏幕的初始化程序中,提取屏幕列表中每个键值对的“键”,添加到临时变量“功能列表”中,并设导航菜单的列表属性为功能列表。

图9-3 导航菜单页面(MENU)的屏幕初始化程序

2、导航菜单的完成选择程序

当用户从导航菜单中选中某项功能时,应用会打开对应的屏幕,并传递一个初始值;当用户选择退出时,将退出账本应用。代码如图9-4所示。

图9-4 选择框的完成选择事件处理程序

注意观察图9-1中的键值对列表,收入查询与支出查询指向了同一个屏幕——QUERY,这是因为这两项功能的代码有80%以上是相同的,为了降低代码的冗余度,将它们放置在一个屏幕中,以初始值来区分用户的选择。图9-4中传递的初始值仅对这两个选项有意义,对于其他选项无意义。

在这一节里我们还有一项重要的任务,就是创建所有的屏幕,屏幕名称必须与屏幕列表中的值完全相同,如图9-5所示。

图9-5 创建应用中的所有屏幕

各个屏幕的名称与功能的对应关系见表9- 2。

表9- 2 屏幕名称与功能的对应关系

五、测试

测试时我们要将开发环境切换到Screen1,否则AI伴侣将直接打开当前正在编辑的屏幕。测试结果如图9-6所示。

图9-6 测试导航菜单的功能(设置应为“系统设置”)

由于我们在上一章的测试中已经成功地设置了登录密码,因此这里直接输入密码,即可进入导航菜单页面。列表显示框(导航菜单)中显示了我们设定的各项功能,我们选中第一项——收入记录,应用转到收入记录页面。

另:在AI伴侣中无法测试“退出程序”功能,只能将应用编译后下载安装到手机中,才能测试该功能。

现在我们已经创建了项目中的所有屏幕,下面我们将完成家庭账本应用的第一个功能——收入记录。

第二节 收入记录的功能描述

我们采用“单条输入,批量保存”的方式,记录收入信息:

  • 功能入口:用户在功能导航页面点击“收入记录”项,进入收入记录页面;
  • 功能出口:用户在收入记录页面点击“返回主菜单”按钮,则返回导航菜单页面;
  • IN_INPUT 屏幕的上方为信息输入区,用户根据页面上的提示信息,选择或输入相关的数据项,并提交数据;已经提交的数据被临时保存在全局变量中,并显示在屏幕下方的列表显示框中;
  • 数据提交之后,清空屏幕上方用户此前输入或选中的内容,等待输入下一条记录;
  • 当用户在列表中选中了某项数据后,可以修改或删除该项数据;
  • 当用户确认已提交的信息准确无误后,可以将数据永久保存到数据库中;
  • 当数据保存到数据库之后,清空屏幕下方的列表,等待新一批数据的输入;
  • 数据项及其来源(7项):
    • 日期:默认系统当前日期,用户可以选择其他日期;
    • 收入类别:系统预设选项,包括工资、奖金、往来等,用户从中选择一项;
    • 发放者:界面输入,必填项;
    • 支付方式:系统预设选项(用户可增删改),包括现金、转帐、实物等,用户从中选择一项;
    • 收入者:系统预设“家庭成员”选项(用户可增删改),用户从中选择一项;
    • 金额:界面输入,必填项;
    • 备忘:界面输入,非必填项。

第三节 收入记录的数据模型

一、对象模型

在第一章中我们引入了对象模型的概念,这里我们用表格的方式来定义我们的收入记录模型,如表9- 3所示。

表9-3 收入记录的对象模型

这里需要解释一下模型中的日期值——毫秒数。在信息管理类应用中,日期或时间是一项非常重要的数据,是信息查询及统计的重要依据,为了便于查询和比较,通常采用毫秒数来表示一个时间点。所谓毫秒数,指的是从1970年1月1日0时起至今的毫秒数,这是一个可以比较大小的整数。毫秒数可以换算成具体的年月日时分秒,这个换算功能由计时器组件来实现。

二、变量模型

1、收入全集

从第一章中我们知道,App Inventor的数据库组件只能保存及读取“标记”所对应的数据集合,对于集合内部数据的处理完全依赖于客户端程序,也就是我们即将编写的程序。为此我们需要将数据库中保存的全部收入记录一次性地读取出来,保存在全局变量中,我们将这个全局变量命名为“收入全集”,列表的结构如图9-7所示。

图9-7 收入全集的列表结构

注意“日期”键对应的值为毫秒数。

2、临时收入列表

用户进入收入记录页面后,开始输入数据,每输入一条收入记录(包含7项数据),就将其添加到一个临时列表中,此时,用户可以删除或修改已经输入的记录,也可以继续添加新的记录,所有的增、删、改操作都是针对临时列表的,直到用户想结束本次输入时,才将已经输入的记录批量地保存到数据库中,同时清空临时列表。我们将这个临时列表命名为“临时收入列表”,临时收入列表的结构与收入全集完全相同。

3、收入字串列表

用户当次输入的收入记录保存在临时收入列表中,但列表的内容不便于直接显示在列表显示框中,需要将列表中的各项数据拼成便于人类用户阅读的字串,将字串保存到另一个全局变量——收入字串列表中,并将收入字串列表设置为列表显示框的列表属性。收入字串列表与临时收入列表的列表项是一一对应的,当用户将本次输入的数据批量保存到数据库时,要同时清空这两个列表。

三、预设项列表

在收入记录中,有三项数据来自于系统的预设项——收入类别、收入者(家庭成员)及支付方式,我们可以从数据库中直接读出这些数据,并将它们设置为对应下拉框的列表属性。数据均为一级列表,默认内容如下:

  • 收入类别:(工资,奖金,补贴,劳务,理财,往来,受赠,其他)
  • 家庭成员:(张老三,李斯,王小五)
  • 支付方式:(现金,转账,其他)

第四节 界面设计

根据以上对功能以及数据模型的描述,我们为收入记录屏幕添加了可视组件及非可视组件,如图9-8所示,组件的属性设置见表9- 4。

图9-8 设计视图中收入记录页面

表9- 4 收入记录屏幕中的组件设置

在屏幕的上半部分共有四个水平布局组件,其中容纳了用于采集信息的全部组件,我们将这些组件合称为输入表单,或表单。我们将在讲解页面逻辑时,使用“输入表单”这个词,来代替所有用于输入及选择的组件。

第五节 页面逻辑

1、声明全局变量

  • 收入全集:初始值为空列表,用于保存全部的收入记录;
  • 临时收入列表:初始值为空列表,用来保存本次所输入的收入记录;
  • 收入字串列表:初始值为空列表,用来保存与临时收入列表相对应的字串。

2、IN_INPUT屏幕初始化:

  • 从数据库中读取“收入记录”信息,将其保存在全局变量收入全集中(首次打开应用时返回空列表);
  • 从数据库中读取“收入类别”信息,设为类别选框的列表属性;
  • 从数据库中读取“家庭成员”信息,设为收入者选框的列表属性;
  • 从数据库中读取“支付方式”信息,设为支付方式选框的列表属性;
  • 设置日期选框的选中日期及显示文本为系统当前日期。

3、新增数据

当用户已经输入或选中了7项(备忘一项可以忽略)数据,并点击了提交按钮时:

  • 采集输入表单中的信息,并以键值对列表的方式(局部变量)将信息组织起来;
  • 将键值对列表添加到全局变量临时收入列表中;
  • 将键值对列表拼成字串,添加到收入字串列表中;
  • 设置收入列表框的列表属性为收入字串列表;
  • 恢复输入表单的初始状态,等待输入下一条信息。 4、修改已输入的数据

当用户从收入列表框中选中某一项时,应用将弹出对话框,并提供三个选择——删除、修改、返回,当用户选择“修改”时:

  • 将选中内容填写到输入表单中,等待用户修改;
  • 当用户修改完毕点击提交按钮时,采集输入表单中的信息,并以键值对列表(局部变量)的方式将信息组织起来;
  • 用键值对列表替换临时收入列表中对应的项;
  • 将键值对列表拼成字串,替换收入字串列表中对应的项;
  • 设置收入列表框的列表属性为收入字串列表;
  • 恢复输入表单的初始状态,等待输入下一条信息;
  • 设置收入列表框的选中项索引值为0。

5、删除已输入数据

当用户从收入列表框中选中某一项,并在对话框中选择“删除”时;

  • 分别从临时收入列表及收入字串列表中删除选中项;
  • 设置收入列表框的列表属性为收入字串列表;
  • 设置收入列表框的选中项索引值为0。

6、取消选择

当用户从收入列表框中选中某一项,并在对话框中选择“返回”时,设置收入列表框的选中项索引值为0。

7、永久保存数据

当用户点击保存按钮时:

  • 将临时收入列表追加到收入全集中;
  • 将收入全集以“收入记录”为标记保存到数据库中;
  • 清空输入表单;
  • 清空临时收入列表及收入字串列表;
  • 设收入列表框的列表属性为收入字串列表。

8、返回主菜单

当用户点击返回按钮时,关闭当前屏幕,应用将回到导航菜单页面。

第六节 编写程序

一、发现过程

在正式开始编写程序之前,我们给自己做一个小测验,重温页面逻辑中的条目,看看能否从上述文字中发现潜在的“过程”。这里给一个提示:寻找那些重复的操作。

至少我们可以发现一条:“恢复输入表单的初始状态,等待输入下一条信息”,这句话共出现了两次,这意味着我们可以创建一个过程,将具体的操作(7个组件对应7个操作)封装起来,可以称其为“组件初始化”过程。此外,“将键值对列表拼成字串”也出现了两次,可以将其封装成有返回值的过程,过程的参数为上面提到的键值对列表,过程名称可以叫做“列表转字串”。别急,还有!“采集输入表单中的信息…组织起来”,这个操作同样重复的两次,可以称其为“采集表单信息”。我们就从编写者三个过程开始我们的任务。

1、组件初始化过程

首先考虑“组件初始化”过程,其实就是将输入表单中的7个组件恢复到屏幕初始化时的状态,其中设置日期选框的选中日期及显示文本属性则需要用到计时器组件,代码如图9-9所示。

图9-9 定义组件初始化过程

上图中调用了计时器组件的“设日期格式”过程,关于时间点及日期格式的说明见本章最后的附录——计时器组件中的时间信息。

2、列表转字串过程

再来创建“列表转字串”过程,我们先模拟采集数据的结果,构造出一个键值对列表,如图9-10所示,并以此作为参数的参考值,创建“列表转字串”过程,代码如图9-11所示。

图9-10以具体的列表数据为参考创建“列表转字串”过程
图9-11 创建列表转字串过程

字串的拼接方式并不是唯一的,这里的拼接方式尚有不妥之处,希望读者自己加以完善。

3、采集表单信息过程

这是一个没有参数但是有返回值的过程,返回值为键值对列表,代码如图9-12所示。

图9-12 采集表单信息过程

二、屏幕初始化

按照页面逻辑编写IN_INPUT屏幕的初始化程序,代码如图9-13所示。

图9-13 屏幕初始化程序

三、新增数据

用户在输入表单中选择或输入必要的信息,并点击提交按钮,完成一次新增操作。输入表单中日期选框的默认选中日期是系统的当前日期,同时,日期选框的显示文本属性也设置为当前日期。注意:选中日期与显示文本是两个属性,对选中日期的设置并不能改变日期选框的显示文本属性,需要手动设置显示文本属性!当用户希望改变默认日期时,可以点击日期选框,修改日期,并点击“完成”按钮,来设置选中日期,然后,再来设置日期选框的显示文本属性,用户的操作界面如图9-14所示,日期选框的完成日期设定程序如图9-15所示(图中的两种代码的执行结果相同)。

图9-14 用户修改收入日期
图9-15 日期选框的完成日期设定程序

按照页面逻辑中的描述,当用户点击提交按钮时,向临时收入记录列表中添加一条记录,但是在阅读了“修改已输入数据”之后,你可能会产生疑问,新增操作与修改操作都要在提交按钮的点击事件中完成,那么如何识别那个点击是新增、那个是修改呢?这种情况下,通常的做法是利用界面组件的某些属性值来作为判断的依据。比如,利用提交按钮的显示文本属性:当用户从收入列表框中选择一项时,我们可以设提交按钮的显示文本为“修改”,当修改操作完成后,在执行组件初始化过程时,再将提交按钮的显示文本改为“提交”。我们选择的判断依据是列表显示框的“选中项索引值”属性:如果索引值为0(表示没有选中项),则执行新增操作,否则执行修改操作。具体代码如图9-14所示。

图9-16 新增一条收入记录

当用户点击提交按钮后,首先判断两个文本输入框是否为空,如果为空,利用对话框组件提示用户填写必要的信息;如果不为空,则执行新增操作。这里我们只处理的新增操作,稍后为如果语句添加否则分支,来处理修改操作。

四、修改数据

1、从收入列表框中选择一项

用户修改已输入数据的前提是从收入列表框中选择一项。在收入列表框的完成选择事件中,调用对话框组件的“显示选择对话框”过程,并提供三种选择,代码如图9-17所示。

图9-17 收入列表框的完成选择程序

2、在对话框中选择“修改”

当用户选择“修改”按钮时,将选中项内容填写到输入表单中,代码如图9-18所示。

图9-18 对话框完成选择程序——用户选择修改

这里顺便实现了“返回”操作,即,设收入列表框的选中项索引值为0。

3、提交修改

用户对输入表单中的数据进行修改,然后点击提交按钮,此时,收入列表框的选中项索引值不为零,于是执行修改操作,代码如图9-19所示。

图9-19 完成对收入信息的修改

注意图9-19中的否则分支,利用收入列表框的选中项索引值属性来更新列表项,当更新完成后,将选中项索引值设置为0。读者不妨试试看,如果缺少设索引值为0这句代码,当你输入一条新的信息时,会发生什么情况。

4、测试

我们需要对上述程序进行测试,测试结果如图9-20所示:先输入一条记录,该记录将显示在收入列表框中,点击该项将弹出对话框(左一图);选择对话框中的修改按钮,将选中项填写到输入表单中(左二图);用户修改表单信息(右二图,修改了日期及发放者);点击提交按钮,修改结果显示在收入列表框中(右一图)。

图9-20 测试修改程序

五、删除数据

当用户在收入列表框中选中一项,并在对话框中选择“删除”按钮时,执行删除操作,代码如图9-21所示。

图9-21 对话框完成选择程序——执行删除操作

六、永久保存数据

当用户确信已经输入了正确的信息,并希望结束本次输入时,可以点击保存按钮,将本次输入的信息永久保存到数据库中。用户并不了解NoSQL数据库的工作原理,但作为开发人员,我们知道,每次保存的不仅仅是本次输入的信息,而是将本次输入的信息连同以往的收入记录一同保存到数据库中。为此我们要用到一个很少会用到的代码块——追加列表块。我们在本章的第三节介绍临时收入列表时,提到过临时收入列表与收入全集的列表结构完全相同,因此这里使用追加块,代码如图9-22所示。

图9-22 永久保存数据

保存成功之后,用对话框提示用户“保存完成”,并将所有变量以及组件恢复到屏幕初始化时的状态,等待用户输入新一批的数据。此时用户可以继续输入新的收入记录,也可以选择“返回主菜单”。

七、返回主菜单

用户点击返回按钮,将关闭当前屏幕,回到导航菜单页面。但是存在一种可能,即,用户并未点击保存按钮,而是在输入数据后,直接点击了返回按钮,在这种情况下,用户此前输入的信息将丢失。对于新用户,在不熟悉应用的使用方法时,很有可能发生这样的事情。为了防止误操作,当用户点击返回按钮时,首先检查临时收入列表的长度,如果长度等于0,则关闭当前屏幕,返回导航菜单页;如果列表长度大于0,说明用户输入的数据尚未保存到数据库中,这时弹出选择对话框,并提供“保存”及“放弃”两个选项供用户选择。代码如图9-23及图所示。

图9-23 返回按钮点击程序

如果用户选择对话框中的“保存”按钮,则执行保存按钮点击程序中的代码,然后关闭当前屏幕,返回导航菜单页;如果用户选择“放弃”,则直接关闭当前屏幕,返回导航菜单页。

为了实现代码的复用,我们要创建一个过程——保存到数据库,将保存按钮点击程序中的代码复制到该过程里,并在保存按钮点击程序中调用该过程,代码如图9-24所示。

图9-24 创建过程,提高代码复用性

另一个调用该过程的地方是对话框的完成选择程序。这里我们要为对话框的完成选择事件添加两个“否则,如果”分支,来实现对“保存”及“放弃”两种选择的处理,代码如图9-25所示。

图9-25 当用户试图返回主菜单时,还有机会保存数据

需要说明一点:在App Inventor中,一个对话框只能有一个完成选择事件处理程序,而在IN_INPUT(输入记录)屏幕中,我们两次调用了对话框的“显示选择对话框”过程,这意味着完成选择事件中的“选择结果”参数有五种可能的值,我们需要小心地设置这五个选项(按钮上的文字),避免重复。

到这里我们的开发任务告一段落,下面进入测试环节。

第七节 测试与改进

首先输入3~5条收入记录,输入的信息可以正常显示在收入列表框中,当输入的记录超过3条时,屏幕上仅能显示3条记录,可以通过划屏让列表框向上滚动,以便查看后面的内容。从列表框中选择一项,然后分别选择“修改”、“删除”及“返回”,发现如下问题:

(1) 当选择“返回”时,对话框关闭后,收入列表框中刚才的选中项仍然显示灰色背景(处于被选中状态),但此时已经设置的收入列表框的选中项索引值为0,这说明选中项索引值的更新并不能导致界面显示状态的更新,改进方法是重新设置收入选择框的列表属性,同时设选中项索引值为0,修改后的代码如图9-26所示。

图9-26重新设置收入列表框的列表属性,实现界面的刷新

你可能会问,既然重新设置了收入显示框的列表属性,那么是否也能自动更新收入列表框的选中项索引值呢?这也是我的猜想。为此,我试着禁用图9-26中的最后一行代码,测试结果显示,收入列表框中没有项被选中(没有灰色背景的数据项),好像最后一行代码真的没有用。不过不能轻易相信界面的显示,我添加了一行测试代码,用屏幕的标题来显示收入列表框的选中项索引值,代码如图9-27所示,测试结果如图9-28所示。

图9-27 测试选中项索引值是否自动更新
图9-28 测试结果

测试结果显示选中项索引值为2,也就是说,重新设置列表属性后,收入显示框中不再显示当前的选中项(没有灰色背景的项),但是选中项索引值并没有自动更新为0,仍然需要手动将其设置为0,否则会给后面的操作带来混乱,因为我们是依据这个索引值来判断“提交”操作的(新增还是修改)。

(2) 当用户选择了收入显示框中的一项,并在对话框中选择“修改”,此时选中项的内容被自动填写到输入表单中;这时接着选择显示框中的另一项,并在对话框中选择“删除”或“返回”,当对话框关闭后,输入表单中仍然显示上一次选择“修改”之后填写的内容,而此时收入选择框的选中项索引值已经为0。为了解决这一问题,我们需要在对话框的“删除”及“返回”分支中,调用组件初始化过程,来清空输入表单中的内容。修改后的代码如图9-29所示。

图9-29 当用户在对话框中选择“删除”或“返回”时,清空输入表单

从图中我们发现,最后的三行代码总是同时出现,而且在提交按钮的点击程序中也有相似的代码组合,为此我们将前两行代码合并到组件初始化过程中,来提高代码的复用性。代码如图9-30所示。

图9-30 在组件初始化过程里添加两行代码

然后删除掉对话框完成选择程序与提交按钮点击程序中重复的两行代码(设收入列表框的列表属性、设收入选择框的选中项索引值属性)。修改后的代码如图9-31所示。

图9-31 删除掉重复的两行代码

我们对本章程序的测试就到这里。测试的过程不仅可以发现程序中的错误,也可以更多地了解开发工具。

附录:计时器组件中的时间信息

一、什么是时间点

本章中有多段代码调用了计时器组件的内置过程,包括“求当前时间”、“设日期格式”、“创建毫秒时间点”等等,其中多次用到一个叫做“时间点”的参数,那么什么是时间点?为什么计时器的当前时间就是时间点?时间点中包含了哪些信息?为了解答这个问题,我们来创建一个临时的应用,其中只有一个标签组件和一个计时器组件,利用标签组件来显示时间点的内容,代码如图9-32所示,程序的运行结果如图9-33所示。

图9-32 新建一个项目,解释什么是时间点
图9-33 时间点中包含的各项信息

所谓“时间点”,也称为时刻,其中包含了时区、年、月、日、时、分、秒、毫秒等信息。图9-33中的深红色汉字解释了这一行信息的含义,其中在编写程序时最常用的就是第一行的time——1970/1/1至今的毫秒数。它是一个整数,可以进行大小的比较,这一点非常便于查询操作。在 “收入查询”一章中,我们会设定查询的起止日期,查找介于这两个日期之间的收入记录,这就要利用毫秒数进行比较。为了比较大小的方便,我们将收入记录中的日期信息直接保存为该日期对应的毫秒数。这里需要说明一下,当我们说某个日期对应的毫秒数时,我们指的是该日期的0时0分0秒0毫秒这个时间点的毫秒数,也就是这个日期中最小的毫秒数。

二、设置日期格式

了解了时间点,我们再来解释一下图9-9中的最后一行代码——设日期选框的显示文本,这里调用了计时器组件的内置过程——设日期格式,如图9-34所示。

图9-34 计时器组件的设日期格式过程

该过程有两个参数:第一个是时间点,用来设定要显示的日期;第二个是日期格式,是一个字串,以2016年4月10为例:

  • y表示年:
    • y或yyyy对应于2016;
    • yy对应于16;
  • M表示月份(注意与小写的m区分,m表示秒——minute)
    • M对应于月份数,即4(如果是12月则显示12);
    • MM对应于两位数的月份,即04;
    • MMM则在两位数的月份后面添加一个汉字“月”,即04月;
  • d表示日期(注意与大写D区分,D表示某日在一整年中的第几天,如2016/4/10是2016年中的第101天);
    • d对应于日期,即10;
    • dd对应于两位数日期,即,如果日期是一位数,则在日期前添加一个0;
  • 格式字串中除了系统规定的字符外,也允许插入其他字符,比如插入汉字的年月日时分秒等字,或“/”、“-”等,以符合不同人群的阅读习惯。

图8- 25中的格式字串“yyyy年MM月dd日”给出的结果是“2016年04月10日”,如果不希望显示04月或01日这样的格式,可以改为“yyyy年M月d日”或更简单的“y年M月d日”,给出的结果是“2016年4月10日”。

三、创建毫秒时间点

在图9-11及图9-18中都调用了计时器组件的“创建毫秒时间点”过程,该过程的参数为“毫秒数”,返回值为该毫秒数所对应的时间点。也就是说,毫秒数中包含了某一时刻全部的时间信息,如图9-33所示。

在编程视图中点击计时器,打开计时器的代码块抽屉,你会看到一长串紫色的代码块,这些都是计时器组件的内置过程,它们全部是有返回值的过程,返回值均为与日期及时间有关的信息。计时器组件是内置过程最多的一个组件。在后续的开发中,我们会用到多个计时器组件的内置过程,届时我们再做详细解释。