嵌入式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架构的设计如此了。其实,只要能做到界面更换简单,功能添加简便,基本上往后的工作就会非常容易,所以初始的架构设计就显得非常重要了。
   
   
   
    后记:好久没写这种纯理论到连自己看了也觉得不知所云的东西了...文中的代码是从我所写的工程中搬出来的,直接用到别的地方肯定是行不通,所以大家仅仅是看看,做做参考就好。:-)

你可能感兴趣的:(UI,String,架构设计,嵌入式,语言,pair)