第五章:布局系統(lv_flex, lv_grid)
歡迎回來!
在第四章:樣式(lv_style)中,我們掌握了如何通過色彩、字體和圓角等特性美化部件。當界面元素具備視覺吸引力后,如何優雅地組織它們便成為新的挑戰。
設想我們擁有多個精美按鈕,希望實現以下布局效果:
- 橫向/縱向等距排列
- 屏幕尺寸變化時自動適配
- 動態增刪元素時自動調整
傳統手工計算坐標的方式顯然低效且難以維護,這正是布局系統的價值所在。
布局系統核心價值
LVGL布局系統受現代網頁設計(CSS Flexbox/Grid)啟發,通過聲明式配置實現:
布局類型概覽
布局類型 | 適用場景 | 典型應用 |
---|---|---|
彈性布局 | 單向流式排列 | 導航欄|設置項列表 |
網格布局 | 二維矩陣排列 | 儀表盤|相冊縮略圖 |
啟用布局模塊
在lv_conf.h
中激活配置:
/*==================* 布局模塊*================*/
#define LV_USE_FLEX 1 // 啟用彈性布局
#define LV_USE_GRID 1 // 啟用網格布局
彈性布局(lv_flex)
1. 容器初始化
lv_obj_t * flex_container = lv_obj_create(screen_main);
lv_obj_set_size(flex_container, LV_PCT(90), LV_PCT(80)); // 相對父容器90%寬/80%高
lv_obj_set_layout(flex_container, LV_LAYOUT_FLEX); // 聲明彈性容器
2. 排列方向控制
// 橫向排列(可換行)
lv_obj_set_flex_flow(flex_container, LV_FLEX_FLOW_ROW_WRAP);// 縱向排列(可換列)
lv_obj_set_flex_flow(flex_container, LV_FLEX_FLOW_COLUMN_WRAP);
3. 對齊方式
// 主軸居中|交叉軸居中|軌道居中
lv_obj_set_flex_align(flex_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
4. 空間分配
lv_obj_t * expand_btn = lv_button_create(flex_container);
lv_obj_set_flex_grow(expand_btn, 2); // 占據剩余空間2份lv_obj_t * normal_btn = lv_button_create(flex_container);
lv_obj_set_flex_grow(normal_btn, 1); // 占據剩余空間1份
5. 間距控制
static lv_style_t flex_style;
lv_style_init(&flex_style);
lv_style_set_pad_column(&flex_style, 10); // 列間距10像素
lv_style_set_pad_row(&flex_style, 15); // 行間距15像素
lv_obj_add_style(flex_container, &flex_style, 0);
網格布局(lv_grid)
1. 容器初始化
lv_obj_t * grid_container = lv_obj_create(screen_main);
lv_obj_set_layout(grid_container, LV_LAYOUT_GRID); // 聲明網格容器
2. 網格結構定義
// 列定義:固定100px|彈性1份|彈性2份
static int32_t col_dsc[] = {100, LV_GRID_FR(1), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST};// 行定義:自適應內容高度|彈性3份
static int32_t row_dsc[] = {LV_GRID_CONTENT, LV_GRID_FR(3), LV_GRID_TEMPLATE_LAST};lv_obj_set_grid_dsc_array(grid_container, col_dsc, row_dsc);
3. 單元格定位
// 按鈕定位到(0,0)單元格,橫向居左|縱向居頂
lv_obj_t * btn = lv_button_create(grid_container);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_START, 0, 1, // 列:起始對齊,第0列,跨1列LV_GRID_ALIGN_START, 0, 1); // 行:起始對齊,第0行,跨1行// 標簽跨兩列
lv_obj_t * label = lv_label_create(grid_container);
lv_obj_set_grid_cell(label,LV_GRID_ALIGN_CENTER, 1, 2, // 列:居中,第1列,跨2列LV_GRID_ALIGN_CENTER, 0, 1); // 行:居中,第0行,跨1行
布局系統工作原理
處理流程
核心機制
- 注冊機制:通過
lv_obj_set_layout()
將容器注冊到布局系統 - 延遲計算:在屏幕刷新周期統一處理布局計算
- 動態響應:容器尺寸變化或子元素增減時自動觸發重新布局
實踐
- 組合使用:在復雜界面中混合使用
Flex
和Grid
布局 - 響應式設計:結合
LV_PCT
百分比單位和媒體查詢實現自適應
- 性能優化:避免深層嵌套布局,控制刷新頻率
- 樣式分離:將布局樣式與視覺樣式分離管理
結論
通過本章學習,我們掌握了:
- 彈性布局的
流式排列
與空間分配
技巧 - 網格布局的
二維矩陣
定位方法 - 布局系統的底層運作原理
- 間距控制與對齊策略
布局系統的引入使界面開發從手工計算邁向聲明式配置,極大提升了開發效率和可維護性。
下一章我們將探索用戶交互的核心——輸入設備管理
。
下一章:輸入設備(lv_indev)
github: https://github.com/lvy010/Cpp-Lib-test/tree/main/LVGL/indev
第六章:輸入設備(lv_indev)
在第五章:布局(lv_flex, lv_grid)中,我們已成為屏幕布局的大師,能夠確保界面元素美觀且自適應。但若用戶無法真正觸摸或交互這些精心設計的界面,再驚艷的UI又有何用?
如何才能讓那個完美居中、藍色圓角按鈕真正被點擊?
這正是**輸入設備(lv_indev
)**大顯身手之時~
假設我們的設備擁有物理觸摸屏、鼠標、鍵盤甚至旋轉編碼器,LVGL需要通過某種方式理解用戶通過這些物理輸入設備的操作
,并將這些動作轉化為屏幕上控件(如按鈕、滑塊或文本輸入框)能夠理解和響應的指令
。
lv_indev
模塊就像用戶交互的通用翻譯器。
它接收來自硬件的原始信息(例如"手指觸摸了X,Y坐標"或"Enter鍵被按下"),并將其轉化為GUI可理解的語義事件。
這使得LVGL應用能夠響應多樣化的物理輸入,而無需為每個交互編寫復雜的硬件專用代碼。
本章目標是理解如何連接常見輸入設備——觸摸屏(屬于"指針
"類設備),讓LVGL按鈕能夠響應點擊操作
什么是lv_indev
?
lv_indev
(LVGL輸入設備縮寫,代碼中以lv_indev_t
結構體表示)是表征單個輸入硬件設備的對象。
它充當微控制器原始輸入數據與LVGL事件系統之間的橋梁。
lv_indev_t
對象管理的關鍵要素:
設備類型
:屬于觸摸屏、鍵盤還是編碼器?數據讀取
:需要開發者提供的特殊函數(“讀取回調”)來獲取硬件當前狀態當前狀態
:按壓/釋放狀態、指向位置或激活的按鍵關聯顯示
:該輸入設備控制的顯示設備(回憶第二章:顯示設備(lv_display))
輸入設備類型(LV_INDEV_TYPE_
)
LVGL支持多種輸入設備類別:
類型 | 描述 | 典型硬件 |
---|---|---|
LV_INDEV_TYPE_POINTER | 能夠指向屏幕具體坐標并觸發按壓/釋放動作的設備 | 觸摸屏 、鼠標、軌跡球 |
LV_INDEV_TYPE_KEYPAD | 提供按鍵輸入的設備,常用于導航和文本輸入 | 物理鍵盤 、數字鍵盤 |
LV_INDEV_TYPE_ENCODER | 帶有增量旋轉(左/右)和可選按壓按鈕的旋轉設備 | 旋鈕編碼器 、滾輪 |
LV_INDEV_TYPE_BUTTON | 映射到屏幕坐標的物理按鍵(如設備外殼上的實體按鈕) | 前面板按鍵 |
本章將以LV_INDEV_TYPE_POINTER
類型的觸摸屏為例進行說明。
連接第一個輸入設備(觸摸屏)
讓我們配置基礎觸摸屏功能,使LVGL能夠檢測按鈕的觸摸操作。
1. 創建輸入設備對象
首先需要通過創建lv_indev_t
對象告知LVGL存在輸入設備。
該操作必須在顯示設備(lv_display)創建之后執行。
#include "lvgl.h" // 始終包含主LVGL頭文件// 在應用初始化函數中(如main.c或app_init())
void setup_input_device()
{// 確保顯示設備已初始化(例如調用第二章的setup_display())// lv_init();// setup_display(); // 需在輸入設備設置前調用!// 1. 創建輸入設備對象lv_indev_t * my_touchpad_indev = lv_indev_create();// ... 后續配置步驟在此添加
}
lv_indev_create()
:創建lv_indev_t
對象,默認關聯到首個創建的顯示設備lv_indev_t * my_touchpad_indev
:該變量持有輸入設備的操作句柄
2. 設置設備類型
告知LVGL輸入設備類型,觸摸屏屬于LV_INDEV_TYPE_POINTER
。
// ...(接續前文代碼)void setup_input_device()
{lv_indev_t * my_touchpad_indev = lv_indev_create();// 2. 設置輸入設備類型為POINTERlv_indev_set_type(my_touchpad_indev, LV_INDEV_TYPE_POINTER);// ... 后續配置步驟在此添加
}
3. 實現讀取回調函數
這是最關鍵的部分!LVGL需要通過開發者提供的"讀取回調"函數定期獲取實際觸摸數據。
my_touchpad_read
函數需要完成:
- 讀取觸摸的當前狀態(按壓或釋放)
- 若處于按壓狀態,讀取X/Y坐標
- 填充
lv_indev_data_t
結構體傳遞這些信息
// 將觸摸數據存儲為全局變量以便硬件驅動更新
//(例如在中斷服務例程或主循環輪詢中更新)
static int32_t touch_x = 0;
static int32_t touch_y = 0;
static bool touch_pressed = false; // 觸摸屏激活時為true// *** 重要:需替換為實際硬件讀取函數!***
// 以下僅為演示概念占位符
// 實際嵌入式系統中應讀取觸摸控制器IC數據
// 示例:*x = get_actual_touch_x(); *y = get_actual_touch_y(); *is_pressed = is_touch_down();void read_touchscreen_hardware(int32_t *x, int32_t *y, bool *is_pressed)
{// 演示用模擬輸入(如PC模擬器中的鼠標)// 實際應用中應從觸摸傳感器獲取真實數據:*x = touch_x;*y = touch_y;*is_pressed = touch_pressed;
}// ****************************************************************************// 3. 自定義讀取回調函數
void my_touchpad_read(lv_indev_t * indev, lv_indev_data_t * data) {// 從實際觸摸硬件讀取當前狀態read_touchscreen_hardware(&touch_x, &touch_y, &touch_pressed);if (touch_pressed) {data->state = LV_INDEV_STATE_PRESSED; // 告知LVGL按壓狀態data->point.x = touch_x; // 設置X坐標data->point.y = touch_y; // 設置Y坐標} else {data->state = LV_INDEV_STATE_RELEASED; // 告知LVGL釋放狀態}
}
lv_indev_t * indev
:觸發回調的輸入設備對象指針lv_indev_data_t * data
:必須填充當前輸入數據的結構體LV_INDEV_STATE_PRESSED
/LV_INDEV_STATE_RELEASED
:指針設備的兩種基本狀態data->point.x
,data->point.y
:按壓狀態時的坐標位置
4. 連接讀取回調
最后將my_touchpad_read
函數關聯至輸入設備對象。
// ...(接續前文代碼)void setup_input_device()
{lv_indev_t * my_touchpad_indev = lv_indev_create();lv_indev_set_type(my_touchpad_indev, LV_INDEV_TYPE_POINTER);// 4. 關聯讀取回調函數lv_indev_set_read_cb(my_touchpad_indev, my_touchpad_read);
}
現在調用setup_input_device()
后,LVGL將周期調用my_touchpad_read
獲取觸摸狀態,并據此判斷控件(如第三章:控件(lv_obj)中的按鈕)是否被點擊
若結合第四章:樣式(lv_style)中的LV_STATE_PRESSED
樣式,我們甚至能看到按鈕在觸摸時的顏色變化
理解控件組(適用于鍵盤/編碼器)
POINTER
設備通過直接點擊
屏幕坐標交互
而KEYPAD
和ENCODER
設備則通過"焦點
"與控件交互。
想象用鍵盤導航網頁:按Tab
鍵在按鈕間切換焦點,Enter
鍵點擊焦點按鈕。
LVGL使用**控件組(lv_group_t
)**實現此機制。
- 創建組:
lv_group_t * g = lv_group_create();
- 添加控件至組:
lv_group_add_obj(g, my_button);
(對所有需導航的交互控件執行此操作) - 為輸入設備分配組:
lv_indev_set_group(my_keypad_indev, g);
當my_keypad_read
回調報告LV_KEY_NEXT
時,焦點將自動在g
組的控件間切換
(Qt的話,有信號和槽機制)
[Qt] 信號和槽(1) | 本質 | 使用 | 自定義
[Qt] 信號和槽(2) | 多對多 | disconnect | 結合lambda | sum
輸入設備工作原理
讓我們觀察觸摸事件從硬件
到LVGL控件
的傳遞過程。
- 輪詢/讀取:LVGL運行周期性定時器(由第一章:配置(lv_conf.h)中的
LV_DEF_REFR_PERIOD
控制,通常10-50ms)。該定時器觸發lv_indev_read_timer_cb
,進而調用各注冊輸入設備的lv_indev_read
- 讀取回調:
lv_indev_read
調用開發者實現的my_touchpad_read
函數,從硬件讀取原始X/Y坐標和觸摸狀態 - 數據處理:將原始數據填入
lv_indev_data_t
結構體并返回 - 查找目標對象:LVGL獲取原始輸入數據后,對指針設備會基于X/Y坐標遍歷顯示設備上的所有活動控件(從頂層系統層到底層),查找位于觸摸點下的控件。此過程涉及坐標和可見性檢查
- 狀態與事件管理:確定目標控件后,LVGL更新其內部狀態(如
LV_STATE_PRESSED
)并觸發相關事件(如LV_EVENT_PRESSED
、LV_EVENT_CLICKED
、LV_EVENT_RELEASED
)。若按壓狀態移動可能觸發滾動或拖拽
簡化序列圖如下:
LVGL內部代碼解析:
調用lv_indev_create()
時,LVGL會為lv_indev_t
結構體分配內存。
該結構體保存指向讀取回調函數
的指針、設備類型
、內部狀態變量
及關聯顯示設備指針
。
核心邏輯位于src/indev/lv_indev.c
,以下是簡化代碼片段:
// 摘自lv_indev.c(簡化版)
lv_indev_t * lv_indev_create(void)
{// 為輸入設備對象分配內存lv_indev_t * indev = lv_ll_ins_head(indev_ll_head);// ... 初始化默認值 ...// 創建周期性調用讀取函數的定時器indev->read_timer = lv_timer_create(lv_indev_read_timer_cb, LV_DEF_REFR_PERIOD, indev);// ...return indev;
}void lv_indev_set_read_cb(lv_indev_t * indev, lv_indev_read_cb_t read_cb)
{// 存儲開發者提供的讀取回調函數指針indev->read_cb = read_cb;
}void lv_indev_read(lv_indev_t * indev)
{lv_indev_data_t data;// 調用開發者實現的讀取回調if(indev->read_cb) {indev->read_cb(indev, &data);}// ... 根據indev->type處理data ...if(indev->type == LV_INDEV_TYPE_POINTER) {indev_pointer_proc(indev, &data); // 處理指針數據}// ... 其他類型處理(鍵盤、編碼器、按鈕)...
}static void indev_pointer_proc(lv_indev_t * i, lv_indev_data_t * data)
{// ... 從data->point更新內部'act_point' ...// ... 通過pointer_search_obj()查找指針下對象 ...// ... 更新內部狀態(如i->pointer.act_obj, i->state)...if (i->state == LV_INDEV_STATE_PRESSED) {indev_proc_press(i); // 處理按壓邏輯} else {indev_proc_release(i); // 處理釋放邏輯}
}static void indev_proc_press(lv_indev_t * indev)
{// ... 檢測新對象、長按、滾動的邏輯 ...// 若啟用,向活動對象(indev_obj_act)發送LV_EVENT_PRESSED事件// 示例:// lv_obj_send_event(indev_obj_act, LV_EVENT_PRESSED, indev_act);
}// lv_indev.h中完整的lv_indev_t定義
// 包含輸入設備狀態和配置的所有相關數據
typedef struct _lv_indev_t
{// ... 其他成員 ...lv_indev_type_t type; /**< 輸入設備類型(POINTER, KEYPAD, ENCODER, BUTTON) */lv_indev_read_cb_t read_cb; /**< 輸入設備數據讀取函數 */lv_indev_state_t state; /**< 當前狀態(PRESSED或RELEASED) */struct _lv_display_t * disp; /**< 關聯的顯示設備 */lv_timer_t * read_timer; /**< 周期性調用read_cb的定時器 */lv_group_t * group; /**< 針對KEYPAD/ENCODER:交互的控件組 */// ... 指針、鍵盤等內部狀態變量 ...// 例如:lv_point_t pointer.act_point; 當前坐標// 例如:uint32_t keypad.last_key; 最后按下的鍵// ... 更多手勢、長按、滾動相關參數 ...
} lv_indev_t;
這種內部結構和處理流程確保了LVGL能夠高效處理多種輸入源,將底層硬件細節與GUI邏輯解耦。
代碼功能
lv_indev_create()
函數是LVGL輸入設備系統的核心接口,用于創建并初始化一個輸入設備實例。
函數返回lv_indev_t
結構體指針,該結構體存儲輸入設備的全部運行時數據。
內存分配與初始化
lv_ll_ins_head(indev_ll_head)
通過鏈表管理器為輸入設備分配內存,同時將新設備插入全局鏈表頭部。
返回的lv_indev_t
指針包含以下關鍵字段:
read_timer
:通過lv_timer_create()
創建定時器,周期性地調用lv_indev_read_timer_cb
觸發輸入事件處理type
:初始化為LV_INDEV_TYPE_NONE
,需通過lv_indev_set_type()
顯式設置read_cb
:初始化為NULL,需通過lv_indev_set_read_cb()
綁定具體設備的讀取函數
回調機制實現
lv_indev_set_read_cb()
將開發者實現的設備讀取函數指針存入indev->read_cb
。當定時器觸發lv_indev_read()
時,會通過該指針調用具體設備的讀取邏輯:
if(indev->read_cb)
{indev->read_cb(indev, &data); // 回調開發者實現的硬件讀取接口
}
輸入數據處理流程
-
類型分發:根據
indev->type
進入對應處理器。以觸摸屏(LV_INDEV_TYPE_POINTER
)為例:indev_pointer_proc(indev, &data); // 處理坐標數據
-
狀態機處理:在
indev_pointer_proc()
中:- 更新坐標
act_point
和當前活動對象act_obj
- 根據
state
字段(PRESSED/RELEASED
)分發給indev_proc_press()
或indev_proc_release()
- 更新坐標
-
事件生成:在按壓處理中通過
lv_obj_send_event()
發送標準事件:lv_obj_send_event(indev_obj_act, LV_EVENT_PRESSED, indev_act);
關鍵數據結構
lv_indev_t
包含輸入設備的完整上下文:
typedef struct _lv_indev_t
{lv_indev_type_t type; // 設備類型標識lv_indev_read_cb_t read_cb; // 設備級讀取回調lv_indev_state_t state; // PRESSED/RELEASED狀態struct _lv_display_t * disp; // 綁定到特定顯示器lv_timer_t * read_timer; // 輸入輪詢定時器union {lv_point_t act_point; // 指針設備當前坐標uint32_t last_key; // 鍵盤設備最后按鍵};// ...其他手勢/滾動參數...
} lv_indev_t;
?union
union 是一種特殊的 C 語言結構,允許同一塊內存存儲不同的數據類型(如 lv_point_t
和 uint32_t
),但同一時間只能使用其中一個成員,以節省內存空間
20.(C語言)聯合和枚舉全
code:
union {lv_point_t act_point; // 用于存儲指針坐標(如觸摸屏位置)uint32_t last_key; // 用于存儲鍵盤按鍵值
};
- 共享內存:
act_point
和last_key
共用同一塊內存,修改其中一個會影響另一個的值 - 應用場景:適合在設備只能觸發一種輸入(如觸摸或按鍵)時復用內存,減少資源占用。
調用
開發者需要實現三個基礎操作:
創建
設備實例設置
設備類型綁定讀取回調
lv_indev_t * touchpad = lv_indev_create();
lv_indev_set_type(touchpad, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(touchpad, my_touchpad_read);
總結
至此我們已成功為LVGL應用連接輸入設備!本章要點包括:
lv_indev
是處理用戶輸入的核心模塊- LVGL支持多種輸入設備類型
- 如何創建
lv_indev_t
對象、設置類型并提供硬件數據讀取回調 lv_indev
如何將原始輸入轉化為控件交互- "控件組"對鍵盤和編碼器導航的重要性
配置完輸入設備后,我們精心設計的樣式化控件已具備完整交互能力!下一步將深入探索控件如何響應這些交互事件。
下一章:事件系統(lv_event)