轉自: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文件分解
- kevin_chow 發表於2008年11月7日 15:15:25 舉報
- 看來,norains兄對設計模式很有心得啊!還想請教,在WinCE上,怎樣使用STL啊?這方面資料真少!
- norains 發表於2008年11月10日 12:47:18 舉報
- Re kevin_chow: STL既然叫“標準C++庫”,那就在很大程度上代碼是可移植的,也就是說,在PC平臺的經驗可以遷移到wince中。不過,EVC4.0有一些stl是不支持的,比如stream等。
- tokens 發表於2008年11月7日 15:26:26 舉報
- 嗯,學習學習
- EchoTiro 發表於2009年3月3日 23:42:56 舉報
- 界面的描述(位置,大小什麼的)和語言都可以放到xml文件裏做,更換界面或語言只要更換對應的xml文件就可以了。