美文网首页
minigui 4.0 源代码分析(六)

minigui 4.0 源代码分析(六)

作者: RonZheng2010 | 来源:发表于2020-11-24 23:04 被阅读0次

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消息,组件的消息处理函数处理它,绘制自己。

相关文章

网友评论

      本文标题:minigui 4.0 源代码分析(六)

      本文链接:https://www.haomeiwen.com/subject/ohqpiktx.html