UCGUI窗体管理及消息处理机制分析
----多对话框/模态窗体/透明窗体支持分析
作者:ucgui
日期: 2005-09-08[v1.0.0.0 2005-06-30完成]
来源: http://www.ucgui.com
版本: v1.0.0.1
版本 |
修改说明 |
时间 |
v1.0.0.0 |
ü 实现UCGUI中多对话框支持。 |
2005-06-30 |
v1.0.0.1 |
ü 增加UCGUI中各种基本消息介绍 。 ü 增加窗体消息LOOP机制介绍。 ü 增加对话框结构说明及其消息LOOP处理移到M ainTask函数中由用户处理的原因剖析,并详细分析了对话框中按钮的点击消息传送到用户自定义对话框回调函数中处理的传递流程。 ü 增加外部输入设备消息处理机制介绍。如滑动操作外设MOUSE及触摸屏输入消息(WM_TOUCH)的处理机制,及按键式操作外设消息(WM_KEY)处理机制。 ü 增加一种更简单的多对话框支持的方法及说明。 ü 增加模态对话框实现原理分析。 ü 增加透明窗体实现原理分析。 |
2005-09-08 |
|
|
|
问题的提出:
Ø [求助]关于对话框处理程序中,想在OK按钮按下后想弹出一个消息框,该怎么做?直接加在程序中好像不行,如何让消息框弹出后成为模态窗体呢?请版主帮帮忙。
Ø [解析]在UCGUI中,对话框只支持单个对话框窗体,不支持多个独立的对话框,现在我们从其源码来分析一下它为什么支持单个对话框窗体以及如何改进它以支持多个独立对话框,要讲解这个问题我们必须首先理解UCGUI中的窗体消息LOOP,没有消息LOOP窗体就是死水一潭,不能接受任何外界的输入,只是一个画在那里的图画而已。
Ø [声明]本文中提到的源码均为UCGUI3.24版源码,新版UCGUI源码会有改动,请下载本文示例代码来结合阅读本文。
摘要: 本文主要介绍了UCGUI中的对话框的消息处理机制,并指出在现有UCGUI上如何增加多窗体支持,并在分析解决问题时着重介绍了其输入设备消息WM_TOUTCH及WM_KEY两类消息处理方法,并同时初步指出一种在UCGUI中实现模态对话框以及透明窗体的原理说明,不还有窗体重画消息WM_PAINT消息处理原理。
一、各种基本消息介绍及处理流程----对话框内部消息流转及外部消息LOOP分析.
UCGUI是采用的消息驱动的,它专门有对外的一套收集消息的接口, 我在模似器中, 就是通过LCD模拟显示屏窗口的MOUSE消息,将MOUSE消息传入到这个接口中, 以驱动UCGUI中的窗体的。
UCGUI中的消息驱动其实与WINDOWS的是类似的,几种基本的消息与WINDOWS是一样的,但UCGUI的更简单且消息更少,对于一些消息的处理得也很简化,没有WINDOWS那么多的消息种类及复杂处理。在WINDOWS中,如我们处理按钮控件的点击事件的是在WM_COMMAND消息中,通过按钮的标志ID来区分不同的按钮,所以按钮标志ID必须不同的,否则无法区别开(除非不在父窗体的WM_COMMAND消息中处理)。
UCGUI中一些基本的消息如下:
Ø WM_CREATE---窗体创建消息,每创建一个窗体完后都会向该窗体发送此消息,如WM_CreateWindowAsChild创建完窗体均会发一此消息,但在UCGUI中对于此消息的很少处理,如果用户想在对话框之后做些初始化操作或是创建其它子窗体的动作,可以处理此消息,不过对话框一般有专门的初始化消息WM_INIT_DIALOG,它是在创建对话框后发送的。
Ø WM_SHOW-----显示窗体消息,此消息在UCGUI中各控件窗体内均未作处理,如果你通过消息发送函数来发送这类没有在UCGUI中各窗体中处理的消息,是没有有什么响应的,不要感到奇怪。要显示窗体一般是通过WM_ShowWindow()函数实现的,这个函数做的也就是改变窗体显示标志[WM_SF_ISVIS],并使窗体矩形区域无效[WM_InvalidateWindow()]以产生重画消息。
Ø WM_SET_ENABLE---设置窗体不能使用消息,UCGUI中有一种复选框为不可改变的,但是这个功能也不完全,如果你对着UCGUI中的按钮使用WM_DisableWindow()来设置其无效,按钮照样还是可以使用,不过要改进这些小毛病还是很容易的,这里只是提醒大家UCGUI中很多没有实现的小地方,不要到时候使用时感到很奇怪,感觉到奇怪时最好去看看源码,看看源码中是否实现了此功能,不要郁闷。
Ø WM_PAINT ----窗体重画消息,当窗体所在区域全部或是部分区域无效时,系统将会发出该重画消息,将无效区域重画,但UCGUI中的处理比较简单,都是将窗体全部区域重画;如果用户自己想在窗体上画上一些信息,一般都在在该消息当中画,UCGUI中的各种提供的系统控件都必须在其系统的提供的消息回调函数中处理此消息来画出控件。当由外部输入操作引起无效窗体区域产生时,系统都会在消息处理中发送该消息到窗体消息回调函数中,以重画此窗体,在下面讲解消息循环机制时将会着重讲解到该消息的产生。
[透明窗体]---经常有朋友想知道在UCGUI中如何实现透明窗体,透明窗
窗体显示在前台时,可以看到部分位于其窗体后的内容,即透过窗体可 以看到窗体背后的图象。在UCGUI中有关于透明窗体的设置选项,可是
没有实现此功能,其实要实现原理如下:第一透明窗体及其所有子窗体
都必须透明处理;第二是对于所有有透明属性的窗体,在绘图时必须使
用透明填充功能的矩形填充函数,主要是修改窗体的WM_PAINT消息中画
窗体时的矩形填充函数为透明的矩形填充;第三透明的矩形填充函数的
实现,通常情况下的矩形填充是以当前前景色来填充,那么关键就是实
现画点函数的透明填充,要使一个透明,可以取当前显存中存点的点的
RGB颜色,然后再与当前要画的颜色按照一个比例进行混合得一个新的
RGB值,再将此值画以屏幕上就可能实现透明填充的效果。
Ø WM_TOUCH----处理类似MOUSE的滑动操作方式的输入外设的消息,如触摸屏一般都是将其消息从硬件接收到后转化为该消息形式发送出去,该消息中必须包含消息在屏幕中的发生位置坐标及输入设备状态(按下状态或弹起状态),此消息在任务消息循环中循环处理,一旦产生就会发送给当前焦点窗体,在后面将详细讲解该消息的处理机制。
Ø WM_KEY------处理类似KEY的按键式操作的输入外设的消息,消息中必须包含按键的按下或弹起状态,此消息也是在任务消息循环中循环处理,一旦产生就会发送给当前焦点窗体,讲解消息LOOP时再详细介绍。
Ø WM_SET_FOCUS----讲到刚才上面的两个消息时,就反复提到了当前焦点窗体的概念,所有外部输入设备消息都是发送给当前焦点窗体的,用户可以通过此消息来设定当前的焦点窗体。外部输入操作也会改变当前焦点窗体,如点击某窗体时会在该窗体的WM_TOUCH消息处理中设置该窗体本身为当前焦点窗体;当在对话框中按键TAB键时,同样也可以将焦点在对话框上各控件间切换,这是在对话框的WM_KEY消息中处理实现的[了解一下WM_SetFocusOnNextChild()函数],是根据创建对话框时指定的资源定义数组中的顺序来切换的,并没有WIN下面指定的TabIndex这样一个值来指定次序的值。
Ø WM_NOTIFY_PARENT---这个消息将子窗体的外设输入的消息传送到它的父窗体,因为一般的情况下消息都是在父窗体中统一处理的,如对话框中的按钮点击事件,一般都是在用户自定义的窗体消息处理函数中处理,所以就必须要子窗体将获取的输入外设的消息传送给父窗体,这样才能在父窗体中进行子窗体的点击事件消息的处理,这个消息的机制类似WIN下面的WM_COMMAND消息,处理该消息时通过控件ID来区别不同的控件,通过消息中的通知码来区别控件被操作的各种状态,具体这个消息的详细说明请参见后面的分析。
Ø WM_DELETE---要删除窗体时发送的消息,主要清除窗体数据结构所占用内存,此消息主要由WM_DeleteWindow()函数发送了,如点击OK按钮关闭对话框时,最终会调用此函数来删除窗体,不过UCGUI中没有最大化最小化关闭等系统功能按钮。最基础窗体结构注解如下,在该结构中有两个很重要的成员,hNextLin是记载窗体的下一个窗体,这个成员用于遍历所有已经创建的窗体;hNext是记载窗体下一个兄弟窗体,这个成员用于遍历每个窗体对应的子窗体;这个结构是最基础,一般的控件在这个结构之上还会有一些扩展的结构,如按钮对应有BUTTON_Obj结构。
typedef struct WM_OBJ_struct WM_Obj;
struct WM_OBJ_struct {
GUI_RECT Rect; /* 窗体矩形区域 */
GUI_RECT InvalidRect; /* 窗体无效矩形区域 */
WM_CALLBACK* cb; /* 窗体消息回调函数 */
WM_HWIN hNextLin; /* 窗体下一个窗体句柄*/
WM_HWIN hParent; /* 父窗体句柄*/
WM_HWIN hFirstChild; /* 第一子窗体句柄*/
WM_HWIN hNext; /* 下一个兄弟窗体句柄 */
U16 Status; /* 窗体当前状态 */
};
Ø WIDGET_HandleActive()—基础控件共通消息处理,在大部分的UCGUI控件中都会在消息回调函数的头部进行这个调用,如果处理了消息后,就直接退出消息回调函数的调用。这个函数中处理如下消息:
² WM_GET_ID[返窗体控件标志ID].
² WM_SET_FOCUS[设置当前窗体为焦点窗体,设置完后还必须向该窗体的父窗体发送一个WM_NOTIFY_CHILD_HAS_FOCUS消息让其父窗体更新它记载的当前焦点子窗体].
² WM_GET_HAS_FOCUS[获取当前窗体是否为焦点窗体].
² WM_SET_ENABLE[设置窗体为不可用窗体] .
² WM_GET_ACCEPT_FOCUS[获取当前窗体是否可设置为焦点窗体].
² WM_GET_INSIDE_RECT[返回窗体内框矩形,如按钮有3D效果时会有效果边框宽度,内框矩形就是窗体矩形被边框剪裁后的矩形].
Ø WM_DefaultProc()----窗体默认消息处理函数,UCGUI中提供一些基础的控件,这些控件有些共通的消息均在此处理,如下:
² WM_GETCLIENTRECT[获取窗体矩形区域,相对于矩形自身]
² WM_GETORG[获取窗体矩形左上角坐标].
² WM_GET_INSIDE_RECT[获取窗体矩形区域,相对屏幕].
² WM_GET_CLIENT_WINDOW[获取窗体客户区子窗体句柄,如对话框的中的子窗体FrameWin即为此种窗体].
² WM_KEY[铵键消息处理,通知父窗体子窗体的按键消息,有些控件自己要处理这个消息,如Edit控件处理完此消息后就没有再调用WM_DefaultProc(),从而没有将WM_KEY消息通父窗体;如Button控件,根本没有对此消息进行处理,直接是通过默认处理发给了父窗体处理;有些控件如Checkbox自己处理该消息,同时也调用默认消息处量将此消息通知父窗本,此种消息源窗体为子控件,目标窗体为父窗体。如此处理WM_KEY消息完全是UCGUI中如此做,在WIN中并没有这样做].
² WM_GET_BKCOLOR[获取窗体背景色,在此未实现,返回0xfffffff值,但 FrameWin窗体实现了此消息处理].
在UCGUI的对话框的窗口消息处理函数中OK按钮的点击事件, UCGUI的处理方法与WIN下面是不同, 它在WM_NOTIFY_PARENT消息中处理[片段如下]:
case WM_NOTIFY_PARENT:
Id = WM_GetId(pMsg->hWinSrc); /* Id of widget */
NCode = pMsg->Data.v; /* Notification code */
switch (NCode) {
case WM_NOTIFICATION_RELEASED: /* React only if released */
if (Id == GUI_ID_OK) { /* OK Button */
GUI_MessageBox("This text is shown/nin a message box",
"Caption/Title", GUI_MESSAGEBOX_CF_MOVEABLE);
}
if (Id == GUI_ID_CANCEL) { /* Cancel Button */
GUI_EndDialog(hWin, 1);
}
break;
}break;
UCGUI中的消息种类不多, 只有差不多不到二十种,但对于嵌入式系统来说已经完全足够了,用户可以自定义消息(从WM_USER起)。 WM_NOTIFY_PARENT这个消息是由子窗体传送给父窗体的, 由消息的名字也可以看出这一点,OK按钮也是一个窗体,当MOUSE点击在它上面时,UCGUI首先会传递一个WM_TOUCH消息到OK按钮的窗口消息处理函数,OK按钮是一个系统提供的控件,系统已经提供了一个默认的消息的窗口消息处理函数,这个函数会处理大部分的默认窗口消息并随后将此消息转发给父窗体,即WM_NOTIFY_PARENT消息,它是由函数WM_NotifyParent(hObj, Notification)实现的.
WM_TOUCH消息在按钮的消息处理函数_BUTTON_Callback中的_OnTouch函数中处理,在处理过程完后会调用WM_NotifyParent向按钮的父窗体发WM_NOTIFY_PARENT消息告诉对话框回调函数按钮被点击了,这个过程再说详细一点是这样的:
Ø 点击OK按钮.
Ø 产生按钮WM_TOUCH消息.
Ø UCGUI中的消息LOOP调用按钮默认的按钮窗口消息处理函数_BUTTON_Callback.
Ø _OnTouch默认处理按钮点击并发送给父窗体WM_NOTIFY_PARENT消息,这里要注意MOUSE点击后,有三种情况:第一种是点击后在按钮范围内弹出MOUSE,这种情况下,会送的消息中还有一个通知码就是WM_NOTIFICATION_RELEASED;第二种情况是点击拖到按钮范围外弹起MOUSE,此时通知码是WM_NOTIFICATION_MOVED_OUT;第三种情况是点击后一直未弹起MOUSE的过程中消息通知码为WM_NOTIFICATION_CLICKED;在这个函数中还会处理设置按钮点击后MOUSE至未弹起前的按下状态,这样在按钮下一次画出时就会以按下的状态显示出来.
Ø 默认的对话框窗体消息处理函数_FRAMEWIN_Callback收到WM_NOTIFY_PARENT消息并最终传送该消息到用户自己定义的对话框消息处理函数,这里要注意的一点是,其实对话框对话框主要是由一个FrameWin子窗体构成的,这个子窗体大小为对话框指定的大小,对话框上的其它控件是都是FrameWin的子窗体,由_FRAMEWIN_Callback传送的消息首先是传送到对话框的默认窗体消息回调函数_cbDialog,然后再经它传送到用户自定义的窗体回调函数当中。
Ø 用户在自己的对话框消息处理函数中处理WM_NOTIFY_PARENT消息,即按钮的点击消息,该消息参数中含有按钮的ID及操作状态,如果通知码是WM_NOTIFICATION_RELEASED,此时证明一次点击事件完成。
void WM_NotifyParent(WM_HWIN hWin, int Notification){
WM_MESSAGE Msg;
Msg.MsgId = WM_NOTIFY_PARENT;
Msg.Data.v = Notification;
WM_SendToParent(hWin, &Msg);
}
这个函数相当简单, 其主要还是WM_SendToParent这个函数的调用, 这个函数再调用void WM_SendMessage(WM_HWIN hWin, WM_MESSAGE* pMsg), 这个函数是最基本的一个消息发送处理函数, 它的第一个参数指定了接受这个要处理的消息的句柄, 第二个指定了是什么消息。这个函数的主要作用是调用相应窗口的消息处理函数来处理消息,如果你有消息要发送给指定的窗体处理,那么也可以使用这个函数。
在上面, 我们刚刚分析了在对话框内部消息处理的流转,其中分析了我们在自己指定的对话框消息处理函数当中是如何可以获得按钮的点击消息并进行处理的,现在我们就再来分析一下对话框外面的消息接收:首先是来了解一下GUI_ExecDialogBox函数,这个函数有几个参数:
Ø 第一个是对话框的资源定义数组,这个数组定义了对话框的组成子窗体,其中数组第一个成员必须是FrameWin窗体,数组每一个成员记载了创建子窗体所用函数/子窗体Caption/子窗体标志ID/子窗体的位置及宽高/创建窗体时样式标志/额外传送的参数.
Ø 第二个参数是上述的数组的大小.
Ø 第三个参数是用户指定的对话框窗体消息回调函数指针.
Ø 第四个参数是对话框的父窗体,默认为0.
Ø 第五、六参数指定对话框的左上角屏幕位置.
GUI_ExecDialogBox主要完成如下几件事:
Ø 根据传进来的对话框资源定义数组创建对话框及对话框中的子窗体.
Ø 根据传进来的窗口消息处理函数,记载到一全局变量保存,当这个全局变量中记载的函数指针为非空时,执行消息LOOP,消息LOOP中会将当前的MOUSE及KEY消息发送给当前焦点窗体.
Ø 当对话框关闭时,记载对话窗体消息回调函数的全局变量会被清为0,此时消息LOOP就会退出,对话框结束.
二、发现存在的问题-----点击OK后无论先关闭消息框还是对话框,另一个不再响应.
点击对话框的OK后弹出消息框, 会出现当按下对话框的Cancel关闭对话框后, 弹出的消息框就没有任何响应的情况. 或者是关闭掉弹出的消息框, 对话框就没有任何响应的情形:从外部粗步分析的原因是调用MainTask的线程已经退出了, 这个线程是在模拟器中开启的专门用于运行GUI任务的线程,它的线程函数是Thread, Thread函数里调用main,main中再调用MainTask,所以该线程退出后也就代表UCGUI任务已经结束了。这是从模拟器的角度来分析, 现在我们分析一下为什么MainTask的调用线程会这么早退出呢?
由我们第一节中关于GUI_ExecDialogBox所做的几件中可以分析到, 当UCGUI中有一个独立的窗体退出后_cb会被清为0, 此时退出GUI窗口LOOP. 即结束了UCGUI窗口消息处理。
其实, GUI_MessageBox弹出的消息框其实也是一种对话框, 这最终调用的还是GUI_ExecDialogBox,开始我们就分析过,进入这个函数后,会有一个全局变量记录当前对话框窗体的消息处理函数指针,但是目前的问题如下:
Ø 已经建立了两个这样的对话框窗体,这样一个全局变量来记载当前对话框的窗体消息处理函数指针显然不够,而且先前打开的对话框的的用户指定的窗体消息回调函数已经不再被调用了,此时第一个对话框的由子窗体回传到父窗体的消息均会传到第二次打开的对话框的用户指定的窗体消息回调函数中.
Ø 第二次弹出消息框再次进入GUI_ExecDialogBox中的 while循环后,先前的对话框中的while循环就被挂起了,直至第二次的GUI_ExecDialogBox中的 while循环退出,无论关闭消息框还是对话框,都会导致知退出第二次消息LOOP。第二次消息LOOP退出后返回点为弹出消息框后的下一句,直至返回到第一个对话框的while循环后退出GUI_ExecDialogBox.
但我们期待的结果是,点击对话框的OK弹出消息框, 关闭掉对话框或是消息框,其它的都要对话框继续有反应,下面我们就来分析一下如何达到这个目标,看看要做些什么具体的改动:
三、UCGUI中的消息LOOP处理分析-----寻找问题的解决办法.
在我们发现这个问题, 我们已经粗步分析了,问题不是出在我们编写程序上, 而上UCGUI的内部,那么要解决这个问题, 我们就要进一步了解UCGUI的窗口体系。其实换一句话说,在嵌入式应用中,窗口的强大直接决定到GUI系统的体积大小,并不是所有的情况都要有这种支持,当然我们希望在下一版本可以有多个对话框的直接支持。
创建对话框:
void MainTask(void)
{
GUI_Init();
WM_SetDesktopColor(GUI_RED);
WM_SetCreateFlags(WM_CF_MEMDEV);
GUI_ExecDialogBox(_aDialogCreate,GUI_COUNTOF(_aDialogCreate), &_cbCallback, 0, 0, 0);
}
上面是我们创建对话框的程序,是我们编写的代码, GUI_ExecDialogBox()这个函数的作用我们已经分析过了,它所做的事用一句话来说就是创建对话框并进入窗体消息LOOP处理,下面将详细分析一下LOOP消息的处理流程:
int GUI_ExecDialogBox(const GUI_WIDGET_CREATE_INFO* paWidget,
int NumWidgets, WM_CALLBACK* cb, WM_HWIN hParent,
int x0, int y0)
{
_cb = cb;
GUI_CreateDialogBox(paWidget, NumWidgets, _cbDialog,hParent, x0, y0);
while(_cb){
if (!GUI_Exec())
GUI_X_ExecIdle();
}
return _r;
}
这个LOOP类似我们非常熟悉的WIN下面的消息LOOP, 其原理是一致的. GUI_CreateDialogBox负责创建对话框的所有子窗体,特别注意它其中一个参数传入是Dialog.c中定义的_cbDialog,这个函数什么也没做,基本上是转而调用_cb,后面我们会提到关于它的修改。_cb是对话框的用户定义窗口消息处理函数,这里面有一个判断,就是_cb非空时,才进行消息LOOP, _cb在Dialog.c中的定义为:[static WM_CALLBACK* _cb;] _cb是一个全局变量,我们程序中创建对话框与弹出消息框时两次调用了GUI_ExecDialogBox,后一次的_cb将会把前面的值冲,它是用户自定义的窗口消息处理函数。
在while中有判断, 那么可见_cb是在GUI_Exec之中是有使用的,对话框的FrameWin子窗体消息流转调如下面的所示,窗口消息处理函数是在WM_SendMessage中通过函数指针的调用中, 注意[]内部的就是真正被调用来处理消息的函数:
GUI_Exec-->GUI_Exec1-->WM_Exec-->WM_Exec1-->WM_HandlePID-->WM_SendMessage-->(*pWin->cb)(pMsg)[_FRAMEWIN_Callback]-->_OnTouch()-->(*cb)(pMsg)[_cbDialog]--> *_cb)(pMsg)[_MESSAGEBOX_cbCallback]
Ø WM_HandlePID()------专门处理类似MOUSE的滑动操作外设消息的函数.
Ø WM_SendMessage()----基层的发送消息的函数,即调用相对应的窗体的消息回调函数来处理消息.
现在讲到了窗体消息LOOP,在窗体系统中最根本一点的就是对外部输入消息的处理,窗体就是靠消息驱动的,其处理代码如下:
int WM_Exec1(void){
if(WM_pfPollPID){/* Poll PID if necessary */
WM_pfPollPID();
}
if(WM_pfHandlePID){
if (WM_pfHandlePID())
return 1; /* We have done something ... */
}
if(GUI_PollKeyMsg()){
return 1; /* We have done something ... */
}
if(WM_IsActive && WM__NumInvalidWindows) {
WM_LOCK();
_DrawNext();
WM_UNLOCK();
return 1; /* We have done something ... */
}
return 0; /* There was nothing to do ... */
}
它主要完成如下几件事:
Ø Poll PID中Poll个词准确的意思应该是统计/测试的意思,这里是调用用户的统计测试滑动操作外设的一个接口,用户可以通过WM_SetpfPollPID()函数来设置自己用于统计/测试滑动操作外设的具体函数。
Ø
处理滑动操作外设 WM_TOUCH消息,真正的处理是在函数WM_HandlePID()中处理的,在后面滑动外设消息处理流程时有详细说明,在新版中更细分此消息为WM_PID_STATE_CHANGED/ WM_MOUSEOVER/ WM_TOUCH三种消息,其实在WIN下面类似消息的处理更为复杂,有移动/滚动/单击DOWN及UP[左右键]/双击[左右键]等七八种MOUSE消息,而且这些消息又分为窗体体客户区与标题区的差别,标题区的都会在消息上加上NC的前辍,如WM_NCLBUTTONUP标题区单击弹起消息。从这里我们也可以看到UCGUI中非常简化的处理,简单得不能再简单了,的确是一个微型的GUI图形支持系统。
Ø
按键式外设消息处理,GUI_PollKeyMsg()函数在发现有新的按键消息生时会调用WM_OnKey()将消息发送到当前焦点窗体处理,如果一直处于按键按下状态时则会将前按钮的虚拟码存在一全部变量中,以供GUI_GetKey()调用来返回当前按下键值。UCGUI中有一个外部的键盘接口,外界通过GUI_StoreKeyMsg()发送键盘消息给UCGUI以驱动键盘,在我的模拟器当中就是将LCD模拟显示屏窗口的所有键盘消息通过GUI_StoreKeyMsg()传送到UCGUI中以驱动键盘消息处理,关于键盘消息的处理UCGUI中也是来一个处理一个,没有任何缓冲处理,如果某些按钮消息处理用时过长,就会造成其后的一些按键消息丢失。
static int _Key; //记载当前按键,GUI_GetKey时返回此值
static int _KeyMsgCnt; //当前键盘消息数量
static struct {
int Key; //键盘虚拟码…
int PressedCnt; //按键次数…
} _KeyMsg;
上面是键盘消息结构,UCGUI中以一个全局的_KeyMsg键盘变量记载当前最新键盘消息,当前按键值用_Key,每产生按下键时用GUI_StoreKey更新一次此值,
UCGUI中没有按键弹起消息的处理。
Ø 检测是否有无效窗体,如果有无效窗体,则向该无效窗体发送重画消息,有一个全局变量WM__NumInvalidWindows用于记载当前无效窗体的数目,在函数_DrawNext()中每次重画一个无效窗体,查找无效窗体时是通过遍历查找的方法,先前说过窗体基本结构中有一个成员hNextLin记载下一个窗体,就在是此处用于遍历所有窗体,找出无效的窗体,发送WM_PAINT消息给窗体。注意这里每次画一个窗体的原因就是为了不影响窗体的消息处理,如果在此处用时太多,会严重影响消息处理的反应速度。
了解了UCGUI中消息处理的具体流程,那么再来分析这个先前提到的问题:无论是消息框还是对话框哪一个先被关掉, 都会掉用GUI_EndDialog,将_cb被清为零,也就意味着消息LOOP到此结束了,所以后面另外一个未被关掉的当然不会再有任何响应了!
void GUI_EndDialog(WM_HWIN hWin, int r) {
_cb = NULL;
_r = r; //通知WM_Exec等消息LOOP返回…
WM_DeleteWindow(hWin);//free该窗体结构占用的内存…
}
现在我们可以得出一个结论:UCGUI中对话框的设计只支持单窗口的消息处理,如果要多窗口的支持,可以如同示例中一样,启用多任务支持,不然在单任务下一个MainTask中只能支持一个独立窗体,但是如果我们只是为了要弹出一个消息框而启动一个任务, 这未免太不实际。
了解UCGUI后初步修改路分析如下:
Ø 消息传送-----经过详细的分析,认识到在消息处理中创建一个对话框窗体后,必须建立一个消息LOOP处理,来向UCGUI中的窗口捕捉并传送外设的输入消息,消息的处理实质上是通过WM_SendMessage函数来调用相应的窗口的消息回调函数。
Ø 消息LOOP----如果创建多个对话框窗体,则会进入一个新的消息LOOP处理层而挂起原来的消息LOOP处理,要避免这种情况发生必须将消息LOOP移到MainTask之外,并在创建完所有对话框之后执行消息LOOP处理。
Ø 消息分发-----用一个数组将所有创建对话框的自定义消息回调函数存放起来,然后在对话框消息分布处(_cbDialog函数处)对应分发各个对话框的消息,要注意和解决的问题是,必须根据消息所对应用窗体来正确分布。
Ø 删除窗体-----在清除独立窗体时,必须将此对话框对应的用户自定义的窗体消息回调函数清零,并清除该窗体与其它窗体的数据关系及其占用资源,使其退出消息处理。
四、对UCGUI源码做出部分修改以实现多独立窗口支持.
在第三节当中,我们通过进一步的分析源码,大致找到了解决问题的办法,但那只是理论上的指导,实际上的修改其实还会带来其它的很多问题,因为在UCGUI体系中,对其源码作出改动,一定都会影响到其它的地方,现在我们就实际的源码修改说明几点要注意的问题:
实际上这个问题有两种决办法,第一种改动很小,只须进行很小的几处改动,是在之后想到的方法,最先采用的是第二种方法,对Dialog.c进行多处比较大的改动,比较复杂,但是之所以还在此处列出,其原因是为了让大家更加清楚的了解UCGUI中的外设输入消息处理机制,只有在了解了第二种方法的基础上才能更好的理解第一种方法,否则对于第一种方法不能理解透。下面分别描述这两种修改办法:
第一种办法:
Ø 将GUI_ExecDialogBox中的消息LOOP提出到 MainTask这个UCGUI应用当中,放在所有的对话框创建之后进行;并将在它当中增加一个创建对话框个数的变量用于统计当前已经创建的对话框数量,在Dialog.c当中有一个现成没作什么用的全局变量static int _r;可以改做此用.
Ø 将GUI_ExecDialogBox中建对话框时中调用GUI_CreateDialogBox所传的窗体消息回调函数改成用户自定义指定的,而非Dialog.c中中默认的_cbDialog,这个函数的作用就是调用用户指定的对话框窗体消息回调函数,所以可以在创建对话框中直接传用户指定的消息回调函数。
Ø 在GUI_EndDialog当中相应的将当前对话框个数减少,每关闭一个对话框减一.
Ø 在Dialog.c增加一个函数用于返回当前已经创建的对话框个数GUI_ExecDialogNum(),用于判断是否应该继续进行消息LOOP处理。
须要修改的几个函数修改成如下所示:
int GUI_ExecDialogBox(const GUI_WIDGET_CREATE_INFO* paWidget,
int NumWidgets, WM_CALLBACK* cb, WM_HWIN hParent,
int x0, int y0)
{
GUI_CreateDialogBox(paWidget, NumWidgets, cb, hParent, x0, y0);
return ++_r;
}
void GUI_EndDialog(WM_HWIN hWin, int r) {
_cb = NULL;
_r--;
WM_DeleteWindow(hWin);
}
int GUI_ExecDialogNum()
{
return _r;
}
void MainTask(void)
{
GUI_Init();
WM_SetDesktopColor(GUI_RED); WM_SetCreateFlags(WM_CF_MEMDEV);
GUI_ExecDialogBox(_aDialogCreate, GUI_COUNTOF(_aDialogCreate), &_cbCallback, 0, 0, 0);
while(GUI_ExecDialogNum()) {
if (!GUI_Exec())
GUI_X_ExecIdle();
}
}
第二种办法:
1、将原来的_cb修改成一个结构为new_cb的结构数组,首设定最多可创建10个对话框窗体:
typedef struct win_cb{
WM_CALLBACK* _cb; //用户自定义消息回调函数..
WM_HWIN hwin; //_cb消息函数对应的对话框窗口..
WM_HWIN hclient; //_cb消息函数对应的对话框FrameWin窗口客户区...
}new_cb,*lpnew_cb;
//在_cb数组中当前可用元素位置.
static int dialog_pos = 0;
//最多可创建对话框窗体数目,其实可以改成支持无数个,但这里作简单处理
static int MAX_DIALOG = 10;
//检查是否还有独立窗体存在, 以决定是否退出消息LOOP...
int checkHasDialog();
//获取当前可用于存放对话框的位置索引, 创建新对话框时调用.
int getDialogIndex(lpnew_cb lp_cb);
//对话框窗口数组,创建对话框后, 将其相关信息记载到该数组当中时,其成员//赋值必须注意几个问题,在下面具体代码中说明:
static new_cb _cb[10];
//新修改后的创建对话框的函数...
int GUI_ExecDialogBox(const GUI_WIDGET_CREATE_INFO* paWidget,
int NumWidgets, WM_CALLBACK* cb, WM_HWIN hParent,
int x0, int y0)
{
dialog_pos = getDialogIndex(_cb);
if(dialog_pos != -1) _cb[dialog_pos]._cb = cb;
else return _r;
GUI_CreateDialogBox(paWidget,NumWidgets,_cbDialog,hParent,x0, y0);
return _r;
}
WM_HWIN GUI_CreateDialogBox(constGUI_WIDGET_CREATE_INFO* paWidget,int NumWidgets, WM_CALLBACK* cb, WM_HWIN hParent,int x0, int y0)
{
WM_HWIN hDialog = paWidget->pfCreateIndirect(paWidget,hParent,x0, y0,cb);
WM_HWIN hDialogClient = WM_GetClientWindow(hDialog);
//加到GUI_CreateDialogBox中的,其余不变...
_cb[dialog_pos].hwin = hDialog;
_cb[dialog_pos++].hclient = hDialogClient;
....
}
Ø getDialogIndex(_cb)---创建新的对话框窗体前,首先必须在对话框数组中查找空位置,如果对话框窗体已达最大数,则不可再创建对话框窗体,这里只做的是简单处理,没有用到动态内存分配,主要是因为是演示,读者自己可以尝试支持无限创建对话框窗体。
Ø _cb[dialog_pos]._cb = cb---对话框窗口的窗口消息处理函数必须在GUI_CreateDialogBox调用之前赋值,因为在GUI_CreateDialogBox中就会用到这个窗口消息处理函数。
Ø _cb[dialog_pos]中的hwin等窗口句柄的处理加到创建对话框函数当中,千万不要在调用创建对话框函数后再根据返回的对话框句柄来赋这个值,因为在创建对话框函数中创建子窗体时就会调用到对话框消息处理函数,如果hwin此时未初始化,则在_cDialog()中就无法分发消息,这样对话框中的子窗体都无法正确显示的。
Ø GUI_ExecDialogBox中的窗口消息LOOP改为放到MainTask中调用,因为当我们把窗体都创建了之后,再来执行消息LOOP的话,可以避免前面创建独立窗体的消息LOOP被后面创建的消息LOOP中断的作用。如果我们要创建多个对话框窗体,那么LOOP当中要分发消息的对象也就是多个窗体而不是其中的一个,所以要从执行单个对话框函数中拿出,由用户来写在图形应用任务当中,如同WIN中主窗体的消息LOOP处理类似。抽出后如下代码所示:
void MainTask(void)
{
GUI_Init();
WM_SetDesktopColor(GUI_RED);
WM_SetCreateFlags(WM_CF_MEMDEV);
GUI_ExecDialogBox(_aDialogCreate,GUI_COUNTOF(_aDialogCreate), &_cbCallback, 0, 0, 0);
while(checkHasDialog()){
if(!GUI_Exec())
GUI_X_ExecIdle();
}
}
2、在第三节中第2点还说到,要分别调用各个对话框窗体的用户自定义的窗口消息函数,必须注意消息的分发,也就根据消息中的窗体句柄在对应的对话框数组中查找并调用相应窗体的回调函数,下面我们看一下具体分发的代码:
static void _cbDialog(WM_MESSAGE* pMsg)
{
char buf[100];
int i = 0;
WM_LOCK();
for(i = 0; i < MAX_DIALOG; i++){
if(_cb[i]._cb != (WM_CALLBACK*)0){
if (WM__IsWindow(pMsg->hWin)){
if(pMsg->hWin==_cb[i].hwin||_cb[i].hclient==pMsg->hWin){
(*(_cb[i]._cb))(pMsg);
}
}
}
WM_UNLOCK();
}
_cbDialog是在对话框的FRAMEWIN_cbClient中调用的形式如下:
if(cb){
pMsg->hWin = hParent;
(*cb)(pMsg);
}
每个独立对话框窗体均是这样,通过其FrameWin子窗体来调用用户自定义的窗口消息处理函数,在分发消息时,其实只须要根据消息中的窗体句柄来分发,因为我们对于每个对话框,均记载了它的窗体句柄及FrameWin子窗体句柄。所有创建的独立窗体的消息均是在_cbDialog中顺序进行处理的。
3、第三节中所说的第3点,独立窗体退出的处理:
void GUI_EndDialog(WM_HWIN hWin, int r) {
int i = 0;
char buf[255];
if (!hWin) return;
WM_LOCK();
if (WM__IsWindow(hWin))
{
for(i = 0; i < MAX_DIALOG; i++){
if(hWin == _cb[i].hwin || _cb[i].hclient == hWin){
_cb[i]._cb = NULL;
_cb[i].hwin = 0;
_cb[i].hclient = 0;
}
}
}
WM_UNLOCK();
_r = r;
WM_DeleteWindow(hWin);
}
4、关于类似MOUSE的滑动操作外设消息的处理函数WM_HandlePID().
消息LOOP中做的最重要的一件事是获取消息并分发到相应窗体进行处理,这当中当然包括外设输入消息的获取与处理,WM_HandlePID()函数就是做这种处理的,它在WM_Exec1中调用。
这个函数是专门负责处理类似MOUSE的滑动操作外设消息,UCGUI中统称为WM_TOUCH消息,在GUI/Core/WMTouch.c文件当中,当你点击或是在触摸屏上按下时均会产生此消息。它当中有两个变量:一个静态的旧消息变量;一个是局部新消息变量。每次均从消息获取接口GUI_PID_GetState()中取当前WM_TOUCH消息,处理时会比较新旧消息发生的屏幕坐标的及外设操作的状态(按下与否)以决定是否处理该消息,每次处理完最新消息后就将最新消息更新到旧消息变量上以避免对相同消息的重复处理,正是基于这一点才会将对话框内的消息LOOP移到MainTask中进行。下面将详细分析如此处理的原因及不如此处理会引发的问题。
五、UCGUI中滑动外设输入消息的处理机制----外设输入消息处理流程及模态对话框框的实现原理初步分析.
UCGUI中的的外设输入消息统一称为WM_TOUCH,WM_HandlePID()就是专门处理这种消息的,如MOUSE及触摸屏等滑动操作外设的消息处理,这种滑动外设消息的处理特征为:一是必须传送消息发生点的屏幕X/Y坐标;二是滑动外设按下与否的操作状态。WM_TOUCH消息的处理流程如下:
1.通过GUI_PID_GetState获取一个GUI_PID_STATE结构的WM_TOUCH消息,消息的获取比较简单,在GUI/Core/GUI_PID.C文件当中处理,这个文件提供的是Pointer input device[指针输入设备]的输入消息处理,但我认为将看作为滑动操作外设更为贴切些,文件中提供有以下几个函数:
Ø GUI_PID_Load--------设定处理此类设备输入消息的处理函数,这里为WM_HandlePID().
Ø GUI_PID_GetState----获取设备最新输入的消息,UCGUI中处理得很简单,用一个全局变量保存当前最新输入的消息,返回此全局变量的一个COPY即可.
Ø GUI_PID_StoreState--设定设备最新输入消息,其实就是设定一个全局变量_State的值,这个函数通常在设备消息接收的任务当中调用,在模拟器中它是在LCD模拟显示窗口的MOUSE消息中调用,这里必须说明的一个问题是,如果UCGUI在处理某些WM_TOUCH消息时用去时间过多的话,那么就会丢失掉部分WM_TOUCH消息,因为UCGUI只是用一个变量来接收消息而没有消息队列或是一个数组来缓存未处理的消息,如果使用队列那么必然会使消息处理更加复杂,所以如果需要在WM_TOUCH消息中处理一个用时比较长的操作,那么最好使用一个新建的任务来完成该操作,否则在此过程中不能再处理任何外设的其它的输入操作,这里与WIN下面是一样的,不过WIN下面单任务情况下只是操作不能及时有反应,但不会丢失后面操作所产生的消息,因为它有消息队列,在此我们也可以看到UCGUI中很多处理机制一切重简.
Ø GUI_PID_Init--------初始化设备相关,此处函数为空.
2.将新获取的消息与函数内静态的旧消息变量进行比较,包括该消息发生点的屏幕坐标及外设操作状态(是否按下):
滑动操作外设消息结构如下:
typedef struct{
int x,y; //消息发生点在屏幕中的x/y坐标..
unsigned char Pressed; //滑动外设是否按下..
} GUI_PID_STATE;
3.如果支持图形鼠标,则根据新消息屏幕位置设置当前使用的鼠标位置,如果此处不进行这个设置,则在打开MOUSE支持后,MOUSE是无法移动的,在触摸屏当中一般不要求画出图形鼠标,一般只有在PS/2鼠标设备时才必须画出图形鼠标,否则无法进行鼠标操作。
4.当新旧消息的操作状态比较发生变化时处理该消息[通过将消息中的外设按下与否的状态相或的值是否为1来判断,为1则表示状态改变],否则不发送此消息到窗体进行处理,而是直接更新新消息到旧消息上,这里我说的情况是UCGUI3.24版源码的处理情况,但是看现在最新的有源码的3.90源码当中就处理了此种情况下的消息为WM_MOUSEOVER,还有一个不同就是将操作状态的变化[即新旧消息中的是否按下不相等时]作为一个独立的消息WM_PID_STATE_CHANGED来发送,除此之外的消息才处理为WM_TOUCH消息,但是只要真正理解了UCGUI3.24版源码中消息处理的根本原理,理解新版源码中的消息处理也是很容易的,其本质并未变化,只是将消息分成了三种处理,这样显得更加明确。
5.构造WM_TOUCH消息所需用要的数据,首先要获取到当前焦点窗体句柄,当前焦点窗体是用WM__hCapture的全局变量记载,如果为0则调用函数WM_Screen2hWin()根据消息发生点的屏幕坐标来获取WM_TOUCH消息的对应的窗体句柄,注意这个函数是根据窗体数据结构,从第一个创建的窗体WM__FirstWin开始来递归查找,先查找窗体的第一个子窗体,然后查找hNext兄弟窗体,要注意理解此函数,其递归查找的本质就是按照屏幕上的窗体层叠次序,从最上层窗体开始查找,窗体的前后台顺序是由父子及兄弟关系决定的,即UCGUI窗体层叠规则为:窗体显示在其下一个兄弟窗体[hNext]之前,父窗体显示在子窗体之后;调整窗体的前后台顺序其实就是调整窗体在兄弟窗体间的位置,注意这里要递归的理解这句话。
6.比较新旧焦点窗体变量,初始旧焦点窗体变量值为0,每当处理消息后,如果是按下状态的WM_TOUCH消息,则会将当前焦点窗体句柄更新到旧焦点窗体变量中保存;如果不是按下状态的WM_TOUCH消息,则会在处理完消息后将旧焦点窗体变量赋为0值。这样处理的原因如下:当操作外设没有处于按下状态,此时进行滑动操作,无论是当前焦点窗体变化与否,都无须接受窗体变化时做额外的操作,可以想象将MOUSE在非按下状态下从按钮范围内滑出的情况,这中间无须做特别的处理;但是如果是在操作外设处于按下状态时进行没动操作, MOUSE滑出到按钮外后,就须要发一条消息通知按钮改变为非按下状态[否则按钮一直处于按下状态],即消息中新旧焦点窗体变量不同且旧窗体变量值非0时,会向旧焦点窗体发送一个消息,这个消息分两种情况:一种是滑出前松开按钮并且下一个接收到的消息就是新焦点窗体的消息[这种情况发生在边界处],此时新消息为非按下状态,则发送旧窗体一个未按下状态的消息;另一种是滑出后依然未松开按钮的,则发送给旧窗体一个消息数据为0的消息。所以在UCGUI的消息处理中有关于当前焦点窗体与旧焦点窗体的比较,如果不同且旧焦点窗体不为0,则必须发送一外设不在处于按下状态的消息告诉旧焦点窗体,否则旧焦点窗体一直处于外设按下状态。
7.向当前焦点窗体发送消息,然后根据对于按下状态的消息更新旧焦点窗体变量值为当前焦点窗体,否则置其值为0。
8.更新当前消息到旧消息变量中,结速本次消息处理。
以上分析了UCGUI中滑动外设输入消息的基本处理流程,正是基于这种简单的消息处理机制,导致产生这样一个问题: 如果上述修改中我们没有将窗口消息LOOP放到GUI_ExecDialogBox函数之外,那么存在一个问题就是在点击OK一次后会重复创建N多的消息框且N不定,原因是对相同的消息进行了重复处理。不是每处理完一次消息就更新了旧消息吗?为什么还会重复处理?这里面根本的问题就是旧消息还未更新;导致旧消息还未更新的原因有两个: 第一是新的消息框弹出后进入新的消息框窗体的LOOP,从而挂起了上一次窗体LOOP处理使得无法进行旧消息的更新,因为消息是在处理完后之更新的,但不幸的是上一次窗体消息LOOP处理完点击消息后就再也没能返回而是进入了新一层的消息LOOP(消息框的);第二是在MOUSE点击消息到下一个新的消息产生这段时间内,有一个时间过程,因为只有在移动MOUSE等操作之后才产生新的消息的,在这个过程中旧消息由于第一个原因中所说的,未能进行更新,所以一直还是按下状态的MOUSE消息,所以在这段时间内进行了多少次重复处理,取决于何时产生新的消息,如果用户点击OK按钮后快速移动MOUSE操作,则只会产生有限几次的重复处理;如果说此后用户再也没有移动MOUSE的操作,那么这个消息的重复处理一直会进行到用完所有可分配的动态内存,因为这个过程中一直在创建消息框窗体,创建窗体需要动态分配的内存,在UCGUI中动态分配的内存是有限的,所以点击弹出消息框后一直不进行MOUSE的任何操作会导致UCGUI中的动态内存用尽为止。
解决这个消息更新之前的重复处理问题有二种办法:第一种是将消息LOOP放出GUI_ExecDialogBox之外,即放入MainTask当中则不存在此问题,因为创建消息框后不会进入到新的循环当中去,它会马上进行旧消息的更新;第二种是改在WM_SendMessage之前就更新旧消息,这样也可以避免由于旧消息未更新而重复处理同一消息。
以上就一个产生的问题分析了这个消息处理的函数,这其实也是这个消息处理函数的基本原理,了解以上就可以了解UCGUI中简单的外设输入消息(WM_TOUCH)的处理机制。比如说如果要实现模态对话框,则必须了解这个WM_TOUCH消息的处理,模态窗体是指它打开显示之后就不能切换到其它窗体的进行工作的窗体,只有关闭它之后才能切换回别的窗体。下面简单探讨一下模态对话框的实现原理:
模态对话框分两种情形:一种是指单个应用中的模态对话框,在WIN下面对于一个单独的应用程序,模拟对话框则是这个应用中的最前台窗体,只有在它关闭之后,应用中的其它窗体才能切换到前台进行操作;一种是整个系统中的模态窗体,这种窗体位于系统中所有应用之前,在它关闭之前,系统中其它应用的所有窗体都不能切换到前台接受操作。
单个应用中的模态对话框的实现:例如在创建一对话框A后,点击OK后弹出消息框B,要求在B关闭之前A不能接受MOUSE操作的输入,这里其实要做的就是:在WM_TOUCH消息的处理时只接受B窗体的消息处理,除开B窗体之外其它的A窗体及其子窗体(递归处理)均不得在A存在之前进行WM_TOUCH消息的处理。如果要实现窗体A对应模拟窗体B,窗体C对应模态窗体D这样相对关系的模态窗体处理,也是比较容易的,不过是要多记载一些模态窗体所相对的窗体信息。具体的模态对话框的实现就不在这里进行详细讲解了,有时间会专门在独立的文章中介绍具体的修改。
修改后的源代码下载:
http://www.ucgui.com/ucgui/GUISim1004_MultDialog.rar
附言:
一般情况下,小型的GUI体系,都很少会有打个多个独立窗口的要求,UCGUI还是在发展中,而且并不成熟, 所以有很多的问题,不完善是肯定的.
我们要学习UCGUI,首先一定要动手写UCGUI的程序,阅读源码,这样才以发现更多的问题,才能更了解UCGUI。我本人就是经常有问题就读源码,在实际编程中,发现的问题是最多的,而且现在模拟器已经还原成源码,底下不再有任何的秘密,所有的问题都摆在源码之下,所以只要花心思研究,应该可以解决很多问题,学习到很多图形处理的深层知识。