最小GUI库GuiLite源码分析--Apple的学习笔记

前言

之前研究qemu的目的之一就是想用用qemu的stm32二次开发版本进行LCD显示实验。但是真的看了qemu stm32的源码后,发现并不支持LCD驱动的。所以我考虑是否由我自己来添加LCD驱动仿真,进行qemu二次开发。而步骤1就是我要先自己玩下基于stm32的LCD驱动应用编程。而我之前买oled屏幕后也买过一块stm32F407的开发板。oled能正常驱动,并且翻出了10年前买的2.4寸tft屏幕(80接口8bit的ili9325)也能正常驱动。

然后我发现就简单的显示内容没有动画效果,觉得不好玩,于是想起来2019年底下载过一个GUI开源软件GuiLite。然后2021年了我去看看它是否在不断的更新。所以我主要分析的是2019年底的版本,2021最新版也看了下,基础内容差距不大。

当然我也尝试了下将GuiLite移植到stm32F407开发板上,按Doc中help截图的操作步骤,还是很容易的。我现在看似在玩应用,其实我研究的还是底层库源码设计机制,所以我定义的一年视觉相关底层的的研究方向没有跑偏哈~


image.png

GuiLite源码研究

首先要明确目标,就是我分析GuiLite源码的目的是想了解GUI的设计原理。因为让我直接写个GUI引擎框架,我暂时不会。因为好奇,所以要去了解,毕竟他就5000行代码搞定的事情,我觉得很神奇。

我看了几个例子后,通过调试跟进源码,基本上已经了解了他的设计方法,让我自己设计的话我也有了方向。虽然源码还没有全看完,但是surface及widgets基本控件的原理都已经了解了,此次的目标已达成。另外的好处是,对这些控件的源码分析后,自己也可以比较灵活的去调用API做些小作品,蛮好玩的。我就喜欢这样小巧的代码,麻雀虽小五脏俱全。如下是我过程中分析源码的笔记,取其精华去其糟粕。我也发现了些bug,以及我觉得某些点,它还有继续完善的空间。
A.HelloStar工程

  1. 范围检查技巧
    宽度和高度超范处理
    x0 = (x0 < 0) ? 0 : x0;
    y0 = (y0 < 0) ? 0 : y0;
    x1 = (x1 > (m_width - 1)) ? (m_width - 1) : x1;
    y1 = (y1 > (m_height - 1)) ? (m_height - 1) : y1;
  1. 随机数不超过范围的技巧,用取余数
    m_x = m_start_x = rand() % UI_WIDTH;
    m_y = m_start_y = rand() % UI_HEIGHT;
  1. 物体移动的技巧
    先清除之前的绘制变成当前的背景,再重新绘制新的。至于只有一个图层为黑色底色的,就是直接画黑色,就是清除的意思,然后再重新绘制新的物体图像,看上去就是移动的效果。

B.HelloLayers工程

  1. 关于Layer的处理技巧
    若有2层surface,则一开始需要申请Z_ORDER_LEVEL_1,然后就会进入如下的for循环,会为m_layers[i].fb分配内存空间。
    若有3层surface,则一开始需要申请Z_ORDER_LEVEL_2。
    void set_surface(Z_ORDER_LEVEL max_z_order, c_rect layer_rect)
    {
        m_max_zorder = max_z_order;
        if (m_display && (m_display->m_surface_cnt > 1))
        {
            m_fb = calloc(m_width * m_height, m_color_bytes);
        }

        for (int i = Z_ORDER_LEVEL_0; i < m_max_zorder; i++)
        {//Top layber fb always be 0
            ASSERT(m_layers[i].fb = calloc(layer_rect.width() * layer_rect.height(), m_color_bytes));
            m_layers[i].rect = layer_rect;
        }
    }

这个内存在fill_rect函数中进行赋值的,m_layers[z_order].fb。若surface只有1级的话,不会为fb赋值。首先要思考下为什么要为fb赋值,其实就是备份的意思。

        if (z_order == m_top_zorder)
        {
            int x, y;
            c_rect layer_rect = m_layers[z_order].rect;
            unsigned int rgb_16 = GL_RGB_32_to_16(rgb);
            for (y = y0; y <= y1; y++)
            {
                for (x = x0; x <= x1; x++)
                {
                    if (layer_rect.pt_in_rect(x, y))
                    {
                        if (m_color_bytes == 4)
                        {
                            ((unsigned int*)m_layers[z_order].fb)[(y - layer_rect.m_top) * layer_rect.width() + (x - layer_rect.m_left)] = rgb;
                        }
                        else
                        {
                            ((unsigned short*)m_layers[z_order].fb)[(y - layer_rect.m_top) * layer_rect.width() + (x - layer_rect.m_left)] = rgb_16;
                        }
                    }
                }
            }
            return fill_rect_on_fb(x0, y0, x1, y1, rgb);
        }

在draw_pixel函数中也有对fb赋值。也就是说,所有绘制的地方,都会对fb进行更新值,记录最新的值。技巧就是判断if (z_order == m_max_zorder)则直接return绘制结果。若为单层,则此条件一定满足,就不会备份fb了,而对于多层则要在更新图像的时候备份fb。目的是为了将上一层级消除的时候对下一层级进行还原。

        if (z_order == m_max_zorder)
        {
            return draw_pixel_on_fb(x, y, rgb);
        }
        
        if (z_order > (unsigned int)m_top_zorder)
        {
            m_top_zorder = (Z_ORDER_LEVEL)z_order;
        }
        if (m_layers[z_order].rect.pt_in_rect(x, y))
        {
            c_rect layer_rect = m_layers[z_order].rect;
            if (m_color_bytes == 4)
            {
                ((unsigned int*)(m_layers[z_order].fb))[(x - layer_rect.m_left) + (y - layer_rect.m_top) * layer_rect.width()] = rgb;
            }
            else
            {
                ((unsigned short*)(m_layers[z_order].fb))[(x - layer_rect.m_left) + (y - layer_rect.m_top) * layer_rect.width()] = GL_RGB_32_to_16(rgb);
            }
        }

对于c_rect对象的还原方法是overlapped_rect,需要调用2句函数,一句是创建一个rect对象,目的是设置工作局域,另外一句是设置要还原的图层。

    c_rect overlapped_rect(LAYER_1_X, LAYER_1_Y, LAYER_1_WIDTH, LAYER_1_HEIGHT);
    s_surface->show_layer(overlapped_rect, Z_ORDER_LEVEL_0);

可以看到在show_layer中会取出m_layers[z_order].fb,重写到LCD上。其实就是还原。

    int show_layer(c_rect& rect, unsigned int z_order)
    {
        ASSERT(z_order >= Z_ORDER_LEVEL_0 && z_order < Z_ORDER_LEVEL_MAX);
        c_rect layer_rect = m_layers[z_order].rect;
        ASSERT(rect.m_left >= layer_rect.m_left && rect.m_right <= layer_rect.m_right &&
        rect.m_top >= layer_rect.m_top && rect.m_bottom <= layer_rect.m_bottom);
        void* fb = m_layers[z_order].fb;
        int width = layer_rect.width();
        for (int y = rect.m_top; y <= rect.m_bottom; y++)
        {
            for (int x = rect.m_left; x <= rect.m_right; x++)
            {
                unsigned int rgb = (m_color_bytes == 4) ? ((unsigned int*)fb)[(x - layer_rect.m_left) + (y - layer_rect.m_top) * width] : GL_RGB_16_to_32(((unsigned short*)fb)[(x - layer_rect.m_left) + (y - layer_rect.m_top) * width]);
                draw_pixel_on_fb(x, y, rgb);
            }
        }
        return 0;
    }
  1. 基于HelloLayers将Hellostar添加入UIcode.c,变成双图层,但是底层图层是动态的。修改后,遇到的问题是,star绘制的时候会擦除顶层的图片。
    load_resource();
    draw_on_layer_0();
    while(1) {
        stars[0].move();
        thread_sleep(70);
        cnt++;
        if (cnt % 60 == 0)
        {
            draw_on_layer_1();  
            layer1 = 0;
        }
        if (cnt % 91 == 0)
        {
            clear_layer_1();
            layer1 = 1;
        }
        if (cnt >= 32767)
        {
            cnt = 0;
        }

见图


image.png

然后想到了办法临时解决下。方法就是star运动绘制后它不是会更新顶层区域的图像嘛,所有我的修改时,当顶层小窗口显示时,下一层star重绘后,我立即重绘下顶层图像。可以解决如上问题,但是感觉刷屏比较频繁,屏幕有闪烁感。这个问题要等我全部看完GuiLite看看还有哪些API可以用来更好的解决我遇到问题。

    load_resource();
    draw_on_layer_0();
    while(1) {
        stars[0].move();
        if (layer1 == 0)
            draw_on_layer_1();
        thread_sleep(70);
        cnt++;
        if (cnt % 60 == 0)
        {
            layer1 = 0;
        }
        if (cnt % 91 == 0)
        {
            clear_layer_1();
            layer1 = 1;
        }
        if (cnt >= 32767)
        {
            cnt = 0;
        }
    }

C.HelloWidgets学习窗口对象的原理

  1. 链表归递的技巧
    UI窗口对象的处理相关函数中会看到链表归递,其实归递函数我平时都不太用的,常用的还是数组,依次扫描。
    通过child->show_window()进行归递,退出条件为child==null,依次扫描的目的是为每个对象调用on_paint进行绘制。而双链表对象是通过add_child_2_tail函数添加到双链表末尾的。
void c_wnd::show_window()
{
    if (ATTR_VISIBLE == (m_attr & ATTR_VISIBLE))
    {
        on_paint();
        c_wnd *child = m_top_child;
        if ( 0 != child )
        {
            while ( child )
            {
                child->show_window();
                child = child->m_next_sibling;
            }
        }
    }
}

c_wnd::connect函数中if (load_child_wnd(p_child_tree) >= 0)的函数load_child_wnd中也是归递,通过调用p_cur->p_wnd->connect实现归递,它由于涉及了父函数的归递,而不是自己本身的归递。退出条件while(p_cur->p_wnd),也就是p_cur->p_wnd为null就退出。所以可以看到传入的窗口对象数组中最后一行都是null。

WND_TREE s_main_widgets[] =
{
    { &s_edit1,     ID_EDIT_1,  "ABC",  150, 10, 100, 50},
……
    { &s_my_dialog, ID_DIALOG,  "Dialog",   200, 100, 280, 312, s_dialog_widgets},
    {NULL, 0 , 0, 0, 0, 0, 0}
};
int c_wnd::load_child_wnd(WND_TREE *p_child_tree)
{
    if (0 == p_child_tree)
    {
        return 0;
    }
    int sum = 0;

    WND_TREE* p_cur = p_child_tree;
    while(p_cur->p_wnd)
    {
        if (0 != p_cur->p_wnd->m_resource_id)
        {//This wnd has been used! Do not share!
            ASSERT(false);
            return -1;
        }
        else
        {
            p_cur->p_wnd->connect(this, p_cur->resource_id, p_cur->str,
                p_cur->x, p_cur->y, p_cur->width, p_cur->height,p_cur->p_child_tree);
        }
        p_cur++;
        sum++;
    }
    return sum;
}
  1. 分析下2019年底GuiLite中的load_cmd_msg()函数,主要就是设置回调函数的,比较关键的变量就是GetMSgEntries,因为2021版本已经源码中已经不是这样设计了。
void c_cmd_target::load_cmd_msg()
{
    const GL_MSG_ENTRY* p_entry = GetMSgEntries();
    if (0 == p_entry)
    {
        return;
    }
    bool bExist = false;
    while(MSG_TYPE_INVALID != p_entry->msgType)
    {
        if (MSG_TYPE_WND == p_entry->msgType)
        {
            p_entry++;
            continue;
        }
     ……
}

在代码中回调函数是这样定义的

GL_BEGIN_MESSAGE_MAP(c_my_ui)
ON_GL_BN_CLICKED(ID_BUTTON, c_my_ui::on_button_clicked)
ON_SPIN_CONFIRM(ID_SPIN_BOX, c_my_ui::on_spinbox_confirm)
ON_SPIN_CHANGE(ID_SPIN_BOX, c_my_ui::on_spinbox_change)
ON_LIST_CONFIRM(ID_LIST_BOX, c_my_ui::on_listbox_confirm)
GL_END_MESSAGE_MAP()

GL_BEGIN_MESSAGE_MAP需要传入对象,它调用的数组都是对象:: GetMSgEntries()函数来获取某个对象的mMsgEntries[]数组内容。所以若要使用,则要在定义类的时候添加上GL_DECLARE_MESSAGE_MAP,进行初始化。

#define GL_BEGIN_MESSAGE_MAP(theClass)                  \
const GL_MSG_ENTRY* theClass::GetMSgEntries() const \
{                                                       \
    return theClass::mMsgEntries;                       \
}                                                       \
const GL_MSG_ENTRY theClass::mMsgEntries[] =            \
{

这个数组对象的结构体类型如下

struct GL_MSG_ENTRY
{
    unsigned int        msgType;
    unsigned int        msgId;
    c_cmd_target*       pObject;
    MSG_CALLBACK_TYPE   callbackType;
    MsgFuncVV           func;
};

而这些callback函数的调用是在notify_parent中。

    switch (entry->callbackType)
    {
    case MSG_CALLBACK_VV:
        (m_parent->*msg_funcs.func)();
        break;
    case MSG_CALLBACK_VVL:
        (m_parent->*msg_funcs.func_vvl)(param);
        break;
    case MSG_CALLBACK_VWV:
        (m_parent->*msg_funcs.func_vwv)(m_resource_id);
        break;
    case MSG_CALLBACK_VWL:
        (m_parent->*msg_funcs.func_vwl)(m_resource_id, param);
        break;
    default:
        ASSERT(false);
        break;
    }

继续倒推分析代码,查看调用关系。比如c_button对象notify_parent被c_button::on_key调用

bool c_button::on_key(KEY_TYPE key)
{
    if (key == KEY_ENTER)
    {
        notify_parent(GL_BN_CLICKED, 0);
        return false;// Do not handle KEY_ENTER by other wnd.
    }
    return true;// Handle KEY_FOWARD/KEY_BACKWARD by parent wnd.
}
  1. c_wnd::on_touch分析
    通过PtInRect来判断传入的x和y点击位置是否在某个控件内,这样就可以找到窗口对象的位置。
    if (true == rect.PtInRect(x, y) || child->m_attr & ATTR_MODAL)
    然后就设置目的对象target_wnd = child;最后调用具体对象的on_touch函数target_wnd->on_touch(x, y, action);这里guilite设计的不好的地方是没有添加break,找到target后还在进行for循环,我觉得比较浪费时间。另外,这样子的设计让我想到若有2个控件窗口位置重叠,然后鼠标点击的是重叠位置,那么找到的对象就是数组后面的对象了。直接实验了下,让2个控件窗口重叠,果然是存在这样的问题,哈哈,我分析源码找到一个bug。
    然后说下点击中down要做的事情,它会绘制2次图像,先画focus的图像,因为set_child_focus中设置m_status = STATUS_FOCUSED,并且调用函数on_paint。然后再设置m_status = STATUS_PUSHED后调用on_paint。注意对button对象若是非TOUCH DOWN,就是TOUCH UP,会调用notify_parent,而一开始设置m_parent的目的,是按钮弹起的时候notify_parent会通过获取m_parent的回调函数数组找到需要执行的回调函数。源码看到这里我在想,为什么要用parent去找到对应的回调函数呢?而不是直接找回调函数,主要原因应该是对象本身成员中并没有定义回调函数成员。
bool c_button::on_touch(int x, int y, TOUCH_ACTION action)
{
    if (action == TOUCH_DOWN)
    {
        m_parent->set_child_focus(this);
        m_status = STATUS_PUSHED;
        on_paint();
    }
    else
    {
        m_status = STATUS_FOCUSED;
        on_paint();
        notify_parent(GL_BN_CLICKED, 0);
    }
    return true;
}

而on_paint是比较底层的绘图函数,可以看到按不同的传入消息,进行不同的绘图处理,如下draw_xxx和fill_rect都是比较熟悉的函数了。

    case STATUS_FOCUSED:
        m_surface->fill_rect(rect, c_theme::get_color(COLOR_WND_FOCUS), m_z_order);
        if (m_str)
        {
            c_word::draw_string_in_rect(m_surface, m_z_order, m_str, rect, m_font_type, m_font_color, c_theme::get_color(COLOR_WND_FOCUS), ALIGN_HCENTER | ALIGN_VCENTER);
        }
        break;
    case STATUS_PUSHED:
        m_surface->fill_rect(rect, c_theme::get_color(COLOR_WND_PUSHED), m_z_order);
        m_surface->draw_rect(rect, c_theme::get_color(COLOR_WND_BORDER), 2, m_z_order);
        if (m_str)
        {
            c_word::draw_string_in_rect(m_surface, m_z_order, m_str, rect, m_font_type, m_font_color, c_theme::get_color(COLOR_WND_PUSHED), ALIGN_HCENTER | ALIGN_VCENTER);
        }
        break;

如下记录了下vs中鼠标点击button后的回调函数。若换成单片机,需要使用touch屏,检查到按下后调用GuiLite的函数。

void CHelloWidgetsDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
    CPoint guilitePos = pointMFC2GuiLite(point);
    sendTouch2HelloWidgets(guilitePos.x, guilitePos.y, false);
}

void CHelloWidgetsDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
    CPoint guilitePos = pointMFC2GuiLite(point);
    sendTouch2HelloWidgets(guilitePos.x, guilitePos.y, true);
}

但是我看了下on_key,用2019年底的guilite只有3种数值。所以最后就是判断为KEY_ENTER后退出。

typedef enum
{
    KEY_FORWARD,
    KEY_BACKWARD,
    KEY_ENTER
}KEY_TYPE;
  1. 关于s_main_widgets中最后一个成员对象,可以理解为存在2级弹出消息窗口,所以UI创建的时候用的是Z_ORDER_LEVEL_2。
WND_TREE s_main_widgets[] =
{
    { &s_edit1,     ID_EDIT_1,  "ABC",  150, 10, 100, 50},
……

    { &s_my_dialog, ID_DIALOG,  "Dialog",   200, 100, 280, 312, s_dialog_widgets},
    {NULL, 0 , 0, 0, 0, 0, 0}
};

另外一个特殊处理就是二级窗口,s_dialog_widgets代表二级窗口对象,若有二级则load_child_wnd(p_child_tree)不会直接返回0,再次调用其中的connect递归函数,进行二次迭代而已,并且二级会继续插入到双链表控件对象,二级最后也一定会设置NULL,最终二级也会调用load_cmd_msg方法来绑定回调函数。

    if (load_child_wnd(p_child_tree) >= 0)
    {
        load_cmd_msg();
        on_init_children();
    }

这个二次递归的退出条件就是是否存在child tree。若为NULL则没有二次迭代。返回上一次都为while循环对一级对象进行递归处理。

    if (0 == p_child_tree)
    {
        return 0;
    }

然后我看了当前2021年最新版本load_cmd_msg绑定回调函数已经没有了,变成了在初始化的时候通过调用函数来绑定。
list_box->set_on_change((WND_CALLBACK)&c_my_ui::on_listbox_confirm);就是为on_click成员函数赋值,将function挂入on_click成员。同理,最后回调函数的调用方式也不是notify_parent(GL_BN_CLICKED, 0);而是直接为对象调用on_click函数进行操作了。
这样的初始化时刻写入,配合使用时候读取,我觉得看起来比较清楚。以前用id关联,判断还要做搜索匹配,匹配id成功后再调用回调函数。还是最新代码的设计看上去比较清爽些。这也是我之前分析的,他在对象中添加了回调函数成员,所以才可以这样设计,哈哈,看来我分析的过程中还真的看出了些问题,所以最新版中也有人看出了这样的问题,并且完成了修改。

            if(on_click)
            {
                (m_parent->*(on_click))(m_id, 0);
            }
  1. 窗口关闭函数
    按下dialog按钮后,弹出二级窗口,点击二级窗口上的退出按钮就可以关闭窗口,而点击退出按钮后会调用btn的回调函数,回调函数中会调用c_dialog::close_dialog,此函数中会调用set_frame_layer_visible_rect方法,此方法中比较关键的就是z_order-1,然后从m_frame_layers中获取此窗口大小内之前的图像数据进行恢复。如下2句是最重要的,先从当前图层获取frame窗口大小,然后图层数据减1,就可以从下一图层获取此窗口大小的图像信息进行重绘还原,也就是取消了弹窗。
    c_rect old_rect = m_frame_layers[z_order].visible_rect;
    //Recover the lower layer
    int src_zorder = (Z_ORDER_LEVEL)(z_order - 1);
    int display_width = m_display->get_width();
    int display_height = m_display->get_height();
    for (int y = old_rect.m_top; y <= old_rect.m_bottom; y++)
    {
        for (int x = old_rect.m_left; x <= old_rect.m_right; x++)
        {
            if (!rect.PtInRect(x, y))
            {
                unsigned int rgb = ((unsigned short*)(m_frame_layers[src_zorder].fb))[x + y * m_width];
                draw_pixel_on_fb(x, y, GL_RGB_16_to_32(rgb));
            }
        }
    }

你可能感兴趣的:(最小GUI库GuiLite源码分析--Apple的学习笔记)