基於 Linux 和 MiniGUI 的嵌入式系統軟體開發指南三——對話方塊和控制元件程式設計
簡介: 本文講述 MiniGUI 中的對話方塊和控制元件程式設計。首先講解 MiniGUI 中的控制元件類和控制元件例項的關係,並舉例說明控制元件子類化的概念及應用;其次講解 MiniGUI 對話方塊的程式設計技術,包括對話方塊模板的定義和對話方塊回撥函式的程式設計;最後解釋模態對話方塊和非模態對話方塊之間的區別。
對話方塊程式設計是一個快速構建使用者介面的技術。通常,我們編寫簡單的圖形使用者介面時,可以通過呼叫 CreateWindow 函式直接建立所有需要的子視窗,即控制元件。但在圖形使用者介面比較複雜的情況下,每建立一個控制元件就呼叫一次 CreateWindow 函式,並傳遞許多複雜引數的方法很不可取。主要原因之一,就是程式程式碼和用來建立控制元件的資料混在一起,不利於維護。為此,一般的 GUI 系統都會提供一種機制,利用這種機制,通過指定一個模板,GUI 系統就可以根據此模板建立相應的主視窗和控制元件。MiniGUI 也提供這種方法,通過建立對話方塊模板,就可以建立模態或者非模態的對話方塊。
本文首先講解組成對話方塊的基礎,即控制元件的基本概念,然後講解對話模板的定義,並說明模態和非模態對話方塊之間的區別以及程式設計技術。
許多人對控制元件(或者部件,widget)的概念已經相當熟悉了。控制元件可以理解為主視窗中的子視窗。這些子視窗的行為和主視窗一樣,即能夠接收鍵盤和滑鼠等外部輸入,也可以在自己的區域內進行輸出�D�D只是它們的所有活動被限制在主視窗中。MiniGUI 也支援子視窗,並且可以在子視窗中巢狀建立子視窗。我們將 MiniGUI 中的所有子視窗均稱為控制元件。
在 Windows 或 X Window 中,系統會預先定義一些控制元件類,當利用某個控制元件類建立控制元件之後,所有屬於這個控制元件類的控制元件均會具有相同的行為和顯示。利用這些技術,可以確保一致的人機操作介面,而對程式設計師來講,可以像搭積木一樣地組建圖形使用者介面。MiniGUI 使用了控制元件類和控制元件的概念,並且可以方便地對已有控制元件進行過載,使得其有一些特殊效果。比如,需要建立一個只允許輸入數字的編輯框時,就可以通過過載已有編輯框而實現,而不需要重新編寫一個新的控制元件類。
如果讀者曾經編寫過 Windows 應用程式的話,應該記得在建立一個視窗之前,必須確保系統中存在新視窗所對應的視窗類。在 Windows 中,程式所建立的每個視窗,都對應著某種視窗類。這一概念和麵向物件程式設計中的類、物件的關係類似。借用面向物件的術語,Windows 中的每個視窗實際都是某個視窗類的一個例項。在 X Window 程式設計中,也有類似的概念,比如我們建立的每一個 Widget,實際都是某個 Widget 類的例項。
這樣,如果程式需要建立一個視窗,就首先要確保選擇正確的視窗類,因為每個視窗類決定了對應視窗例項的表象和行為。這裡的表象指視窗的外觀,比如視窗邊框寬度,是否有標題欄等等,行為指視窗對使用者輸入的響應。每一個 GUI 系統都會預定義一些視窗類,常見的有按鈕、列表框、滾動條、編輯框等等。如果程式要建立的視窗很特殊,就需要首先註冊一個視窗類,然後建立這個視窗類一個例項。這樣就大大提高了程式碼的可重用性。
在 MiniGUI 中,我們認為主視窗通常是一種比較特殊的視窗。因為主視窗程式碼的可重用性一般很低,如果按照通常的方式為每個主視窗註冊一個視窗類的話,則會導致額外不必要的儲存空間,所以我們並沒有在主視窗提供視窗類支援。但主視窗中的所有子視窗,即控制元件,均支援視窗類(控制元件類)的概念。MiniGUI 提供了常用的預定義控制元件類,包括按鈕(包括單選鈕、複選鈕)、靜態框、列表框、進度條、滑塊、編輯框等等。程式也可以定製自己的控制元件類,註冊後再建立對應的例項。表 1 給出了 MiniGUI 預先定義的控制元件類和相應類名稱定義。
表 1 MiniGUI 預定義的控制元件類和對應類名稱
控制元件類 | 類名稱 | 巨集定義 | 備註 |
靜態框 | "static" | CTRL_STATIC | |
按鈕 | "button" | CTRL_BUTTON | |
列表框 | "listbox" | CTRL_LISTBOX | |
進度條 | "progressbar" | CTRL_PRORESSBAR | |
滑塊 | "trackbar" | CTRL_TRACKBAR | |
單行編輯框 | "edit"、"sledit" | CTRL_EDIT、CTRL_SLEDIT | |
多行編輯框 | "medit"、"mledit" | CTRL_MEDIT、CTRL_MLEDIT | |
工具條 | "toolbar" | CTRL_TOOLBAR | |
選單按鈕 | "menubutton" | CTRL_MENUBUTTON | |
樹型控制元件 | "treeview" | CTRL_TREEVIEW | 包含在 mgext 庫,即MiniGUI 擴充套件庫中。 |
月曆控制元件 | "monthcalendar" | CTRL_MONTHCALENDAR | 同上 |
旋鈕控制元件 | "spinbox" | CTRL_SPINBOX | 同上 |
在 MiniGUI 中,通過呼叫 CreateWindow 函式,可以建立某個控制元件類的一個例項。控制元件類既可以是表 1 中預定義 MiniGUI 控制元件類,也可以是使用者自定義的控制元件類。與 CreateWindow 函式相關的幾個函式的原型如下(include/window.h):
904 HWND GUIAPI CreateWindowEx (const char* spClassName, const char* spCaption, 905 DWORD dwStyle, DWORD dwExStyle, int id, 906 int x, int y, int w, int h, HWND hParentWnd, DWORD dwAddData); 907 BOOL GUIAPI DestroyWindow (HWND hWnd); 908 909 #define CreateWindow(class_name, caption, style, id, x, y, w, h, parent, add_data) \ 910 CreateWindowEx(class_name, caption, style, 0, id, x, y, w, h, parent, add_data) |
CreateWindow 函式建立一個子視窗,即控制元件。它指定了控制元件類、控制元件標題、控制元件風格,以及視窗的初始位置和大小。該函式同時指定子視窗的父視窗。CreateWindowEx 函式的功能和 CreateWindow 函式一致,不過,可以通過 CreateWindowEx 函式指定控制元件的擴充套件風格。DestroyWindow 函式用來銷燬用上述兩個函式建立的控制元件或者子視窗。
清單 1 中的程式,利用預定義控制元件類建立控制元件。其中hStaticWnd1 是建立在主視窗 hWnd 中的靜態框;hButton1、hButton2、hEdit1、hStaticWnd2則是建立在 hStaicWnd1 內部的幾個控制元件,並作為 hStaticWnd1 的子控制元件而存在,建立了兩個按鈕、一個編輯框和一個靜態按鈕;而 hEdit2 是 hStaicWnd2 的子控制元件,是 hStaticWnd1 的子子控制元件。
清單1 利用預定義控制元件類建立控制元件
#define IDC_STATIC1 100 #define IDC_STATIC2 150 #define IDC_BUTTON1 110 #define IDC_BUTTON2 120 #define IDC_EDIT1 130 #define IDC_EDIT2 140 int ControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam) { static HWND hStaticWnd1, hStaticWnd2, hButton1, hButton2, hEdit1, hEdit2; switch (message) { case MSG_CREATE: { hStaticWnd1 = CreateWindow (CTRL_STATIC, "This is a static control", WS_CHILD | SS_NOTIFY | SS_SIMPLE | WS_VISIBLE | WS_BORDER, IDC_STATIC1, 10, 10, 180, 300, hWnd, 0); hButton1 = CreateWindow (CTRL_BUTTON, "Button1", WS_CHILD | BS_PUSHBUTTON | WS_VISIBLE, IDC_BUTTON1, 20, 20, 80, 20, hStaticWnd1, 0); hButton2 = CreateWindow (CTRL_BUTTON, "Button2", WS_CHILD | BS_PUSHBUTTON | WS_VISIBLE, IDC_BUTTON2, 20, 50, 80, 20, hStaticWnd1, 0); hEdit1 = CreateWindow (CTRL_EDIT, "Edit Box 1", WS_CHILD | WS_VISIBLE | WS_BORDER, IDC_EDIT1, 20, 80, 100, 24, hStaticWnd1, 0); hStaticWnd2 = CreateWindow (CTRL_STATIC, "This is child static control", WS_CHILD | SS_NOTIFY | SS_SIMPLE | WS_VISIBLE | WS_BORDER, IDC_STATIC1, 20, 110, 100, 50, hStaticWnd1, 0); hEdit2 = CreateWindow (CTRL_EDIT, "Edit Box 2", WS_CHILD | WS_VISIBLE | WS_BORDER, IDC_EDIT2, 0, 20, 100, 24, hStaticWnd2, 0); break; } ....... } return DefaultMainWinProc (hWnd, message, wParam, lParam); } |
使用者也可以通過 RegisterWindowClass 函式註冊自己的控制元件類,並建立該控制元件類的控制元件例項。如果程式不再使用某個自定義的控制元件類,則應該使用 UnregisterWindowClass 函式登出自定義的控制元件類。上述兩個函式以及和視窗類相關函式的原型如下(include/window.h):
897 BOOL GUIAPI RegisterWindowClass (PWNDCLASS pWndClass); 898 BOOL GUIAPI UnregisterWindowClass (const char* szClassName); 899 char* GUIAPI GetClassName (HWND hWnd); 900 BOOL GUIAPI GetWindowClassInfo (PWNDCLASS pWndClass); 901 BOOL GUIAPI SetWindowClassInfo (const WNDCLASS* pWndClass); |
RegisterWindowClass 通過 pWndClass 結構註冊一個控制元件類;UnregisterWindowClass 函式則登出指定的控制元件類;GetClassName 活得視窗的對應視窗類名稱,對主視窗而言,視窗類名稱為"MAINWINDOW";GetWindowClassInfo 分別用來獲取和指定特定視窗類的屬性。
清單 2 中的程式,定義並註冊了一個自己的控制元件類。該控制元件用來顯示安裝程式的步驟資訊,MSG_SET_STEP_INFO 訊息用來定義該控制元件中顯示的所有步驟資訊,包括所有步驟名稱及其簡單描述。MSG_SET_CURR_STEP 訊息用來指定當前步驟,控制元件將高亮顯示當前步驟。
清單2 定義並註冊自定義控制元件類
#define STEP_CTRL_NAME "mystep" #define MSG_SET_STEP_INFO (MSG_USER + 1) #define MSG_SET_CURR_STEP (MSG_USER + 2) static int StepControlProc (HWND hwnd, int message, WPARAM wParam, LPARAM lParam) { HDC hdc; HELPWININFO* info; switch (message) { case MSG_PAINT: hdc = BeginPaint (hwnd); /* 獲取步驟控制元件資訊 */ info = (HELPWININFO*)GetWindowAdditionalData (hwnd); /* 繪製步驟內容 */ ...... EndPaint (hwnd, hdc); break; /* 控制元件自定義的訊息:用來設定步驟資訊 */ case MSG_SET_STEP_INFO: SetWindowAdditionalData (hwnd, (DWORD)lParam); InvalidateRect (hwnd, NULL, TRUE); break; /* 控制元件自定義的訊息:用來設定當前步驟資訊 */ case MSG_SET_CURR_STEP: InvalidateRect (hwnd, NULL, FALSE); break; case MSG_DESTROY: break; } return DefaultControlProc (hwnd, message, wParam, lParam); } static BOOL RegisterStepControl () { int result; WNDCLASS StepClass; StepClass.spClassName = STEP_CTRL_NAME; StepClass.dwStyle = 0; StepClass.hCursor = GetSystemCursor (IDC_ARROW); StepClass.iBkColor = COLOR_lightwhite; StepClass.WinProc = StepControlProc; return RegisterWindowClass (&StepClass); } static void UnregisterStepControl () { UnregisterWindowClass (STEP_CTRL_NAME); } |
採用控制元件類和控制元件例項的結構,不僅可以提高程式碼的可重用性,而且還可以方便地對已有控制元件類進行擴充套件。比如,在需要建立一個只允許輸入數字的編輯框時,就可以通過過載已有編輯框控制元件類而實現,而不需要重新編寫一個新的控制元件類。在 MiniGUI 中,這種技術稱為子類化或者視窗派生。子類化的方法有三種:
- 一種是對已經建立的控制元件例項進行子類化,子類化的結果是隻影響這一個控制元件例項;
- 一種是對某個控制元件類進行子類化,將影響其後建立的所有該控制元件類的控制元件例項;
- 最後一種是在某個控制元件類的基礎上新註冊一個子類化的控制元件類,不會影響原有控制元件類。在 Windows 中,這種技術又稱為超類化。
在 MiniGUI 中,控制元件的子類化實際是通過替換已有的視窗過程實現的。清單 3 中的程式碼就通過控制元件類建立了兩個子類化的編輯框,一個只能輸入數字,而另一個只能輸入字母:
清單 3 控制元件的子類化
#define IDC_CTRL1 100 #define IDC_CTRL2 110 #define IDC_CTRL3 120 #define IDC_CTRL4 130 #define MY_ES_DIGIT_ONLY 0x0001 #define MY_ES_ALPHA_ONLY 0x0002 static WNDPROC old_edit_proc; static int RestrictedEditBox (HWND hwnd, int message, WPARAM wParam, LPARAM lParam) { if (message == MSG_CHAR) { DWORD my_style = GetWindowAdditionalData (hwnd); /* 確定被遮蔽的按鍵型別 */ if ((my_style & MY_ES_DIGIT_ONLY) && (wParam < '0' || wParam > '9')) return 0; else if (my_style & MY_ES_ALPHA_ONLY) if (!((wParam >= 'A' && wParam <= 'Z') || (wParam >= 'a' && wParam <= 'z'))) /* 收到被遮蔽的按鍵訊息,直接返回 */ return 0; } /* 由老的視窗過程處理其餘訊息 */ return (*old_edit_proc) (hwnd, message, wParam, lParam); } static int ControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam) { switch (message) { case MSG_CREATE: { HWND hWnd1, hWnd2, hWnd3; CreateWindow (CTRL_STATIC, "Digit-only box:", WS_CHILD | WS_VISIBLE | SS_RIGHT, 0, 10, 10, 180, 24, hWnd, 0); hWnd1 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_VISIBLE | WS_BORDER, IDC_CTRL1, 200, 10, 180, 24, hWnd, MY_ES_DIGIT_ONLY); CreateWindow (CTRL_STATIC, "Alpha-only box:", WS_CHILD | WS_VISIBLE | SS_RIGHT, 0, 10, 40, 180, 24, hWnd, 0); hWnd2 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_BORDER | WS_VISIBLE, IDC_CTRL2, 200, 40, 180, 24, hWnd, MY_ES_ALPHA_ONLY); CreateWindow (CTRL_STATIC, "Normal edit box:", WS_CHILD | WS_VISIBLE | SS_RIGHT, 0, 10, 70, 180, 24, hWnd, 0); hWnd3 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_BORDER | WS_VISIBLE, IDC_CTRL2, 200, 70, 180, 24, hWnd, MY_ES_ALPHA_ONLY); CreateWindow ("button", "Close", WS_CHILD | BS_PUSHBUTTON | WS_VISIBLE, IDC_CTRL4, 100, 100, 60, 24, hWnd, 0); /* 用自定義的視窗過程替換編輯框的視窗過程,並儲存老的視窗過程。*/ old_edit_proc = SetWindowCallbackProc (hWnd1, RestrictedEditBox); SetWindowCallbackProc (hWnd2, RestrictedEditBox); break; } ...... } return DefaultMainWinProc (hWnd, message, wParam, lParam); } |
在 MiniGUI 中,對話方塊是一類特殊的主視窗,這種主視窗只關注與使用者的互動�D�D向用戶提供輸出資訊,但更多的是用於使用者輸入。對話方塊可以理解為子類化之後的主視窗類。它針對對話方塊的特殊性(即使用者互動)進行了特殊設計。比如使用者可以使用 TAB 鍵遍歷控制元件、可以利用 ENTER 鍵表示預設輸入等等。在 MiniGUI 當中,在建立對話方塊之前,首先需要定義一個對話方塊模板,該模板中定義了對話方塊本身的一些屬性,比如位置和大小等等,同時定義了對話方塊中所有控制元件的初始資訊,包括位置、大小、風格等等。在 MiniGUI 中,用兩個結構來表示對話方塊模板(src/window.h):
1172 typedef struct 1173 { 1174 char* class_name; // control class 1175 DWORD dwStyle; // control style 1176 int x, y, w, h; // control position in dialog 1177 int id; // control identifier 1178 const char* caption; // control caption 1179 DWORD dwAddData; // additional data 1180 1181 DWORD dwExStyle; // control extended style 1182 } CTRLDATA; 1183 typedef CTRLDATA* PCTRLDATA; 1184 1185 typedef struct 1186 { 1187 DWORD dwStyle; // dialog box style 1188 DWORD dwExStyle; // dialog box extended style 1189 int x, y, w, h; // dialog box position 1190 const char* caption; // dialog box caption 1191 HICON hIcon; // dialog box icon 1192 HMENU hMenu; // dialog box menu 1193 int controlnr; // number of controls 1194 PCTRLDATA controls; // poiter to control array 1195 DWORD dwAddData; // addtional data, must be zero 1196 } DLGTEMPLATE; 1197 typedef DLGTEMPLATE* PDLGTEMPLATE; 1198 |
結構 CTRLDATA 用來定義控制元件,DLGTEMPLATE 用來定義對話方塊本身。在程式中,應該首先利用 CTRLDATA 定義對話方塊中所有的控制元件,並用陣列表示。控制元件在該陣列中的順序,也就是對話方塊中使用者按 TAB 鍵時的控制元件切換順序。然後定義對話方塊,指定對話方塊中的控制元件數目,並指定 DLGTEMPLATE 結構中的 controls 指標指向定義控制元件的陣列。如清單 4 所示。
清單 4 對話方塊模板的定義
DLGTEMPLATE DlgInitProgress = { WS_BORDER | WS_CAPTION, WS_EX_NONE, 120, 150, 400, 130, "VAM-CNC 正在進行初始化, 0, 0, 3, NULL, 0 }; CTRLDATA CtrlInitProgress [] = { { "static", WS_VISIBLE | SS_SIMPLE, 10, 10, 380, 16, IDC_PROMPTINFO, "正在...", 0 }, { "progressbar", WS_VISIBLE, 10, 40, 380, 20, IDC_PROGRESS, NULL, 0 }, { "button", WS_TABSTOP | WS_VISIBLE | BS_DEFPUSHBUTTON, 170, 70, 60, 25, IDOK, "確定", 0 } }; |
在定義了對話方塊模板資料之後,需要定義對話方塊的回撥函式,並呼叫DialogBoxIndirectParam 函式建立對話方塊,如清單 5 所示,所建立的對話方塊如圖 1 所示。
清單 5 定義對話方塊回撥函式,並建立對話方塊
/* 定義對話方塊回撥函式 */ static int InitDialogBoxProc (HWND hDlg, int message, WPARAM wParam, LPARAM lParam) { switch (message) { case MSG_INITDIALOG: return 1; case MSG_COMMAND: switch (wParam) { case IDOK: case IDCANCEL: EndDialog (hDlg, wParam); break; } break; } return DefaultDialogProc (hDlg, message, wParam, lParam); } static void InitDialogBox (HWND hWnd) { /* 將對話方塊和控制元件陣列關聯起來 */ DlgInitProgress.controls = CtrlInitProgress; DialogBoxIndirectParam (&DlgInitProgress, hWnd, InitDialogBoxProc, 0L); } |
圖 1 清單 5 程式建立的對話方塊
DialogBoxIndirectParam 以及相關函式的原型如下:
1203 int GUIAPI DialogBoxIndirectParam (PDLGTEMPLATE pDlgTemplate, 1204 HWND hOwner, WNDPROC DlgProc, LPARAM lParam); 1205 BOOL GUIAPI EndDialog (HWND hDlg, int endCode); 1206 void GUIAPI DestroyAllControls (HWND hDlg); |
在 DialogBoxIndirectParam 中,需要指定對話方塊模板(pDlgTemplate)、對話方塊的託管主視窗控制代碼(hOwner)、對話方塊回撥函式地址(DlgProc),以及要傳遞到對話方塊過程的引數值(lParam)。EndDialog 用來結束對話方塊過程。DestroyAllControls 用來銷燬對話方塊(包括主視窗)中的所有子控制元件。
在清單 5 中,對話方塊回撥函式並沒有進行任何實質性的工作,當用戶按下"確定"按鈕時,呼叫 EndDialog 函式直接返回。
對話方塊回撥函式是一類特殊的主視窗回撥函式。使用者在定義自己的對話方塊回撥函式時,需要處理 MSG_INITDIALOG 訊息。該訊息是在 MiniGUI 建立根據對話方塊模板建立對話方塊以及控制元件之後,傳送到對話方塊回撥函式的。該訊息的 lParam 引數包含了由 DialogBoxIndirectParam 函式的第四個引數傳遞到對話方塊回撥函式的值。使用者可以利用該值進行對話方塊的初始化,或者儲存起來以備後用。例如,清單 7 中的程式將 MSG_INITDIALOG 訊息的 lParam 引數儲存到了對話方塊視窗控制代碼的附加資料中,這樣可以確保在任何需要的時候,方便地從對話方塊視窗的附加資料中獲取這一資料。
static int DepInfoBoxProc (HWND hDlg, int message, WPARAM wParam, LPARAM lParam) { struct _DepInfo *info; switch(message) { case MSG_INITDIALOG: { /* 將對話方塊引數 lParam 儲存為視窗的附加資料,以備後用*/ info = (struct _DepInfo*)lParam; SetWindowAdditionalData2 (hDlg, (DWORD)lParam); break; } case MSG_COMMAND: { /* 從視窗的附加資料中取出儲存的對話方塊引數 */ info = (struct _DepInfo*) GetWindowAdditionalData2 (hDlg); switch(wParam) { case IDOK: /* 使用 info 結構中的資料 */ ...... case IDCANCEL: EndDialog(hDlg,wParam); break; } } } return DefaultDialogProc (hDlg, message, wParam, lParam); } |
通常而言,傳遞到對話方塊回撥函式中的引數是一個結構的指標,該結構包含一些初始化對話方塊的資料,同時也可以將對話方塊的輸入資料儲存下來並傳遞到對話方塊之外使用。
簡單而言,模態對話方塊就是顯示之後,使用者不能再切換到其他主視窗進行工作的對話方塊,而只能在關閉之後,才能使用其他的主視窗。MiniGUI 中,使用 DialogBoxIndirectParam 函式建立的對話方塊就是模態對話方塊。實際上,該對話方塊首先根據模板建立對話方塊,然後禁止其託管主視窗,並在主視窗的 MSG_CREATE 訊息中建立控制元件,併發送 MSG_INITDIALOG 訊息給回撥函式,最終建立一個新的訊息迴圈,並進入該訊息迴圈,直到程式呼叫 EndDialog 函式為止。
實際上,我們也可以在 MiniGUI 中利用對話方塊模板建立普通的主視窗,即非模態對話方塊。這時,我們使用 CreateMainWindowIndirect 函式。該函式以及相關函式的原型如下(src/window.h):
1199 HWND GUIAPI CreateMainWindowIndirect (PDLGTEMPLATE pDlgTemplate, 1200 HWND hOwner, WNDPROC WndProc); 1201 BOOL GUIAPI DestroyMainWindowIndirect (HWND hMainWin); |
使用 CreateMainWindowIndirect 根據對話方塊模板建立的主視窗和其他型別的普通主視窗沒有任何區別。
對話方塊程式設計是 MiniGUI 應用開發中使用最為常見的一種技術。通過定義一個對話方塊模板,就可以自動建立一個具有複雜輸入輸出介面的對話方塊。本文講述了 MiniGUI 中的控制元件類和控制元件例項的關係,並舉例說明控制元件子類化的概念及應用;然後講解了 MiniGUI 對話方塊的程式設計技術,包括對話方塊模板的定義和對話方塊回撥函式的程式設計;最後說明了模態對話方塊和非模態對話方塊之間的區別。