嵌入式UI架構設計漫談

轉自:http://blog.csdn.net/norains/archive/2008/10/31/3194979.aspx

 

//========================================================================
//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架構的設計如此了。其實,只要能做到界面更換簡單,功能添加簡便,基本上往後的工作就會非常容易,所以初始的架構設計就顯得非常重要了。
   
   
   
    後記:好久沒寫這種純理論到連自己看了也覺得不知所云的東西了...文中的代碼是從我所寫的工程中搬出來的,直接用到別的地方肯定是行不通,所以大家僅僅是看看,做做參考就好。:-)

發表於 @ 2008年10月31日 18:20:00|評論(3 <script type="text/javascript"></script> )|收藏

新一篇: string也可以很精彩 | 舊一篇: vcf文件分解

<script type="text/javascript"></script> <script src="http://hi.images.csdn.net/js/blog/feedback.js" type="text/javascript"></script>
kevin_chow 發表於2008年11月7日 15:15:25  IP:舉報
看來,norains兄對設計模式很有心得啊!還想請教,在WinCE上,怎樣使用STL啊?這方面資料真少!
norains 發表於2008年11月10日 12:47:18  IP:舉報
Re kevin_chow: STL既然叫“標準C++庫”,那就在很大程度上代碼是可移植的,也就是說,在PC平臺的經驗可以遷移到wince中。不過,EVC4.0有一些stl是不支持的,比如stream等。
tokens 發表於2008年11月7日 15:26:26  IP:舉報
嗯,學習學習
EchoTiro 發表於2009年3月3日 23:42:56  IP:舉報
界面的描述(位置,大小什麼的)和語言都可以放到xml文件裏做,更換界面或語言只要更換對應的xml文件就可以了。
發佈了20 篇原創文章 · 獲贊 2 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章