Avkon视图切换架构之调试小结
----Symbian OS传统程序架构续
在我看来,理解了传统的 Symbian OS程序架构可以轻松的帮助我们理解Avkon视图切换架构,因为我们可以事先理解一些看起来比较抽象的概念,比如容器,窗口,复合控件等,了解一下最基本的Symbian程序框架。实际上,Avkon视图切换架构无非就是在传统的Symbian程序架构上做的一种扩展而已。最大的区别就是多了一个视图类,即继承自CAvkonView类的自定义View。
    在SDK里,有一个Avkon视图切换的例子MultiViews,我看了一下,这个例子比较非常简单,框架非常清晰,很适合初学者,不过我感觉仅仅研究这个例子的实际意义并不大,因此本文并不针对这个例子,而是我们通过向导创建S60程序时,向导为我们提供的框架即S60 View based application。这个框架提供的架构要比例子MultiViews稍微复杂一点,因为多了一个新的概念,即将面板应用到了程序中,我将这个演示程序取名为MultiViewsTest,因为仅为了演示学习用,所以我基本上没有对框架提供的代码做修改。
    通过这个例子MultiViewsTest学习Avkon视图切换架构,重点无非放在MultiViewsTestContainer.cpp、MultiViewsTestView.cpp、MultiViewsTestAppUi.cpp这三个源程序文件,以及它们中定义的类的成员函数上,当然因为有了面板(StatusPane),我们也会在这里对面板的相关知识作一小记。
    在做这个例子的时候,我采取的办法是,先删除上面三个源程序文件的内容,然后自己根据它们对应的头文件,自己编写函数代码,自己确定需要引入的头文件,因为我认为光看代码不去亲自调试对新手来说,是不会有太多提高的。在我写代码的过程中,因为不仔细、记错了或不理解等各种原因,出现了不少错误,因此,我也会在这里将这些错误记录下来,并做一下错误分析,希望各位同仁不再犯我所经历过的错误。
一、
    在这三个文件里,MultiViewsTestContainer.cpp和传统的Symbian程序架构中的Container文件可以说完全相同,这个容器中也是只有两个Label子控件,因此我们就从这个最简单的开始。
    在文件MultiViewsTestContainer.cpp中我们定义了它所对应的头文件MultiViewsTestContainer.h中定义的容器类 CmultiViewsTestContainer的成员函数的具体实现。在编代码的时候,出了几个小错误,摘抄如下:
void CMultiViewsTestContainer::ConstructL(const TRect& aRect)
{
     CreateWindowL();//忘了"L",嘿嘿
 
     iLabel = new(ELeave)CEikLabel;
     iLabel->SetContainerWindowL(*this);
     iLabel->SetTextL(_L("Container_Label_1"));
 
     iToDoLabel = new(ELeave)CEikLabel;
     iToDoLabel->SetContainerWindowL(*this);
     iToDoLabel->SetTextL(_L("Container_Label_2"));
 
     SetRect(aRect);//设置窗口范围
     ActivateL();//忘记了加"L",嘿嘿
}
――――――――――――――――――――――――――――――――――――――――――――――
//设置标签的显示位置和大小
void CMultiViewsTestContainer::SizeChanged()
{
     iLabel->SetExtent(TPoint(10,10),iLabel->MinimumSize());//这里的SetExtent()方法,没有L
     iToDoLabel->SetExtent(TPoint(10,100),iToDoLabel->MinimumSize());
}
――――――――――――――――――――――――――――――――――――――――――――――
void CMultiViewsTestContainer::Draw(const TRect& aRect) const
{
     CWindowGc& gc=SystemGc();//SymtemGc()是类CCoeControl的方法,获取图形上下文
     gc.SetBrushColor(KRgbRed);
     gc.SetBrushStyle( CGraphicsContext::ESolidBrush );
     gc.DrawRect( aRect );
}
――――――――――――――――――――――――――――――――――――――――――――――
 
在编这个文件的时候,没有碰到大的障碍,因为这里的这个容器类和传统架构中的容器类是一摸一样的,呵呵,但是还是出了几个小错误。
1、 Symbian中编码和其他C++编码不太一样,对于可能产生异常的函数,名字都会以“L”来结尾,因此往往会搞混的就是,这些常用函数中,哪个有L,哪个没有L,在上面我就搞混了,因此对于这些常用的函数,我们应该熟记。
再写一遍:
CreateWindowL(); ActivateL(); SetExtent();
2、 忘了绘图函数 Draw()中,定义图形上下文的类,及产生图形上下文实例的方法:
CwindowGc& = SystemGc();
当然还不能忘了该函数的最后一步,就是绘制区域(注意绘制的不一定非要是整个窗口,通过参数 aRect具体确定):gc.DrawRect(aRect);
 
二、自定义的 继承自CAknView类的视图类CmultiViewsTestView
这个类我们在 MultiViewsTestView.cpp中定义具体实现,这个视图类实际上起到在Container和AppUi之间的一个桥梁作用,AppUi不在操作Container,而是通过View间接操作。当然这个Container要作为View类的一个private成员,即:
private:
    CmultiViewsTestContainer* iContainer;
 
当然,关于 iContainer所指向的容器实例的创建和释放也要在这个View类中来完成。关键就是在View类的哪个函数中编写创建和释放的具体代码呢?这就是在
void CMultiViewsTestView::DoActivateL(const TVwsViewId& aPrevViewId,TUid aCustomMessageId,
const TDesC8& aCustomMessage)编写创建代码,在
void CMultiViewsTestView::DoDeactivate()中编写释放代码。
 
一定要注意,不要在 View类的ConstructL()方法中创建iContainer,但可以在View类的析构函数中释放iContainer(只不过这和DoDeactivate()方法中的代码重复了)。那么可能有人会问,那么构造函数和析构函数中定义什么呢?它们中的代码定义如下:
#include < MultiViewsTest.rsg >
void CMultiViewsTestView::ConstructL()
{
     BaseConstructL(R_MULTIVIEWSTEST_VIEW1);//将资源文件中定义的视图资源传入
}
――――――――――――――――――――――――――――――――――――――――――――――
CMultiViewsTestView::~CMultiViewsTestView()
{
     if(iContainer!=NULL)
     {
     //不是RemoveFromStackL(iContainer),而是AppUi()->RemoveFromViewStack( *this, iContainer )
     //IMPORT_C CAknViewAppUi* CAknView::AppUi() const [protected]
          AppUi()->RemoveFromViewStack( *this, iContainer );
         delete iContainer;
          iContainer=NULL;
     }
}
分析:
1、      ConstructL()方法中仅仅创建了一个View的基本框架,通过方法BaseConstructL(R_MULTIVIEWSTEST_VIEW1),其中参数R_MULTIVIEWSTEST_VIEW1是在.rss资源文件中定义的一个View资源名。注意:为此我们需要将资源文件#include,但我们引入的不能是MultiViewsTest.rss,而是MultiViewsTest.rsg,在我们编译程序的时候,编译器会将.rss编译出一个资源索引文件.rsg,并把该.rsg文件放在系统的头文件include目录中,也可能放在项目的Group目录中。
2、      可以看到析构函数中,我们释放了 iContainer,实际上这是和DoDeactivate()函数中的定义重复的。这句代码 AppUi()->RemoveFromViewStack( *this, iContainer );其中 AppUi()是继承自类CAknView中的成员函数,可以获得这个View所对应的ViewAppUi的指针(即程序中的AppUi类的对象指针), RemoveFromViewStack( *this, iContainer );从 View的Stack中将这个iContainer移除。
 
再看 DoActivateL()和DoDeActivate()的代码:
DoActivateL():构造容器,并绘制窗口
 
void CMultiViewsTestView::DoActivateL(const TVwsViewId& aPrevViewId,TUid aCustomMessageId,
            const TDesC8& aCustomMessage)
{
     iContainer = new(ELeave)CMultiViewsTestContainer;
     iContainer->SetMopParent(this);
     iContainer->ConstructL(ClientRect());//创建并显示容器内容
     AppUi()->AddToStackL(*this,iContainer);//将容器推入栈顶
}
―――――――――――――――――――――――――――――――――――――――――――――
DoDeActivate():销毁容器对象,跟View类的析构函数功能类似。
 
void CMultiViewsTestView::DoDeactivate()
{
     if(iContainer!=NULL)
     {
          //RemoveFromStackL(iContainer); //AppUi()->RemoveFromViewStack( *this, iContainer );
          AppUi()->RemoveFromViewStack( *this, iContainer );
         delete iContainer;
          iContainer=NULL;
     }
}
分析:
1、 在 DoActivate()方法中,主要是创建容器iContainer和将iContainer推入栈顶,以便接收用户事件。创建之所以比较复杂,是因为我没有在Container类中定义NewL()和NewLC()方法。所以显得稍微复杂。
2、 可以看到DoDeactivate()方法中的代码和View类的析构函数代码是一样的。
 
问题:有点疑问就是容器入栈的时候用的是 AddToStackL()方法,而出栈的时候却是RemoveFromViewStack()方法,为什么不匹配呢?
据 xiaobai网友验证,改为AppUi()->RemoveFromStack()后,程序也没有错误,我的理解是:View的Stack和程序的Stack是相通的,只是个人的猜想,不正确的话,还请高手指教,谢谢。
除了上面的四个方法,我们继续看 View类所特有的其他方法:
 
TUid CMultiViewsTestView::Id() const
{
     return KViewId;
}
在这里 KviewId是我们事先定义的这个View的UID,即const TUid KViewId = {1};格式是大括号“{ }”包含的1,而不是直接用1初始化。
View类必须包含一个Id()函数,从而系统可以标志这个类。
 
―――――――――――――――――――――――――――――――――――――――
视图 View可以有自己的菜单资源,当然这也需要在.rss文件中进行定义,格式如下:
 
RESOURCE AVKON_VIEW r_multiviewstest_view1
    {
    hotkeys = r_multiviewstest_hotkeys;
    menubar = r_multiviewstest_menubar_view1; 
    cba     = R_AVKON_SOFTKEYS_SELECTION_LIST;   
    }
 
RESOURCE MENU_BAR r_multiviewstest_menubar_view1
    {
    titles =
        {
        MENU_TITLE { menu_pane = r_multiviewstest_app_menu; txt = "App"; },
        MENU_TITLE { menu_pane = r_multiviewstest_view1_menu; txt = "View"; }
        };
    }
 
RESOURCE MENU_PANE r_multiviewstest_view1_menu
    {
    items =
        {
        MENU_ITEM { command = EMultiViewsTestCmdAppTest; txt = qtn_view1_option_item; }
        };
}
 
RESOURCE MENU_PANE r_multiviewstest_app_menu
    {
    items =
        {
        MENU_ITEM { command = EMultiViewsTestCmdAppTest; txt = qtn_appl_option_item; },
        MENU_ITEM { command = EAknCmdExit; txt = qtn_appl_exit; }
        };
}
――――――――――――――――――――――――――――――――――――――――――――――
下面是 View中的HandCommandL()方法和AppUi中的HandCommandL()方法:
void CMultiViewsTestAppUi::HandleCommandL(TInt aCommand)
{
     switch(aCommand)
     {
         case EEikCmdExit:
              Exit();
              break;
         case EMultiViewsTestCmdAppTest:
              iEikonEnv->InfoMsg(_L("test"));
            break;
          default:
              break;
     }
}
 
void CMultiViewsTestView::HandleCommandL(TInt aCommand)
{
     switch ( aCommand )
    {
        case EAknSoftkeyOk:
            {
            iEikonEnv->InfoMsg( _L("view1 ok") );
            break;
            }
        case EAknSoftkeyBack:
            {
            AppUi()->HandleCommandL(EEikCmdExit);
            break;
            }
        default:
            {
            AppUi()->HandleCommandL( aCommand );
            break;
            }
     }
}
在上面的代码中,出现了 4个菜单命令事件EEikCmdExit、EMultiViewsTestCmdAppTest、EAknSoftkeyOk、EAknSoftkeyBack。
那么其中哪些是自定义的,哪些又是系统定义的呢?
这里只有命令 EmultiViewsTestCmdAppTest是自己在.hrh文件中定义的。如下:
enum TMultiViewsTestCommandIds
{
    EMultiViewsTestCmdAppTest = 1
};
 
通过查看查看 View的HandCommandL()方法会发现,它只处理自己的一个菜单命令EaknSoftkeyOk,而其他不属于自己视图所有的菜单的命令去调用AppUi里的HandCommandL()方法。这样做的好处是,实现了代码的公用,也就是说,如果有多个视图,并且多个视图都有相同的命令的话,这时候,我们就可以将菜单命令分为两类:一类是各个View所特有的菜单命令,另一类是每个View都公有的。View特有的命令在自己类的HandCommandL()中定义执行操作,而公有的菜单命令,则可以放到AppUi的HandCommandL()里面去定义执行操作。
―――――――――――――――――――――――――――――――――――――――
在 View类中还有一些其他的成员函数:如:
//用户区大小改变时响应
void CMultiViewsTestView::HandleClientRectChange()
{
     if(iContainer) //即iContainer!=NULL
     {
          iContainer->SetRect(ClientRect());
     }
}
这个函数应该是一个回调函数,用在用户区域大小改变时,不过,我没有在 SDK中找到这个函数,希望知道的朋友告知这个函数是在哪里定义的。
当然向这样类似功能的函数肯定还有不少,根据我们的程序不同,所采用的肯定不同,需要我们日后多多积累。在这里只是想介绍简单的 View程序架构,所以向这样功能的函数,也就不作过多的解释了。
―――――――――――――――――――――――――――――――――――――――
 
三、 View架构的AppUi
好了,现在来看稍微有点麻烦的 AppUi类,即CMultiViewsTestAppUi。实际上,如果本例中不采用面板StatusPane的话,这个AppUi还是比较简单的,因此,这里所谓的麻烦,主要还是关于StatusPane的,通过这个小例子,在理解View程序架构的同时,我们还可以对StatusPane有个简单的认识,呵呵。
 
先需要注意的一点不同就是,程序中用到的 View类,不需要作为AppUi类的私有成员,这个和传统架构有点不同,Container需要作为AppUi的私有成员,在这里是Container作为了View的私有成员,需要注意,我们只需在AppUi的ConstructL()中,创建View,并把它们添加到View服务器中,然后设置一个默认显示的视图即可:
void CMultiViewsTestAppUi::ConstructL()
{
     BaseConstructL();
    
     //创建一个状态面板指针,一个创建另一个
     CEikStatusPane* sp = StatusPane(); //CEikStatusPane类名忘了e,嘿嘿
 
     iNaviPane = (CAknNavigationControlContainer*)sp->ControlL(
        TUid::Uid(EEikStatusPaneUidNavi));
 
     iDecoratedTabGroup = iNaviPane->ResourceDecorator();
 
     if (iDecoratedTabGroup)
    {
          iTabGroup = (CAknTabGroup*) iDecoratedTabGroup->DecoratedControl();
          iTabGroup->SetObserver( this );
     }
    
//将CMultiViewsTestView对象的二阶段构造代码放在UI里面了,最好在CMultiViewsTestView类里。
     CMultiViewsTestView* view1 = new (ELeave) CMultiViewsTestView;
     CleanupStack::PushL(view1);
     view1->ConstructL();
     AddViewL(view1);
     CleanupStack::Pop(view1); //这个Pop()方法没有"L",但上面的PushL()方法有"L"
 
     CMultiViewsTestView2* view2 = new(ELeave) CMultiViewsTestView2;
     CleanupStack::PushL(view2);
     view2->ConstructL();
     AddViewL(view2);
     CleanupStack::Pop(view2);
    
     SetDefaultViewL(*view1);//SetDefaultViewL()方法忘了"L",嘿嘿
}
上面的红色部分,即为所需代码,剩余的是关于面板StatusPane的了。
―――――――――――――――――――――――――――――――――――――――
下面来看有关面板 StatusPane的代码:
为了在程序中使用面板,我们需要在 .rss文件中定义面板资源,相关定义如下:
RESOURCE STATUS_PANE_APP_MODEL r_multiviewstest_status_pane
    {
     panes =
         {
          SPANE_PANE
              {
              id = EEikStatusPaneUidNavi;
              type = EAknCtNaviPane;
              resource = r_multiviewstest_navi_decorator;
              }
         };
    }
 
//    r_multiviewstest_navi_decorator
RESOURCE NAVI_DECORATOR r_multiviewstest_navi_decorator
    {
    type = ENaviDecoratorControlTabGroup;
    control = TAB_GROUP
         {
          tab_width = EAknTabWidthWithTwoTabs; // two tabs
         active = 0;
         tabs = {
              TAB
              {
                id = EMultiViewsTestView1Tab; // from application hrh
                txt = qtn_view1_tab;
              },
              TAB
              {
                id = EMultiViewsTestView2Tab;
                txt = qtn_view2_tab;
              }
              };
         };
     }
这是面板资源的定义格式,其中两个面板的 id,即EMultiViewsTestView1Tab和EMultiViewsTestView2Tab需要在.hrh文件中定义,即如下:
enum TMultiViewsTestTabViewId
{
    EMultiViewsTestView1Tab= 1,
    EMultiViewsTestView2Tab
};
――――――――――――――――――――――――――――――――――――――――――――――
有了上面的前提之后,我们就来看具体的 AppUi中有关StatusPane的源程序文件:
先需要给 AppUi类定义三个private成员,如下:
private :
    CAknNavigationControlContainer* iNaviPane;
    CAknTabGroup*                   iTabGroup;
CAknNavigationDecorator*        iDecoratedTabGroup;
 
为了在程序中使用 StatusPane,我们需要定义上面三个对象指针,其中:
1、   CAknNavigationControlContainer:
2、   CAknTabGroup:面板的函数集
3、   CAknNavigationDecorator:
下面是 AppUi类的ConstructL()中初始化这些指针的代码(这里只截取了部分ConstructL()和StatusPane有关的代码):
void CMultiViewsTestAppUi::ConstructL()
{
     CEikStatusPane* sp = StatusPane(); //CEikStatusPane类名忘了e,嘿嘿
    
     iNaviPane = (CAknNavigationControlContainer*)sp->ControlL(
        TUid::Uid(EEikStatusPaneUidNavi));
 
     iDecoratedTabGroup = iNaviPane->ResourceDecorator();
 
     if (iDecoratedTabGroup)
    {
          iTabGroup = (CAknTabGroup*) iDecoratedTabGroup->DecoratedControl();
          iTabGroup->SetObserver( this );
     }
}
从上面的代码,可以看出,为了使用状态面板,我们必须要用用到除面板类CEikStatusPane之外的一些其他类,(而不是简单的只定义一个CEikStatusPane就可以了,需要注意,这里的CEikStatusPane并没有作为AppUi类的成员存在。),并且这几个指针的创建都是顺序的,即先创建了sp之后,再依次创建其他指针,最终是为了创建iTabGroup。
―――――――――――――――――――――――――――――――――――――――
看一下为了使用 StatusPane需要用到的AppUi的成员函数:
TKeyResponse CMultiViewsTestAppUi::HandleKeyEventL(
            const TKeyEvent& aKeyEvent,TEventCode aType)
{
     if(iTabGroup == NULL)
     {
         return EKeyWasNotConsumed;
     }
     //如果按下的是左方向键或右方向键
     if(aKeyEvent.iCode==EKeyLeftArrow || aKeyEvent.iCode == EKeyRightArrow)
     {
         return iTabGroup->OfferKeyEventL(aKeyEvent, aType);
     }
     else
     {
         return EKeyWasNotConsumed;
     }
}
 
//这是一个回调函数,定义在接口MAknTabObserver中,需要继承
void CMultiViewsTestAppUi::TabChangedL(TInt aIndex)
{
     ActivateLocalViewL(TUid::Uid(iTabGroup->TabIdFromIndex(aIndex)));//激活视图
}
―――――――――――――――――――――――――――――――――――――――
最后就是 AppUi类的析构函数,因为程序中的View,并不是该类的成员,所以析构函数中,无需销毁视图类,只需销毁该类中为类成员分配了堆内存的内存即可。所以代码如下:
CMultiViewsTestAppUi::~CMultiViewsTestAppUi()
{
     delete iDecoratedTabGroup;
}
查看析构函数,非常简单,仅仅释放了 iDecoratedTabGroup。那么视图对象在什么地方销毁的呢?如果不通过我们自己销毁的话,应该就是由视图服务器在程序退出的时候自动销毁的。还有就是 StatusPane等对象,为什么也没有在析构函数中删除,我的理解是这些对象都是由框架进行管理的,所以不需要我们自己显示销毁,在程序关闭时,会由程序框架将它们自动销毁。
 
―――――――――――――――――――――――――――――――――――――――
小结:程序可能存在着多个 View,但是一次只能显示一个,因此它们之间切换显示的方法比较重要,也是经常使用的,采用的函数就是CAknViewAppUi中定义的
IMPORT_C void CAknViewAppUi::ActivateLocalViewL
TUid 
aViewId
 ) 
 
通过传递 View的UID实现切换。
 
 
 
 
备注:这个例子程序中由于使用了 StatusPane,所以略微显得累赘,如果仅想了解View切换架构,还是建议看SDK自带的MultiViews这个例子。
应该说 StatusPane也是程序中经常用到的,它位于屏幕的上层,因为应用程序窗口的标准面板由状态面板、主面板和软键面板组成,而主面板就是客户矩形显示的位置,也就是除去状态面板和软键面板后剩余的区域。状态面板StatusPane又分为信号面板、上下文面板、标题面板、导航面板、电池面板等子面板,因此,要想操作这些子面板需要事先通过StatusPane获得这些子面板的指针,通过sp->ControlL()方法即可。
本例的程序代码,朋友可以在根据 S60SDK2.1的向导自动生成,程序名取MultiViewsTest即可。
 
声明:这只是我作为一个 Symbian新人对View架构的理解而已,写下来只是希望比我还新的新人在碰到这种架构问题时,看到我的文章,能够有所启发。限于本人水平有限,可能文中有理解错误之处,还请高手不吝赐教。
转载请注明出处,谢谢。
 
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐