本文將介紹以下內容:
有關 GUI 編程的問題
創建窗口對象
處理事件和通知
窗體和控件
本文使用以下技術:
Win32 API、C++
目錄
兼有本機和可移植性
無 windows.h
處理每個窗口
直觀的代碼
控件與窗體
窗體編程
處理窗體
脫離舊 ID
事件和通知
菜單、快捷方式及類似項
選項卡控件和窗體
調整大小
與 Visual Studio 2005 集成
實現行為
使用 C++ 進行 GUI 編程的問題是大多數庫的級別太低,給編程人員帶來了太多負擔。這些庫依賴類似 C 語言的結構,或者它們的包裝類不能隱藏足夠的復雜性。而且,它們不能使事件編程足夠簡單,反而迫使您必須了解有關基礎 WM_ 消息的知識。
在本文中,我將為您介紹 eGUI++,這是我編寫的一個 C++ 庫,可為您(客戶端編程人員)提供一種處理 GUI 應用程序的高級界面。它可以隱藏復雜性,通過完全隱藏 WM_ 消息的知識使事件編程變得相當簡單。您不需要處理任何類似 C 語言的原始結構;始終只需要處理類。總之,eGUI++ 客戶端代碼易於閱讀,也易於編寫。
eGUI++ 只在 Windows® 中運行。我實在不信任跨平台的 GUI 應用程序,除非是在不太重要(只是比較簡單的測試框架、原型)或者僅供教學的場合使用這樣的應用程序。更重要的是,我真的認為應該善用基礎操作系統提供的所有功能。而 Windows XP 和 Windows Vista® 的確提供了不少功能。
兼有本機和可移植性
那些期望使用 CLR 代碼的人,你們已掌握了 C++。那是一個很好的平台,所以無需對其進行改進。其余渴望使用良好的庫來為 Windows 2000 及更新的操作系統生成本機 Windows 代碼的人,請繼續閱讀。您會對結果滿意的;該庫利用您的目標操作系統,使用起來很直觀。並且您根本不需要使用 Microsoft® .NET Framework。您編寫的代碼使用起來就像 C++ 代碼。此外,您將編寫的代碼不是特定於 Visual C++ 編譯器的。如果您願意,可使用 g++(GNU C++ 編譯器)4.1 編譯自己寫的代碼。基本上,如果您封裝了 Win32® API,就沒什麼能阻止您編寫可移植代碼了。
也就是說,對於重要的 GUI,您需要良好的 IDE,如 Visual Studio® 2005 或 Visual Studio 2008 速成版。我已調整了我的庫,以與 Visual Studio 2005 Express 及更新的版本集成,為您提供更好的 GUI 體驗。我真正重視的是代碼補全功能,以便確保當您創建新的 GUI 類或擴展現有 GUI 類時 IDE 會盡力提供幫助。
我希望享受編寫 GUI 應用程序的過程。因此,我創建 eGUI++ 的目的是使 GUI 代碼易於閱讀和編寫。例如,我已在所有可能的地方實現了代碼補全功能。這樣,GUI 編程便安全了(如果存在錯誤,只要有可能,我就會在編譯時將其捕獲;否則,將引發運行時異常)。eGUI++ 適合資源編輯器(它與 Visual Studio 2005 和資源編輯器的更新版本進行了集成)。
無 windows.h
包括 windows.h 的主要問題是它太容易產生錯誤。什麼能讓您停止監視永遠不會發生的事件?假設您是 button 類,而在等待鍵盤事件;您將永遠不可能等到。
那麼,您為什麼還需要 windows.h 呢?您應該能夠使用常規 C++ 類用 C++ 編寫 Windows 應用程序。因此,您無需知道 windows.h 的內部信息:無需知道 WM_LBUTTONDBLCLK、WM_LBUTTONUP 以及其他較長的事件名稱。無需知道 LPNMITEMACTIVATE、NMHDR 或其他任何可疑的 C 結構。也無需知道更多的 C 樣式轉換。良好的 C++ GUI 庫的主要功能應該是抽象出 Win32 API 並允許您處理類。
您是否對原始 C 結構(如圖 1 所示)做了充分的處理?我知道我做了。因此,我現在可以按如下方式編寫代碼:
wnd<rebar> w = new_(parent);
rebar::item i(rebar::item::color | rebar::item::text);
w->add(i);
圖 1 補全舊窗口代碼
// The old way
hwndRB = CreateWindowEx(WS_EX_TOOLWINDOW,
REBARCLASSNAME, NULL,
WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|
WS_CLIPCHILDREN|RBS_VARHEIGHT|
CCS_NODIVIDER,
0,0,0,0, hwndOwner, NULL, g_hinst, NULL);
...
rbi.cbSize = sizeof(REBARINFO);
rbBand.cbSize = sizeof(REBARBANDINFO);
rbBand.fMask = RBBIM_COLORS | RBBIM_TEXT |
RBBIM_BACKGROUND;
rbBand.fStyle = RBBS_CHILDEDGE;
您可以看到,eGUI++ 隱藏了復雜性:您只需處理 C++ 類,而無需記住復雜的 API 函數(如 CreateWindowEx)、常數名稱(WS_* 常數)或類似 REBARPARAMINFO 的復雜 C 結構。
eGUI++ 面向 Windows 2000 及更新版本。默認情況下,它的目標操作系統是 Windows XP SP2。但是,您可以選擇讓它面向其他操作系統,以便減少或增加可用功能。如果希望使其面向其他操作系統,只需在包含任何 eGUI++ 標頭之前將 #define EGUI_OS 指定為其他操作系統常數(參見圖 2)。
圖 2 指定操作系統
// code from eGUI++
struct os {
typedef enum type {
win_2k,
win_2k_sp4,
win_xp,
win_xp_sp2,
win_vista
};
};
#ifndef EGUI_OS
#define EGUI_OS os::win_xp_sp2
#endif
當知道此代碼中的 #ifdefs 很少時,您會非常高興,因為這樣閱讀起來就容易多了。而您也會注意到某些屬性是特定於操作系統的:
property<int,os::win_xp> some_prop;
例如,當您面向早期版本的操作系統並嘗試使用上面的屬性時,會發生編譯時錯誤。
處理每個窗口
通常是您(編程人員)決定對象存在多長時間。但是,GUI 編程中一個很大的問題是可視窗口和窗口對象之間存在區別 — 決定何時關閉窗口的是用戶。因此,在您的代碼中,您可以設置指向窗口已被用戶銷毀的窗口對象的有效指針或對它的引用。由於這使維護一對一對應關系(也就是說屏幕上的一個窗口代表一個對象實例,反之亦然)變得困難,所以由此方案產生的一個明確限制就是您無法擁有本地窗口實例(當作用域存在時將被銷毀):
{
form f(...);
f.show();
...
}
假定您的窗體 f 顯示在屏幕上。當您退出 f 的作用域時,會發生什麼?您有兩種選擇:在銷毀與該窗體對應的 C++ 實例時,銷毀該(屏幕上)窗體或將其留在屏幕上。兩個選擇都是不合理的。在第一種情況下,用戶可能會感到迷惑,不知道該窗體哪裡去了。在第二種情況下,雖然您將窗體留在屏幕上,但因為其對應的 C++ 實例不在了,所以它不會響應任何事件。此外,用戶可能已在當 f 位於作用域中時關閉了此屏幕上窗體。因此您應中止對不存在於屏幕上的對象的處理。
解決方案:始終通過(間接)指針訪問窗口。然後,當此用戶關閉屏幕上窗口時,相應的 C++ 實例會標記為無效;如果您嘗試訪問它,將引發異常。您總是可以了解某窗口是否有效,並可以自行銷毀該窗口,如下所示:
wnd<> w = ...;
// is window valid?
if ( is_valid(w) ) w->do_something();
// is window valid?
if ( w) w->do_something();
// destroy the window
delete_(w);
由於屏幕上的每個窗口都有一個相應的 C++ 實例,您將使用 wnd<> 模板類處理窗口,該類代表指向窗口的共享(引用計數)指針。wnd<> 類具有一個可選參數:窗口類型。這基本上在您預想之中。默認情況下,此參數為 window_base。它也可以是文本、標簽、rebar、編輯等窗口類。圖 3 顯示了幾個使用多種窗口對象並在它們之間進行轉換的示例。
圖 3 創建窗口對象並轉換
// when constructing a window, you can specify its type
wnd<> w = new_<form>(parent);
w->bg_color( rgb(0,0,0));
// when constructing a window, if you don't specify its type,
// it will guess it, based on who you assign it to
wnd<button> b = new_(w, rect(10,10,200,20) );
b->events.click += &my_func;
// destroying a window
delete_(b);
// casting - if it fails, it throws
wnd<form> f = wnd_cast(w);
// casting - if it fails, returns null
if ( wnd<edit> e = try_wnd_cast(e) )
e->text = "not nullio";
請注意,使用 new_ 函數創建窗口,使用 delete_ 函數刪除窗口(同樣,通常是由用戶關閉窗口)。另外,如果您擁有一個類型為 X(默認為 window_base)的窗口,並希望知道它是否也屬於類型 Y,就可以使用轉換。轉換始終是顯式的。轉換可以分為兩類:wnd_cast,如果失敗,將引發異常;try_wnd_cast,如果失敗,將返回空窗口。
開發 eGUI++ 類時,除從其基類派生的行為之外,您有時希望繼承一些其他行為,例如調整大小、更換外觀等。在這種情況下,您可以創建多個可再用的行為類,然後從這些行為類派生其他行為類。
直觀的代碼
看到 GUI 代碼易於編寫和閱讀是很讓人興奮的。我在此書中用盡各種辦法來確保代碼補全功能提供盡可能多的幫助。處理大型 GUI 庫時,總會有一些容易遺忘的內容:屬性名、事件名稱、標志等。我已對其一一進行處理。我曾經使用 doxygen 處理文檔;效果很好。我越常使用,就越喜歡它。
浏覽文檔變得非常簡單。要查看屬性名,只需鍵入 w->,便可以看到方法和屬性,如圖 4 所示(屬性顯示為成員變量,以便於區分)。對於事件名稱,記住類可以處理的事件是很容易的;只需鍵入 class_name::ev:: 和范圍運算符,之後代碼補全功能便會啟動並向您顯示事件。但處理標志才是 eGUI++ 真正的亮點所在。對於每個可由標志組成的屬性,要找出可用的標志選項,只需向該標志屬性添加“.”,這樣代碼補全功能就再次派上用場了。作為補充,我還為屬性添加了運算符重載。因此,以下代碼是有效的:
w->text = "hello";
w->text += " world";
w->style |= w->style.tiled;
圖 4 代碼補全功能
為了防止忘記您可以自由支配的控件列表,我已專門為該列表添加了名為 egui::ctrl 的命名空間。
控件與窗體
如果您以前進行過 Win32 GUI 編程,應該對對話框很熟悉。您也應該很了解 API 處理對話框創建 (::CreateDialog) 的方式與處理窗口創建的方式 (::CreateWindow[Ex]) 有很大不同。作為編程人員,您無需記住兩個簽名差異很大的復雜函數。它們均屬於窗口類型。eGUI++ 只有一種創建窗口的方式:new_ 函數。
對於不同類型的窗口,窗體這個名稱要比對話框這個名稱更有表現力。它描述顯示其他包含數據的控件的窗口。這兩個名稱我都接受,但是我更喜歡用窗體這個名稱。實際上,從代碼中您會看到:
typedef form dialog;
從概念上講,只有兩種窗口類型:控件和窗體。控件是顯示一些數據的窗口,它可能允許用戶進行修改。每個控件類都是從“控件”類派生的。窗體是承載一個或多個控件以及它們的一些自身邏輯(例如,允許對某些數據進行操作的邏輯)的窗口。
每個窗口類型的實際功能根據該窗口的用途而變化。例如,您需要注意,窗體允許您枚舉其子控件,而控件不允許;這樣,您的代碼就不易出錯了。同時,您也很少需要創建控件;通常它們已經存在於窗體上了 — 因為您已使用資源編輯器將它們放置在那裡。
窗體本身劃分為兩種類型:模式對話框和消息框。要創建模式對話框,只需在創建窗體時添加 form::style::modal。要創建消息框,請使用 msg_box<> 函數將按鈕指定為模板參數:
if ( msg_box<mb::ok | mb::cancel>("q") == mb::ok)
std::cout << "ok pressed";
此外,msg_box<> 知道按鈕組合在編譯時是否有效:
// ok
msg_box<mb::yes | mb::no>("q");
// compile-time error
msg_box<mb::ok | mb::yes>("q");
窗體編程
重申一下,窗體在 Win32 API 中稱為“對話框”。對於 Windows 窗體而言,已證明窗體編程是一項成功的策略。每個窗體上均有一些控件,而每個窗體只解決一項任務。您可以使用能承載控件或其他窗體的選項卡,而無需使用又舊又復雜的單文檔界面 (SDI) 或多文檔界面 (MDI)。因此,您不會看到任何 CFrameWnd、CMDIChildWnd 或類似內容;它們沒有存在的必要。如果您希望在一個窗體上承載多個窗體,只需使用 tab_form 類。使用該類,您可添加子窗體,每個子窗體都位於自已的選項卡上。
處理窗體
盡管我討厭向導,但我知道有時一些向導確實可以使編程任務變得簡單。因此,我在創建窗體之前創建了“新建類”向導。在類視圖中,選擇“添加類”,然後在“類別”中,選擇“eGUI”。在左側,選擇“eGUI 窗體”,單擊“添加”。指定類名稱,便完成了創建(圖 5)。該向導將創建一個名為“<dlgname>.h”的頭文件,一個名為“<dlgname>.cpp”的源文件以及一個名為“<dlgname>_form_resource.h”的附加頭文件,eGUI++ 會在內部維護這些文件。
圖 5 添加類
最後一個頭文件包含您在窗體中使用的所有控件名稱。因此,您無需創建額外的控件變量和使用數據交換(像在 MFC 中一樣),而直接使用控件。假設您擁有一個登錄對話框,該對話框具有兩個編輯框(“用戶名稱”和“密碼”)和兩個按鈕(“確定”和“取消”),如圖 6 所示。
圖 6 編輯框和按鈕
將為您生成下列文件:
// login.h
#pragma once
#include "login_form_resource.h"
struct login : form,
private form_resource::login {};
// login.cpp
#include "stdafx.h"
#include "login.h"
請注意,此代碼相當簡單;沒有類似 "enum {IDD = ...}" 的向導樣式代碼或消息映射。如果您不需要自定義構造函數則不需要提供,使用默認的構造函數即可。
登錄類只從 form_resource::login 派生,而 form_resource::login 是在 login_form_resource.h(此文件由 eGUI++ 庫維護)中實現的;form_resource::login 類包含有關窗體的控件的信息(它們的名稱和類型,以及從控件捕捉通知的能力);您可以選擇更改派生的可訪問性類型,不過我反對這樣做。像您的類成員數據通常是私有的一樣,窗體同樣如此 — 其控件應是私有的。
這樣,生成的 form_resource::login 看起來類似於以下代碼:
// login_form_resource.h
#pragma once
struct form_resource::login {
// ... (code to allow
// handling of notifications)
wnd<edit> username;
wnd<edit> passw;
wnd<button> ok, cancel;
};
這使您可以輕松地處理窗體的控件。假設您希望確保密碼是“secretword”:
void login::on_button_click(ev::button_click &, ok_) {
if ( passw->text == "secretword")
{ pass_ok = true; visible = false; }
}
您可以看到,像使用 Visual Basic® 一樣,要隱藏窗體,只需將其 visible 屬性設置為 false。
脫離舊 ID
您以前可能處理過資源編輯器,並且遇到過許多資源前綴類型,例如 ID_、IDD_、IDC_、IDR_、IDS_ 等。前綴適用於資源編輯器。但是,在代碼中,它們只是額外信息,您不需要記住或考慮它們。在 eGUI++ 應用程序中,因為這些前綴會被徹底忽略,所以您根本不需要記。
例如,以前的名稱(username、passw、ok、cancel)是資源編輯器的快捷方式。eGUI++ 庫自動去除了它們的 ID* 前綴。而原始名稱本應為 IDC_username、IDC_passw、IDOK 和 IDCANCEL。
事件和通知
我曾提到,您無需記住單個 WM_ 消息。也就是說,事件是很難馴服的。事件實在太多了,所以您需要一種簡便方法來找出您可以響應的事件並輕松地響應它們。您需要找到簡便的方法,以便在窗體控件上發生事件時您能夠得到通知,從而可以擴展控件和添加自己的事件。
每個窗口類(控件或窗體)都可以生成事件。對於每個窗口類,都有一個可捕捉所有事件的事件處理程序。對於每個事件,都會定義一個函數來處理該事件;該函數是虛擬的,其實現不會起任何作用。每個事件處理程序函數都具有一個參數:事件數據。
對於現有控件,對應的事件類稱為 handle_events::control_name。每個現有 eGUI++ 窗口類 wnd_name 都已從 handle_events::wnd_name 派生。如果擴展現有的窗口類,則您始終可以處理其事件。(簡而言之,所有事件處理程序函數均以“on_”開始。)例如:
struct my_btn : button {
void on_char(ev::char& e) {
cout << "typed " << e.ch;
}
};
如果您曾處理過其他 GUI 庫,就會知道眼見不一定為憑,事情沒有您想的那麼簡單。現有的控件不發送事件,而是發送通知。通知以 WM_COMMAND/WM_NOTIFY 消息的形式發送,並且發送到該控件的父級,而不發送到該控件自身。最初,這似乎很合乎情理:確實是控件的父級(窗體)需要通知。但是,這使擴展控件類變得相當困難。如果您希望構建樹來使當前文件系統可視化,該如何做呢?您需要捕獲將要發送到控件的父級的事件,如項目擴展 (TVN_ITEMEXPANDING)。接著,您需要一種方法將通知向下傳遞到控件自身。
對於 eGUI++ 來說,通知便是事件。因此,這些通知總是發送到控件,然後發送到控件的父級。當通過繼承擴展控件類時,每個通知都將轉換成其他事件。例如,如果您希望在用戶編輯第一個列時創建顯示復合框的列表控件而不是編輯控件,代碼應該如下所示:
struct list_with_combo : list {
...
void on_begin_label_edit(
ev::begin_label_edit & e) {
e.allow_default = false;
combo->rect(...);
combo->visible = true;
}
wnd<combo_box> combo;
};
要處理事件,需重載事件處理程序函數,如下所示:
struct my_btn : button {
void on_char(ev::char& e);
};
此處您是響應字符按下事件;如果您喜歡使用 Win32 API,則是響應 WM_CHAR 消息。
請記住,對於 on_my_event 事件處理程序函數,事件參數始終為 ev::my_event 類型。您的類可以處理的所有事件都是 ev:: 結構。只需鍵入 ev::,代碼補全功能便會向您顯示您的類可以處理的所有事件(參見圖 7)。請注意,查找事件信息的最簡便方法是鍵入 e.,從而使代碼補全功能顯示所有與此事件有關的數據(參見圖 8)。
圖 7 代碼補全顯示事件
圖 8 獲得事件信息
您可以通過浏覽文檔來查看控件的事件:只需在選擇該控件後選擇其 ev 類,就會看到它的所有事件。此庫可以將同一事件發送到多個事件處理程序(例如,將通知發送到控件,然後發送到此控件的父級)。
所有事件都具有 .sender 屬性;它代表發送事件的控件(此屬性對於通知很有用,尤其是在查找通知的發送人方面)。所有事件都具有 .handled 屬性;此屬性可能具有兩個值:handled_partially(默認)和 handled_fully。通過將此屬性設置為 handled_fully,您可以停止事件處理;即使有更多事件處理程序,也不會調用它們。例如,如果您正在擴展編輯類,並希望阻止通知父級文本更改,您應編寫以下代碼:
struct independent_edit : edit {
void on_change(ev::change &e) {
e.handled = handled_fully;
}
};
我在上面介紹過,擴展控件很簡單。不過,在窗體上處理通知也應該很簡單。當處理通知時,您需要知道發送方 (e.sender)。除此之外,您還需要能夠從特定控件處理通知。因此,事件處理程序函數另有一個額外參數:控件名稱,後面加下劃線 (_)。例如,要查找用戶在用戶名編輯框鍵入的內容,應運行以下代碼:
void login::on_change(
edit::ev::change &e, username_) {
cout << "name=" << e.sender->text;
}
例如,假設您希望在美元和歐元之間轉換貨幣。當您在“EUR”框輸入值並鍵入內容時,“USD”框會更新。當您在“USD”框輸入值並鍵入內容時,“EUR”框會更新,如圖 9 所示。以下是執行此操作的代碼:
struct convert : form, form_resource::convert {
double rate;
convert() : rate(1.5) {}
int mul_str(const string& a, double b) { ... }
void on_change(edit::ev::change&, eur_) {
usd->text = mul_str ( eur->text, rate); }
void on_change(edit::ev::change&, usd_) {
eur->text = mul_str ( usd->text, 1/rate); }
};
圖 9 貨幣交換器
此代碼不言而喻;mul_str 通過將字符串轉換成雙精度型並將其與轉換率相乘,使雙精度型與字符串相乘。
要像上面那樣處理事件,我必須做大量工作。假設您擁有一個具有三個編輯框的窗體。其中每個編輯框都將生成某一組事件。對於每個這樣的事件(例如,on_change),我可以為每個控件生成一個可覆蓋的函數
void on_change(edit::ev::change& e, ctrlname_);
或者只生成一個可覆蓋的函數:
void on_change(edit::ev::change& e);
我更喜歡前一種解決方案 — 客戶端代碼比較簡單(與 Visual Basic 方法更加類似)。您可以輕松地看到處理的內容(後一種解決方案與其不同,在事件的實施過程中,您必須手動查詢通過 e.sender 生成事件的控件)。
這就是我實施第一種解決方案的原因。但是,實際上這其中也包含大量工作。eGUI++ 會監視資源編輯器。當添加新的控件或重命名控件時,會更新所有 <dlgname>_form_resource.h 文件。請注意,對於 form_resource::<dlgname> 類中的每個 <dlgname>_form_resource.h 文件,您必須從現有控件覆蓋所有通知,而對於每個這樣的已覆蓋通知,則查找可以發送它的控件。下一步是生成將轉發到每個控件的另一個可覆蓋函數的實現。例如,圖 10 顯示了適用於具有兩個編輯框和兩個按鈕的登錄窗體的代碼。
圖 10 登錄窗體代碼
struct form_resource::login {
wnd<edit> name;
wnd<edit> passw;
wnd<button> ok, cancel;
typedef ... ok_;
typedef ... cancel_;
typedef ... name_;
typedef ... passw_;
virtual void on_change(edit::ev::change& e, name__) {}
virtual void on_change(edit::ev::change& e, passw__) {}
virtual void on_change(edit::ev::change& e) {
if ( e.sender == name) on_change(e, name__());
else if ( e.sender == passw) on_change(e, passw__());
}
// ... same for other edit notifications
virtual void on_click(button::ev::click & e, ok__) {}
virtual void on_click(button::ev::click & e, cancel__) {}
virtual void on_click(button::ev::click & e) {
if ( e.sender == ok) on_click(e, ok__());
else if ( e.sender == cancel) on_click(e, cancel__() );
}
// ... same for other button notifications
};
最後,您可以通過從 new_event<> 派生事件來創建自己的事件。無論您是發送現有事件還是您自己的事件,過程是相同的:使用 send_event 函數:
struct hover : new_event<hover> {
int x,y; // position
hover(int x,int y) : x(x),y(y) {}
};
w->send_event( hover(x,y) );
此庫是線程安全的。另外,每個窗口都具有 m_cs 互斥變量(基本上是 CRITICAL_SECTION),我用該變量來確保每個方法訪問都是線程安全的。在擴展窗口類時,您可以重用 m_cs 變量或創建自己的變量 — 這由您決定。
菜單、快捷方式以及類似項
如果以前進行過 GUI 編程,您應該知道按下菜單命令和按下鍵都會導致發送 WM_COMMAND。因此,當您收到 WM_COMMAND 時,很難知道該事件是來自控件還是菜單(或者說鍵盤快捷方式)。eGUI++ 通過直接將菜單放置在窗體(對話框)上來解決此問題的第一方面。如果發送到窗體的命令不是來自按鈕,便是來自菜單。
這樣可保護菜單命令。現在讓我們看一下快捷方式。快捷方式的問題是可以隨時鍵入(例如,在編輯框中時)。鍵盤快捷方式(加速器)首先路由到即時窗口,然後路由到承載該窗口的窗體,接著路由到窗體的父級,並沿層次結構向上路由直至到達最上面的窗口。您第一次找到該快捷方式的事件處理程序時,處理便停止(給定快捷方式不會由兩個或多個窗口處理)。
剩下的就是工具欄 — 它們與菜單和快捷方式緊密相關。當按下工具欄按鈕時,該事件會轉換成菜單命令並直接路由到承載它的窗體(無論該命令是來自菜單、快捷方式或者工具欄按鈕,都無關緊要)。
假設您正在實現一個窗體以處理菜單命令,如下所示:
void on_menu_command( ev::menu&,
menu::some_menu_id) { ... }
要處理 new_file 和 open_file 這兩個菜單命令,您需要創建以下處理程序:
void on_menu_command( ev::menu&,
menu::new_file) { ... }
void on_menu_command( ev::menu&,
menu::open_file) { ... }
選項卡控件和窗體
選項卡是極為常用的 GUI 模式。我已擴展了選項卡控件,以允許 tab_type 屬性使用標准值或 one_dialog_per_tab 值(在這種情況下,該控件可承載其他窗體)。在後一種情況下,您可以添加新的窗體,如下所示:
tab->add_form<form_type>( new_([args]) );
要添加我前面提到的登錄窗體,您應編寫:
tab->add_form<login>( new_() );
當添加至少一個窗體後,您便可以指定此選項卡窗體可承載的選項卡的數目:
tab->count = 5;
這裡,我需要五個選項卡。這會選用最後添加的窗體,並根據需要多次進行復制。假設以前只有一個選項卡,則需要將第一個選項卡上的窗體另外克隆四次。沒錯。您可以克隆現有的任何窗口!
到目前為止,我已向您介紹了事件的侵入式處理。換句話說,就是擴展窗口類並最終響應其事件(或者說,在實現窗體時,響應其通知)。但是,您有時需要實現應用於多個窗口(彼此有些不相關)的行為。
例如,調整大小和改變外觀。您可以以侵入的方式(創建實現該行為的那些類,然後從那些類派生 GUI 類)實現這類行為。但是,這會使代碼復雜化,而且這並非始終可行(以改變外觀為例)。通過非侵入方式實現行為,可以在其他應用程序中重用這種行為,也可以輕松關閉它。
例如,創建非侵入式事件處理程序類,再創建其實例,然後注冊它。創建完新窗口後,會通知您的處理程序實例,而您可以選擇是否監視此實例。如果選擇監視,則需要手動指定需要監視的事件,如下所示:
// monitor button clicks
struct btn_handler : non_intrusive_handler {
void on_new_window_create(wnd<> w) {
if ( wnd<button> b = try_cast(w)) {
b->events.on_click += mem_fn(&on_click,this);
}
}
void on_click(button::ev::click&) { ... }
};
注冊事件處理程序很容易。只需執行以下代碼:
btn_handler bh;
window_base::add_non_intrusive_handler(bh);
可以使用多種方式實現可調整大小行為,具體取決於您的應用程序。例如,您可以在每個窗體上覆蓋 on_size 事件並基於新窗體大小更新控件的位置(很笨的方法,工作量大)。或者,可在每個窗體上創建控件間的關系,如“a.x = b.x + b.width + 4;”(此方法非常靈活,但工作量也很大)。
或者,可在每個窗體上將控件標記為在每個軸上可調整大小或可移動。如果將控件標記為在某軸上可調整大小,則當窗體大小更改時,控件大小將更新;如果將控件標記為在某軸上可移動,則當窗體大小更改時,控件將移動。這對於大多數應用程序已經足夠了。我從 WTL 的 CResizeWindow 借用了這一理念,並使用非侵入式處理程序實現了它。假設您有一個類似圖 11 的對話框。如果您希望在調整大小後使其外觀類似圖 12 中的樣子,則需要使用下面的代碼:
resize(name, axis::x, sizeable);
resize(desc, axis::x | axis::y, sizeable);
resize(ok, axis::x | axis::y, moveable);
resize(cancel, axis::x | axis::y, moveable);
圖 11 對話框
圖 12 調整大小後的對話框
每個失敗的 GUI 操作都將觸發異常。這樣,您就知道出錯了。在調試模式中,這將生成失敗的聲明,並且程序會中斷調試模式。這比無提示地忽略錯誤好得多,因為這可以了解到有東西出錯了(以可視的方式),然後可以查找該錯誤。
與 Visual Studio 2005 集成
Visual Studio 是出色的 IDE,它的一個主要優點是可以擴展。eGUI++ 利用了這個優點;它附帶提供新建窗體類向導的加載項。它還提供了類似 Visual Basic 的欄,該欄使您可以在窗體上處理控件通知。要執行此操作,只需選擇一個控件,然後查看該控件可以生成的通知的列表。請注意,已經處理的通知會以粗體顯示;單擊某事件,將添加一個處理程序(如果以前不存在此處理程序)— 請參見圖 13 中的示例。
圖 13 在窗體上處理控件通知
實質上,eGUI++ 會監視資源編輯器,以便在內容發生更改時可以更新 _form_resource.h 文件(如果需要)。它使用代碼補全功能完成此任務,對此我已經進行了詳細介紹。
實現行為
構建 GUI 後,下一步是實現行為和考慮數據綁定。很多窗體只用於收集數據。對於初學者來說,可以實現一個通用窗體類,該類在構建過程中獲取要處理的數據並將其綁定到窗體的控件。然後,您可以指定一組用於驗證數據的規則。在析構過程中,如果驗證規則成功,則使用來自控件的值更新原始數據;否則,原始數據將保持不變。因此,對於每種新窗體,只需在資源編輯器中創建該窗體,然後指定用於驗證數據的一組規則(與創建新的窗體類並復制邏輯的過程相對)。
展望未來,您可以將標准模板庫 (STL) 數組和集合、列表控件和樹控件聯系起來。假設我擁有一個員工數組和一個列表控件。那麼我可以將此數組綁定到該控件,如下所示:
list_ctrl->bind(employees);
您可能預料到了,這將更新列表控件。而且,列表控件單元格上的任何更改都會自動與員工數組同步。
我構建 eGUI++ 的目的是創建一個優良的庫,以使 GUI 編程體驗變得愉快。如果您是 C++ 編程人員,我非常希望您同意我這麼說。您可以到 torjo.com 下載源文件和二進制文件。