基于 Linux 和 MiniGUI 的嵌入式系统软件开发指南(三)
对话框和控件编程
()
自由撰稿人
2001 年 7 月
本文讲述 MiniGUI 中的对话框和控件编程。首先讲解 MiniGUI
中的控件类和控件实例的关系,并举例说明控件子类化的概念及应用;其次讲解 MiniGUI
对话框的编程技术,包括对话框模板的定义和对话框回调函数的编程;最后解释模态对话框和非模态对话框之间的区别。
对话框编程是一个快速构建用户界面的技术。通常,我们编写简单的图形用户界面时,可以通过调用 CreateWindow
函数直接创建所有需要的子窗口,即控件。但在图形用户界面比较复杂的情况下,每建立一个控件就调用一次 CreateWindow
函数,并传递许多复杂参数的方法很不可取。主要原因之一,就是程序代码和用来建立控件的数据混在一起,不利于维护。为此,一般的 GUI
系统都会提供一种机制,利用这种机制,通过指定一个模板,GUI 系统就可以根据此模板建立相应的主窗口和控件。MiniGUI
也提供这种方法,通过建立对话框模板,就可以建立模态或者非模态的对话框。
本文首先讲解组成对话框的基础,即控件的基本概念,然后讲解对话模板的定义,并说明模态和非模态对话框之间的区别以及编程技术。
许多人对控件(或者部件,widget)的概念已经相当熟悉了。控件可以理解为主窗口中的子窗口。这些子窗口的行为和主窗口一样,即能够接收键盘
和鼠标等外部输入,也可以在自己的区域内进行输出――只是它们的所有活动被限制在主窗口中。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
中,对话框是一类特殊的主窗口,这种主窗口只关注与用户的交互――向用户提供输出信息,但更多的是用于用户输入。对话框可以理解为子类化之后的主窗口类。
它针对对话框的特殊性(即用户交互)进行了特殊设计。比如用户可以使用 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
对话框的编程技术,包括对话框模板的定义和对话框回调函数的编程;最后说明了模态对话框和非模态对话框之间的区别。
魏永明(),男,27
岁,工学硕士。国内最有影响的自由软件项目之一--MiniGUI 的创始人及主要开发人员。著有《Linux 实用教程》与《学用 Linux 与
Windows NT》,并主持翻译了《Red Hat Linux 奥秘》、《Linux 编程宝典》 等大量优秀的 Linux
技术著作。是清华大学 AKA Linux 编程技术系列讲座的主讲人。 |
阅读(365) | 评论(0) | 转发(0) |