未来将会有诸多应用,这些应用将通过菜单进行有序组织和管理。因此,我们需要率先打造好菜单。
LCD 驱动通常是直接写屏的,虽然速度较快,但用于界面制作则不太适宜。所以,最好能拥有一套 UI 框架。如前所述,我们的目标是学习,故而不采用如 LVGL 之类的框架,一切都需亲力亲为。
构建一个简易的 UI 框架:

每个页面,我们称之为场景(scean),其中包含众多控件。所有页面存放在链表中,每次仅执行最后一个页面的操作。若删除页面,则会退回到前一个页面。
每个页面可以拥有多个控件。这些控件放置在一个链表中。每次仅执行最后一个控件的操作。当前掌机没有触摸功能,故暂不考虑多个控件同时接收事件。若仅有一个控件,也可不使用链表。
之前已设计好,大循环放置在主 main 函数内。每次通过 tick 方法将运行间隔传递给下面的场景(scean),再传递给下面的用户控件(usercontrol)。
好了,开始我们的实操:
菜单分为多个级别,每级都有自己的页面,同时每个应用也有自己的页面。我们将每个页面用场景(SCEAN)来表示。所有实例化的场景都存放在场景列表(sceanList)中进行管理,以便于页面的前进和后退。
个人比较习惯JAVA的面向对象技术,所以编码风格带有JAVA的色彩。
IScean.h
- #ifdef __cplusplus
-
- enum SceanResult{
- SceanResult_EXIT = 1,
- SceanResult_Done = 2
- };
-
- class IScean {
- public:
- IScean();
- virtual ~IScean();
- // 纯虚函数
- virtual SceanResult tick(u32 ticks) = 0;
- virtual int scean_init(cJSON* param) {return 0;};
- virtual int scean_resume(void){return 0;};
- virtual int scean_pause(void){return 0;};
- protected:
- map_t iconMap = NULL;
- unsigned char* getImageResource(char* fn);
- };
-
- #endif
scean中有几个重要的方法:初始化,暂停,恢复,执行(tick)
原则上没有使用系统中断、钩子之类的东西。而是在main函数中进行循环,处理系统级任务,每每次循环的时间消耗通过tick函数传递给当前scean。
main.cpp
- void UserMain(void)
- {
- 。。。
-
- while(1){
- handleMainScreanTask();
- IScean* cur= ((IScean*)sceanList->prev->data);
-
- _tmp_Run_Ticks = tls_os_get_time();
- keyAdepterProc(_tmp_Run_Ticks - _last_Run_Ticks);
- res = cur->tick(_tmp_Run_Ticks - _last_Run_Ticks);
- _last_Run_Ticks = _tmp_Run_Ticks;
-
-
- switch (res) {
- case SceanResult_EXIT:
- delete cur;
- ListPopBack(sceanList);
- ((IScean*)sceanList->prev->data)->scean_resume();
- break;
- case SceanResult_Done:
- break;
-
- }
- 。。。
- }
- }
scean实现中,加入了scean图标的读取。
IScean.cpp
- #include "IScean.h"
- #include "../driver/psram.h"
-
-
- int iterate(any_t item, any_t data){
- psram_heap_free((unsigned char *)data);
- return MAP_OK;
- }
-
-
- IScean::IScean(){
- iconMap = hashmap_new();
- }
-
- IScean::~IScean(){
- any_t val;
- hashmap_iterate(iconMap, iterate, val);
- hashmap_free(iconMap);
- }
-
- unsigned char* IScean::getImageResource(char* fn){
-
- int error;
- unsigned int w,h;
- unsigned char *val;
- error= hashmap_get(iconMap, fn, (void**)(&val));
-
- if(error == MAP_MISSING){
- lodepng_decode32_file(&val, &w, &h, fn);
- hashmap_put(iconMap, fn, val);
- }
- return val;
- }
随着东西的增多,我们不能每次都直接写屏,所以需要适当抽一些用户控件以方便页面交互。比如说菜单就需要一个List控件。
IUserControl.h
- class IUserControl
- {
- public:
- IUserControl();
- virtual ~IUserControl();
-
- void setPosi(u16 _x, u16 _y, u16 _width, u16 _height){
- x=_x;
- y=_y;
- width = _width;
- height= _height;
- };
-
- virtual void update(void) = 0;
- virtual int tick(u32 ticks) = 0;
- u8 src;
- const char *statusInfo;
-
-
- protected:
- u16 x,y, width, height;
- };
里面用一个链表保存所有的选项,然后处理各种按键事件,实现显示效果。
CtlList.h
- typedef struct ListItem
- {
- const char *text;
- unsigned char *image;
- void* tag;
- }ListItem;
-
-
- class CtlList : public IUserControl
- {
- public:
- CtlList(void);
- ~CtlList(void);
- int tick(u32 ticks);
- void addItem(ListItem *item);
- void clear(void);
- void update(void);
- void up(void);
- void down(void);
- u16 value(void){
- return cntIdx;
- };
-
- ListItem* selectItem(void){
- return (ListItem*)(ListGetNodeAt(items, cntIdx)->data);
- };
-
- private:
- u16 cntIdx=0;
- ListNode *items;
- u8 zk_num;
-
- DisplayOption optionC = {FONT_SIZE_1516, WHITE, BLUE,0,1};
- DisplayOption optionD = {FONT_SIZE_1516, WHITE, BLACK,0,1};
-
- };
CtlList.cpp
- #include "CtlList.h"
-
- CtlList::CtlList(void)
- {
- items =ListCreate();
- }
-
- CtlList::~CtlList(void)
- {
- printf("Destory CtlList items.\n");
- clear();
- items = NULL;
- }
-
- #define ItemHeifht 34
-
- void CtlList::update(void){
- ListNode *node;
- u16 showc = height / ItemHeifht;
- u16 ty = 0;
- u16 count = ListCount(items);
- if(count==0) return;
-
- u16 itemH = height / count;
- Display_Fill_Rectangle(x + width-10, y, x + width-1, y + height, GRAY);
- for(int i= cntIdx - showc /2 ;i<= cntIdx + showc /2;i++){
- Display_Fill_Rectangle(x, y + ty * ItemHeifht, x + width-11, y + (ty+1) * ItemHeifht - 2, BLACK);
- if(i>=0 && i< count){
- node = ListGetNodeAt(items,i);
- if(i == cntIdx){
- Display_Fill_Rectangle(x+32, y + ty * ItemHeifht, x + width-11, y + (ty+1) * ItemHeifht - 2, BLUE);
- Display_String(x+38, y + ty * ItemHeifht + 6, &optionC, (const char*) ((ListItem *)node->data)->text);
- }else{
- Display_String(x+38, y + ty * ItemHeifht + 6, &optionD, (const char*) ((ListItem *)node->data)->text);
- }
- if(((ListItem *)node->data)->image !=NULL){
- Display_Show_Picture_RGBA(x, y + ty * ItemHeifht, 32, 32, (const unsigned char*) ((ListItem *)node->data)->image);
- }
- Display_Fill_Rectangle(x + width-9, y + i*itemH, x + width-2, y + + (i+1)*itemH, GREEN);
- }
- ty++;
- }
- }
-
- void CtlList::addItem(ListItem *item){
- ListPushBack(items, (LTDataType)item);
- }
-
- void CtlList::up(void){
- if(cntIdx > 0 ) cntIdx--; else cntIdx = ListCount(items) -1;
- update();
-
- }
-
- void CtlList::down(void){
- if(cntIdx < ListCount(items) -1 ) cntIdx++; else cntIdx = 0;
- update();
- }
-
- void CtlList::clear(void){
- Display_Fill_Rectangle(x, y, x + width-1, y + height, BLACK);
- if(items !=NULL){
- for(ListNode *cur = items->next; cur != items; cur=cur->next){
- delete (ListItem*)cur->data;
- }
- }
- ListDestory(items);
- items=ListCreate();
- cntIdx = 0;
- }
-
- int CtlList::tick(u32 ticks){
- if(KEY_UP) {up();}
- if(KEY_DOWN) {down();}
- return 0;
- }
menu要实现ISCEAN,里面有个CtlList。
menu.h
- class Menu : public IScean
- {
- public:
- Menu(void);
- ~Menu(void);
- SceanResult tick(u32 ticks);
- int scean_init(u32 param);
- int scean_resume(void);
- int scean_pause(void){return 0;};
- int id = 100;
- private:
- void showMenu();
- IScean* createScean(u32 idx);
- unsigned char* getMenuImage(char* fn);
- ListNode *menuList;
- CtlList *ctlList;
- u8 gameMode=0;
- cJSON *menuRoot;
-
- };
-
menu.cpp
- #include "menu.h"
- #include "wm_osal.h"
- #include "../driver/KeyAdepter.h"
- #include "NetConfig.h"
- #include "About.h"
- #include "Tetris.h"
- #include "yingyu.h"
- #include "YuWenTs.h"
-
- extern ListNode *sceanList;
-
- #define SceanId_About 102
- #define SceanId_NetConfig 103
-
- #define SceanId_YingYu 301
- #define SceanId_YuWen 302
-
- #define SceanId_Nes 400
-
- #define SceanId_Tetris 501
- #define SceanId_Plane 502
-
-
- Menu::Menu(void)
- {
- menuList = ListCreate();
- ctlList = new CtlList();
- ctlList->setPosi(100, 70, 280, 170);
-
- unsigned char *readBuffer;
- size_t readsize = fatfs_readFile("menu/menu.txt", &readBuffer);
- printf("load menu finished! %d\n", readsize );
- menuRoot = cJSON_Parse((char *)readBuffer);
- }
-
-
- Menu::~Menu(void)
- {
- ListDestory(menuList);
- delete menuList;
- delete ctlList;
- cJSON_Delete((cJSON*)menuRoot);
-
- }
- //×○△□
- const char *topMenuInfo=" SEL:选择";
- const char *subMenuInfo="EXIT:返回 SEL:选择";
-
- int Menu::scean_init(u32 param){
- scean_resume();
- return 0;
- }
-
- int Menu::scean_resume(void){
- clear_screen();
- ListDestory(menuList);
- menuList = ListCreate();
- ListPushBack(menuList, menuRoot);
- showMenu();
- setKeyAdepterIntervalAll(200);
- setKeyAdepterInterval(KEY_GPIO_SEL, 65535);
- setKeyAdepterInterval(KEY_GPIO_EXIT, 65535);
- return 0;
- }
-
- unsigned char* Menu::getMenuImage(char* fn){
-
- if(fn[0] == 45){
- return NULL;
- }
-
- int error;
- unsigned int w,h;
- unsigned char *val;
- error= hashmap_get(iconMap, fn, (void**)(&val));
-
- if(error == MAP_MISSING){
- lodepng_decode32_file(&val, &w, &h, fn);
- hashmap_put(iconMap, fn, val);
- }
- return val;
- }
-
- void Menu::showMenu(){
- ctlList->clear();
-
- cJSON* currentMenu = (cJSON *)menuList->prev->data;
- int count = cJSON_GetArraySize(currentMenu);
- printf("menu count=%d\n", count);
- for(u8 i=0;i< count;i++){
- cJSON* cj = cJSON_GetArrayItem(currentMenu, i);
- ListItem *_item = new ListItem();
- _item->text = cJSON_GetObjectItem(cj,"t")->valuestring;
- if(cJSON_GetObjectItem(cj,"i")->valuestring[0] != '-')
- _item->image = getImageResource(cJSON_GetObjectItem(cj,"i")->valuestring);
- _item->tag= (void *) cj;
- ctlList->addItem(_item);
- }
- ctlList->update();
-
- if(currentMenu == menuRoot)
- show_status_info(topMenuInfo);
- else
- show_status_info(subMenuInfo);
- }
-
- SceanResult Menu::tick(u32 ticks){
- if(KEY_EXIT){ // 返回
- cJSON* currentMenu = (cJSON *)menuList->prev->data;
- if( currentMenu == menuRoot) return SceanResult_Done;
- ListPopBack(menuList);
- showMenu ();
- return SceanResult_Done;
- }
-
- if(KEY_SEL){ //进入
- cJSON* item = (cJSON*) ctlList->selectItem()->tag;
- int sceanId = cJSON_GetObjectItem(item,"d")->valueint;
- if(sceanId!= 0){
- IScean *scean =createScean(sceanId);
- if(scean !=NULL){
- scean->scean_init(cJSON_GetObjectItem(item,"g"));
- ListPushBack(sceanList, scean);
- }
- return SceanResult_Done;
- }
- cJSON* sitem = cJSON_GetObjectItem(item,"s");
-
- if(cJSON_GetArraySize(sitem) > 0){
- ListPushBack(menuList, sitem);
- showMenu();
- return SceanResult_Done;
- }
- return SceanResult_Done;
- }
- ctlList->tick(ticks);
- ran_max(10);
- return SceanResult_Done;
- }
-
- IScean* Menu::createScean(u32 idx){
- switch (idx) {
- case SceanId_About:
- return new About();
- case SceanId_Tetris:
- return new Tetris();
- case SceanId_NetConfig:
- return new NetConfig();
- case SceanId_YingYu:
- return new YingYu();
- case SceanId_YuWen:
- return new YuWenTS();
-
- }
- return NULL;
- }
-
在其实例化的过程中完成菜单JSON的读取。JSON文件放在SD卡中:menu/menu.txt
- {
- "t": "三分钟限时挑战",
- "i": "menu/yuwen.png",
- "d": 0,
- "g": {},
- "s": [
- {
- "t": "语文",
- "i": "menu/yuwen.png",
- "g": {},
- "s": [
- {
- "t": "唐诗三百首",
- "i": "-",
- "d": 301,
- "g": {"w":1, "m":1},
- "s": []
- },
- {
- "t": "宋词三百首",
- "i": "-",
- "d": 301,
- "g": {"w":1, "m":2},
- "s": []
- }
- ]
- }
- ]
- }
-
其中:
t为菜单项标题
i为菜单项的图标
d为需要启动的应用编号,根据此编号启动对应的应用
g为将传递给应用的各种参数
s为下一级菜单
- void UserMain(void)
- {
- 。。。
-
- sceanList = ListCreate();
- Menu *menu = new Menu();
- menu->scean_init(0);
- ListPushBack(sceanList, menu);
- SceanResult res;
- u32 _last_Run_Ticks = tls_os_get_time();
- u32 _tmp_Run_Ticks = tls_os_get_time();
-
- while(1){
- handleMainScreanTask();
- IScean* cur= ((IScean*)sceanList->prev->data);
-
- _tmp_Run_Ticks = tls_os_get_time();
- keyAdepterProc(_tmp_Run_Ticks - _last_Run_Ticks);
- res = cur->tick(_tmp_Run_Ticks - _last_Run_Ticks);
- _last_Run_Ticks = _tmp_Run_Ticks;
-
-
- switch (res) {
- case SceanResult_EXIT:
- delete cur;
- ListPopBack(sceanList);
- ((IScean*)sceanList->prev->data)->scean_resume();
- break;
- case SceanResult_Done:
- break;
-
- }
- FrameCount++;
- }
- }
我们看看效果:
W801学习笔记十四:掌机系统——菜单——尝试打造自己的UI