专栏: 基于linux和minigui的嵌入式系统软件开发指南
第一节:MiniGUl-Threads 和 MiniGUl-Lite 的选择
第二节:理解消息循环和窗口过程 ----------- MiniGUI编程 “ Hello World ”
本文讲述 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预定义的控件类和对应类名称
在MiniGUI中,通过调用CreateWindow函数,可以建立某个控件类的一个实例。控件类既可以是表1中预定义 MiniGUI控件类,也可以是用户自定义的控件类。与CreateWindow函数相关的几个函数的原型如下(include/window.h):
HWND GUIAPI CreateWindowEx(const char* spClassName,const char* spCaption,DWORD dwStyle,DWORD dwExStyle,int id,int x,int y, int w,int h,HWND hParentWnd,DWORD dwAddData);
BOOL GUIAPI DestroyWindow (HWND hWnd);
#define CreateWindow(class_name,caption,style,id, x, y,w, h, parent, add_data)\CreateWindowEx (class_name,caption,style,0, id,x,y,w,h, parent,add_data)
CreateWindow函数建立一个子窗口,即控件。它指定了控件类、控件标题、控件风格,以及窗口的初始位置和大小。该函数同时指定子窗口的父窗口。
CreateWindowEx函数的功能和CreateWindow函数一致,不过,可以通过CreateWindowEx函数指定控件的扩展风格。DestroyWindow函数用来销毁用上述两个函数建立的控件或者子窗口。
清单1中的程序,利用预定义控件类创建控件。其中 hStaticWndl 是建立在主窗口hWnd 中的静态框;hButtonl、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, LPARAMlParam)
{
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,O);
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_CHLD| 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) :
BOOL GUIAPI RegisterWindowClass (PWNDCLASS pWndClass) ;
BOOL GUIAPI UnregisterWindowClass (const char* szClassName);
char* GUIAPI GetClassName (HWND hWnd);
BOOL GUIAPI GetWindowClassInfo (PWNDCLASS pWndClass);
BOOL GUIAPI SetWindowClassInfo (const WNDCLASS* pWndClass);
RegisterWindowClass通过pWndC1ass 结构注册一个控件类;
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 Ox0002
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_CHILDWS_VISIBLE | SS_RIGHT,0,10,10,180,24,hWnd,O);
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_CHILDWS_VISIBLE | SS_RIGHT,0,10,40,180,24,hWnd,O);
hWnd2 = CreateWindow (CTRL_EDIT,"",WS_CHILD | WS_BORDERWS_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,O);
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,O);
/*_用自定义的窗口过程替换编辑框的窗口过程,并保存老的窗口过程*/
old_edit_proc = SetWindowCallbackProc (hWnd1,RestrictedEditBox);
SetWindowCallbackProc (hWnd2,RestrictedEditBox);
break;
}
......
}
return DefaultMainWinProc (hWnd,message,wParam,lParam);
}
三、对话框和对话框模板
在 MiniGUI中,对话框是一类特殊的主窗口,这种主窗口只关注与用户的交互一一向用户提供输出信息,但更多的是用于用户输入。对话框可以理解为子类化之后的主窗口类。它针对对话框的特殊性(即用户交互)进行了特殊设计。比如用户可以使用TAB键遍历控件、可以利用ENTER键表示默认输入等等。在MiniGUI当中,在建立对话框之前,首先需要定义一个对话框模板,该模板中定义了对话框本身的一些属性,比如位置和大小等等,同时定义了对话框中所有控件的初始信息,包括位置、大小、风格等等。在 MiniGUI中,用两个结构来表示对话框模板(src/window.h) :
typedef struct
{
char*class_name; //control class
DWORD dwStyle; // control style
int x,y, w,h; // control position in
int id; // control identifier
const char* caption; // control caption
DWORD dwAddData; // additional data
DWORDdwExStyle; // control extended style
}CTRLDATA;
typedef CTRLDATA* PCTRLDATA;
typedef struct
{
DWORD dwStyle; // dialog box style
DWORD dwExStyle; // dialog box extended
int x,y, w,h; // dialog box position
const char* caption; //dialog box caption
HICON hIcon; //dialog box icon
HMENU hMenu; //dialog box menu
int controlnr; // number of controls
PCTRLDATA controls; // poiter to control array
DWORD dwAddData; // addtional data,must be
}DLGTEMPLATE;
typedef DLGTEMPLATE* PDLGTEMPLATE;
结构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,locin,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,OL);
}
DialogBoxIndirectParam以及相关函数的原型如下:
int GUIAPI DialogBoxIndirectParam (PDLGTEMPLATE pDlgTemplate,HWND hOwner,WNDPROC DlgProc,LPARAM lParam);
BOOL GUIAPI EndDialog (HWND hDlg,int endCode);
void GUIAPI DestroyAl1Controls (HWND hDlg);
在 DialogBoxIndirectParam中,需要指定对话框模板(pDlgTemplate)、对话框的托管主窗口句柄(hOwner)、对话框回调函数地址(DlgProc),以及要传递到对话框过程的参数值(lParam)。EndDialog用来结束对话框过程。
DestroyAllControls 用来销毁对话框(包括主窗口)中的所有子控件。
在清单5中,对话框回调函数并没有进行任何实质性的工作,当用户按下"确定"按钮时,调用_EndDialog函数直接返回。
四、MSG_INITDIALOG消息
对话框回调函数是一类特殊的主窗口回调函数。用户在定义自己的对话框回调函数时,需要处理MSG_INITDIALOG 消息。该消息是在 MiniGUI建立根据对话框模板建立对话框以及控件之后,发送到对话框回调函数的。该消息的 lParam 参数包含了由DialogBoxIndirectParam 函数的第四个参数传递到对话框回调函数的值。用户可以利用该值进行对话框的初始化,或者保存起来以备后用。例如,清单7中的程序将MSG_INITDIALOG 消息的 lParam参数保存到了对话框窗口句柄的附加数据中,这样可以确保在任何需要的时候,方便地从对话框窗口的附加数据中获取这一数据。
static int DepInfoBoxProc (HWND hDlg, int message,WPARAM wParam,LPARAMlParam)
{
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) :
HWND GUIAPI CreateMainWindowIndirect (PDLGTEMPLATE pDlgTemplate,HWND hOwner,WNDPROC WndProc);
BOOL GUIAPI DestroyMainWindowIndirect (HWND hMainWin);
使用CreateMainWindowIndirect根据对话框模板建立的主窗口和其他类型的普通主窗口没有任何区别。
总结
对话框编程是MiniGUI应用开发中使用最为常见的一种技术。通过定义一个对话框模板,就可以自动创建一个具有复杂输入输出界面的对话框。本文讲述了MiniGUI中的控件类和控件实例的关系,并举例说明控件子类化的概念及应用;然后讲解了MiniGUI 对话框的编程技术,包括对话框模板的定义和对话框回调函数的编程;最后说明了模态对话框和非模态对话框之间的区别。