观's profileTo the world,you may jus...PhotosBlogListsMore Tools Help

Blog


    03/11/2009

    项目中待解决问题

       项目的时间越来越近了,听到公司的意向订单也越来越多,压力袭来,必须尽快了,还有一些待解决问题:

    • 电源管理中对电池电量的探测以及通知(公司为了节约成本,将本来用的一块电源管理芯片MC13783替换成了一个简单的供电芯片,很多东西都要自己来改。以后一定要记住,对功耗要求高的产品,前期一定要重视起电源管理!)
    • Winceshell的定制,需要解决将wince自带的UI屏蔽掉。
    • 处理音视频解码中,定时器对整个程序性能的影响问题。
    • 低功耗处理(这个是最难的了,我会另起一篇文章来写这个的)

      总的就是这么多了,想到就继续补充吧!
    04/07/2009

    WINCE应用的UI实现方案 —— 上篇:几种UI实现方案比较

    一、MFC的硬伤

    在接手现在这个项目之前,我对WINDOWS平台上的UI开发还是个白痴,除了MFC,就只知道GDI了。而且居然大言不惭地说用MFC只能画画灰色的对话框和按钮。但不论如何,在嵌入式这种对成本极度敏感的项目上,我是不会拍板用MFC的。假设极端情况,定制后的系统是31.8M,我放一个ARMV4I上的MFC DLL进去,大概500多K,那么只有两种选择,要么把32M的FLASH换成64M的——我的上司会把我给砍了,要么把应用层的UI代码全部重写——我的下属会把我给剁了。另一方面,WINCE上的应用软件我看过不少开源代码,也接触了一些外包的软件,还真没见过谁用MFC的。网上公论用MFC后会导致程序在不同平台上移植性降低,因为你不能指望别人的平台给你准备好奢侈的MFC。另一方面,多数高手都不屑用。我不是高手,但可以学人家摆谱,于是“不会用”就变成了“不屑用” ^_^

    二、GDI的痛苦

    把整套UI从CreateWindow开始写起,的确很累人。我写了500多行才勉强实现BUTTON类,另一个同事也用了500行左右才实现了TRACK BAR类,而且还未经测试,也没有很正式的CODE REVIEW。如果工业设计中心多增加几种图样,那么我们就得多些几个基类,然后再赔进去CODE REVIEW的时间、测试时间、BUG FIX的时间。不痛苦,那是不可能滴~。

    三、GWES的探路,我不是先锋

    群众的智慧是无穷的。当我这组同事的思维都受制于我的GDI方案时,从通信部过来协助完成项目的软件工程师从WINCE500的一个应用SAMPLE CODE里把DialogBox函数给抓出来了。我认为自己在定UI实现方案上很失败的一点就是习惯性思维地从eVC里建立DIALOG RESOURCE后,立刻就要去点Class Wizard, 然后就是关联MFC类。而他却画出来的DIALOG和BUTTON后,拿着RESOUCE ID从DialogBox函数建立起UI。并且我又习惯性思维地认为DialogBox并不在STANDARD SDK 500里面,但他确实从STANDARDSDK_500里不引用其它LIB和DLL就把DialogBox和BUTTON用起来了,然后过来找我谈论如何把图片叠加在DIALOG和BUTTON上。泪奔一百里~ 我应该去找块豆腐撞死~

    四、最后的攻关,GWES API能否成为我们需要的坚实地基
    GWES系列API能否实现我们所需的所有UI功能呢?没有人知道,需要评估。刚才起草稿时,我把这些都写在同一篇文章里了。现在觉得还是分篇好些,毕竟主题不同。

    嵌入式UI架构设计

    //========================================================================
    TITLE: 嵌入式UI架构设计漫谈
    AUTHOR: norains
    DATE: Friday 31-October-2008
    Environment:  NONE
    //========================================================================

        和桌面清一色的采用explorer不同,嵌入式设备更多的采用是自定义的简单UI,即使是含有explorer的wince也是如此。因为对于嵌入式设备而言,功能强大并不是主打,简单易用才是根本。以目前国内的手持车载设备为例,大部分的公司卖的都是硬件,利润很大一部分取决于硬件成本的多寡。并且,每个系列的产品都会有不同的外围器件,而这也决定了无法所有的产品都用同一个UI程序。
        
        虽然UI程序无法使用同一个,但从总体上而言,基本上是相同的;最有可能不同的地方无非是界面多了某些按钮,调用某些功能而已。另一方面,UI程序往往也需要配合产品的外观,风格尽可能和外观相符合。
        
        于是由此,基于可重用性考虑,嵌入式设备的UI基本上必须具有如下特点:
        1.界面更换方便
        2.功能增删方便
      
        下面我们就这两点具体到代码的层次去说说相应的设计。
        
        界面更换方便,这个方便不仅是对于程序编写者而言,也是针对图片设计者。如果是方案提供商,则后者显得更为重要。如果程序能够做到每次更换图片不需要重新编译,那么对于客户而言,他们只需要重新设计图片,然后替换就能立马看到效果。这点是非常重要的,如果每更换一次图片就必须要重编译,意味着多一个客户就会多一个烦恼。
        
        以读取BMP图片为例,最简单的方法就是将bmp图片导入到IDE环境的resource中,在使用的时候调用MAKEINTRESOURCE宏来获取相应的字符串地址即可。不过这会有一个非常严重的问题,因为图片是全部包含于可执行文件中,如果图片很多容量很大,那么单一的可执行文件的大小就会非常可观了。何况在wince中还会有个问题,如果程序体积大于8M,那么读取程序内部包含的图片将有可能会导致失败。
        
        鉴于以上原因,图片放在外部读取是最佳的选择。
        
        如果图片放在外部存储器,读取的速度将是一个不能忽略的问题。假使一张bmp图片的大小为800*480,然后再加上界面上的ICON,如果每次显示时都会分配缓冲然后绘制图片,那么会感觉到有延迟。一般像这种情况之下,我们都会选择一次读取,多次使用的方式。也就是说,只有第一次使用的使用才会将图片保存到缓冲区,以后都只是从这缓冲区获取图片数据而已。
        
        为了能够最大限度节省内存,以及使用的便利性,我们将对图片的读取和获取采用类封装的形式。最为简便的方式是,我们传入一个图片的序号,然后获取一个可供绘制的HDC。
        
        基本的形式概括如下;
        namespace ImageTab
        {
         enum ImageIndex
         {
          NONE,
          BKG_WND_MAIN,
          
          ...
          
          }
          
         struct ImageInfo
         {
          HDC hdc;
          SIZE size;
         }; 
        }

        class CImageTabBase
        {
        public:
         bool GetImageInfo(ImageTab::ImageIndex imgIndex, ImageTab::ImageInfo &imgInfo) ;
         
         ...
        }

        绘制图片时可以简单如此:
        ImageTab::ImageInfo imgInfo = {0};
        if(m_ImgTab.GetImageInfo(m_BkInfo.Image,imgInfo) != false)
        {
         StretchBlt(memDC.GetDC(),0,0,iWndWidth,iWndHeight,imgInfo.hdc,0,0,imgInfo.size.cx,imgInfo.size.cy,SRCCOPY);
        }

        使用类的方式还有一个好处,就是如果遇到图片架构变更,只要添加新的ImageTab类即可,甚至可以通过配置文件来确定当前运行的程序应该选用何种界面:
        switch(m_Option.GetImgTab())
       {
        case Option::IMG_A:
         m_pImgTab = new CImageTabA(); 
         break;
        case Option::IMG_B:
         m_pImgTab = new CImageTabB(); 
         break;
        default:
         m_pImgTab = new CImageTabA(); 
         break;
       }
       
       这对于需要频繁更改界面的需求无疑是一个比较好的方式。
       
       
       和图片类似,显示的文字也是一个比较重要的议题。不像桌面PC,日常使用中只需要一种语言。当然,对于嵌入式设备,平时确实也只是一种,但这只是对于用户而言;如果对于开发者,则必须考虑到多种语言如何方便性地共存。最典型的例子便是手机,普遍性地说,手机的系统语言都会有英文,简体中文和繁体中文选项。
       
       语言的切换其实很简单,关键在于方式的简便与否。
       
       最为笨拙的方法无非是直接采用switch方式:
       switch(language)
       {
         case EN:
          m_Info1.SetText(TEXT(""))
          break;
        case CHS:
          m_Info1.SetText(TEXT(""))
          break;
        case CHT:
          m_Info1.SetText(TEXT(""))
          break;
       }
       
       ....
       
       switch(language)
       {
         case EN:
          m_Info2.SetText(TEXT(""))
          break;
        case CHS:
          m_Info2.SetText(TEXT(""))
          break;
        case CHT:
          m_Info2.SetText(TEXT(""))
          break;
       }
       
       从代码中就可以很容易看见这种方式的弊端:每增加一个信息显示控件就必须增加一个switch语句块,每增加一个语言就必须要增加一个case语句。而对于嵌入式设备而言,增加新的控件和语言是常事,这弊端带来的结果就是不胜其烦的代码添加。更为严重的一个问题是,语言资源在代码编译阶段已经确定,如果是通过资源配置而达成文字的不同,则需要对源代码进行大量的更替。基于以上原因考虑,该方式为鸡肋。
       
       所以还是和图片方式一样,采用类封装的方式:

      namespace StrTab
      {
       enum Language
       {
        LANG_EN = 0x01,
        LANG_CHS,
        LANG_CHT
       };
       
       enum String
       { 
        STR_NONE,
        STR_INFO1,
        STR_INFO2,
      
          ....
       };
      }
      
      class CStrTabBase
      {
      public:
       void SetLanguage(StrTab::Language lang);
       virtual TSTRING GetString(StrTab::String strIndex);
      };

        采用类封装方式,之前通过switch语句更新资源的代码可以更改如下:
        CStrTabBase StrTab;
        
        ...
        
        //设置语言
        StrTab.SetLanguage(StrTab::LANG_CHS);
        
        ...
        
        //设置字符串
        m_Info1.SetText(StrTab.GetString(StrTab::STR_INFO1));            
        m_Info2.SetText(StrTab.GetString(StrTab::STR_INFO2));         
        
        这样的好处显而易见,语言只需要设置一次,然后文字设置可以避免采用大量的switch语句。还有另外一个不为人注意的好处是,字符串的设置只和CStrTabBase的GetString函数有联系,而不管其内部是如何获得的。也就是说,语言的资源即可以在编译时确定,也可以在运行时获取,但究竟采用何种方式,对于界面的字符串设置代码来说都是一致的,并不需要做任何更改。
        
        因为动态获取语言资源方式繁多,在此不再累赘,只是简单说说如何编译时期确定语言资源如何才能做到最简便。如果还是像之前采用switch块,则显得有点换汤不换药的味道。鉴于此,我们采用STL库的map。
        
        我们使用三个map变量,用来存储相应的语言资源:
        std::map<StrTab::String,TSTRING> mpChinesSimplified;
        mpChinesSimplified.insert(std::make_pair(StrTab::STR_TV,TEXT("移动电视")));
        m_mpString.insert(std::make_pair(StrTab::LANG_CHS,mpChinesSimplified));
        
        std::map<StrTab::String,TSTRING> mpChinesTraditional; 
        mpChinesTraditional.insert(std::make_pair(StrTab::STR_TV,TEXT("數位電視")));
        m_mpString.insert(std::make_pair(StrTab::LANG_CHT,mpChinesTraditional));
         
        std::map<StrTab::String,TSTRING> mpEnglish; 
        mpEnglish.insert(std::make_pair(StrTab::STR_TV,TEXT("TV")));
        m_mpString.insert(std::make_pair(StrTab::LANG_EN,mpEnglish));
        
        获取函数则可以非常简单:
        TSTRING CStrTabBase::GetString(StrTab::String strIndex)
        { 
         return (m_mpString[m_Lang])[strIndex];
        }
        
        以后当我们需要添加新的字符串资源时,只需要在初始化添加相应的字符串即可。这样不仅避免了大量的case语句,还能令代码条理清晰,方便简洁。
        
        细心的朋友可能发现,CImgTabBase和CStrTabBase有所不同。对于图片资源来说,不同的方案,表现的图片会不一样,比如说,同样代表“设置”的图标,可能给A公司和给B公司的完全不同(否则就撞车了);但文字资源,无论是A公司或是B公司,功能都叫“设置”。因此,图片类实际获取资源的是取决于子类,而文字资源则是基类。文字资源的子类,只是更改部分某些不同的数值而已。
        
        与此相对,一些常用的数值也可以先用类封装,方便之后的更改: 
        
        namespace ValTab
        {
         enum CtrlColor
         {
          COLOR_TXT_ITEM,
          COLOR_TXT_WND_TITLE,
          ...
         };
        
        
         enum CtrlSize
         {
          TXT_ITEM_WEIGHT,
          TXT_ITEM_POINT_SIZE,
            ...
         };
        
        }
        
        class CValTabBase
        {
        public:
         virtual COLORREF GetColor(ValTab::CtrlColor ctrlColor);
         virtual DWORD GetSize(ValTab::CtrlSize ctrlSize);
        
        };
        
        类似于此,很多随着环境会改变的数值,按钮的位置等等,都可以采用此形式封装。
        
        如果全部采用封装形式,每次添加新值只需要在初始化中添加相应的数值即可:

        BOOL CMainCtrl::Initialize(HINSTANCE hInstance)
        { 
         switch(m_Option.GetImgTab())
         {
          case Option::IMG_A:
           m_pImgTab = new CImageTabA(); 
           break;
          case Option::IMG_B:
           m_pImgTab = new CImageTabB(); 
           break;
          
           ...
           
          //如果有新值,在这里添加
          
           ...
          
          default:
           m_pImgTab = new CImageTabA(); 
           break;
         }
        
         switch(m_Option.GetPosTab())
         {
          case Option::POS_A:
           m_pPosTab = new CPosTabA();
           break;
          case Option::POS_B:
           m_pPosTab = new CPosTabB();
           break;
          
           ...
           
          //如果有新值,在这里添加
          
           ...
          
          default:
           m_pPosTab = new CPosTabA();
           break;
         }
        
         switch(m_Option.GetValTab())
         {
          case Option::VAL_A:
           m_pValTab = new CValTabA();
           break;
          case Option::VAL_B:
           m_pValTab = new CValTabB();
           break;
          
           ...
           
          //如果有新值,在这里添加
          
           ...
          
          default:
           m_pValTab = new CValTabA();
           break;
         }
        
         switch(m_Option.GetStrTab())
         {
          case Option::STR_A:
           m_pStrTab = new CStrTabA();
           break;
          case Option::STR_B:
           m_pStrTab = new CStrTabB();
           break;
          
           ...
           
          //如果有新值,在这里添加
          
           ...
          
          default:
           m_pStrTab = new CStrTabA();
           break;
         }
         m_pStrTab->SetLanguage(m_Option.GetLanguage());
        
        
         return TRUE;
        }

        万变不离其宗,对于windows程序而言,最主要的还是窗口。很多时候,大家常用的做法是一个界面,就写一个源代码文件。这样当然简单,但带来的问题就是代码重复度高,没增加一个窗口就要增加一个文件,显得很累赘。所以,关于窗口,我们是采用只用一个窗口类,通过设置不同的数值,来生成形式各异的界面。
        
        我们先来分析一下像这种简单UI程序不同界面的区别。一般来说,像界面无非是有如下控件:
        
        1.按钮:用来实现特定功能
        
        2.文本:用来显示不同的文字
        
        3.进度条:用来显示某些特殊功能的状态
        
        4.背景:窗口的背景图片。
        
        这四点,便是界面的共通点。所以我们在设计窗口类时,给这四点留出接口即可:
        
        class CUserWnd
        {
        public:
         BOOL SetButtonInfo(const std::vector<UserData::ButtonInfo> &vtBtnInfo);
         BOOL SetTextInfo(const std::vector<UserData::TextInfo> &vtTxtInfo); 
         BOOL SetProgressInfo(const std::vector<CProgress> &vtPrgInfo);
         BOOL SetBackground(UserData::BackgroundInfo bkInfo); 
          
          ...
        }
        
        然后对于不同功能的界面,我们只需要设置不同的数值即可:
        
        //背光窗口
        pWndBacklight->SetButtonInfo(vtBtnInfo1);
        pWndBacklight->SetTextInfo(vtTxtInfo1);
        pWndBacklight->SetBackground(bkInfo1);
        
        //电池窗口
        pWndBattery->SetButtonInfo(vtBtnInfo2);
        pWndBattery->SetTextInfo(vtTxtInfo2);
        pWndBattery->SetBackground(bkInfo2);
        
        这样我们只需要一个窗口类,就能实现变化各异的界面。
        
        在这里还有一点需要提一下,一般来说,我们显示界面和功能的实现应该分开,这样代码看起来就不会变得杂乱无章。我们可以采用这么一种做法,定义一个CCommand类,主要是执行按钮的相关功能操作,然后窗口类继承于该类即可:
        
        class CCommand
        {
        protected:  
         BOOL ExecuteCmd(UserData::CtrlIndex ctrlIndex,DWORD dwParam);
         
         ....
         
        }
        
        BOOL CCommand::ExecuteCmd(UserData::CtrlIndex ctrlIndex,DWORD dwParam)
        {
         switch(ctrlIndex)
         {   
          case UserData::BTN_EXIT:
          {
           return OnCmdExit();
          }
          case UserData::BTN_EXPLORER:
          {
           //Execute the explorer and then exit the application
           CCommon::Execute(m_Option.GetPathTab(Option::PATH_EXPLORER).c_str());
          }
          
          ...
          
         }
        }
        
        //窗口的继承
        class CUserWnd:
         public CCommand
        {
          ...
        }
        
        基本上,嵌入式UI架构的设计如此了。其实,只要能做到界面更换简单,功能添加简便,基本上往后的工作就会非常容易,所以初始的架构设计就显得非常重要了。
        
        
        
        后记:好久没写这种纯理论到连自己看了也觉得不知所云的东西了...文中的代码是从我所写的工程中搬出来的,直接用到别的地方肯定是行不通,所以大家仅仅是看看,做做参考就好。:-)