嵌入式GUI FTK设计与实现-主循环
作者: 李先静
日期: 2010-03-28
本文介绍了嵌入式 GUI FTK 的主循环的设计思想和实现方法。

带图形用户界面(GUI)的应用程序和传统的批处理程序是不同的:

  • 批处理程序是一步一步的执行,直到完成任务为止,完成任务后程序立即退出。
  • 图形用户界面应用程序则是事件驱动的,它等待事件发生,然后处理事件,如此循环,直到用户要求退出为止。

两种执行模型如下图所示:

两种执行模型
两种执行模型

通常我们把等待事件/处理事件的循环称为主循环(MainLoop),主循环是GUI应用程序必要组件之一,FTK当然也离开不主循环 (MainLoop)。大多数嵌入式GUI都采用了Windows类似的主循环:


    while(GetMessage(&msg))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

我不太喜欢这种主循环,主要原因有两点:

  1. 它看似简洁,但内部实现并不简洁。它从当前线程的队列里获取消息,然后处理消息。这些消息从来哪里的?当然是由其它线程传递给当前线程的,这就意味着GUI需要多线程的支持。而FTK不想引入多线程来折磨自己,至少在GUI部分是不想的。
  2. 这是面向过程的处理方式。消息是一个对象,按照面向对象的思想来说,对象的数据和行为应该绑定在一起。而这里的消息是纯粹的数据,所有消息都由目标窗口的消息处理函数来处理的。FTK希望每类消息都有自己的处理函数,而不是全部由窗口来处理。

FTK采用了GTK类似的主循环:


ftk_run();

它看起来更简洁,内部实现也不需要多线程的支持。这里采用了POSA(面向模式的软件架构)中的Reactor模式,它的主要好处有:

  1. 用单线程处理多个事件源。
  2. 增加新事件源时不会影响事件分发的框架。

整个主循环由下列组件构成:

主循环组件图
主循环组件图

FtkSource 是一个接口,是所有事件源的抽象,它要求事件源实现下列函数:

  1. ftk_source_get_fd 用来获取文件描述符,当然这个文件描述符不一定是真正的文件描述符,只要是MainLoop能挂在上面等待的句柄(Handle)即可。
  2. ftk_source_check 用来检查事件源要求等待的时间。-1表示不关心等待时间。0表示要马就有事件发生,正数表示在指定的时间内将有事件发生。
  3. ftk_source_dispatch 用来处理事件,每个事件源都有自己的处理函数,而不是全部耦合到窗口的处理函数中。

FtkSourcesManager负责管理所有事件源。主要提供下列函数:

  1. ftk_sources_manager_add 增加一个事件源。
  2. ftk_sources_manager_remove 删除一个事件源。
  3. ftk_sources_manager_get_count 获取事件源总数。
  4. ftk_sources_manager_get 获取指定索引的事件源。

FtkMainLoop负责循环的等待事件发生,然后调用事件源的处理函数去处理。主要提供下列函数:

  1. ftk_main_loop_run 启动主循环
  2. ftk_main_loop_quit 退出主循环
  3. ftk_main_loop_add_source 增加一个事件源
  4. ftk_main_loop_remove_source 删除一个事件源

FtkMainLoop提供了add_source和remove_source两个函数对 FtkSourcesManager相应函数进行包装,这里包装不是简单的调用FtkSourcesManager的函数,而是发送一个事件:


Ret ftk_main_loop_add_source(FtkMainLoop* thiz, FtkSource* source)
{
    FtkEvent event = {0};
    return_val_if_fail(thiz != NULL && source != NULL, RET_FAIL);

    event.type = FTK_EVT_ADD_SOURCE;
    event.u.extra = source;

    return ftk_source_queue_event(ftk_primary_source(), &event);
}

Ret ftk_main_loop_remove_source(FtkMainLoop* thiz, FtkSource* source)
{
    FtkEvent event = {0};
    return_val_if_fail(thiz != NULL && source != NULL, RET_FAIL);

    event.type = FTK_EVT_REMOVE_SOURCE;
    event.u.extra = source;

    return ftk_source_queue_event(ftk_primary_source(), &event);
}

这个事件由Primary Source进行处理:


static Ret ftk_source_primary_dispatch(FtkSource* thiz)
{
    ...
    switch(event.type)
    {
        case FTK_EVT_ADD_SOURCE:
        {
            ftk_sources_manager_add(ftk_default_sources_manager(), event.u.extra);
            break;
        }
        case FTK_EVT_REMOVE_SOURCE:
        {
            ftk_sources_manager_remove(ftk_default_sources_manager(), event.u.extra);
            break;
        }
    }
    ...
}

为什么要多此一举呢?原因这样的:FTK是单线程的,GUI线程只负责用户界面的管理,由后台工作的线程负责长时间的操作。但是后台工作的线程经常需要更新用户界面,比如下载网络数据的线程要更新下载进度界面。FTK需要提供一种机制,让后台线程来更新用户界面但又不需要引入互斥机制。这可以通过idle来串行化对GUI的操作,后台线程要更新GUI时,就增加一个idle source,后台线程不能直接调用ftk_sources_manager_add,那样需要互斥机制,而且也不能唤醒主循环去处理这个idle。所以它通过Primary Source的管道发送一个事件,这个事件会唤醒睡眠中的主循环,然后调用Primary Source分发函数去处理事件。

现在我们来看ftk_main_loop_run的实现,ftk_main_loop_run的实现是平台相关的,对于支持select的平台,用 select是最好的方法。下面是基于select的实现:

1.找出最小等待时间和文件描述符


        for(i = 0; i < ftk_sources_manager_get_count(thiz->sources_manager); i++)
        {
            source = ftk_sources_manager_get(thiz->sources_manager, i);
            if((fd = ftk_source_get_fd(source)) >= 0)
            {
                FD_SET(fd, &thiz->fdset);
                if(mfd < fd) mfd = fd;
                n++;
            }

            source_wait_time = ftk_source_check(source);
            if(source_wait_time >= 0 && source_wait_time < wait_time)
            {
                wait_time = source_wait_time;
            }
        }

这里遍历所有source,找出一个最小的等待时间和要等待的文件描述符。

2. 等待事件发生


        tv.tv_sec = wait_time/1000;
        tv.tv_usec = (wait_time%1000) * 1000;
        ret = select(mfd + 1, &thiz->fdset, NULL, NULL, &tv);

3.检查事件源并调用相应的事件处理函数


            if( (ret > 0) && (fd = ftk_source_get_fd(source)) >= 0 && FD_ISSET(fd, &thiz->fdset))
            {
                if(ftk_source_dispatch(source) != RET_OK)
                {
                    /*as current is removed, the next will be move to current, so dont call i++*/
                    ftk_sources_manager_remove(thiz->sources_manager, source);
                    ftk_logd("%s:%d remove %p\n", __func__, __LINE__, source);
                }
                else
                {
                    i++;
                }
                continue;
            }
            //这里处理timer和idle。
            if((source_wait_time = ftk_source_check(source)) == 0)
            {
                if(ftk_source_dispatch(source) != RET_OK)
                {
                    /*as current is removed, the next will be move to current, so dont call i++*/
                    ftk_sources_manager_remove(thiz->sources_manager, source);
                    //ftk_logd("%s:%d remove %p\n", __func__, __LINE__, source);
                }
                else
                {
                    i++;
                }
                continue;
            }

如果事件源处理函数的返回值不是RET_OK的事件,我们认为出错了或者是事件要求自己被移除,那就把它移除掉。

GUI是事件驱动的,创建一个窗口后,函数马上就返回了,窗口中的控件对用户事件处理是在以后的事件循环中进行的。这对于大多数情况是正常的,但有时我们需要用户确认一些问题,根据确认的结果做相应的处理。比如,用户删除一个文件,我们要确认他是否真的想删除后才能去删除。也就是在创建对话框后,函数不是马上返回,而且等用户确认,关闭对话框后才返回。

为了做到这一点,我们要在一个事件处理函数中,创建另外一个主循环来分发事件。模态对话框就是这样实现的:


int ftk_dialog_run(FtkWidget* thiz)
{
    DECL_PRIV1(thiz, priv);
    return_val_if_fail(thiz != NULL, RET_FAIL);
    return_val_if_fail(ftk_widget_type(thiz) == FTK_DIALOG, RET_FAIL);

    ftk_widget_show_all(thiz, 1);
    priv->main_loop = ftk_main_loop_create(ftk_default_sources_manager());
    ftk_main_loop_run(priv->main_loop);
    ftk_main_loop_destroy(priv->main_loop);
    priv->main_loop = NULL;

    return ftk_widget_id(ftk_window_get_focus(thiz));
}

对话模型提供了一个用于退出该主循环的函数:


Ret ftk_dialog_quit(FtkWidget* thiz)
{
    DECL_PRIV1(thiz, priv);
    return_val_if_fail(ftk_dialog_is_modal(thiz), RET_FAIL);

    ftk_main_loop_quit(priv->main_loop);

    return RET_OK;
}

文章出处:http://www.limodev.cn/blog

作者联系方式:李先静 <xianjimli@gmail.com>