《深入BREW开发》——第八章 BREW的事件处理

第八章 BREW的事件处理
       在上一章里,我们创建了一个叫做HelloWorld的应用程序,是的,Hello BREW,我们已经进入到了你的世界!在领略了基本的BREW应用程序之后,我们将继续进发,去仔细的看一看应用程序的每一个核心的东西。
       BREW应用程序最为基本的内容就是它的事件驱动和处理的机制,通过这样的机制,我们可以使用相对简单的思路开发出我们所需要的应用程序。基于事件驱动机制开发的好处就是我们可以把复杂的程序容易的分割成各个小模块程序,同时针对事件驱动机制的特点我们还可以实现一个状态机的结构,这样的程序框架对于开发大型的应用程序来说是十分的方便的。
       在这一章里面,我们将详细的剖析一下BREW的事件处理的方式,以及我们的应用程序是如何捕获(Handle)和处理它们的。同时,根据这种方式的特点我们还将构建一个状态机的应用程序框架,而且这个框架将有利于我们今后应用程序的开发。
8.1 理解事件驱动模型
       做为BREW平台最为核心的一块,BREW的事件驱动模型是我们开发BREW应用程序必须要提前理解和消化的。这也是我们开发应用程序的基本的一步。
8.1.1处理一个事件
       对于一个BREW应用程序来说,不管愿不愿意处理这个事件,都将收到来自系统或使用者的事件,并且将这些事件传递到应用程序中的控件或者接口当中。正如前面章节所描述的一样,BREW通过应用程序的HandleEvent函数将事件传递给应用程序,应用程序处理这些事件并且通过函数的返回值告知系统这个事件的处理情况。
与大多数的操作系统不同,我们的应用程序不需要采用的轮询的方式来查看是否有事件传递过来,BREW系统将会根据相关的事件主动的调用应用程序的HandleEvent函数将事件传递给我们的应用程序。这个区别是至关重要的,因为我们的应用程序在捕获一个事件的时候,必须进行尽可能简单的处理。不像一些大型的操作系统平台,如Windows CE和其他的嵌入式系统操作系统,BREW运行在一个单线程的环境,因此,我们必须尽可能快地处理每一个事件,这样才不至于影响系统的正常运行。否则,应用程序的表象可能就会是响应用户非常迟钝,这样的应用程序是用户不会喜欢的。
       基于这样的一个 BREW 事件驱动环境,要求我们的应用程序及时处理事件。这意味着应用程序应该迅速处理事件并立即返回。BREW将事件传递给应用程序时,应用程序会通过返回TRUE(表示已处理)或FALSE(表示未处理)来指示应用程序是否已处理事件。如果小程序必须将事件传递给其它事件处理程序(如控件或接口),它只需返回调用这个接口HandleEvent函数的结果。
BREW应用程序的事件处理(HandleEvent)函数接受三种与事件相关参数的输入,它们分别作为HandleEvent函数的第二、第三和第四个参数传递。下面是一个事件处理程序函数的示例:
boolean HandleEvent(MyApp* pMe, AEEEvent eCode, uint16 wParam, uint32 dwParam);
第二个参数为AEEEvent类型,用于指定应用程序接收的主要事件。第三个和第四个参数是根据接收事件来定义的短数据和长数据。 这些值取决于事件本身,并根据事件的定义来定义。
典型的,我们的应用程序的事件处理函数是一个巨大的switch语句的程序结构,在这个结构中我们可以看到我们所感兴趣的事件,或者直接将某些事件传递到一些二级事件处理函数之中,这些二级事件处理函数既可以是我们为了程序结构的清晰而编写的函数,也可以是某个控件或接口的事件处理函数。关于这个switch语句的结构,我们可以在上一章创建的应用程序中看到。
执行应用程序时,我们只需考虑和处理应用程序可能需要处理的事件。一般情况下,我们可以忽略很多事件。例如,如果应用程序执行一个只需使用上下左右箭头键作为输入的游戏,并收到与0-9按键对应的事件,那么它将返回FALSE来表示应用程序不处理这些事件。体现在代码上就是我们的switch语句中不必“case”这些事件。
8.1.2捕获系统事件
       在我们的应用程序当中,最起码需要处理EVT_APP_START、EVT_APP_STOP、EVT_APP_SUSPEND和EVT_APP_SUSPEND事件,上一章里面我们已经了解了它们的作用。而除了这些事件之外,应用程序中还可能涉及到的系统事件有:
       1、EVT_APP_MESSAGE事件。此事件表示当前系统发送了一个文本短消息到我们的应用程序中。
       2、EVT_ALARM事件。此事件表示系统发送了一个闹钟事件到我们的应用程序之中。
       3、EVT_NOTIFY事件。此事件表示有一个系统的通知事件到达。发送通知事件的通常包括呼叫等事件。我们的应用程序需要注册相应的通知事件才能接收到这些通知。
这些系统事件是属于BREW的系统级别的事件,无论当前我们的应用程序处在什么样的状态下,都会处理这些系统事件。也就是说即便当前我们的应用程序处于关闭状态,也同样的可以接收到这些系统事件。
我们还可以定义大于EVT_USER的事件代码,通过ISHELL_PostEvent接口来向特定的应用程序发送自定义事件。例如:
bRet = ISHELL_PostEvent( pIShell, classid, eCode, wParam, dwParam );
       其中pIShell是IShell接口的指针,classid表示事件发送到哪一个应用程序,eCode就是我们要发送的事件。通过这样的方式,就像系统事件一样,无论目标应用处于什么样的状态,都可以收到这个事件进行处理。bRet代表事件是否发送成功,注意,这里面返回值代表的是事件是否发送成功,而不是事件的处理结果。
8.1.3捕获用户接口事件
       在我们的应用程序中处理的大多数事件是键盘事件或者菜单选择事件,这些事件的特点是只有在应用程序处于活动状态的时候才能处理,这些事件就是典型的用户接口事件。这些事件的处理也是我们应用程序的核心内容,因为这些事件描述了当前用户的输入信息。
       最简单的用户输入就是用户按一下按键,然后释放按键。这其中BREW当前活动的应用程序将收到三个事件:EVT_KEY_PRESS、EVT_KEY和EVT_KEY_RELEASE。EVT_KEY_PRESS描述用户已经按下了一个按键,EVT_KEY_RELEASE表示用户已经释放了这个按键,EVT_KEY则是介于两者之间的一种按键装态的描述。通常情况下我们的应用程序只需要处理EVT_KEY事件就好了。
       对于每一个按键事件,系统通常使用一个16位的按键代码来表示当前按下的是哪一个按键。这些按键代码定义在AEEVCode.h中,最为常用的按键代码如下:
按键
按键代码
0
AVK_0
1
AVK_1
2
AVK_2
3
AVK_3
4
AVK_4
5
AVK_5
6
AVK_6
7
AVK_7
8
AVK_8
9
AVK_9
*
AVK_STAR
#
AVK_POUND
清除键(返回键)
AVK_CLR
上方向键
AVK_UP
下方向键
AVK_DOWN
左方向键
AVK_LEFT
右方向键
AVK_RIGHT
选择键(确认键)
AVK_SELECT
呼叫键
AVK_SEND
结束键(关机键)
AVK_END
       通过上面的表格我们可以发现,与其他系统相比BREW缺少了一些独立的字符按键。这是因为通常一个BREW设备都是只有一个数字键盘和少数的几个控制按键,而字符的输入则是通过文件控件来完成。文本控件通过用户输入方式和所选择的不同输入法,如数字、字母以及拼音或笔画等,来实现对应的文本输入。事实上,要做到这些我们需要将应用程序捕获到的用户输入事件传入文本事件中去,传入的方式是通过文本控件的HandlEvent方法。
       在我们的应用程序中通常使用控件来处理一些用户的输入事件,而且在应用程序中通过控件的HandleEvent函数将这些事件传入,而控件在处理了相关事件之后,也同样会以某种方式来保存结果或者通知应用程序。例如,文本控件会保存并显示用户输入的文本,而菜单控件则会通过发送EVT_COMMAND事件来告知应用程序用户的选择项目是什么。在后面的章节中我们将逐渐的看到这些应用。
       有一个特殊的按键事件需要我们特别的注意,这个事件就是带有AVK_CLR参数的EVT_KEY事件。在默认情况下,当BREW内核捕获到这个事件的时候所执行的动作是关闭当前的应用程序。因此,如果我们不想BREW在收到这个事件的时候关闭我们的应用程序的话,我们需要在应用程序中处理这个事件,也就是在我们的应用程序接收到这个事件的时候返回TRUE,以使得BREW内核不会再处理这个事件。
8.2 构建应用程序框架
       由于在每一个BREW应用程序中很多事件的处理方式都基本相似,因此,如果我们可以把这些基本的事件处理采用统一的方式来处理,那么将极大地降低我们构建新应用程序的工作量。在这一部分中,我们将来构建这样的一个应用程序框架,在这个框架中可以自动处理诸如EVT_APP_START和EVT_APP_SUSPEND等事件。
       在本章中,这个应用程序框架将从一个叫做文件浏览器的应用程序中进行构建。在这个程序中,所实现的功能就是将BREW Shared文件夹下面的文件枚举出来,选中相应的文件后可以显示文件的基本信息,如大小等。
8.2.1创建文件浏览器应用程序
       由于我们现在才开始构建第一个真正包含了一定功能的应用程序,因此,在这里将一步一步的讲解如何构建这个应用程序。同时,还可以参考上面一章里面关于创建应用程序的步骤说明,在后面的示例中我们将省略构建应用程序的步骤。好,我们现在打开MicroSoft Visual Studio .Net2003,选择新建项目后如下图:
图8.1 生成FileExplorer应用程序
选择BREW向导,输入项目名称为FileExplorer。如果现在您看不到这个BREW向导的话,说明还没有安装BREW Addins。这个插件现在包含在BREW SDK Tools中,和MIFEditor和ResourceEditor一起。我们可以从高通网站上下载并安装BREW SDK Tools应用程序。
确定后出现如图8.2所示的下对话框,其中我们要选上File,因为我们的应用程序中将使用文件处理的接口。然后我们选择Options选项,看到如图8.3种所示,在是否输出注释的部分选择“No”,因为这些注释将会占领很大的文档篇幅。然后选择MIF Editor按钮,建立此应用程序的模块信息文件,打开MIF Editor应用程序。如图8.4种所示一样。最后,点击“Finish”,建立应用程序。
在MIF文件编辑器的Applets选项卡中,我们选择“List of Applets defined in this module”中的增加Applet按钮,显示如图8.5所示。我们选择从本地生成Class ID,并输入Class ID的值为0x99999999,输入Applet的名称为FileExplorer,然后点击确定,保存生成.bid文件。这样,一个Applet就加入到一个模块中去了。
图8.2 设置FileExplorer应用程序的包含头文件
 
图8.3 设置FileExplorer应用程序的选项
 
图8.4 MIF Editor
图8.5 在模块中增加Applet
       增加完Applet之后,还需要到Privileges中增加File的访问权限,否则在我们的应用程序中将不能使用文件系统的接口。最后保存生成的文件为FileExplorer.mfx文件,同时,选择build菜单,生成mif文件。
       至此,一个由向导生成的FileExplorer应用程序就声称完毕了,我们接下来要做的就是在这块“白纸”上勾勒出我们的应用程序框架。
8.2.2使用状态表示应用程序
       如果我们写一个应用程序只需要几百行的代码的话,那么这个世界对于程序员来说简直是太美妙了。但是,这仅仅是一个愿望,因为实际的程序代码通常需要成千上万行。就算是用BREW这样高度集成的平台来开发一个稍微复杂的应用程序,都需要上万行的代码才能完成。而且对于程序员来说,每增加一行代码,就相当于打开了一个潘多拉盒子,不知道这一行将来会带来什么样的后果,这种情况在代码较多的时候尤为明显。把这个潘多拉的盒子关上是所有程序员一直的的梦想,于是,各种各样的程序结构诞生了,函数分割、文件分割、以及状态分割等形式相继出现了,又于是C语言出现了、C++语言出现了、可视化编程出现了。这些都是程序员们为了关闭这个潘多拉盒子所做的努力,事实证明这些努力没有白费。这些编程语言以及对应的代码组织方式极大的方便的程序的管理,使得开发大型程序不再那么复杂了。
当然,凡事总有穷尽,无论怎样的努力,程序中间总会有缺陷。或者这是人类思维方式的一种缺陷,或者受限于编程者的水平,这个潘多拉盒子还是时常会打开。“当我们改变不了环境的时候就改变我们自己吧”,对于程序缺陷,既然我们无法避免那就容忍它的存在吧!我们唯一能够做的就是尽可能的减少它们的存在。
       编写简单的程序可以在很大程度上减少程序的缺陷,而让程序变得简单的方法就是将程序都分割成一小块一小块的,然后通过某种方式将这些小块的程序连接起来,共同构成一个大型的应用程序。一种较好的方式就是我们将要介绍的状态机,我们可以把应用程序分割成不同的状态。例如,在HelloWorld应用程序中,显示“Hello World”的界面就可以看成一个显示字符串的状态。为了在BREW应用程序开发的过程中能够拥有更加清晰的程序流程,我们特意设计了这样的一个状态机的应用程序框架。
       我们的基本思路是这样的,划分应用程序状态的方法是每一个显示界面都作为一个状态,在进入状态的时候创建界面,同时状态机停止运转,此时等待用户在当前的界面进行操作,用户操作完成后关闭当前的界面,并启动状态机继续运行。由于BREW的每一个应用程序都是通过HandleEvent方法来获得事件的,在每一个界面等待用户输入的时候,每一个界面都对应一个界面的HandleEvent函数,用来捕获用户在这个界面的输入。这样,我们的应用程序将被分成状态和界面两种单元。
图8.6 状态机基本原理
       状态机在代码上讲就是一个for(;;)循环,只有在创建一个用户界面之后才会暂时终止运行,可以实现这样功能的代码如下(可以在本书所附的示例代码Test5/FileExplorer应用目录下的AppStateMachine.c中找到这个函数):
/*====================================================================
函数     : CStateMachine_RunFSM
说明     : 有限状态机引擎。
参数     : pMe [in]:指向CStateMachine对象结构的指针
返回值 : 无。
备注     : 无
====================================================================*/
static void CStateMachine_RunFSM(CStateMachine *pMe)
{
    NextFSMAction nextFSMAction = NFSMACTION_WAIT;
   
    for(;;)
    {
        nextFSMAction = pMe->m_pfnState((IStateMachine *)pMe,
                                         pMe->m_pUserState);
       
        pMe->m_eWndRet      = WNDRET_CREATE;
           pMe->m_wWndParam    = 0;
           pMe->m_dwWndParam   = 0;
          
        if (nextFSMAction == NFSMACTION_WAIT)
        {
            break;
        }
    }
} // End CStateMachine_RunFSM
       pMe->m_pfnState就是我们在应用程序中的状态函数,状态机根据这个函数的返回值nextFSMAction来判断是否暂停状态机的运行。从代码中可以看到当nextFSMAction为NFSMACTION_WAIT的时候这个for循环将被break语句打破。
       我们定义的状态机结构体如下所示(下面的代码综合了AppStateMachine.h和.c的内容):
// 状态处理函数返回给状态处理主函数的值类型
typedef enum _NextFSMAction{
    NFSMACTION_WAIT,
    NFSMACTION_CONTINUE
} NextFSMAction;
 
typedef NextFSMAction (*PFNSTATE)(IStateMachine *po, void *pUser);
 
typedef struct _CStateMachine{
    AEEVTBL(IStateMachine) *pvt;
    uint32          m_nRefs;
   
    // IShell interface
    IShell         *m_pShell;
   
    PFNSTATE     m_pfnState;     // 状态处理函数
    void *          m_pUserState;   // 用户数据
    PFNAEEEVENT m_pfnEvent;     // 事件处理函数
    void *          m_pUserEvent;   // 用户数据
       int             m_eWndRet;      // Windows返回值
       uint16          m_wWndParam;    // Windows返回值word参数
       uint32          m_dwWndParam;   // Windows返回值dword参数
      
       AEECLSID     m_dwAppClsID;   // 当前使用此接口的应用程序Class ID
       boolean         m_bSuspending; // 当前状态机是否被挂起
}CStateMachine;
       应用程序中将通过一个指针变量来保存一个这样的结构体。我们将创建的用户界面叫做一个Window。状态机使用m_pfnState来获得状态,使用m_pfnEvent来处理用户Window的事件,在m_pfnState状态函数执行的时候创建Window。
8.2.3实现状态机管理的接口
       为了能够管理状态机,我们必须提供一些管理状态机的接口:
       1、启动状态机,通过这个接口应用程序可以选择在何时启动这个状态机。
       2、终止状态机,通过这个接口应用程序可以选择在何时终止状态机的运行。
       3、挂起状态机,通过这个接口应用程序可以暂停状态机的运行,同时保存相关的状态数据,以便于恢复状态机的运行。
       4、恢复状态机,通过这个接口应用程序可以恢复一个被挂起的状态机继续运行。
       5、转移状态,通过这个接口应用程序可以转移到下一个状态去处理。
       6、创建一个窗口(Window),通过这个接口应用程序可以创建一个显示界面。
       7、关闭一个窗口(Window),通过这个接口应用程序可以关闭一个显示界面,同时设置相应的Window返回值给状态处理函数。
       8、Window事件捕获函数,应用程序将通过这个接口将事件传递给Window的事件捕获函数。
       9、辅助函数,如获得当前的状态,以及获得当前窗口的返回值等操作。
       与上面描述功能基本相近的各个函数已经定义在了FileExplorer应用程序中的AppStateMachine.c的文件中。他们的形式如下:
/*====================================================================
函数     :IStateMachine_HandleEvent
描述     :状态机的事件捕获函数,在此函数中将事件传递给在当前状态中打开的窗口
参数     :po[in] 指向当前状态机结构体的指针
          eCode[in] 当前的事件代码
          wParam[in] 事件的16位参数
          dwParam[in] 事件的32位参数
返回值 :如果捕获事件则返回TRUE,否则返回FALSE
====================================================================*/
static boolean IStateMachine_HandleEvent(IStateMachine *po,
                                         AEEEvent eCode,
                                         uint16    wParam,
                                         uint32    dwParam)
{
    CStateMachine *pMe = (CStateMachine*)po;
    return pMe->m_pfnEvent?pMe->m_pfnEvent(pMe->m_pUserEvent,eCode,wParam,dwParam):FALSE;
}// End IStateMachine_HandleEvent
 
/*====================================================================
函数     :IStateMachine_Start
描述     :开始状态机的运行,状态机开始之后可以调用ISTATEMACHINE_Stop终止状态机
          的运行,或者ISTATEMACHINE_Stop挂起状态机
参数     :po[in] 指向当前状态机结构体的指针
          pfnState[in] 启动时的状态函数,同时也是状态ID
          pUser[in] 传递给状态函数的用户数据
返回值 :无
====================================================================*/
static void IStateMachine_Start(IStateMachine *po,
                                PFNSTATE       pfnState,
                                void          *pUser)
{
    CStateMachine *pMe = (CStateMachine*)po;
    pMe->m_pfnState     = pfnState;
    pMe->m_pUserState   = pUser;
   
    if(pMe->m_pfnState){
        CStateMachine_RunFSM(pMe);
    }
}// End IStateMachine_Start
 
/*====================================================================
函数     :IStateMachine_Stop
描述     :终止状态机的运行,终止之后只能使用ISTATEMACHINE_Start重新启动状态机
参数     :po[in] 指向当前状态机结构体的指针
返回值 :无
====================================================================*/
static void IStateMachine_Stop(IStateMachine *po)
{
    CStateMachine *pMe = (CStateMachine*)po;
   
    IStateMachine_Suspend(po);
    pMe->m_pfnState = NULL;
    pMe->m_pfnEvent = NULL;
}// End IStateMachine_Stop
 
/*====================================================================
函数     :IStateMachine_Suspend
描述     :挂起当前正在运行的状态机,挂起之后使用ISTATEMACHINE_Start重新启动
          状态机,或使用ISTATEMACHINE_Resume恢复当前的状态机运行
参数     :po[in] 指向当前状态机结构体的指针
返回值 :无
====================================================================*/
static void IStateMachine_Suspend(IStateMachine *po)
{
    CStateMachine *pMe = (CStateMachine*)po;
    if(pMe->m_pfnState){
        pMe->m_bSuspending = TRUE;
        IStateMachine_CloseWnd(po,0,0,0);
    }
}// End IStateMachine_Suspend
 
/*====================================================================
函数     :IStateMachine_Resume
描述     :恢复被挂起的状态机
参数     :po[in] 指向当前状态机结构体的指针
返回值 :无
====================================================================*/
static void IStateMachine_Resume(IStateMachine *po)
{
    CStateMachine *pMe = (CStateMachine*)po;
   
    if(pMe->m_pfnState){
        pMe->m_bSuspending = FALSE;
        CStateMachine_RunFSM(pMe);
    }
}// IStateMachine_Resume
 
/*====================================================================
函数     :IStateMachine_GetState
描述     :获取状态机当前的状态
参数     :po[in] 指向当前状态机结构体的指针
返回值 :无
====================================================================*/
static void* IStateMachine_GetState(IStateMachine *po)
{
    CStateMachine *pMe = (CStateMachine*)po;
       return pMe->m_pfnState;
}// IStateMachine_GetState
 
/*====================================================================
函数     :IStateMachine_MoveTo
描述     :转移当前的状态到下一个状态
参数     :po[in] 指向当前状态机结构体的指针
          pfnState[in] 状态处理函数,也是状态的ID
          pUser[in] 状态处理函数使用的用户数据
返回值 :无
====================================================================*/
static void IStateMachine_MoveTo(IStateMachine *po, 
                                 PFNSTATE       pfnState,
                                 void          *pUser)
{
    CStateMachine *pMe = (CStateMachine*)po;
    pMe->m_pfnState     = pfnState;
    pMe->m_pUserState   = pUser;
}// End IStateMachine_MoveTo
 
/*====================================================================
函数     :IStateMachine_OpenWnd
描述     :打开一个窗口,用来显示用户界面和处理用户事件
参数     :po[in] 指向当前状态机结构体的指针
          HandleEvent[in] 窗口事件处理函数,也是窗口的ID
          pUser[in] 窗口事件处理函数使用的用户数据
返回值 :无
====================================================================*/
static void IStateMachine_OpenWnd(IStateMachine *po,
                                  PFNAEEEVENT    HandleEvent,
                                  void          *pUser)
{
    CStateMachine *pMe = (CStateMachine*)po;
    pMe->m_pfnEvent     = HandleEvent;
    pMe->m_pUserEvent   = pUser;
    (void)ISHELL_PostEvent( pMe->m_pShell,
                            pMe->m_dwAppClsID,
                            EVT_WND_OPEN,
                            0, 0);
}// End IStateMachine_OpenWnd
 
/*====================================================================
函数     :IStateMachine_CloseWnd
描述     :关闭当前的窗口
参数     :po[in] 指向当前状态机结构体的指针
          eWndRet[in] 窗口关闭时的返回值,状态处理函数通过这个值来决定下一步
                      怎么做
          wParam[in] 窗口返回值的16位参数
          dwParam[in] 窗口返回值的32位参数
返回值 :无
====================================================================*/
static void IStateMachine_CloseWnd(IStateMachine *po,
                                   int            eWndRet,
                                   uint16         wParam,
                                   uint32         dwParam)
{
    CStateMachine *pMe = (CStateMachine*)po;
   
    if(pMe->m_pfnEvent){
        (void)ISHELL_HandleEvent(pMe->m_pShell, EVT_WND_CLOSE, 0, 0);
        pMe->m_pfnEvent     = NULL;
        pMe->m_pUserEvent   = NULL;
        pMe->m_eWndRet      = eWndRet;
        pMe->m_wWndParam    = wParam;
        pMe->m_dwWndParam   = dwParam;
       
        // 状态机被挂起,等待状态机恢复或重新Start
        if(FALSE == pMe->m_bSuspending){
            CStateMachine_RunFSM(pMe);
        }
    }
}// End IStateMachine_CloseWnd
 
/*====================================================================
函数     :IStateMachine_GetWndRet
描述     :获得最近关闭窗口的返回值
参数     :po[in] 指向当前状态机结构体的指针
          *pwParam[in/out] 窗口返回值的16位参数
          *pdwParam[in/out] 窗口返回值的32位参数
返回值 :int 窗口的返回值
====================================================================*/
static int IStateMachine_GetWndRet(IStateMachine *po,
                                   uint16        *pwParam,
                                   uint32        *pdwParam)
{
    CStateMachine *pMe = (CStateMachine*)po;
       if(pwParam){
              *pwParam = pMe->m_wWndParam;
       }
 
       if(pdwParam){
              *pdwParam = pMe->m_dwWndParam;
       }
       return pMe->m_eWndRet;
}// End IStateMachine_GetWndRet
       如果我们完全使用状态机来构建一个应用程序的话,那么,我们可以在在应用程序的EVT_APP_START事件中直接调用IStateMachine_Start函数来启动状态机,在调用这个函数时需要指定初始状态;在EVT_APP_STOP事件中调用IStateMachine_Stop来终止状态机的运行;在EVT_APP_SUSPEND事件中调用IStateMachine_Suspend来挂起状态机;在EVT_APP_RESUME事件中调用IStateMachiine_Resume来恢复状态机的运行。在状态机运行的过程中,使用IStateMachine_MoveTo来转移到下一个状态,使用IStateMachine_GetState来获得当前的状态。
       在应用程序中,表示状态的是一个如下类型的函数:
typedef NextFSMAction (*PFNSTATE)(IStateMachine *po, void *pUser);
       其中,po就是传递了CStateMachine结构体的一个指针,pUser是用户在使用IStateMachine_MoveTo中指定的状态处理函数所需要传入的用户数据。在这个状态处理函数中通常判断当前的窗口(Window)返回值是什么来决定如何处理状态,这些返回值预定在下面的枚举变量中(AppStateMachine.h):
// 窗口返回值
enum{
    WNDRET_CREATE = 0,
    WNDRET_OK,
    WNDRET_CANCELED,
    WNDRET_YES,
    WNDRET_NO,
    WNDRET_USER1,
    WNDRET_USER2,
    WNDRET_USER3,
    WNDRET_USER4,
    WNDRET_USER5,
    WNDRET_MAX
};
       WNDRET_CREATE表示用户要在这个状态中创建一个窗口(Window),此时应该调用IStateMachine_OpenWnd接口打开一个窗口(Window)。调用此接口的时候,窗口(Window)的事件处理函数将接收到一个EVT_WND_OPEN事件。关闭窗口(Window)的动作是由窗口(Window)本身通过调用IStateMachine_Close来完成的,在这个接口执行的过程中将向窗口(Window)事件处理函数发送EVT_WND_CLOSE事件,同时重新启动状态机。启动状态机的时候将再次执行当前的状态处理函数,所不同的是此时执行状态处理函数的时候,窗口(Window)的返回值不同了(在关闭窗口的时候重新替换成了上面的一些定义)。此时根据窗口的返回值决定如何操作。
       到此为止您可能有这样的一个疑问,除了CStateMachine外,里面还有一个IStateMachine类型是做什么的呢?在这里呢,我先不说,具体的情况将会在第三篇扩展BREW接口中谈到。当然,有兴趣的读者可以自己试着将AppStateMachine.c和AppStateMachine.h两个文件通读一遍,提前研究一下有好处。在这里唯一要说明的是,要想使用StateMachine的接口函数,需要调用AppStateMachine_New函数来获得IStateMachine的接口指针。
8.3 使用应用程序框架构建应用
       在上面一节当中,我们已经介绍了状态机相关的管理接口,现在我们就要用上面的接口来构建FileExplorer应用程序的内容了。本章第一节当中我们已经建立了这个应用程序,这一节将主要讲解如何使用应用程序框架来构建我们的应用程序。
8.3.1文件浏览器应用程序的启动代码
       文件浏览器要完成的主要功能是使用菜单控件枚举出在fs:/shared路径下的文件,当选择这些文件的时候可以查看文件的信息,包括大小和创建时间。为此,我们在这个应用程序中使用了一个BREW接口IFileMgr和一个控件IMenuCtl。首先,我们要做的事情就是在应用程序的结构体中增加相应的变量。完整的应用程序的结构体变成如下样式:
typedef struct _FileExplorer {
    AEEApplet      a ;          // First element must be AEEApplet
    AEEDeviceInfo DeviceInfo; // Hardware device information
    AEERect        m_myRC;      // Applet的矩形框
   
    IStateMachine *m_pStateMachine; // 状态机实例指针
    IFileMgr      *m_pFileMgr;      // 文件管理接口实例指针
    IMenuCtl      *m_pMenuList;     // 菜单控件接口实例指针
 
    FileInfo       m_pFileInfo;   // 保存所选文件信息
} FileExplorer;
       然后在FileExplorer_InitAppData函数中初始化这些变量:
static boolean FileExplorer_InitAppData(FileExplorer* pMe)
{
    int nRet;
   
    pMe->DeviceInfo.wStructSize = sizeof(pMe->DeviceInfo);
    ISHELL_GetDeviceInfo(pMe->a.m_pIShell,&pMe->DeviceInfo);
    pMe->m_myRC.dx = pMe->DeviceInfo.cxScreen;
    pMe->m_myRC.dy = pMe->DeviceInfo.cyScreen;
   
    // 获得状态机实例
    nRet = AppStateMachine_New(pMe->a.m_pIShell,&pMe->m_pStateMachine);
    if(nRet != SUCCESS){
        return FALSE;
    }
   
    // 创建文件管理接口
    nRet = ISHELL_CreateInstance( pMe->a.m_pIShell,
                                  AEECLSID_FILEMGR,
                                 &pMe->m_pFileMgr);
    if(nRet != SUCCESS){
        return FALSE;
    }
   
    // 创建一个菜单控件
    nRet = ISHELL_CreateInstance( pMe->a.m_pIShell,
                                  AEECLSID_MENUCTL,
                                 &pMe->m_pMenuList);
    if(nRet != SUCCESS){
        return FALSE;
    }
     return TRUE;
}// End FileExplorer_InitAppData
       为了保证分配和释放接口的对等关系,我们需要在FileExplorer_FreeAppData函数中释放创建的接口实例:
void FileExplorer_FreeAppData(FileExplorer* pMe)
{
    if(pMe->m_pStateMachine){
        ISTATEMACHINE_Release(pMe->m_pStateMachine);
    }
   
    if(pMe->m_pFileMgr){
        IFILEMGR_Release(pMe->m_pFileMgr);
    }
   
    if(pMe->m_pMenuList){
        IMENUCTL_Release(pMe->m_pMenuList);
    }
}// End FileExplorer_FreeAppData
       之后的主要任务是修改应用程序的事件捕获函数为如下样式:
static boolean FileExplorer_HandleEvent(FileExplorer *pMe,
                                        AEEEvent      eCode,
                                        uint16        wParam,
                                        uint32        dwParam)
{
    // 首先将事件传递给状态机
    if(ISTATEMACHINE_HandleEvent(pMe->m_pStateMachine,eCode,wParam,dwParam)){
        // 状态机捕获了事件,直接返回
        return TRUE;
    }
   
    switch (eCode)
    {
        case EVT_APP_START:
            // 启动状态机
            ISTATEMACHINE_Start(pMe->m_pStateMachine, FE_STATE_INIT, pMe);
            return(TRUE);
 
        case EVT_APP_STOP:
            // 停止状态机
            ISTATEMACHINE_Stop(pMe->m_pStateMachine);
            return(TRUE);
 
        case EVT_APP_SUSPEND:
            // 挂起状态机
            ISTATEMACHINE_Suspend(pMe->m_pStateMachine);
            return(TRUE);
 
        case EVT_APP_RESUME:
            // 恢复状态机运行
            ISTATEMACHINE_Resume(pMe->m_pStateMachine);
            return(TRUE);
 
        default:
            break;
    }
   
    return FALSE;
}// End FileExplorer_HandleEvent
       从这个函数中我们可以看到事件将首先传递给应用程序框架。而且在EVT_APP_START事件中,我们启动的状态是FE_STATE_INIT。这个状态处理函数如下:
static NextFSMAction FE_STATE_INIT(IStateMachine *po, FileExplorer* pMe)
{
    ISTATEMACHINE_MoveTo(po,FE_STATE_SHOWLIST,pMe);
    return NFSMACTION_CONTINUE;
}// End FE_STATE_INIT
       从这个状态开始,我们就开始正式的进入了状态机运行阶段。我们也可以看到,目前这个状态所做的就是转移到FE_STATE_SHOWLIST状态。我们可以在这个状态中做一些使用状态机所必需的初始化操作。当然,当前我们的应用程序很简单,因此没有作什么具体的处理。
8.3.2文件浏览器应用程序的文件列表状态
       状态机运行之后,就会从初始化状态(FE_STATE_INIT)转移到文件列表状态(FE_STATE_SHOWLIST)。文件列表状态的处理内容如下:
static NextFSMAction FE_STATE_SHOWLIST(IStateMachine *po, FileExplorer* pMe)
{
    switch(ISTATEMACHINE_GetWndRet(po, NULL, NULL)){
    case WNDRET_CREATE:
        // 打开WndShowList_HandleEvent窗口,显示界面,状态机进入等待状态
        ISTATEMACHINE_OpenWnd(po,WndShowList_HandleEvent,pMe);
        return NFSMACTION_WAIT;
 
    case WNDRET_OK:
        // 转移状态到FE_STATE_DRAWINFO
        ISTATEMACHINE_MoveTo(po,FE_STATE_DRAWINFO,pMe);
        break;
       
    case WNDRET_CANCELED:
        // 转移状态到FE_STATE_EXIT
        ISTATEMACHINE_MoveTo(po,FE_STATE_EXIT,pMe);
        break;
 
    default:
        break;
    }
   
    return NFSMACTION_CONTINUE;
}// End FE_STATE_SHOWLIST
       在处理状态的WNDRET_CREATE窗口(Window)返回值时,我们创建了一个窗口(Window)WndShowList_HandleEvent,同时返回NFSMACTION_WAIT告诉应用程序框架状态机应处于等待状态直至窗口(Window)关闭。由于WNDRET_CREATE是进入状态时默认的值,因此进入这个状态之后就会创建文件类表窗口。在WndShowList_HandleEvent函数中将进行具体的窗口事件处理:
static boolean WndShowList_HandleEvent(FileExplorer *pMe,
                                       AEEEvent      eCode,
                                       uint16        wParam,
                                       uint32        dwParam)
{
    // 将事件传递给菜单控件
    if(IMENUCTL_HandleEvent(pMe->m_pMenuList, eCode, wParam, dwParam)){
        // 菜单控件捕获了事件,直接返回
        return TRUE;
    }
   
    switch (eCode){
    case EVT_WND_OPEN: // 打开窗口事件
    {
        int    nItemID = 1; // 菜单的起始ItemID
        // 菜单标题“Shared Files”
        AECHAR wTitle[] = {'S','h','a','r','e','d',
                           ' ','F','i','l','e','s','/0'};
       
        // 设置菜单控件的显示方式
        IMENUCTL_SetRect(pMe->m_pMenuList, &pMe->m_myRC);
        IMENUCTL_SetTitle(pMe->m_pMenuList, NULL, NULL, wTitle);
        IMENUCTL_SetProperties(pMe->m_pMenuList,MP_UNDERLINE_TITLE);
       
        // 枚举文件夹中的文件
        (void)IFILEMGR_EnumInit(pMe->m_pFileMgr, AEEFS_SHARED_DIR, FALSE);
        for(;;){
            FileInfo myFileInfo;
            AECHAR   pszFile[AEE_MAX_FILE_NAME];
           
            // 枚举一个文件
            if(!IFILEMGR_EnumNext(pMe->m_pFileMgr, &myFileInfo)){
                // 枚举失败,证明已经枚举完成,跳出循环
                break;
            }
           
            // 将文件名加入菜单控件
            STRTOWSTR(myFileInfo.szName, pszFile, sizeof(pszFile));
            IMENUCTL_AddItem( pMe->m_pMenuList,
                              NULL, NULL, nItemID++,
                              pszFile, 0);
        }
       
        // 激活并重绘菜单控件
        IMENUCTL_SetActive(pMe->m_pMenuList, TRUE);
        IMENUCTL_Redraw(pMe->m_pMenuList);
        return(TRUE);
    }
   
    case EVT_WND_CLOSE: // 关闭窗口事件
        // 删除菜单控件中的全部条目并停用菜单控件
        IMENUCTL_DeleteAll(pMe->m_pMenuList);
        IMENUCTL_SetActive(pMe->m_pMenuList, FALSE);
        return(TRUE);
 
    case EVT_KEY:
        switch(wParam){
        case AVK_CLR:
        case AVK_SOFT2:
            // 关闭当前窗口并返回取消状态给状态机处理
            ISTATEMACHINE_CloseWnd(pMe->m_pStateMachine, WNDRET_CANCELED,0,0);
            return TRUE;
        default:
            break;
        }
        break;
   
    case EVT_COMMAND:
    {
        CtlAddItem itemInfo;
        char   pszFile[AEE_MAX_FILE_NAME];
       
        // 获得当前选中文件信息,并存储到pMe->m_pFileInfo中去
        IMENUCTL_GetItem(pMe->m_pMenuList, wParam, &itemInfo);
        WSTRTOSTR(itemInfo.pText, pszFile, sizeof(pszFile));
        IFILEMGR_GetInfo(pMe->m_pFileMgr, pszFile, &pMe->m_pFileInfo);
       
        // 关闭当前窗口,同时返回OK状态给状态机处理
        ISTATEMACHINE_CloseWnd(pMe->m_pStateMachine, WNDRET_OK, 0, 0);
        return TRUE;
    }
   
    default:
        break;
    }
   
    return FALSE;
}// End WndShowList_HandleEvent
       EVT_WND_OPEN事件时应用程序框架打开一个窗口时发送给应用程序的,应用程序将把这个事件传递给应用程序框架,然后,应用程序框架再将事件传递给WndShowList_HandleEvent函数。我们可以看见,在这个事件中,应用程序设置了菜单控件的属性,同时将枚举出来的文件添加到菜单控件中去。然后刷新界面,显示出文件列表。
       现在,应用程序将停留在这个界面上等待用户的输入。具体的,当我们按返回键或者软键2的时候将关闭当前的窗口,并将窗口的返回值指定为WNDRET_CANCELED。同时应用程序将接收到EVT_WND_CLOSE事件,在这个事件中我们清理了菜单控件的内容。关闭窗口之后将重新启动状态机运行。此时,我们可以看到FE_STATE_SHOWLIST状态中处理WNDRET_CANCELED的方法是进入FE_STATE_EXIT状态:
static NextFSMAction FE_STATE_EXIT(IStateMachine *po, FileExplorer* pMe)
{
    // 关闭应用程序,同时状态机进入等待状态
    ISHELL_CloseApplet(pMe->a.m_pIShell, FALSE);
    return NFSMACTION_WAIT;
}// End FE_STATE_EXIT
       这个状态就是简单的关闭当前应用程序就完事了。
当我们在WndShowList_HandleEvent选择一个文件的时候,菜单控件将发送一个EVT_COMMAND事件给应用程序,这个事件的wParam参数代表了选中的条目,我们根据这个信息获得了我们所选的文件名。同时,根据这个文件名,我们将获得所选的文件信息,并保存在应用程序结构体的m_pFileInfo变量中。调用ISTATEMACHINE_CloseWnd关闭当前窗口,并设置返回状态为WNDRET_OK,在FE_STATE_SHOWLIST状态中,处理这个返回值的方式是进入FE_STATE_DRAWINFO状态。
8.3.3文件浏览器应用程序的文件信息状态
       FE_STATE_DRAWINFO状态的处理函数如下:
static NextFSMAction FE_STATE_DRAWINFO(IStateMachine *po, FileExplorer* pMe)
{
    switch(ISTATEMACHINE_GetWndRet(po, NULL, NULL)){
    case WNDRET_CREATE:
        // 打开WndDrawInfo_HandleEvent窗口,显示界面,状态机进入等待状态
        ISTATEMACHINE_OpenWnd(po,WndDrawInfo_HandleEvent,pMe);
        return NFSMACTION_WAIT;
 
    case WNDRET_OK:
        // 转移状态到FE_STATE_SHOWLIST
        ISTATEMACHINE_MoveTo(po,FE_STATE_SHOWLIST,pMe);
        break;
 
    default:
        break;
    }
   
    return NFSMACTION_CONTINUE;
}// End FE_STATE_DRAWINFO
       在处理WNDRET_CREATE返回值的时候,创建了WndDrawInfo_HandleEvent窗口:
static boolean WndDrawInfo_HandleEvent(FileExplorer *pMe,
                                       AEEEvent      eCode,
                                       uint16        wParam,
                                       uint32        dwParam)
{
    switch (eCode){
    case EVT_WND_OPEN: // 打开窗口事件,使用pMe->m_pFileInfo中的信息进行显示
    {
        char FileName[] = "Name: %s";
        char FileSize[] = "Size: %d Bytes";
        char FIleDate[] = "Date: %04d.%02d.%02d %02d:%02d:%02d";
        char DestBuf[AEE_MAX_FILE_NAME+10];
        AECHAR StrBuf[AEE_MAX_FILE_NAME+10];
        JulianType myFileDate;
       
        IDISPLAY_ClearScreen(pMe->a.m_pIDisplay);
       
        // Draw file name
        SPRINTF(DestBuf, FileName, pMe->m_pFileInfo.szName);
        STRTOWSTR(DestBuf, StrBuf, sizeof(StrBuf));
        IDISPLAY_DrawText(pMe->a.m_pIDisplay ,0,StrBuf,-1,0,0,NULL,0);
 
        // Draw file size
        SPRINTF(DestBuf, FileSize, pMe->m_pFileInfo.dwSize);
        STRTOWSTR(DestBuf, StrBuf, sizeof(StrBuf));
        IDISPLAY_DrawText(pMe->a.m_pIDisplay ,0,StrBuf,-1,0,20,NULL,0);
       
        // Draw date
        GETJULIANDATE(pMe->m_pFileInfo.dwCreationDate, &myFileDate);
        SPRINTF(DestBuf, FIleDate,
                myFileDate.wYear, myFileDate.wMonth, myFileDate.wDay,
                myFileDate.wHour, myFileDate.wMinute, myFileDate.wSecond);
        STRTOWSTR(DestBuf, StrBuf, sizeof(StrBuf));
        IDISPLAY_DrawText(pMe->a.m_pIDisplay ,0,StrBuf,-1,0,40,NULL,0);
        IDISPLAY_Update(pMe->a.m_pIDisplay);
        return(TRUE);
    }
 
    case EVT_WND_CLOSE: // 关闭窗口事件
        return(TRUE);
 
    case EVT_KEY:
        // 按任意键返回OK给状态机处理
        ISTATEMACHINE_CloseWnd(pMe->m_pStateMachine, WNDRET_OK, 0, 0);
        return(TRUE);
       
    default:
        break;
    }
   
    return FALSE;
}// End WndDrawInfo_HandleEvent
       在EVT_WND_OPEN事件中,我们在屏幕上依次描绘了文件名、尺寸和创建日期。无论接收到任何按键事件都将关闭当前的窗口,并设置返回值为WNDRET_OK。在FE_STATE_DRAWINFO状态中处理WNDRET_OK的方式是返回FE_STATE_SHOWLIST状态。
8.4 小结
       在这一章里,我们熟悉了BREW处理事件的方式,以及主要需要处理哪些事件。我们了解了系统事件和用户事件之间的区别,初步理解了BREW的事件驱动模型。更为重要的是,根据这个模型,我们构建了一个通用的应用程序框架,通过这个框架,我们可以简化复杂应用程序的开发。需要各位读者了解的是,这个应用程序框架采用了一个有限循环状态机。通过对这个状态机的理解,将有助于我们加深对BREW的认识。
1、在一个状态中创建了一个窗口,然后再窗口事件处理函数中关闭了这个窗口,并返回了WNDRET_OK,问,在这个返回值的处理中还可以创建窗口吗?也就是一个状态处理函数之中可以多次创建窗口吗?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值