1. 窗口
1.1 MAINWIN与HWND
MAINWIN定义了窗口,使用者提供它的窗口过程。CONTROL定义了组件,CONTROL的实现着提供它的窗口过程。
- 位置left、top、right、bottom
- 窗口风格dwStyle
- 标题spCaption
- 菜单句柄hMenu
- 光标句柄 hCursor
- 字体pLogFont,这是LOGFONT结构。
- 窗口图标hIcon
- 消息队列 pMessages。这是MSGQUEUE结构。
- 窗口消息处理函数MainWindowProc()、和窗口自身的属性数据dwAddData和dwAddData2。
- 父窗口hParent,子窗口队列hFirstChild。
- DataType,数据类型。对于窗口,为TYPE_HWND。
- WinType,窗口类型。对于MAINWIN,为TYPE_MAINWIN。
#define TYPE_MAINWIN 0x11 // 主窗口
#define TYPE_CONTROL 0x12 // 组件
#define TYPE_ROOTWIN 0x13
HWND是窗口的句柄,它实际上是指向窗口的指针。对于MAINWIN,就是MAINWIN*。
typedef GHANDLE HWND;
typedef void* GHANDLE;

1.2 窗口、组件、消息队列、线程
窗口之间可以有隶属关系。MAINWIN的成员pHosting指向父窗口。如果MAINWIN是顶层窗口,则成员pHosting为NULL。
Minigui自身创建顶层窗口HWND_DESKTOP,它指向MAINWIN实例sg_desktop_win。
static MAINWIN sg_desktop_win;
- 单线程环境下,HWND_DESKTOP就是顶层窗口,使用者在它下层创建窗口。
- 多线程环境下,使用者可以在另外的线程下创建顶层窗口。这个线程有唯一的消息队列,它的所有窗口共享这个消息队列,MAINWIN的成员pMessages指向这个消息队列。
窗口中可以有若干CONTROL。CONTROL的成员next使CONTROL可以链成一个链表。 MAINWIN的成员pFirstChild指向这个链表的头部。

线程调用GetMessage()获得自己窗口的消息,调用DispatchMessage()向窗口派发消息,DispatchMessage()最终调用窗口的窗口过程处理它。线程也直接调用窗口过程处理。
组件与消息队列不直接关联。窗口在窗口过程处理消息时,找到当前激活的组件,将消息投递给它。投递前消息可能需要预处理,比如处理鼠标、键盘事件,相应改变组件的激活状态。窗口的缺省窗口过程负责预处理。如图中的(3)。
与用户创建的窗口比,HWND_DESKTOP窗口扮演特殊角色。它的线程(不一定是主线程)从输入设备中读取输入事件后,向HWND_DESKTOP的消息队列,投递相应的消息。如图中的(1)。
HWND_DESKTOP的窗口过程处理消息时,根据当前的窗口Z-Order,将消息投递给正确的窗口。其中有些消息需要预处理,并衍生出一系列消息再投递。如图中的(2)。

1.3 再论MSGQUEUE与MSG
MSGQUEUE与MAINWIN深度绑定,是一个完全为MAINWIN设计的结构。
- 成员dwState标识队列的状态。
- 成员msg[]用作消息的环形缓存队列。一般的消息推入这个队列。
- 成员pFirstSyncMsg是一个链表,保存同步消息。

有些消息需要尽快处理,直接在MSGQUEUE.dwState中标识,而不是按先进先出的方式保存在msg[]中。
比如,向队列中推送窗口刷新消息MSG_PAINT时,dwState置为QS_PAINT。而如果没有这些特殊消息,dwState则置为QS_POSTMSG。
如下是dwState的部分可能取值。
#define QS_SYNCMSG 0x20000000
#define QS_POSTMSG 0x40000000
#define QS_QUIT 0x80000000
#define QS_PAINT 0x02000000
#define QS_EMPTY 0x00000000
1.4 异步消息MSG 与 同步消息SYNCMSG
一般的消息以异步方式投递,消息发送者发送消息后立即返回,不等待处理完成。与之相对的是同步消息。发送者发送消息后,调用wait()在semaphore上等待,窗口处理完成后,通过这个semaphore通知发送者。
MSG定义了消息。
- hwnd是要处理消息的窗口。
- message是消息编号
- wParam、lParam是消息的参数
- time是消息的事件戳
- 如果MSG是一个同步消息,则成员pAdd指向它所属的SYNCMSG。
SYNCMSG在MSG的基础上,定义了同步消息。
- msg是消息体
- retval是处理消息的结果
- sem_handle是用于通知的信号量。
同步消息在MSGQUEUE中也是特殊处理的,它保存在成员pFirstSyncMsg中。这时MSGQUEUE的状态改为QS_SYNCMSG。
1.5 每线程消息队列
如前面所说,每个线程上有一个消息队列,线程上的所有窗口共享这个队列。
mg_InitMsgQueueThisThread()分配属于线程的消息队列。

- 调用malloc()分配MSGQUEUE实例
- 调用mg_InitMsgQueue(初始化MSGQUEUE实例
- 调用pthread_setspecific()将这个结构保存在全局变量__mg_threadinfo_key指定的位置。
pthread_key_t __mg_threadinfo_key;
1.6 PostMessage()
PostMessage()向窗口投递消息。
- 调用kernel_GetMsgQueue()得到窗口的消息队列
- 调用kernel_QueueMessage()向队列投递消息,也就是向MSGQUEUE的成员msg[]“写入”消息。特殊的消息,如MSG_PAINT,则改变队列的状态为QS_PAINT,不推送消息。以后处理消息的时候,也是检查队列状态。这样窗口的刷新可以得到优先处理。

1.7 SendSyncMessage()
SendSyncMessage()投递同步消息。这时队列的状态更改为QS_SYNCMSG。
- 调用kernel_GetMsgQueue()得到窗口的消息队列。
- 将消息推送到MSGQUEUE的成员pFirstSyncMsg链表中。
- 调用POST_MSGQ()通知接收方有消息到达。
- 调用sem_getvalue(),检查队列的成员MSGUEUE.sync_msg,是否有信号。如果没有,则调用sem_post()将它置为有信号。这样接收方就开始处理。
- 调用sem_wait(),在消息的成员MSG.sem_handle上等待。接收方处理完后,应该置它为有信号。

SendMessage()是SendSyncMessage()的变种。

1.8 PeekMessageEx()
PeekMessageEx()从窗口的队列中获取消息。
- 如前面所说,有些消息没有保存在msg[]中,而是由队列状态MSGQUEUE.dwState标识,所以这里根据不同情况获取消息。

1.9 TranslateMessage()
TranslateMessage()将键盘输入消息转换为字符消息,并调用SendMessage()重新发送。

1.10 DispatchMessage()
DispatchMessage()派发消息进行处理。
- 调用GetWndProc(),得到窗口的处理函数WndProc()
- 直接调用WndProc()。

2. 组件
2.1 CTRLCLASSINFO、WNDCLASS、CONTROL
Minigui预定义了一系列组件类,如Static、Button、ProgressBar、ListBox等。
CTRLCLASSINFO定义组件的模板,也就是组件类。
- 成员name是组件类的名字
- 成员函数ControlProc()负责处理组件的消息。
WNDCLASS是注册组件类时,需提供的参数。AddNewControlClass()用于注册组件类。WNDCLASS的成员是组件的属性集。
CONTROL定义了组件。
组件是一种特殊的窗口,所以从起始地址开始,它与MAINWIN大部分成员二进制兼容。(为什么没有为这些成员定义一个公共的结构?奇怪。)
除了公共的部分,其他不一样的成员包括:
- next、prev将窗口的若干子组件链成一个双向链表。
- pParent指向父窗口
- active指向当前的激活窗口。

全局数组ccitable[]保存注册的组件类。
CTRLCLASSINFO* ccitable[26];
成员next使CTRLCLASSINFO实例可以链成一个链表,所以ccitable[]实际上是一个Hash表。
ccitable[]的长度为26,其中的位置依次对应A-Z中的字符,组件类按其名字首字符链到相应的链表。类名字必须为a-z或A-Z,如果是小写,则转换成大写处理。如Button组件名字为“Button”,就链到位置1的链表中。

2.2 AddNewControlClass()
AddNewControlClass()注册组件类。
- 调用toupper(),将组件类名转换成大写。
- 根据类名首字符,找到cci_table[]中的相应元素,这是一个链表。
- 调用malloc()创建CTRLCLASSINFO实例,用WNDCLASS指定的属性初始化它。
- 将CTRLCLASSINFO实例链入链表中。

2.3 gui_GetControlClassInfo()
gui_GetControlClassInfo()根据名字找到组件类。
- 调用toupper(),将组件类名转换成大写。
- 根据类名的首字符,找到cci_table[]中的相应元素,这是个链表。
- 遍历链表,找到名字相同的组件类。

3. 缺省窗口消息处理函数
窗口MAINWIN的成员函数MainWindowProc()负责处理消息。窗口的创建者需要提供这个函数。
但窗口的消息处理有很多通用的部分,这些部分不必创建者自己去实现,所以minigui提供了缺省的消息处理函数,也就是PreDefMainProc(),给它们使用。
组件和对话框是特别的窗口,还有属于它们的通用处理部分。minigui也给它们提供了特别的消息处理函数PreDefControlProc()和PreDefDialogProc()。这两个函数在完成它们自己的通用处理后,将其他消息委托给PreDefMainWinProc()。
3.1 PreDefMainProc()
对于一般窗口,缺省消息处理函数是PreDefMainWinProc()。

- minigui的消息按类型,如鼠标相关、键盘相关,分为若干区段。这里根据消息在哪个区段,调用相应的函数。
#define MSG_FIRSTMOUSEMSG 0x0001
#define MSG_LASTMOUSEMSG 0x0014
#define MSG_FIRSTKEYMSG 0x0015
#define MSG_LASTKEYMSG 0x001F
-
鼠标消息,调用DefaultMouseMsgHandler()。其中,
- wndMouseInWhichControl()先调用PtInRect()判断鼠标位置在哪个组件,然后发送消息给它。这里一个消息可能会衍生多个消息。
-
窗口刷新消息,调用DefaultPaintMsgHandler()。
-
MSG_NCACTIVATE消息,调用DefaultPaintMsgHandler(),其中调用wndActiveMainWindow()。这个函数负责绘制窗口和组件的标准部分,如标题栏、标题、边界等。后面“窗口的绘制”部分会进一步说明。
3.2 PreDefControlProc()
组件的缺省函数是PreDefControlProc()。

3.3 PreDefDialogProc()
对话框的缺省函数是PreDefDiaglogProc()。

3.4 宏DefaultMainXXXProc()
窗口的消息处理函数不是直接引用PreDefMainProc()等函数,而是通过DefaultMainWinProc()等宏。这组宏被扩展到一个全局函数数组__mg_def_proc[3]。
// window.h
#define DefaultMainWinProc (__mg_def_proc[0])
#define DefaultDialogProc (__mg_def_proc[1])
#define DefaultControlProc (__mg_def_proc[2])
//desktop.c
/*default window procedure*/
WNDPROC __mg_def_proc[3];
在初始化函数InitGUI()中,__mg_def_proc[]数组被初始化为PreDefMainWinProc()等函数。
// init.c
int GUIAPI InitGUI (int args, const char *agr[])
{
// init.c
/*Initialize default window process*/
__mg_def_proc[0] = PreDefMainWinProc;
__mg_def_proc[1] = PreDefDialogProc;
__mg_def_proc[2] = PreDefControlProc;
}
4. 窗口绘制
4.1 渲染器WINDOW_ELEMENT_RENDERER
WINDOW_ELEMENT_RENDERER定义了渲染器。渲染器实例按照自己的风格(look and feel)绘制视觉上的部件,如组件、光标、文字等。
- 成员name是渲染器名字
- 成员函数draw_3dbox()、draw_checkbox()等分别用于绘制各种图形对象。
LFINFO保存了WINDOW_ELEMENT_RENDERER的名字和实例。
- name是WINDOW_ELEMENT_RENDERER实例的名字
- wnd_rdr是实例指针。

数组wnd_lf_info[]保存了一组WINDOW_ELEMENT_RENDERER实例,包括classic、flat和skin。每个实例实现不同的视觉风格。这里说明的是__mg_wnd_rdr_classic。
LFINFO wnd_lf_info [MAX_NR_RENDERERS] =
{
{"classic", &__mg_wnd_rdr_classic},
{"flat", &__mg_wnd_rdr_flat},
{"skin", &__mg_wnd_rdr_skin},
};
WINDOW_ELEMENT_RENDERER __mg_wnd_rdr_classic = {
"classic",
init,
calc_3dbox_color,
draw_3dbox,
draw_radio,
...
}
4.2 set_window_renderer()
窗口和组件的共同成员we_rdr就是渲染器WINDOW_ELEMENT_RENDERER。
WINDOW_ELEMENT_RENDERER* we_rdr;
函数set_window_renderer()设置这个成员。
- 调用GetWindowRendererFromName()。遍历数组wnd_lf_info[],根据名字查找匹配的实例。
- 设置窗口的成员rdr为找到的实例。如果没找到,设置为__mg_def_rendrerer。
WINDOW_ELEMENT_RENDERER * __mg_def_renderer = &__mg_wnd_rdr_classic;

4.3 MSG_NCACTIVATE消息
当收到MSG_NCACTIVATE消息时,窗口过程调用wndActivateMainWindow()。
- 调用get_valid_dc()得到DC句柄。
- 调用WINDOW_ELEMENT_RENDERER的成员函数draw_caption()、draw_caption_button()、draw_border()绘制标题、标题栏上的按钮、边界等。

以draw_caption()为例,
- 调用GetWindowElementAttr()得到窗口的属性,如激活和非激活状态下的brush颜色、text颜色、背景色。然后根据当前是否激活,调用SetBrushColor()、SetTextColor()、SetBkMode()进行设置。
- 依次调用calc_we_area()得到指定的窗口组成部分的位置大小,并调用相应的函数绘制它。如FillBox()绘制标题栏,DrawIcon()绘制标题栏上的icon,TextOutOmitted()绘制窗口标题。TextOutOmitted()将在“字体”一节中说明。
其中FillBox()最终调用GAL_memset4()向Surface的成员screen,也就是gvfb的显存区域,写入数据。

4.4 MSG_PAINT消息
收到MSG_PAINT消息时,窗口过程绘制使用者自己的视觉部件。
cell-phone-ux-demo是minigui的示例工程。这里说明其中的窗口过程InfoBarProc()。
- 调用GetResource()得到作为背景图的bitmap图片。
- 调用BeginPaint()。其中调用get_valid_dc()得到用于绘制的DC句柄HDC,调用SelectClipRegion()设置绘制的裁剪区域。
- 调用FixBoxWithBitmap()将bitmap图片绘制到DC上。其中调用_begin_fill_bitmap()和_fill_bitmap()。
- 调用EndPaint()释放HDC。
- 调用DefaultMainWinProc()。其中绘制窗口的边框等其他元素。

_begin_fill_bitmap()得到窗口的DC。
- 调用__mg_check_ecrgn()。其中调用dc_HDC2PDC()从HDC得到PDC。
- 调用coor_LP2SP(),将DC的位置大小从设备坐标转到屏幕坐标。
- 调用SetRect()设置DC的输出范围,调用NormalizeRect()将这个范围归一化,也就是缩到1的范围。
- 调用__mg_enter_drawing()。设置DC,准备开始绘制。

_fill_bitmap()调用_dc_fillbox_bmp_clip()进行绘制。其中,
-
调用IntersecRect()得到有效绘制区域,并调用Set_GAL_CLIPRECT()设置它。
-
用DC的成员surface成员调用GAL_PutBox(),这是一个GAL_Surface实例。进而调用_PutBoxAlpha()。其中,
- 分别得到GAL_Surface的绘制缓冲和bitmap图的数据地址
- 调用DUFFS_LOOP4()将bitmap的数据复制到GA_Surface的缓冲(也就是成员pixels)中,这时绘制就完成了。

值得解释一下的是DUFFS_LOOP4()这个宏,它在多个开源软件中被使用。
#define DUFFS_LOOP4(pixel_copy_increment, width) { \
int n = (width+3)/4; \
switch (width & 3) { \
case 0: do { pixel_copy_increment; \
case 3: pixel_copy_increment; \
case 2: pixel_copy_increment; \
case 1: pixel_copy_increment; \
} while ( --n > 0 ); \
} \
}
DUFF_LOOP4()负责复制数据,但不是简单地在一个循环中依次复制每个字(四字节),而是在每次循环中复制相邻的4个字。 这是通过将do/while 语句糅合进switch/case语句中来实现的。这样做的目的,是把复制任务分解成4个可并行化的任务,在多核cpu的系统上提高复制的效率。更详细的解释可以参考如下两个链接:
Unrolling Loops
HLS增大运算吞吐量的硬件优化
5. 窗口创建
5.1 创建窗口CreateMainWindow()
CreateMainWindow()创建窗口。

- 调用calloc()分配MAINWIN示例。
- 调用GetMsgQueueThisThread()得到对应的消息队列。如果队列还没有创建,则调用mg_InitMsgQueueThisThread()创建。
- 如果窗口不是顶层窗口,则将它的成员pHosting绑定到当前的顶层窗口上。
- 调用set_window_render()设置渲染器。
- 从配置文件读取字体设置信息,并设置字体。如果没有读到,则调用GetSystemFont()得到系统字体并设置。
- 发送MSG_ADDNEWMAINWIN消息给HWND_DESKTOP窗口,向它注册自身。HWND_DESKTOP负责窗口的Z-Order位置。
- 向窗口发送MSG_CREATE消息。这时窗口的窗口过程可以创建更多的子窗口和组件。
5.2 创建组件CreateWindowEx()
CreateWindowEx()创建组件,它调用CreateWindowEx2()。
- 调用SendMessage(),发送消息MSG_GETCTRLCLASSINFO给窗口HWND_DESKTOP,并等待。
- 在另外一个线程中,DestkopMain()调用DesktopWinProc()处理这个消息。它调用gui_getControlClassInfo(),得到组件类实例WNDCTRLCLASSINFO,并返回。
- CreateWindowEx()得到组件类实例。
- 调用malloc()创建CONTROL实例,用WNDCTRLCLASSIFO的属性初始化它。
- 调用set_control_renderer()设置渲染器WINDOW_ELEMENT_RENDERER。
- 发送消息MSG_NEWCTRLINSTANCE给窗口HWND_DESKTOP,注册CONTROL。
- 给组件发送MSG_CREATE消息,组件的消息处理函数处理它。
- 给组件发送MSG_NCPAINT消息,组件的消息处理函数处理它,绘制自己。

网友评论