Sun推出的Java 2 Micro Edition (J2ME) 可以让开发人员创建专用于各类手持设备的应用程序,比如传统的PDA到所谓的智能电话等。为了利用MIDP(Mobile Information Device Profile:移动信息设备框架)向读者介绍J2ME的开发知识,我将引领读者了解和掌握一个完整J2ME应用程序的创建过程,这一程序在本系列文章中都被命名为Expenses。
在上一篇文章里我向读者介绍了Expenses采用的主要MIDlet,同时还讨论了两种lcdui用户界面组件: Command和 List。本文接着介绍用来编辑现有开销项目或者添加新项目的详细情况,由此令开发人员从此步入Item组件一族的殿堂。
现在我们就开始编程!首先下载 Expenses的升级源代码,然后看看新的DetailForm表单,如图A中的DefaultGrayPhone模拟器所示。
图A
新的细节表单
首先让我们查看新表单,然后是Expenses类,最后在应用程序中编写DetailForm。
设计新表单
DetailForm扩展了Form基类(而后者又扩展了Screen,所以该类包含了其他组件)并且实现了commandListener和ItemStateListener接口,同时可以处理自己的事件。假如你已经阅读过相应的代码,那么你应该注意到DetailForm的API中有个setDestroyListener方法,该方法接受一个类实例,而这个类实例则实现了可定制的DestroyListener接口。初始化lcdui所采用的事件通知机制就可以通知DetailForm的控制MIDlet (用户已经完成了该组件,它要用到该表单)。一旦MIDlet收到事件,它就会使用Display.setCurrent 激活其他Screen组件。
为什么我要不厌其烦地讨论事件机制的细节呢?这是因为我想更多地用到DetailForm表单而非仅仅一个MIDlet,当然,最开始还是先让咱们看看Items。
查看构造器
DetailForm的构造器代码如程序清单A所示。这个构造器以ExpenseInfo的一个实例作为其参数,这样我就可以处理现有项目的编辑功能,同时还可以在同一代码的基础之上创建新项目。DetailForm通过对基类Form构造器的调用设置其名称,然后创建4个 Item用户界面组件,它们是一个DateField、两个TextFields和一个ChoiceGroup,其功能就是显示ExpenseItem的有关信息,另外还要加上两个Commands。
正如我们现在都知道的那样,Item组件族派生于Item抽象类,同大多数家族成员一样,这类组件有很多性质是相同的。首先, Items 可以放在Screen对象上。其次,它们共享单一事件itemStateChange,该事件提示组件所表示的数据发生了变化。
DateField
DateField和TextField组件的用途从它们的名字就可以看出来。DateField显示日期,用户可以由此通过熟悉的日历界面选择新的日期。DateFields具有若干种模式,具体设置可以这样做:传递三种静态DateField常量之一作为构造器的第2个参数。
DateField.DATE_TIME设置DateField为显示日期和时间。
DateField.DATE设置组件显示和编辑日期(不对时间操作)。
DateField.TIME只显示和编辑时间(不对日期操作)。
因为我们只关心Expenses程序中的日期,所以我创建的dfDateDateField组件具有唯日期属性,如清单A所示。
TextFields
TextField组件显示文本,同时允许用户编辑文本,在功能上类同于桌面应用程序中的文本框。TextFields 支持某些基本的输入限制功能,而且这类功能可以在创建组件的时候由构造器的最后一个参数设置。除了对输入信息进行限制以外,这些约束条件还可以简化电话风格键盘的数据输入操作,因为约束只承认给定键的某些特定字符(比如,只有数字按钮上的数字可以输入)。可能的约束值如下:
TextField.ANY:任何信息都可以输入;文本、特殊字符和数值等。
TextField.EMAILADDR:只有组成有效电子邮件地址的字符才可以输入。
TextField.NUMERIC:只能输入数值。记住,MIDP不支持浮点数值,所以该约束条件只允许输入整数。
TextField.PASSWORD:和传统的密码输入方式相同,用星号(*)取代用户输入的密码信息。
TextField.PHONENUMBER:只能输入组成有效电话号码的信息。
TextField.URL:只能输入合法组成URL的字符。
fmDetail表单包含了两个TextFields:其一是tfDesc,它允许输入事件的文本说明(比如“Lunch”)。其二是tfAmount,它记录开销的美元数量,因此通常由TextField.NUMERIC约束创建。
ChoiceGroup
细节表单的cgCategory组件是一个ChoiceGroup,用户可以把开销放到预先设置的开销类别里: Meals、Lodging、Car、Entertainment或者Miscellaneous。ChoiceGroups 在功能上等同于List组件,后者我在上一篇文章中已经有所介绍,但是它们只支持EXCLUSIVE和 MULTIPLE格式(没有IMPLICIT ChoiceGrou这类东西)。同样的,作为Item组件,ChoiceGroups 会在操作事件的时候触发itemStateChanged事件,而Lists则触发 commandAction事件。
给ChoiceGroup添加项目有两种方法。首先,你可以构造一个空组件然后通过append或者 insert方法加入项目。其次,你还可以传递一个包含项目的String数组(这些项目是你希望ChoiceGroup显示的)给重载的构造器。这两种方法各有高低,但我在这里采用了第2种方法,主要是为了能让我对中央位置保存的类别列表(作为静态数组ExpenseInfo的成员)有一定的选择能力。这样就可以很方便地添加新类别或者让用户在将来具有这样做的能力。
在ChoiceGroup中给每一项指定图标也是可以的。当然,这里没必要做了免得程序变得复杂,所以我打算在将来的文章中再讨论。
事件处理
DetailForm上的Items 都能再用户操作其数据时触发itemStateChanged事件。如程序清单 B所示。itemStateChanged的工作方式和commandAction事件相同:它检查产生事件的Item,确定应该采取的行动。在本文的例子中, DetailForm更新其私有的ExpenseItem实例来匹配用户修改的数据。
这里唯一需要详细解释的是tfAmount变动事件的处理。不支持浮点数字显然令美元和美分数量的处理变得复杂起来,我也遇到了这样的难题。有两种解决办法:给美元和美分分别提供TextFields 作为数值表示部分,或者总是假定最后两位数字代表美分。不过这就需要用户用美分来记录数值(100代表1美元)。
当然,DetailForm也处理自身的两个组件cmOK和cmCancel所触发的commandAction事件。只要用户调用了以上两个组件之一, DetailForm就会调用DestroyListener.requestDestroy。如果用户调用cmOK,私有的ExpenseInfo实例就会被告之保存对其所进行的修改。
归到一块
现在只需要修改Expenses MIDlet来利用DetailForm即可。期间需要三个步骤:
实现Expenses中的DestroyListener以便在用户通过其两个组件之一关闭程序时破坏DetailForm并且显示lsMain。
修改Expenses.commandAction (程序清单C) 以便用户在选择项目时内容将显示在DetailForm中。
修改Expenses.commandAction,这样一旦用户调用了cmAdd,DetailForm就会被创建并显示一个新的、空白的ExpenseInfo实例。
设计问题:为什么要有两个MIDlets?
你在看代码的时候可能注意到了,代码中有两个MIDlets,最初的Expenses和新的NewExpense MIDlet。这样做的原因很简单:在大多数情况下,Expenses的用户会打开程序仅仅添加某个新的开销项目。所以我们的设计反映了两个基本的移动应用程序设计原则:方便用户操作,提供最常用功能的快捷方式。
NewExpense简单地复制了Expenses中调用cmAdd命令时发生的行为。它创建一个新的、空白的ExpenseInfo实例和一个显示前者的DetailForm实例。当然,在这个时候,应用程序还没有受到数据存储的支持,所以任何通过NewExpense新加入的项目都不会出现在Expenses里。没关系,后面我们就要说到这一点了。