前言
之前研究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工程
- 範圍檢查技巧
寬度和高度超範處理
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;
- 隨機數不超過範圍的技巧,用取餘數
m_x = m_start_x = rand() % UI_WIDTH;
m_y = m_start_y = rand() % UI_HEIGHT;
- 物體移動的技巧
先清除之前的繪製變成當前的背景,再重新繪製新的。至於只有一個圖層爲黑色底色的,就是直接畫黑色,就是清除的意思,然後再重新繪製新的物體圖像,看上去就是移動的效果。
B.HelloLayers工程
- 關於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;
}
- 基於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學習窗口對象的原理
- 鏈表歸遞的技巧
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;
}
- 分析下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.
}
-
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;
- 關於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);
}
- 窗口關閉函數
按下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));
}
}
}