最小GUI庫GuiLite源碼分析--Apple的學習筆記 前言 GuiLite源碼研究

前言

之前研究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截圖的操作步驟,還是很容易的。我現在看似在玩應用,其實我研究的還是底層庫源碼設計機制,所以我定義的一年視覺相關底層的的研究方向沒有跑偏哈~


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;
        }

見圖


然後想到了辦法臨時解決下。方法就是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));
            }
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章