第十二章 简易家庭帐本——收支查询

第一节 功能描述

一、收入查询

  1. 入口:用户在导航菜单页选择“收入查询”项,进入收入查询页面;
  2. 出口:用户点击“返回主菜单”按钮,返回导航菜单页;
  3. 查询条件:
    • 按日期查询:用户选择起始日期及终止日期,可查询该时间段内的全部收入记录;
    • 按日期及收入类别查询:可查询某个时间段内某一类收入的全部记录;
    • 按日期及收入者查询:可查询某个时间段内某位家庭成员的全部收入记录;
    • 按日期及支付方式查询:可查询某个时间段内某一类支付方式的全部收入记录;
  4. 查询结果显示:
    • 分页逐行显示收入记录的部分信息(受屏幕尺寸限制),包括日期、类别、收入者、金额等;
    • 当用户点击某一条记录时,显示该条收入的详细信息;
    • 用户通过划屏的方式查看下一页或上一页信息;
  5. 删除单条收入记录:
    • 用户选中某一条收入记录后,可以将其删除;
    • 删除记录后,收入列表更新;
  6. 批量删除收入记录:用户可将查询结果批量删除,为防止用户误删除,将请求用户输入密码进行确认;
  7. 数据导出:用户可以将查询结果导出为逗号分隔字串组成的文本文件(可以在记事本或Excel中打开);

二、支出查询

  1. 入口:用户在导航菜单页选择“支出查询”项,进入支出查询页面;
  2. 出口:用户点击“返回主菜单”按钮,返回导航菜单页;
  3. 查询条件:
    • 按日期查询:用户选择起始日期及终止日期,可查询该时间段内的全部支出记录;
    • 按日期及支出分类查询:可查询某个时间段内某个一级分类的全部支出记录,也可查询某个一级分类下属的某个二级分类的全部支出记录;
    • 按日期及支出专项查询:可查询某个时间段内某个专项的全部支出记录;
    • 按日期及受益人查询:可查询某个时间段内某个特定受益人的全部支出记录;
  4. 查询结果显示:
    • 分页逐行显示支出记录的部分信息(受屏幕尺寸限制),包括日期、一级分类、名称、金额等;
    • 当用户点击某一条记录时,显示该条支出的详细信息;
    • 用户通过划屏的方式查看下一页或上一页信息;
  5. 删除单条支出记录:
    • 用户选中某一条支出记录后,可以将其删除;
    • 删除记录后,支出列表更新;
  6. 批量删除支出记录:用户可将查询结果批量删除,为防止用户误删除,将请求用户输入密码进行确认;
  7. 数据导出:用户可以将查询结果导出为逗号分隔字串组成的文本文件(可以在记事本或Excel中打开)。

从功能描述上可以看出,收入查询与支出查询,除了在查询条件及显示内容上有所不同,其余功能几乎完全相同,因此,我们在同一个屏幕中来处理这两项查询功能,以便最大程度地复用代码。

第二节 数据模型

一、对象模型

在收入记录与支出记录功能的实现过程中,我们已经对这两项信息的对象模型有所了解,这里仍然用列表来表示这两个对象模型,如图12- 1所示(左图列表项采用外挂式显示方式)。

图12-1 收入记录与支出记录的对象模型

二、动态变量模型

QUERY屏幕中有许多全局变量,用来保存静态的或动态的数据,我们这里仅列出动态变量,即,程序运行过程中,值会发生变化的全局变量。

虽然将收入与支出的查询放在同一个屏幕中,但在某个时刻,只能实现其中的一项功能,因此我们用一组全局变量来处理两类查询操作,为此,变量的命名要具有通用性。

1、查收入

定义一个“查收入”的逻辑型全局变量,来标记当前执行的查询功能,其取值有两个,当在MENU页面中选择“收入查询”时,该值为“真”,否则为“假”。后续操作都要以此为判断依据。

2、数据全集

打开屏幕时,根据“查收入”的值,从数据库中读取全部的“收入记录”或“支出记录”,并保存在该变量中。该变量为三级列表,在程序中与查询及删除操作相关。

3、查询结果

当用选定了查询条件,开始执行查询操作时,将从数据全集中逐项筛选满足条件的记录,并将筛选结果添加到全局变量“查询结果”中。该变量的列表结构与数据全集完全相同。

4、结果集索引值

查询结果中的每个列表项在该列表中的位置。当用户在数据表格中选中某一项时,需要求出该数据项在“查询结果”中的位置,即,结果集索引值,该值可用于列表项的删除操作。

5、分页数据

用于在页面上实现显示功能的数据集合。在本应用中,每页最多能够显示10行,因此当查询结果多于10项时,将按照每页10条记录,将查询结果拆成若干页,非末尾页包含10个列表项(对应于10行),末尾页行数小于或等于10。“分页数据”为三级列表,第一级列表中的列表项是页数据,列表长度等于页数;第二级列表为单页数据,其列表项为行数据,列表长度小于或等于10;第三级列表为行数据,其中包含6个列表项,是我们最终要显示的具体内容(限于手机屏幕的宽度,最多只能显示6列)。列表结构如表12- 1所示。

表12- 1 分页数据的列表结构

5、页码

分页数据列表的长度,就是数据分页的页数,其中每个列表项(页数据)在分页数据列表中的位置,则与页码相对应。

6、行号

分页数据中的二级列表的内容为单页数据,其中包含10行或小于10行数据,每行数据在本页中的序号称为行号,行号、页码与结果集索引值之间存在着对应关系,如表12- 2所示。

表12- 2 行号与结果集索引值之间的关系

在删除单条记录时,将利用选中行的行号求出该条记录在查询结果中的索引值,并根据此索引值查询该数据项在数据全集中的位置,最后从数据全集中删除该项数据。

7、当前页数据

分页数据中的列表项之一(单页数据集合),当前正处于显示状态,包含10条或小于10条记录。

8、支出二级分类

用于保存从数据库中读取的支出二级分类,并在每一个二级分类的首位添加一个列表项“全部”;当用户选中某一级分类时,从变量支出二级分类中选择一项,作为下拉框的列表属性。

第三节 界面设计

查询功能的用户界面设计如图12- 2所示,组件的命名及属性设置见表12- 3。

图12-2 查询功能的用户界面设计

表12- 3 组件的命名及属性设置

第四节 技术准备——绘制动态表格

你一定注意到,在屏幕的中央有一个画布组件,这就是我们用来绘制表格的组件。在App Inventor中有三个与列表有关的组件——列表选择框、列表显示框、下拉框,在前面几章中,我们使用过这几个组件,它们都存在一个的共同的问题,即,无法设置行高,半个屏幕最多只能容纳3~4条记录,而我们希望用真正的表格来显示查询结果,并且可以从显示结果中进行选择。App Inventor中没有这样现成的表格组件,因此我们只能自力更生,用画布组件来模拟出表格的样式及功能。如图12- 3所示,这是我们即将绘制的表格。

图12-3 绘制表格的样例

一、表格的属性

用画布来显示数据,这相当于我们利用App Inventor开发一个动态显示数据的表格组件。与App Inventor的自有组件一样,我们自己定义的组件也具有某些属性以及过程。

首先我们需要说明几个与表格有关的属性,包括表格的自然属性及数据属性,这些属性将体现在程序中。

1、表头列表

如图12- 3(以下简称样例图)所示,表格最上面一行被称为表头,它的颜色较深,文字颜色为白色;表头文字的内容可以由列表来定义,例如,图12- 4中定义的是支出记录的表头。

图12-4 定义表头文字的列表

这里的表头列表为静态数据,其中的列表项内容是我们事先设置好的。也许有朝一日我们会开发一个全功能的动态数据列表组件,届时,连表头的显示内容也可以由用户来定义。

2、数据行

除表头外,表格中其他的行均称为数据行,在本应用中,屏幕上最多只能显示10个数据行,每个数据行中包含6项数据。数据行被划分为奇数行与偶数行,第1、3、5等行为奇数行,第2、4、6等行为偶数行,这样划分的目的是为了设置数据行的背景颜色,以便于表格的阅读;样例图中表头及数据行的颜色取值如图12- 5所示。

图12-5 表头及数据行的颜色值

3、行高

每一行的高度,以像素为单位,包括表头行及数据行;行高的设定与表格中文字的字号有关,图12- 3中的字号为18,行高为26。

4、列宽

样例图中每行有6列数据,每一列数据的字符数不等,因此每一列的宽度也不相同。实际上我们无法设置每一列的宽度,只能设置每列文字中心点的x坐标,而列宽是我们计算的结果。样例图中6列文字中心点的x坐标如图12- 6所示(单位为像素)。我们来计算一下第一列的宽度:你可以将分界线想象为20与65的中点,即41.5,因此第一列的宽度为41.5个像素;同样,第二列的宽度从41.5至92.5(65与130的中点),因此宽度为41个像素,以此类推。

图12-6 每一列中文字的x坐标

5、线宽

指的是画布的画笔线宽属性,我们利用画布组件的画线功能绘制表头及数据行的背景颜色。线宽与行高相关,样例图中的行高为26,线宽为25,它们之间的差值(1像素)是行与行之间的间隔——一条1个像素高的白线(因为画布的背景色为白色)。

6、文字在Y方向的基准点

用画布写字时,需要提供三个参数——文本、x坐标及y坐标,如图12- 7所示。文字在水平方向的基准点位于一组文字的中央,垂直方向的基准点位于文字的底部,如图中“日期”两字的基准点在红色圆点的位置:x坐标为20,y坐标为18,文字的字号为18(字号的单位不是像素)。图中还给处了行高(26像素)和线宽(25像素)的标注。

图12-7 用画布绘制表格时的行高、线宽与文字坐标

以上是表格的外观属性,在本应用中,它们的值是事先设定好的。在编写程序之前,我们会将用全局变量来设置这些属性,这一类全局变量我们称之为静态变量(在大部分的编程语言中被称为常量)。

下面是表格的数据属性,或者说动态属性。

7、记录数

是指用户的查询结果中包含的记录数,也就是全局变量“查询结果”列表的长度。

8、页

如表12- 3所示,画布的高度是286像素,是行高(26像素)的11倍,因此,画布中最多只能容纳一行表头行及1行数据;当记录数大于10时,将分页显示数据,每10行为一页,末尾页的行数小于或等于10。与页相关的属性包括页数、页码;与页相关的操作是“翻页”。

9、页数

当记录数大于10时,将分页显示数据,页数=就高取整(记录数/10),例如,当查询结果中包含23条数据时,23÷10=2.3,2.3就高取整的结果为3,即,页数为3;当记录数小于或等于10条时,页数=1。

10、页码

从1开始,页的顺序号,页码的最大值=页数。页数及页码的值将显示在表格的下方,格式为“第(页码)页/共(页数)页”,括号中的部分为数字,以提示用户查询结果的数据量。

11、翻页

当页数大于1时,用户通过划屏动作翻页:向后翻页时,页码+1;向前翻页时,页码-1。

12、行

数据在表格中逐行显示,每条记录占据一行,与行相关的属性包括行数及行号。

13、行数

某一页中包含的数据行的数量(数据条目数),最大为10行,最小为1行。

14、行号

表格中某一行的顺序号,自上而下递增,最小行号为1(紧邻表头的行),最大行号为10。

二、绘制单页数据表格

有了上述表格的属性,我们可以来定义与表格的行为——绘制数据表格,它们对应于程序中一系列的过程。从最简单的绘制单页表格开始。由于绘制表格的代码量较大,截图非常困难,为此需要将绘制表格的一系列操作分解成若干个功能单一的操作。

1、绘制表头

绘制表头包括绘制表头背景以及写表头文字,代码如图12- 8所示。

图12-8 绘制表头过程

首先用表头颜色(蓝色)绘制背景,此时行高为26像素,画笔线宽为25像素。画布组件的画线过程的参数——y坐标的基准点在画笔的中心位置(13),因此,如果设y坐标参数为行高的一半,则背景线恰好画在行的中心线上。背景绘制完成后,将画笔颜色设置为白色,并根据X坐标列表中预设的坐标值绘制文字(想想看,如果先写字,后画背景会怎样)。

2、绘制数据行的单行背景

“绘制单行背景”过程如图12- 9所示,根据过程的参数——行号来判断该行是奇数行还是偶数行,据此来设定画笔颜色,并在指定的行的中心线上绘制背景。

图12-9 绘制数据行背景的过程

3、写单行文字

“写单行文字”过程如图12- 10所示。与写表头文字一样,文字的X坐标来自于全局变量“X坐标”列表(图12- 6),这一点保证了数据与表头之间的居中对齐。

图12-10 写单行数据过程

4、生成实验数据

为了绘制表格,我们需要有一组可供绘制的数据,利用循环语句,我们在每一个单元格中标明该单元格所处的行和列,行号与列号之间用破折号分隔,如“3-5”将显示在第3行第5列的单元格中。我们首先生成一组单页的实验数据,过程名为“单页实验数据”,代码如图12- 11所示。其中的参数“记录数”限制在1~10之间,在调用该过程时为参数指定一个数值。

图12-11 人为生成的实验数据

5、绘制单页实验数据

利用查询按钮的点击事件来绘制表格,代码如图12- 12所示。我们用一个5~10之间随机数来决定生成数据的条数,对应于表格的行数。

图12-12 绘制单页数据表格

上述代码的测试结果如图12- 13所示。显然两个图中的随机数分别为6和8。

图12-13 绘制单页表格——用随机数来确定表格行数

三、绘制多页表格

1、生成多页实验数据

创建一个“实验数据”过程,如图12- 14所示。

图12-14 生成多页实验数据的过程

该过程的参数为“记录数”,它可以是任意的自然数,在调用该过程时设定;该过程的返回值为分页数据,是一个三级列表,第一级列表中包含的列表项个数对应于页数(页数=就高取整(记录数/10));第二级列表中,非末尾页的列表项个数为10,末尾页的列表项个数小于或等于10;第三级列表中包含6个列表项。

2、绘制多页表格之首页

为了绘制多页数据,需要声明一个全局变量“页码”来记录当前正在显示的数据所对应的页,另外还要将生成的实验数据保存到另一个全局变量“分页数据”中。我们仍然利用查询按钮的点击事件来生成多页数据,并绘制首页数据,代码如图12- 15所示。

图12-15 生成多页数据,并绘制首页数据

上述代码的测试结果如图12- 16所示。我们利用屏幕的标题属性来显示生成的数据条数,图中共生成了64条数据。

图12-16 测试:生成多页数据,并绘制首页数据

3、表格的翻页

利用画布组件的划动事件来翻页,向左划动时,页码减1,向右划动时,页码加1,代码如图12- 17所示。

图12-17 利用画布的划动事件实现表格的翻页

当划动事件发生时,首先清空画布,再绘制表头,并根据划动速度的x分量判断用户的划动方向,当速度x分量小于0时(向左侧划动),页码加1;当速度x分量大于0时(向右侧划动),页码减1。利用页码从分页数据列表中获取当前页数据,并绘制在画布上。代码的测试结果如图12- 18所示。

图12-18 测试翻页

从屏幕的标题上我们得知,上图中共生成了38条数据,并分4页显示。以上是与绘制表格相关的代码,这些代码稍加修改,就可以用于绘制收入或支出记录,稍后我们再来改写。

四、选中一行数据

选择表格中的数据是动态数据表格的另一种行为。App Inventor的列表类组件都具有选择完成事件,当用户选中某一项时,可以针对选中的数据执行相关操作。我们自己绘制的表格没有这样的功能,不过,画布组件可以侦听按压等事件,而且可以获得按压位置的x、y坐标,这就为我们提供了获得选中项的可能性。我们可以利用简单的算术运算,获得点击位置所在的行,并由此获得数据在列表中的位置。代码如图12- 19所示,测试结果如图12- 20所示(图中按压了最后一行)。

图12-19 在画布的按压事件中获取选中的行号
图12-20 测试——求按压位置对应的行

五、选中行的闪烁效果

当用户选中某一行时,我们希望屏幕上能够有所响应,如,选中行背景色闪烁一下。假设闪烁时背景色为白色,这个闪烁包含了4个动作:

  1. 在画布的按压事件中,在选中行画白色背景线(此时文字已经被抹掉了);
  2. 在选中行重新写该行的文字;
  3. 在画布的释放事件中,在选中行画数据行(奇数行或偶数行)背景线(此时文字再次被抹掉);
  4. 在选中行重新写该行的文字。

为了绘制闪烁时的背景,我们需要改造一下“绘制单行背景”过程,为过程添加一个逻辑类型的参数“闪烁”,当闪烁为真时,以白色背景画线,代码如图12- 21所示。

图12-21 为“绘制单行背景”过程添加一个逻辑型参数——闪烁

然后分别在按压事件及释放事件中调用绘制背景及写文字过程,代码如图12- 22所示。

图12-22 利用画布的按压及释放事件产生闪烁效果

我们发现在按压及释放事件中的代码差别极小,仅在调用绘制单行背景过程时设定的参数不同,于是我们将这部分代码封装成“绘制数据行”过程,并在按压及释放事件中调用该过程。代码如图12- 23所示。

图12-23 创建“绘制数据行”过程,并在按压及释放事件中调用该过程

注意到上图中有一个全局变量“当前页数据”。考虑到程序中多处要使用当前页数据,虽然该项数据可以由“分页数据”及“页码”求得,但考虑到代码的复用性,我们声明了“当前页数据”变量,并在查询按钮点击事件以及画布的划动事件中,设置该变量的值,代码如图12- 24及图12- 25所示。

图12-24 在查询按钮点击事件中将首页信息设置为“当前页数据”的值
图12-25 在画布的划动事件中设置“当前页数据”的值

以上我们利用画布组件的按压与释放事件,制造出选中行的闪烁效果,读者可以自己测试一下程序的执行结果,也可以根据自己的喜好,将白色替换为其他颜色。

六、显示页码

在画布组件的下方有一个占位标签,该标签的高度属性为“充满”,使得其下方的组件(包含三个按钮的水平布局组件)可以贴近屏幕的下边界。不过它的另一个作用是显示页码,代码如图12- 26所示。

图12-26 过程——显示页码

需要在查询按钮的点击程序以及画布的划动程序中调用该过程,以更新页码的显示,代码如图12- 27所示。

图12-27 只有这两个事件会引起页码的更新

以上代码的测试结果如图12- 28 所示。

图12-28 显示页码程序的测试结果

与数据展示相关的绘制表格技术就介绍这些,有了以上的思路,我们来具体设计页面的逻辑。

第五节 界面逻辑

1、屏幕初始化

  1. 设置全局变量“查收入”:当屏幕初始文本值=”收入查询”时,该值为真,否则为假;
  2. 设置屏幕标题:
    • 如果“查收入”为真,则显示“简易家庭帐本_收入查询”;
    • 如果“查收入”为假,则显示“简易家庭帐本_支出查询”;
  3. 设“筛选条件”下拉框的列表属性:
    • 如“查收入”为真,则包含“全部”、“收入类别”、“收入者”及“支付方式”四项;
    • 如“查收入”为假,则包含“全部”、“支出分类”、“支出专项”及“受益人”四项;
  4. 从数据库中读取全部记录,并保存到“数据全集”中:
    • 如“查收入”为真,则读取“收入记录”;
    • 如“查收入”为假,则读取“支出记录”。

2、设置查询日期

  • 当用户选中起始日期时,设起始日期选框的显示文本为选中日期,格式为“y-M-d”;
  • 当用户选中终止日期时,设终止日期选框的显示文本为选中日期,格式为“y-M-d”;

3、选择筛选条件

根据选中的筛选条件,设置“主筛选项”下拉框的列表属性,并设“次筛选项”下拉框的允许显示及列表属性,当筛选条件为:

  1. 全部:隐藏“次筛选项”,设“主筛选项”的列表属性为空列表;
  2. 收入类别:隐藏“次筛选项”,从数据库读取收入类别,将其设为“主筛选项”的列表属性;
  3. 支出分类:显示“次筛选项”,从数据库读取支出一级分类,将其设为“主筛选项”的列表属性;从数据库读取支出二级分类,保存在全局变量“支出二级分类”中;在每个二级分类的首位插入列表项“全部”,并将该变量中的第1个列表项设置为“次筛选项”的列表属性;
  4. 受益人及收入者:隐藏“次筛选项”,从数据库中读取“家庭成员”,将其设为“主筛选项”的列表属性;
  5. 支出专项:隐藏“次筛选项”,从数据库中读取“支出专项”,并提取出“专项名称”列表,将其设为“主筛选项”的列表属性;
  6. 支付方式:隐藏“次筛选项”,从数据库中读取“支付方式”,将其设为“主筛选项”的列表属性;

4、求查询结果集

当用户选定了查询条件,并点击查询按钮时,根据查询条件,对“数据全集”列表进行筛选,并返回不同的结果集:

* 当筛选条件为全部时,仅按日期进行筛选,返回“日期筛选集”;
* 否则,当筛选条件为支出分类,并且次筛选项不等于“全部”时,返回“二级筛选集”:
* 其余筛选条件返回“一级筛选集”。

集合之间的包含关系为:

数据全集 ⊇ 日期筛选集 ⊇ 一级筛选集 ⊇ 二级筛选集

符号“⊇”读作包含,用于表示集合之间的关系,例如集合{1,2,3}⊇集合{1,2}。

5、数据分页处理

查询结果集要经过两项处理,才能成为可以显示的数据——分页数据:

  1. 数据拣选(纵向切割):由于屏幕宽度的限制,表格中无法容纳全部的数据项(帐本中假设最多只能容纳6列),因此,需要有选择地显示部分数据:
    • 收入记录:显示日期、收入类别、发放者、收入者、金额及支付方式等六项;
    • 支出记录:显示日期、支出一级分类、名称、数量、单位及金额等六项;
  2. 数据分页(横向分割):将查询结果集按照每页10行的规格进行分页,分割后的数据保存在全局变量“分页数据”中。

6、数据显示

当用户点击查询按钮时,首先对数据进行筛选,求出结果集,再对结果集进行纵向切割与横向分割,以求出“分页数据”,此时,设全局变量“页码”为1,并将第1页数据绘制在表格中。当页数大于1时,用户可以通过划屏动作改变页码的值,并显示不同页的数据。

7、选中数据

当用户在表格中点击(触碰)某一行数据时,应用将弹出选择对话框,显示该条记录的完整信息,并提供“删除”及“返回”选项,当用户选择“删除”时:

  1. 删除数据:分别从数据全集和查询结果中删除该数据项;
  2. 保存数据:将更新后的数据全集保存到数据库中;
  3. 重新分页:对数据进行重新分页,并更新全局变量“当前页数据”;
  4. 绘制表格:用更新后的当前页数据重新绘制数据表格。

当用户选择“返回”时,不执行任何操作。

8、批量删除数据

当用户点击批量删除按钮时,可以将本次的查询结果从数据全集中全部删除。此项操作事关重大,因此应用会弹出输入对话框,并要求用户输入密码,当确认密码正确后,执行删除操作,并设全局变量查询结果为空列表,清空页面上显示的数据表格及页码。

9、数据导出

当用户点击数据导出按钮时,将查询结果导出成逗号分隔的文本文件,文件将保存到手机上,用户可以用表格软件在电脑上打开文件。

10、返回主菜单

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

第六节 编写程序——创建全局变量与过程

一、名副其实的变量

如图12- 29所示,这7个全局变量在程序运行过程中,它们的值会发生变化:由初始值的空列表变为有数据的列表,或由1变为其他正整数,或由假变成真,我们称之为“真变量”。

图12-29 程序中名副其实的全局变量

二、不变的变量

与“真变量”相对而言的是“假变量”,这些全局变量被赋予初始值,在整个程序运行过程中,它们始终保持初始值不变。其中一部分“假变量”与表格绘制有关,如图12- 30所示。

图12-30 与表格绘制有关的“假变量”

以上变量定义了表格的背景颜色、行高、文字的位置以及表头文字的内容,在变量“表头”中,第一个列表项为收入记录的表头,另一个是支出记录的表头。

另外一些“假变量”与数据查询有关,如图12- 31所示。

图12-31 与数据查询有关的“假变量”

上图中的“筛选条件”是“筛选条件”下拉框列表属性的数据来源,第1项为收入查询的选项,第2项为支出查询的选项;“表头键”用于生成分页数据,这个变量的使用可以减少程序的代码量——用循环语句来拣选表格中的显示项。

三、可以充当变量的过程——有返回值的过程

有返回值的过程可以当作变量来使用,这类过程的命名尽量采用名词,这样代码更易于阅读。打开“内置块”的最后一项——过程,在过程抽屉的最下面显示的是有返回值的过程,如图12- 32所示。右下角的两个过程与应用无关,是为了讲解绘制表格技术而创建的。

图12-32 过程抽屉中的有返回值过程

下面我们逐一介绍这些过程。

1、表头、键表及X坐标

最简单的三个过程是“表头”、“键表”与“X坐标”,根据全局变量“查收入”的值,来获取假变量——表头列表、表头键及X坐标列表中的列表项。代码如图12- 33所示。

图12-33 过程——表头、键表与x坐标

2、月_日

无论是收入记录还是支出记录,其中保存的日期信息均为毫秒数,必须将它们转换成文字形式,才能显示在表格中。如图12- 34所示,“月_日”过程将毫秒转化为“月-日”的格式。

图12-34 过程——月_日

3、行号

程序中有三处需要计算画布上的点对应的表格行号,如图12- 35所示,

图12-35 过程——行号

4、结果集索引值

当用户点击某一行的数据时,可以根据行号求出该项数据在全局变量“查询结果”中的位置——结果集索引值,行号与索引值的关系见表12- 2,过程的代码如图12- 36所示。

图12-36 过程——结果集索引值

5、专项名称列表

在支出记录功能中,我们创建过类似的过程,但此处我们不对“激活”状态进行筛选,将所有支出专项的名称提取到一个列表中,返回给调用者。代码如图12- 37所示。

图12-37 过程——专项名称列表

6、查询结果集——日期筛选集

如前所述:数据全集 ⊇ 日期筛选集 ⊇ 一级筛选集 ⊇ 二级筛选集,可见日期筛选集直接来源于数据全集。如果用户在“筛选条件”下拉框中选择了“全部”,则日期筛选集就是最终的查询结果,代码如图12- 38所示。

图12-38 过程——日期筛选集

这段代码并不难理解,需要注意的是终止日期的计算:由于日期对应的毫秒数是以当天的0时为时间点的,因此,必须给这个日期加上1天,才能使终止日期包含在查询日期范围内,否则,查询日期中将不包含终止日期这一天。

7、一级筛选集与二级筛选集

一级筛选集直接来源于日期筛选集。如果用户在“筛选条件”下拉框中没有选择“全部”及“支出分类”,那么一级筛选集就是最终的查询结果;如果用户选择了“支出分类”,同时在“次筛选项”下拉框中选择了“全部”,那么一级筛选集也是最终的查询结果;只有当用户选择了“支出分类”,同时在“次筛选项”中选择了“全部”以外的选项时,才需要求二级筛选集。二级筛选集直接来源与一级筛选集。两个筛选集的代码如图12- 39及图12- 40所示。

图12-39 过程——一级筛选集
图12-40 过程——二级筛选集

在上述代码中,局部变量“键”用来在键值对列表(日期筛选集)中查询“实际值”,注意区分两个下拉框的名称:筛选条件与主筛选项。例如,当筛选条件为“收入类别”时,键也为“收入类别”,此时,假设“主筛选项”选中了“工资”,则查询目标就是“工资”,而实际值可能是“工资”、“奖金”或“理财”等等,那么只有“实际值=工资”的项才能被添加到结果集中,这就是筛选的意义。

在筛选条件下拉框中,“支出分类”并不是支出记录中的“键”,因此需要一个“如果…则…否则”语句进行转换。其实我们也可以将“支出分类”改为“一级分类”,这样在代码的处理上就可以省去那个条件语句,但是考虑到支出分类中还包含了二级分类,因此,在含义上“支出分类”更准确一些。

8、列表转字串收入与列表转字串支出

如图12- 41及图12- 42所示,这两个过程分别来自“收入记录”屏幕及“支出记录”屏幕。在最新版的App Inventor中,添加了“代码背包”功能,可以将某一段需要复制的代码放在背包中,并在其它屏幕或其它项目中打开背包,将代码取出来,这一功能非常好用,解除了我们编写重复代码的烦恼。代码背包的操作非常简单,对准代码点击右键,在右键菜单中选择“将代码块加入背包”即可,如图12- 43所示。

图12-41 过程——列表转字串_收入
图12-42 过程——列表转字串_支出

列表转字串_支出过程与支出记录屏幕中的过程不完全相同,读者请注意区分。

图12-43 将需要复制的代码添加到代码背包中

四、改变世界的过程——无返回值过程

如果把我们的程序比喻成一个世界,那么无返回值过程就是推动这个世界运转的内在动力。无返回值过程可以改变世界的外在特性——组件的属性值,也可以改变世界的内部特性——全局变量的值。图12- 44中显示了程序中的过程(截图自内置块过程抽屉)。

图12-44 无返回值过程

1、改变全局变量的过程——查询及求分页数据

先来看查询过程,如图12- 45所示,有了日期筛选集等三个有返回值的过程,查询过程写起来非常简单。查询过程改变的全局变量“查询结果”,而查询结果是分页数据的直接来源。

图12-45 过程——查询

再来看“求分页数据”过程,如图12- 46所示,该过程改变了全局变量“分页数据”。

图12-46 过程——求分页数据

2、绘制表格及显示页码的过程

(1) 绘制收支表头

在本章技术准备一节中,我们创建了一个适用于支出查询的“绘制表头”过程(图12- 8),现在的“绘制收支表头”过程能够同时适用于收支信息的查询,代码如图12- 47所示。

图12-47 过程——绘制收支表头

与此前的“绘制表头”过程相比,这里声明了局部变量“表头”及“x坐标”,通过调用“表头”及“x坐标”过程(图12- 33),实现了对收入与支出的判断,使得该过程成为适用于收入与支出查询的通用过程。

(2) 绘制单行背景:见图12- 9

(3) 写收支单行文字

如图12- 48所示,该过程也是收支通用过程,这里通过参数“每行数据”来传递不同类型的数据。

图12-48 过程——写单行收支文字

(4) 绘制数据行:见图12- 23

(5) 绘制数据表

如图12- 49所示,该过程集成了绘制表格的各项功能——绘制收支表头、绘制单行背景以及写收支单行文字。

图12-49 过程——绘制数据表

(6) 显示页码:见图12- 26

第七节 编写程序——事件处理

一、屏幕初始化

屏幕初始化时,根据MENU屏幕传递过来的初始文本值,设定全局变量(查收入与数据全集)及组件的属性值(屏幕的标题属性、次筛选项的允许显示属性以及筛选条件的列表属性)。代码如图12- 50所示。

图12-50 屏幕初始化程序

提醒读者注意,判断条件中的屏幕初始文本值不等于“收入查询”四个字符,而是等于“”收入查询””六个字符,其中的两个半角双引号是App Inventor自动添加的。

二、日期选择事件

如图12- 51所示,当用户选中了起始日期与终止日期后,让日期选框显示选中的日期。

图12-51 日期选框的选择程序

三、筛选条件选择程序

如图12- 52所示,在“筛选条件”下拉框的完成选择事件中,主要任务包含以下3项:

  1. 设置“主筛选项”列表属性;
  2. 设置“次筛选项”的列表属性;
  3. 设置“次筛选项”的允许显示属性。 这里我们没有借用“查收入”作为条件判断的依据,而是直接根据“筛选条件的选中项”为判断依据,设置相关的变量及属性。
图12-52 筛选条件下拉框的选择程序

注意代码中对“次筛选项”允许显示属性的设置,仅当“筛选条件”选中“支出分类”时,才允许“次筛选项”显示在用户界面上。注意条件语句的使用方法,将“支出分类”以外的选项处理全部放在“否则”分支中,这样只需设置一次“次筛选项的允许显示为假”。

四、主筛选项选择程序

当“筛选条件”选中了“支出分类”时,“主筛选项”的选择事件将导致“次筛选项”列表数据的重新绑定。代码如图12- 53所示。

图12-53 主筛选项下拉框的完成选择程序

五、查询按钮点击程序

如图12- 54所示,在查询按钮的点击程序中,设置全局变量“页码”为1,并调用了3个无返回值的过程。“查询”过程设置了全局变量“查询结果”(见图12- 45),“求分页数据”过程设置了全局变量“分页数据”(见图12- 46),而“绘制数据表”过程里调用“绘制收支表头”、“绘制单行背景”及“写支出单行文字”过程(见图12- 49)。

图12-54 查询按钮的点击程序

六、划屏翻页事件

当用户在屏幕上画布所在的区域划动手指时,首先清除画布,然后根据手指运动的速度在x方向的分量,来判断划动方向,并以此为依据改写全局变量“页码”的值,最后调用“绘制数据表”过程,来实现数据表格的绘制。代码如图12- 55所示。

图12-55 画布的划动程序

七、选中单行数据程序

在图12- 23中,我们已经实现了选中行背景闪烁的功能,但“绘制数据行”过程仅对支出查询有效,我们需要将其中的“写单行文字”过程替换为“写收支单行文字”过程,代码如图12- 56所示。

图12-56 利用背景闪烁效果提示用户选中的数据项

八、与删除单行数据相关的程序

1、画布的触碰程序

当用户点击画布上的某个数据行时,将触发画布的“触碰”事件,此时应用将弹出选择对话框,显示完整的记录信息,并提供“删除”及“返回”两个选项,代码如图12- 57所示。

图12-57 在画布的触摸事件中弹出选择对话框

上述代码中声明了一个全局变量——结果集索引值,用来保存选中项在全局变量“查询结果”中的索引值,以便当用户选择了“删除”时,将选中项从“查询结果”及“数据全集”中删除。

2、对话框完成选择程序

在画布的触摸事件处理程序中,我们已经为全局变量“结果集索引值”赋值,现在要利用该索引值求出全集索引值——选中项在数据全集中的位置,以便执行删除操作;数据删除后,将更新后的数据全集保存到数据库中,然后将选中项从“查询结果”中删除,并重新“求分页数据”并绘制数据表格。代码如图12- 58所示。

图12-58 在对话框完成选择事件中删除选中数据,并显示更新后的数据

九、数据导出

数据导出功能允许用户将查询结果导出为逗号分隔的文本文件,保存在安卓设备中。代码如图12- 59所示。

图12-59 导出查询结果

十、批量删除

批量删除操作通常在数据导出之后进行,为了减少数据库中的数据量,提高查询速度,用户可以定期将数据导出,然后再将数据批量删除。代码如图12- 60所示。

图12-60 将本次的查询结果批量删除

十一、返回主菜单

如图12- 61所示,每个屏幕中都要编写此程序。

图12-61 返回导航菜单页

第八节 测试与改进

一、收入查询测试

与其它类型的应用不同的是,信息管理类应用的测试要依赖于数据,数据在数量上要足够多,在质量上要具备多样性,只有这样,才能测试各种可能的操作。因此,在正式开始测试之前,我们要利用程序,来生成必要的实验数据。

1、生成实验数据

首先生成收入数据,代码如图12- 62所示。

图12-62 为了完成测试而生成的实验数据

在上述代码中,为了让数据看起来不那么整齐划一,我们使用了“列表中任意项”块,从备选列表中随机选取列表项;上述数据的时间起点是从当前时间起向前倒推60天,并且每次循环天数加1,共60次循环,因此共生成了到今天(2016年5月24日)为止的60条数据;最后,将生成的数据保存到数据库中。

在QUERY屏幕的初始化程序中调用该过程,如图12- 63所示。

图12-63 在屏幕初始化时生成收入数据

注意将调用“生成收入数据”过程块放在初始化程序的第一行,以便可以将新生成的数据读取到全局变量“收入全集”中。测试一下程序的运行结果,如图12- 64所示。

图12-64 测试新生成的60条收入数据

2、设置查询条件

(1) 按收入类别查询 图12- 64中的查询条件只包含起止日期,现在我们设定其他查询条件,首先设定收入类别,测试结果如图12- 65所示。

图12-65 测试——按收入类别查询

查询结果是理财收入的条目最多,共11条(占据2页)。

(2) 按收入者查询 测试结果如图12- 66所示。随机生成的数据中,张老三及李斯各占据3页——21条及25条,而王小五仅占居2页——14条,合计为60条。

图12-66 测试——按收入者查询

(3) 按支付方式查询 如图12- 67所示,现金收入19条,转账收入23条,其他收入18条,合计60条。

图12-67 测试——按支付方式查询

3、删除单条记录

如图12- 68所示,我们选择删除日期结果集中第6页的最后一行数据(日期为5-23),删除后表格更新,第6页只剩下9行数据。

图12-68 测试——删除单条记录

4、批量删除数据

我们将删除王小五的全部收入,如图12- 69所示,删除后表格中的数据并未更新,这时点击查询按钮,占位标签显示“查询结果为空”。

图12-69 测试——批量删除数据

5、数据导出

将筛选条件设置为“收入者”,查询“张老三”的全部收入,然后点击数据导出按钮。程序没有任何反映(我们没有让它有反映)。打开手机中的文件夹“AppInventor/data”,可以看到已经导出的文件,如图12- 70所示,其中修改日期为“2016/5/24 11:08”的便是刚刚导出的数据文件。

图12-70 手机文件夹中的数据导出文件

用记事本打开文件accountBook_20160524.txt,如图12- 71所示,可以看到里面有若干行逗号分隔的文本,其中收入者全部为“张老三”。

图12-71 导出的数据内容

以上是对收入查询功能的测试,我们发现存在两个问题: (1) 批量删除数据之后,数据表格没有更新; (2) 导出数据成功之后,系统没有任何提示。

此外有一点需要特别说明:在开发测试过程中,导出的数据文件保存在手机存储卡的“AppInventor/data”文件夹下,当应用开发完成,项目将编译为APK文件并安装到手机上,此时导出文件的位置将发生变化,为此我们需要重新设置导出文件的文件名。我们将在完成支出查询测试后,连同上述两项统一加以改进。

二、支出查询测试

1、生成实验数据

支出记录的数据项,从数量上说要多于收入记录,从数据结构上说,要比收入记录复杂,因此如果要写一个像“生成收入数据”那样的过程,难度会比较大,尤其是考虑到一、二级分类之间的对应关系,以及像名称、数量、单位这些输入项之间的匹配关系,需要设计一套相当复杂的逻辑,才能避免生成荒唐的数据。基于这样的原因,我们采用另一套方法来生成实验数据——手工编写数据表格,再将数据导入到项目中。

用Microsoft Office中的Excel表格来编辑数据,如图12- 72所示。

图12-72 用Excel编辑支出记录的数据文件

保存文件的时候,选择“另存为”,在“保存类型”选项中选择“CSV(逗号分隔)”,注意,在保存按钮的左侧有一个“工具”下拉框,选择其中的第二项“Web选项”,设置保存数据的编码格式,如图12- 73所示。

图12-73 将文件保存为CSV格式

在Web选项的窗口中,选择“编码”页,如图12- 74所示,在图中的下拉菜单中选择“Unicode(UTF-8)”,并点击“确定”按钮。这一选择是必须的,只有这样,才能在App Inventor中用文件管理器读取到到正常的文本,否则汉字的部分会变成乱码。

图12-74 将文件保存为UTF-8 格式

保存完成之后,将out.csv文件复制到手机SD卡的“AppInventor/data”文件夹下,如图12- 75所示。

图12-75 将out.csv复制到手机的AppInventor/data文件夹下

准备工作就绪后,我们开始编写加载数据的程序。在QUERY屏幕的初始化程序中,利用文件管理器加载数据,然后在文件管理器的收到文本事件中解析数据,并将数据保存到数据库中;数据保存成功后,用对话框发出通知,代码如图12- 76所示。(注意:禁用此前测试收入数据时的“生成收入数据”过程)。

图12-76 从out.csv文件中加载、解析数据,并保存到数据库中

连接AI伴侣,对上述代码进行测试。当数据保存成功后,在屏幕初始化程序中,单步执行加载数据指令,如图12- 77所示,右键点击图中的代码块,并选择最后一行“执行该代码块”,此项操作将从数据库中读取刚刚保存的全部数据,并将数据保存到全局变量“数据全集”中。

图12-77 加载已经保存成功的支出记录

然后,在测试手机上设置查询日期,起始日期要小于数据中的起始日期(2016/3/24),终止日期选择默认的当天日期(2016/5/25),并点击查询按钮,程序运行结果如图12- 78所示。

图12-78 加载数据的测试结果
看来文件的保存还是有问题,我们重新用记事本打开手机中的out.csv文件,并选择“另存为”,如图12- 79所示,将编码设置为“UTF-8”,注意将保存类型设为“所有文件”,将文件名设置为完整的名称“out.csv”。
图12-79 用记事本重新保存文件,编码为UTF-8

文件保存之后继续测试,结果如图12- 80所示。

图12-80 用记事本保存过的文件日期的数据无法识别

错误出在求日期的代码中,用“MM/DD/YYYY”格式的日期文本可以创建与日期对应的“时间点”,进而可以求日期所对应的毫秒数。在out.csv文件中,同样表示日期的字串“03/24/2016”,用EXCEL保存后,App Invenot可以将其识别为日期(如图12- 78),但用记事本保存之后,App Inventor认不出它了。经过一番尝试之后,决定放弃使用csv文件中的日期,而是用程序来生成日期数据,就像生成收入数据时那样。修改后的代码如图12- 81所示。

图12-81 设置固定的时间起点

2、功能测试——查询全集(日期筛选集)

首先测试数据全集,如图12- 82所示,我们设置了涵盖全部数据的起止日期,查询结果显示为7页(csv文件中共61条数据),我们截取了其中奇数页的查询结果。

图12-82 上述代码的测试结果——日期筛选集

3、功能测试——分类查询

下面测试分类查询,当筛选条件分别为支出分类、支出专项及受益人时,测试结果如图12- 83所示。

图12-83 测试结果——分类查询支出信息

左侧的三张图中,筛选条件均为“支出分类”:其中左一图中主筛选项为支出一级分类的“吃喝”,次筛选项为“全部”,查询结果共3页;左二图中将左一图中的次筛选项改为“粮油”,查询结果中包含7行;左三图将主筛选项改为“娱乐”,次筛选项改为“旅游”,查询结果为9行;右二图中筛选条件改为“支出专项”,主筛选项为“徒步运河”,查询结果为8行;最右边的图中筛选条件为“受益人”,主筛选项为“李斯”,查询结果未6行。

4、功能测试——数据的删除与导出

首先测试选中单条记录并删除,测试结果如图12- 84所示。

图12-84 测试结果——支出记录的单条删除

测试过程中,我们选择左一图中的最后一行删除,从测试结果看,删除之后数据表格自动更新,右一图中的最有一条日期变成了5-21。

下面测试批量删除,测试结果如图12- 85所示。

图12-85 测试结果——支出记录的批量删除

上图中左侧两张是点击“批量删除”按钮之前以及之后的截图,显然,批量删除后,数据表格没有自动更新,此时点击查询按钮,标签显示“查询结果为空”,说明刚才的批量删除操作是成功的,只是显示结果没有及时更新,这也是我们接下来要改进的部分。

最后来测试数据导出功能。我们将导出 “徒步运河”专项的全部记录(图12- 83中的右二图)。点击数据导出按钮后,查看手机中的“AppInventor/data/”文件夹,发现了刚刚生成的文本文件(第一行),如图12- 86所示。

图12-86 在测试手机的文件夹中找到了刚刚生成的文件

打开文件,查看内容,如图12- 87所示。

图12-87 支出数据的导出结果

与收入查询的测试结果类似,当批量删除数据以及导出数据时,应用缺乏必要的反馈,用户面对这样悄无声息的程序,会不知所措,因此我们下面给出改进。

三、功能改进——提供操作反馈

1、批量删除结果反馈

在对话框的完成输入事件中,当更新后的数据全集已经保存到数据库之后,添加3行代码:设查询结果为空列表,清空画布,让占位标签显示“查询结果为空”,并用对话框提示用户“批量删除成功”,代码如图12- 88所示。

图12-88 程序改进,为批量删除操作提供反馈

2、数据导出反馈

如图12- 89所示,在导出按钮的点击程序中,添加一个局部变量——文件名,在文件管理器完成保存操作后,用对话框提示用户数据导出成功,并告知用户文件的存放位置及文件名。

图12-89 程序改进——为数据导出操作提供反馈

下面对改进结果进行测试,如图12- 90所示,数据导出成功后,显示文件名及文件位置,虽然文字的排列效果不甚理想,不过功能已经具备;数据批量删除后,清空了画布,对话框提示用户“批量删除成功”,右一图中的页码信息应该改为“查询结果为空”,留给读者自行改进。

图12-90 改进后的程序运行效果

3、设置导出文件的位置

在图12- 59中,我们将导出的数据保存在文件“accountBook_20160526.txt”中,使用AI伴侣测试时,文件将保存在“AppInventor/data”文件夹下,但当应用编译并安装到手机上时,文件将保存在项目的私有文件夹下,使用文件管理器无法找到已经导出的文件。为此,我们需要重新设置导出文件的位置:在文件名前面添加“/AppInventor/data/”,即可确保导出的文件保存在“AppInventor/data”文件夹下。修改后的代码如图12- 91所示,测试结果如图12- 92及图12- 93所示。

图12-91 为导出的文件设置存放位置
图12-92 测试——导出数据文件
图12-93 导出文件的位置

本章到这里,收入与支出的查询功能已经完成,在最后的测试与改进一节中,我们不仅仅是测试与改进,测试之前的生成实验数据环节,在软件开发中占有很重要的地位,尤其是对于信息管理类应用,如果没有足够多样的数据,测试就很难进行。