網(wǎng)站主要盈利模式上海百度推廣平臺
MVC模式,是天底下編寫GUI程序最為經(jīng)典、實效的一種軟件架構(gòu)模式。當(dāng)一個人學(xué)完菜單欄、開始學(xué)習(xí)工具欄時,就是他的一生中,最適合開始認(rèn)識 MVC 模式的好時機之一。這節(jié)將安排您學(xué)習(xí):
- Model-View-Controller 模式
- 如何創(chuàng)建工具欄以及如何于其上創(chuàng)建普通、多選、單選工具按鈕
- 控件之間如何通過“空閑”事件實現(xiàn)狀態(tài)統(tǒng)一
0. 課堂視頻
建議先看視頻,再看文本教程。最后到 d2school 課堂做本課的學(xué)習(xí)強化練習(xí)。
GUI07-學(xué)工具欄,懂 MVC
1. 才不是題外話:MVC
MVC模式,是天底下編寫GUI程序最為經(jīng)典、實效的一種軟件架構(gòu)模式。當(dāng)一個人學(xué)完菜單欄、開始學(xué)習(xí)工具欄時,就是他的一生中,最適合開始認(rèn)識 MVC 模式的好時機之一。
MVC 是 “Model-View-Controller” 的簡寫:
- Model / 模型:可認(rèn)為就是我們在代碼中定義的各種業(yè)務(wù)數(shù)據(jù);
- View / 視圖:業(yè)務(wù)數(shù)據(jù)的展現(xiàn)形式,一種模型(數(shù)據(jù))往往有多種展現(xiàn)形式;
- Controller / 控制器: 用戶通過控制器,以可定制的方式,獲取數(shù)據(jù)模式,及指定形式展現(xiàn)(視圖)。
程序員開發(fā)軟件時,將代碼按照 Model-View-Controller 加以分離,能讓軟件代碼:
- 邏輯,意圖更清晰
- 耦合更低
- 更可維護(hù)
MVC 比較適用于中大型軟件代碼的組織,我們前幾節(jié)課所寫的程序,看似簡單,但其實, GUI軟件沒有小項目,是底層的庫(比如 wxWidgets)默默地做出大量支持,包括 MVC 架構(gòu)。
以前面課堂寫的小例程為例:我們在窗體上鼠標(biāo)位置下顯示鼠標(biāo)所在的坐標(biāo)值,并且允許用戶做如下控制:
- 是否顯示坐標(biāo)值,或僅顯示一行提示文字;
- 文字以藍(lán)或紅哪種顏色顯示。
則,至少有兩個數(shù)據(jù)模型:
編號 | Model |
---|---|
M1 | 表達(dá)坐標(biāo)的兩個整數(shù):x, y |
M2 | 表達(dá)用戶選中顏色的菜單項ID |
以及,這兩個數(shù)據(jù),至少四種組合態(tài)的展現(xiàn)形式:
編號 | View (數(shù)據(jù)視圖) |
---|---|
V1 | 藍(lán)色 + 顯示坐標(biāo) |
V2 | 紅色 + 顯示坐標(biāo) |
V3 | 藍(lán)色 + 不顯示坐標(biāo)(僅顯示提示) |
V4 | 紅色 + 不顯示坐標(biāo)(僅顯示提示) |
以下是實際運行時,顯示的 V2 狀態(tài)界面截圖:
正如本課視頻所講到,View 并不僅僅包含業(yè)務(wù)數(shù)據(jù)的展現(xiàn),用于實現(xiàn)和用戶交互的 GUI 界面控制,比如我們已學(xué)的菜單、工具欄、狀態(tài)欄等界面控件,也是 View 的組成。它們也會有和業(yè)務(wù)數(shù)據(jù)緊密關(guān)聯(lián)的狀態(tài),比如菜單或工具圖標(biāo)的多選或單選狀態(tài)。
以上歸納了 Model 和 View,那 Controller是什么呢?是控制流程。在我們所寫的程序中,程序接收用戶的輸入(鼠標(biāo)點擊等),然后調(diào)用相應(yīng)的事件函數(shù),在事件函數(shù)中修改 Model 數(shù)據(jù),最后引發(fā)視圖刷新,包括業(yè)務(wù)視圖刷新和人機交互界面中控件狀態(tài)的改變,這整個過程即為 Controller。
初接觸時,對 MVC 的三個最常見的誤解為:
-
誤解一:以為 View的展現(xiàn)內(nèi)容只能給人看。正解:一個程序的輸出可以是另一個程序的輸入,此時可視為后者是前者的“用戶”,同理,同一個程序的控制,也可能是來自另一個程序的輸入。正因為 Views 不一定真實的人,所以在服務(wù)端程序中,特別是多層網(wǎng)絡(luò)架構(gòu)的軟件系統(tǒng)中,MVC 的應(yīng)用也廣泛可見;
-
誤解二:以為人機交互過程中界面控件,就是 Controller 。正解:Controller 是接受用戶(不一定是人,見上)的輸入,然后引發(fā)(即動作的發(fā)起者)Model 被修改,并最終體現(xiàn)到 View 變化的每一個完整過程。
-
誤解三:以為只有業(yè)務(wù)數(shù)據(jù)的展現(xiàn)是 Views。正解:凡是需要展現(xiàn)給用戶看的內(nèi)容,基本都是 View,包括控件的狀態(tài)。
2. 創(chuàng)建圖標(biāo)
在 Windows 系統(tǒng)下,應(yīng)用程序工具欄按鈕所需要的圖標(biāo),需要使用 ICON 類型的圖標(biāo),在 wxWidgets中,對應(yīng)到 wxIcon 類。
2.1 從文件直接創(chuàng)建
可在程序運行時,通過讀取指定路徑(相對或絕對)讀取指定的圖標(biāo)文件,以構(gòu)造出一個wxIcon對象,示例代碼:
wxIcon myIcon = wxIcon(wxT("路徑/圖標(biāo)文件.ico"), wxBITMAP_TYPE_ICO, 16, 16);
注意,創(chuàng)建所得是普通的棧對象,非堆對象。
- 入?yún)?:圖標(biāo)的磁盤位置(包括路徑和文件名,可使用絕對或相對路徑);
- 入?yún)?:圖標(biāo)文件的類型(注意是 wxBITMAP_TYPE_ICO,而非 wxBITMAP_TYPE_ICON);
- 入?yún)?,4:兩個整數(shù),圖標(biāo)的長、寬(像素)。
在程序運行時讀取外部文件以創(chuàng)建圖標(biāo)(或其它資源)——
- 好處:可隨時更換圖標(biāo)——不少應(yīng)用程序支持 “換膚”,對于工具欄按鈕來說,這是一種簡便可行的方法;
- 壞處:程序無法單獨運行。比如你想把程序發(fā)給你的朋友,就得同時附上這些圖標(biāo)文件,并需確保路徑正確。
2.2 從程序嵌入資源中創(chuàng)建
Windows 支持將一些資源,嵌入到可執(zhí)行文件(.exe)“體內(nèi)”,從而實現(xiàn)可執(zhí)行文件可獨立運行。
Code::Blocks 向?qū)傻?wxWidgets 項目中,已經(jīng)自帶 resource.rc 的文件,打開它,默認(rèn)內(nèi)容為:
aaaa ICON "wx/msw/std.ico"
#include "wx/msw/wx.rc"
默認(rèn)包含的 wxWidgets 程序圖標(biāo)。其中,aaaa 為資源的名字,ICON 為資源類型,此處為圖標(biāo),最后一列 “wx/msw/std.ico” 為圖標(biāo)在磁盤上的路徑和名字(僅編譯時需用到,運行時不再需要此路徑)。
假設(shè)我們已經(jīng)在當(dāng)前項目下創(chuàng)建名為 icons 的子目錄,并于其下準(zhǔn)備好 about.ico、show_info.ico、blue_txt.ico、red_txt.ico、quit.ico 等圖標(biāo),則可將 resouce.rc 內(nèi)容修改成:
aaaa ICON "wx/msw/std.ico"about ICON "icons/about.ico"
blue_txt ICON "icons/blue_txt.ico"
red_txt ICON "icons/red_txt.ico"
quit ICON "icons/quit.ico"
show_info ICON "icons/show_info.ico"#include "wx/msw/wx.rc"
這樣,在編譯后,相關(guān)圖標(biāo)資源就會內(nèi)嵌在編譯生成的可執(zhí)行文件中,并可通過 wxIcon 的另一個構(gòu)造函數(shù)讀出并且成圖標(biāo)對象:
wxIcon icon = wxIcon(wxT("資源名"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16);
比如,創(chuàng)建 quit 圖標(biāo):
wxIcon iconQuit = wxIcon(wxT("quit"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16);
本文后續(xù)代碼均從內(nèi)嵌資源創(chuàng)建圖標(biāo)。
3. 工具欄與工具欄按鈕
3.1 創(chuàng)建工具欄
wxFrame::CreateToolBar() 用于創(chuàng)建一個空白的工具欄,該方法返回一個 wxToolBar * (指針)。
示例代碼:
wxToolBar* tb = this->CreateToolBar(); // 創(chuàng)建工具欄
以上代碼中的 this 工具欄所屬的框架窗口,它也將負(fù)責(zé)該工具欄的生命周期(在框架窗口關(guān)閉前,將自釋放上面創(chuàng)建工具欄堆對象)。
以下各種工具欄按鈕,都需要由工具欄(wxToolBar)對象來創(chuàng)建。
3.2 普通工具按鈕
使用工具欄對象,創(chuàng)建一個普通工具欄按鈕,方法名為 “AddTool()”示例代碼:
// 準(zhǔn)備圖標(biāo) (從內(nèi)嵌資源中讀取)
wxIcon iconQuit = wxIcon(wxT("quit"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16);// 創(chuàng)建帶圖標(biāo)的普通工具按鈕
tb->AddTool(idMenuQuit, _("Quit"), iconQuit, _("Quit the Application"));
- 入?yún)?:工具按鈕要執(zhí)行的命令對應(yīng)的ID,通常來自已經(jīng)創(chuàng)建的菜單項所綁定的事件ID。比如本例的 idMenuQuit 為 “Quit” 菜單項的ID,該命令用于退出整個應(yīng)用程序;
- 入?yún)?:工具按鈕的標(biāo)簽,默認(rèn)狀態(tài)下并不顯示,復(fù)雜情況下,可設(shè)置為在圖標(biāo)底部或右側(cè)顯示按鈕的名字;
- 入?yún)?:前面創(chuàng)建的圖標(biāo)對象(并非指針);
- 入?yún)?:鼠標(biāo)在該工具按鈕上浮動時出現(xiàn)的提示。
也可將構(gòu)造圖標(biāo)對象和創(chuàng)建工具按鈕合成一步:
tb->AddTool(idMenuQuit, _("Quit"), wxIcon(wxT("quit"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16), _("Quit the Application"));
實際還需要第三步,但當(dāng)連續(xù)為同一個工具欄創(chuàng)建多個工具按鈕時,僅需在最后調(diào)用一次:
tb->Realize(); // 真實展現(xiàn),最后調(diào)用,只需一次
僅在調(diào)用該方法后,工具欄才真正地將新創(chuàng)建的各工具按鈕展現(xiàn)出來。
3.3 復(fù)選工具按鈕
使用方法為:AddCheckTool(),示例代碼:
//復(fù)選工具按鈕(是否顯示鼠標(biāo)坐標(biāo))
tb->AddCheckTool(idMenuShowMotionInfo, _("Show Info"),wxIcon(wxT("show_info"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16),wxNullBitmap, // 按鈕變灰時顯示的圖標(biāo),由程序自動生成_("Show motion info or no")); // 提示
第四個入?yún)⒂糜谥付?#xff0c;該復(fù)選按鈕在不可用(disabled)狀態(tài)下如何展現(xiàn),除非你確實想為之提供一種特別的樣子,否則,可如代碼所示,指定使用代表空圖片的對象 wxNullBitmap,系統(tǒng)將自動以 “灰度” 方式生成該狀態(tài)的展現(xiàn)樣式。
因為方法名已經(jīng)是 “AddCheckTool ()”,故無需像普通的 “AddTool ()” 那樣,再由入?yún)⒅付ò粹o的類型,下面用于創(chuàng)建單選工具按鈕的方法,也是如此。
3.4 單選工具按鈕
使用方法為:AddRadioTool(),示例代碼:
//單選工具按鈕(兩個:選擇使用藍(lán)色文本或使用紅色文本)
tb->AddRadioTool(idMenuBlueText, _("Blue Text"),wxIcon(wxT("blue_txt"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16),wxNullBitmap, // 按鈕變灰時顯示的圖標(biāo),由程序自動生成_("Set text blue")); // 提示tb->AddRadioTool(idMenuRedText, _("Red Text"),wxIcon(wxT("red_txt"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16),wxNullBitmap,_("Set text red"));
和復(fù)選按鈕不同,單獨一個單選按鈕沒有意義,通常需要接連創(chuàng)建一組,讓用戶從多個中選擇一個(即為單選之意)。
同一工具欄上如有多組單選按鈕,除了可考慮使用分隔(見下面小節(jié))在視覺上加以分組之外,并不需要再額外在邏輯上分組,因為工具按鈕通常對應(yīng)到菜單項,而對菜單項,我們已經(jīng)作了邏輯分組,詳見 《第5節(jié) 玩轉(zhuǎn)主菜單》。
3.5 分隔線
tb->AddSeparator(); // 分隔線
3.6 連續(xù)代碼
以下是在示例項目中, wxToolBarFrame 構(gòu)造函數(shù)尾部,創(chuàng)建工具欄及相應(yīng)按鈕、分隔線的代碼片段:
// 工具欄wxToolBar* tb = this->CreateToolBar(); // 創(chuàng)建工具欄wxIcon iconQuit = wxIcon(wxT("quit"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16);tb->AddTool(idMenuQuit, _("Quit"), iconQuit, _("Quit the Application"));tb->AddSeparator(); // 分隔線//復(fù)選工具按鈕(是否顯示鼠標(biāo)坐標(biāo))tb->AddCheckTool(idMenuShowMotionInfo, _("Show Info"),wxIcon(wxT("show_info"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16),wxNullBitmap, // 按鈕變灰時顯示的圖標(biāo),由程序自動生成_("Show motion info or no")); // 提示tb->AddSeparator(); // 分隔線// 單選工具按鈕(兩個)tb->AddRadioTool(idMenuBlueText, _("Blue Text"),wxIcon(wxT("blue_txt"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16),wxNullBitmap,_("Set text blue"));tb->AddRadioTool(idMenuRedText, _("Red Text"),wxIcon(wxT("red_txt"), wxBITMAP_TYPE_ICO_RESOURCE, 16, 16),wxNullBitmap,_("Set text red"));tb->Realize(); // 真實展現(xiàn),最后調(diào)用,只需一次
4. 控件狀態(tài)同步
為了使用方便,GUI 程序往往允許用戶從不同控件入口發(fā)起操作,在本例,用戶通過菜單欄或工具欄,都可以修改文字的顏色。窗體上輸出的文字歸屬 View,但是菜單項或工具按鈕的顯示狀態(tài),也歸于 View 范疇,它們的選中狀態(tài),必須保持同步。
舉例:用戶選中 “變帥” 菜單項,則 工具欄上 “變帥” 按鈕也需要在操作后,變成“被按下”的狀態(tài);反過來也一樣:用戶按下 “變帥” 按鈕,則同名的菜單項之前,必須打上勾。這還是復(fù)選控件,如果是單選控件,則需要的維護(hù)一組控件的狀態(tài),會更加啰嗦,容易出錯。
因此,無論是大家比較熟悉的 Windows 或 蘋果的 GUI 系統(tǒng),還是 Linux 下的各種 GUI 桌面系統(tǒng),或者是新興的智能手機 Android 系統(tǒng),或者是為瀏覽器前端開發(fā)提供支持各種前端庫,基本都會提供一套機制,來輔助程序員實現(xiàn)界面組件的狀態(tài)同步。
GUI 程序有一個共同點:它不可能很忙。有人說,我打開 Word,然后以平均每分鐘 120 個漢字,接近 500 次按鍵的速度打字,這 Word 肯定忙壞了呀? 不是的,這點輸入對現(xiàn)在的計算機和程序來說真是一點壓力沒有,程序仍然能在你的指法間隙間,“擠出”一大把無所事事的時間(不信你可以打開 Windows 任務(wù)管理器,看看你要能以怎樣的打字速度,讓 CPU 占用率提高 0.1%?)。
程序這么閑,自然就可以利用起來,閑著也是閑著,不如打打孩子——不是,不如去檢查一下,有哪個控件狀態(tài)不對,我們好把它糾正過來。
這種空閑事件,在很多 GUI 系統(tǒng)(比如 Windows 或 Android)中稱為 “Idle-Event” (對應(yīng)的事件處理器稱為 “Idle-Handler”)。我們正在學(xué)習(xí)的 wxWidgets 又將它細(xì)分出一個 “wxUpdateUIEvent”,即專門用來處理用戶界面(UI)的控件狀態(tài)更新(Update)的事件。對應(yīng)的,綁定事件函數(shù)的宏為:“EVT_UPDATE_UI”。
以用戶可以切換坐標(biāo)是否顯示這一功能為例,按照 MVC 的劃分原則,我們首先需要有的 Model 數(shù)據(jù)是:
- a) 坐標(biāo)數(shù)據(jù) x,y (之前課堂已經(jīng)添加了);
- b) 是否顯示,之前課堂沒有添加,因此本課需為框架窗口類加上該成員數(shù)據(jù),取名 showMotionInfo,一個 bool 值。
接著,當(dāng)用戶依賴某個操作路徑(菜單或工具按鈕),發(fā)起切換操作時,作為一個GUI開發(fā)框架,wxWidget將配合作為一個 GUI 操作系統(tǒng)的 Windows,共同幫我們完成復(fù)雜的 Controller 過程(視頻里說的那些“線”),最終調(diào)用到我們寫的事件響應(yīng)函數(shù),該函數(shù)修改作為 Model 數(shù)據(jù)之一的 showMotionInfo。如何知道將它修改成 true 或 false 呢?也就是說,如何知道用戶當(dāng)前操作是選中或是取消選中呢?仍然得感謝 wxWidgets 和其下的操作系統(tǒng),它已經(jīng)將該信息,放在事件數(shù)據(jù)中的 “IsCheck()” 方法里了。
接受用戶輸入,修改 Model 數(shù)據(jù)(showMotionInfo)的示例代碼:
// 1 綁定到菜單上:
EVT_MENU(idMenuShowMotionInfo, HelloToolBarFrame::OnShowMotionInfo)...// 2 在事件響應(yīng)函數(shù)中,修改 Model,狀態(tài)信息在事件中:
void HelloToolBarFrame::OnShowMotionInfo(wxCommandEvent& event)
{this->showMotionInfo = event.IsChecked(); // 修改 model
}
接下來,程序在某個時刻發(fā)現(xiàn)自己很閑,于是它去開始檢查并糾正哪個“孩子”(控件)狀態(tài)不對——但是,它自己并不知道,也不記錄具體哪個控件所應(yīng)該處于正確狀態(tài)是什么——誰知道,當(dāng)然只有作為程序員的我們知道,所以程序會自動觸發(fā) UpdateUIEvent 事件,讓我們在這個事件里去告訴它,某個“孩子”現(xiàn)在的正確狀態(tài)應(yīng)該是:在睡覺……如果有十個不同ID的控件需要更新狀態(tài),程序會觸發(fā)十次這樣的事件。
以 “idMenuShowMotionInfo” 為例,空閑的程序想知道綁定該ID的菜單項和工具圖標(biāo)當(dāng)前應(yīng)該處于什么狀態(tài)時,它會觸發(fā)以下的事件函數(shù)被調(diào)用:
// 3 綁定控件ID
EVT_UPDATE_UI(idMenuShowMotionInfo, HelloToolBarFrame::OnUpdateShowMotionInfo)...// 4 在事件中,由程序員告訴程序,當(dāng)前控件的正確狀態(tài)(是否選中)
void HelloToolBarFrame::OnUpdateShowMotionInfo(wxUpdateUIEvent& event)
{event.Check(this->showMotionInfo); // 有沒有選中
}
請對比代碼中的 3 和 4,從 “SET/寫” 和 “GET/讀” 的角度,加以理解。
以上講的是控件的復(fù)選狀態(tài),它的狀態(tài)就是 “選中” 或 “沒選中”;對比之下,單選狀態(tài)稍顯復(fù)雜。因為單選狀態(tài)的控件通常成組出現(xiàn)(至少兩個),對應(yīng)的詳細(xì)狀態(tài)應(yīng)該是:“選中哪一個了?”。再次感謝所有 GUI 庫,當(dāng)它們在觸發(fā)控件的 “UpdateEvent” 時,會帶上這個控件的 ID,因此,我們只需檢查它是不是我們記錄的,用戶選中的那個控件ID即可。
比如顏色選擇有兩項:藍(lán)或紅,當(dāng)用戶做出選擇,我們就用 Model 數(shù)據(jù)(selectedColorId) 記錄下來:
// 5 綁定控件ID,同一組單選控件需要全部綁定到同一個事件函數(shù):
EVT_UPDATE_UI(idMenuBlueText, HelloToolBarFrame::OnUpdateTextColor)
EVT_UPDATE_UI(idMenuRedText, HelloToolBarFrame::OnUpdateTextColor)...// 6 記錄用戶選的是哪個ID
void HelloToolBarFrame::OnTextColorSelected(wxCommandEvent& event)
{selectedColorId = event.GetId();
}
對應(yīng)的,當(dāng)程序因為閑著也是閑著,又要 “打孩子” 時,我們是這樣告訴它哪個 “孩子” 需要被選中,哪個 “孩子” 不需要:
// 7 同樣需要綁定一組單選控件(的ID)
EVT_UPDATE_UI(idMenuBlueText, HelloToolBarFrame::OnUpdateTextColor)
EVT_UPDATE_UI(idMenuRedText, HelloToolBarFrame::OnUpdateTextColor)...// 8 在事件中,由程序員告訴程序,它來“問”的控件,是不是被選中的那個
void HelloToolBarFrame::OnUpdateTextColor(wxUpdateUIEvent& event)
{event.Check(event.GetId() == this->selectedColorId);
}
5 Model 數(shù)據(jù)
最后,匯總一下本例程用到的 Model 數(shù)據(jù):
int xPos, yPos;int selectedColorId = idMenuBlueText;bool showMotionInfo = false;
其中,前兩項是在之前的課堂定義的。