在上篇文章中对多级菜单式GUI进行了简单的介绍,也给出了实际的演示视频,可以看出实际效果中我使用的文字作为选项,但不管是文字选项还是图标选项都是一样的,只不过是显示内容不一样而已,只需要更改一个函数就可以随意转换,今天讲讲源码实现相关的内容吧。
先贴上源码:(注意绘线、绘框、绘制位图提供的方法不一定可用,最好自己提供)
`#ifndef __M_GUI_H
define __M_GUI_H
include <string.h>
include <stdint.h>
ifndef COUNTOF
define COUNTOF(a) (sizeof(a) / sizeof(*(a)))
endif
ifndef NULL
define NULL ((void *)0)
endif
typedef uint8_t M_U8; typedef int8_t M_S8; typedef uint16_t M_U16; typedef int16_t M_S16; typedef uint32_t M_U32; typedef int32_t M_S32;
typedef struct sTitle tTitle; typedef struct shmi thmi; typedef struct sMenu tMenu; typedef struct sItem tItem;
struct sTitle{ //标题名 char *str; //行坐标 M_U16 x; //列坐标 M_U16 y; //字体宽 // M_U8 f_w; //字体高 // M_U8 f_h; };
/按键响应后的操作/ struct shmi{ void (hmi_up)(void); void (hmi_down)(void); void (hmi_left)(void); void (hmi_right)(void); void (hmi_enter)(void); void (hmi_exit)(void); };
struct sItem{ //选项 tTitle pszTitle; //子菜单 tMenu psSubMenu; };
struct sMenu { //菜单标题 tTitle pszTitle; //子选项 tItem psItem; //子选项数量 M_S8 nItems; //当前选项索引 M_S8 idx; //单页要排列的行 M_S8 nRow; //单页要排列的列 M_S8 nCol; //交互 thmi hmi;
//内容 void (*pfContent)(void); //事件 volatile M_S32 event; //上级菜单 tMenu last; };
/外部按键状态获取/ typedef struct { M_U32 (up)(void); M_U32 (down)(void); M_U32 (left)(void); M_U32 (right)(void); M_U32 (enter)(void); M_U32 (exit)(void); }keyTypeDef,*tkey;
/2d 图形绘制/ typedef struct { //点亮像素点 void (pset)(M_U16, M_U16, M_U8); //清屏 void (pclear)(void); //绘制线 void (pdrawLine)(M_U16, M_U16, M_U16, M_U16, M_U8); //绘制框 void (pdrawFrame)(M_U16, M_U16, M_U16, M_U16, M_U8); //绘制位图 void (pdrawBitMap)(M_U16, M_U16, M_U16, M_U16, M_U8 ); //字串显示 void (pputString)(M_U16, M_U16, M_U8, void ); }opsTypeDef,*tops;
typedef struct { tMenu active; tkey key; tops ops; }MENU_GUI;
extern const struct shmi hmi_def[];
//gui初始化 void gui_init(MENU_GUI g, opsTypeDef op, keyTypeDef k); //gui选择 void gui_select(MENU_GUI g); //设置当前激活菜单 void gui_set_active(tMenu m); //获取当前激活菜单 tMenu gui_get_active(void); //发送信号到指定菜单 void gui_send_sig(tMenu obj); //按键响应后调用相应的处理函数(周期调用周期应该不小于键盘扫描周期) void gui_key_scan(void); //gui刷新(周期调用,时间越短响应越佳) void gui_flush(void);
//绘点 void gui_draw_point(M_U16 x, M_U16 y, M_U8 c); //清屏 void gui_clear(void); //绘线 void gui_draw_line(M_U16 x1, M_U16 y1, M_U16 x2, M_U16 y2, M_U8 c); //绘框 void gui_drawFrame(M_U16 x1, M_U16 y1, M_U16 x2, M_U16 y2, M_U8 c); //绘制位图 void gui_draw_bitmap(M_U16 x, M_U16 y, M_U16 h, M_U16 w, M_U8 p); //字串显示 void gui_put_string(M_U16 x, M_U16 y, M_U8 f,void str);
endif/__M_GUI_H/
`
`#include "./m_gui/m_gui.h"
static MENU_GUI *gui;
void gui_init(MENU_GUI g, opsTypeDef op, keyTypeDef *k) { g->key = k; g->ops = op; g->active = NULL; }
void gui_select(MENU_GUI *g) { gui = g; }
void gui_set_active(tMenu m) { gui->active = m; }
tMenu gui_get_active(void) { return gui->active; }
void gui_send_sig(tMenu obj) { if(obj->event) { return ; }
obj->event++; }
void gui_key_scan(void) { if(!gui->key) { return ; } if(!gui->active) { return ; } if(!gui->active->hmi) { return ; }
if(gui->key->up && gui->key->up()) { gui->active->hmi->hmi_up(); }
if(gui->key->down && gui->key->down()) { gui->active->hmi->hmi_down(); }
if(gui->key->left && gui->key->left()) { gui->active->hmi->hmi_left(); }
if(gui->key->right && gui->key->right()) { gui->active->hmi->hmi_right(); }
if(gui->key->enter && gui->key->enter()) { gui->active->hmi->hmi_enter(); }
if(gui->key->exit && gui->key->exit()) { gui->active->hmi->hmi_exit(); } }
/菜单项显示/ void gui_item_flush(void) { M_U32 i = 0,tmpIdx = 0,tmp = 0; tTitle pTmp2 = NULL;
if(!gui->active) { return ; }
tmp = gui->active->nRow gui->active->nCol; /如果单页行列元素小于最大选项数*/ if(tmp && (tmp < gui->active->nItems)) { //计算当前索引对应页的开始选项 i = gui->active->idx - (gui->active->idx % tmp); tmpIdx = i; }
for( ;(i < gui->active->nItems) && (i < (tmp + tmpIdx));i++) { pTmp2 = gui->active->psItem[i].pszTitle; if(pTmp2 && pTmp2->str) { gui->ops->pputString(pTmp2->x, pTmp2->y, i == gui->active->idx?1:0, pTmp2->str); } } }
void gui_flush(void) { if(gui->active->event > 0) { gui->active->event--;
gui->ops->pclear();
if(gui->active->pfContent) { gui->active->pfContent(); }
gui_item_flush(); } }
void gui_key_up(void) { /如果索引为 0 则直接退出/ if(gui->active->idx <= 0) { return ; }
/如果索引小于当前页列数则跳到第一列,否则跳到上一行对应列/ gui->active->idx -= gui->active->nCol; if(gui->active->idx < 0) { gui->active->idx = 0; }
gui_send_sig(gui->active); }
void gui_key_down(void) { /如果当前索引不小于最大选项数则直接退出/ if(!(gui->active->idx < (gui->active->nItems - 1))) { return ; }
/如果跳到下一行对应列大于了最大选项数则跳到最后一个元素,否则跳到下一行对应列/ gui->active->idx += gui->active->nCol; if(gui->active->idx > gui->active->nItems - 1) { gui->active->idx = gui->active->nItems - 1; }
gui_send_sig(gui->active); }
void gui_key_left(void) { /如果到了首选项,则跳到尾选项/ gui->active->idx--; if(gui->active->idx < 0) { gui->active->idx = gui->active->nItems - 1; }
gui_send_sig(gui->active); }
void gui_key_right(void) { /如果到了尾选项,则跳到首选项/ gui->active->idx++; if(gui->active->idx > gui->active->nItems - 1) { gui->active->idx = 0; }
gui_send_sig(gui->active); }
void gui_key_enter(void) { if(!gui->active->psItem[gui->active->idx].psSubMenu) { return ; }
//先保存当前菜单到下级菜单的last中 gui->active->psItem[gui->active->idx].psSubMenu->last = gui->active; //将下级菜单作为激活菜单 gui->active = gui->active->psItem[gui->active->idx].psSubMenu;
gui_send_sig(gui->active); }
void gui_key_exit(void) { if(!gui->active->last) { return ; }
//读取上级菜单 gui->active = gui->active->last;
gui_send_sig(gui->active); }
const struct shmi hmi_def[] = { { .hmi_up = gui_key_up, .hmi_down = gui_key_down, .hmi_left = gui_key_left, .hmi_right = gui_key_right, .hmi_enter = gui_key_enter, .hmi_exit = gui_key_exit, }, };
/ 绘点 x,y点坐标 c绘线还是消线 */ void gui_draw_point(M_U16 x, M_U16 y, M_U8 c) { if(gui->ops->pset) { gui->ops->pset(x,y,c);return ; } }
/ 清屏 */ void gui_clear(void) { if(gui->ops->pclear) { gui->ops->pclear();return ; } }
/ 绘线 x1,y1起始坐标 x2,y2结束坐标 c绘线还是消线 / void gui_draw_line(M_U16 x1, M_U16 y1, M_U16 x2, M_U16 y2, M_U8 c) { M_S16 n, dx, dy, sgndx, sgndy, dxabs, dyabs, x, y, drawx, drawy;
if ( x2 < x1 ) { n = x2; x2 = x1; x1 = n; } if ( y2 < y1 ) { n = y2; y2 = y1; y1 = n; }
/ Is hardware acceleration available? / if ( gui->ops->pdrawLine ) { gui->ops->pdrawLine(x1,y1,x2,y2,c);return ; }
dx = x2 - x1; dy = y2 - y1; dxabs = (dx>0)?dx:-dx; dyabs = (dy>0)?dy:-dy; sgndx = (dx>0)?1:-1; sgndy = (dy>0)?1:-1; x = dyabs >> 1; y = dxabs >> 1; drawx = x1; drawy = y1;
gui->ops->pset(drawx, drawy,c);
if( dxabs >= dyabs ) { for( n=0; n<dxabs; n++ ) { y += dyabs; if( y >= dxabs ) { y -= dxabs; drawy += sgndy; } drawx += sgndx; gui->ops->pset(drawx, drawy,c); } } else { for( n=0; n<dyabs; n++ ) { x += dxabs; if( x >= dyabs ) { x -= dyabs; drawx += sgndx; } drawy += sgndy; gui->ops->pset(drawx, drawy,c); } } }
/ 绘框 x1,y1起始坐标 x2,y2结束坐标 c绘线还是消线 / void gui_drawFrame(M_U16 x1, M_U16 y1, M_U16 x2, M_U16 y2, M_U8 c) { / Is hardware acceleration available? / if ( gui->ops->pdrawFrame ) { gui->ops->pdrawFrame(x1,y1,x2,y2,c);return ; }
gui_draw_line(x1,y1,x2,y1,c); gui_draw_line(x1,y2,x2,y2,c); gui_draw_line(x1,y1,x1,y2,c); gui_draw_line(x2,y1,x2,y2,c); }
/ 绘制位图 x,y起始坐标 h,w位图高,宽 p位图数据 / void gui_draw_bitmap(M_U16 x, M_U16 y, M_U16 h, M_U16 w, M_U8 p) { /h:高,w:宽*/ M_U32 i = 0,j = 0; M_U8 sx = 0,sy = 0;
sx = x; sy = y;
/ Is hardware acceleration available? / if ( gui->ops->pdrawBitMap ) { gui->ops->pdrawBitMap(sx,sy,h,w,p);return ; }
for(i = 0;i < (w + 7) / 8 * h;i++) { for(j = 0;j < w;j++) { gui->ops->pset(sx,sy + j,p[i + (j >> 3)] & (0x80 >> (j & (8 - 1)))); }
i += (j - 1) >> 3; sx++; } }
/ 字串显示 * x,y起始坐标
- f:0正显,1反显 str字串内容 / void gui_put_string(M_U16 x, M_U16 y, M_U8 f,void str) { / Is hardware acceleration available? */ if ( gui->ops->pputString ) { gui->ops->pputString(x,y,f,str);return ; } }`
从源码可以看到,目前只实现了基本的菜单切换功能,肯定是不能够满足实际的开发需求,项目中可能会有数值修改、状态显示、屏保等各种功能,后续可能我会继续更新,如果你想自己实现也是非常方便的,只需要模仿hmi_def的实现就可以轻松的完成上述功能。
关于OLED显示相关的内容全部都是需要开发者自己实现的(源码中只有几个功能,需要扩展可以自己添加),这和其它GUI库区别很大,因为其它GUI库都只需要移植一个绘点函数或者再添加一个读点函数即可,但是我这样设计肯定有这样设计的理由,在实际项目中,我使用过多款OLED屏,其中有些屏是集成屏(也就是厂家已经完成了一次开发,我们只需要通过他给定的通信协议就可以进行二次开发),这样的屏好处就是使用简单,进行2D绘图或者汉字显示操作只需要发送相应的协议数据即可,上手是非常快的,坏处呢就是相对好处来说了,因为集成所以不够灵活,只能通过其给定的接口进行操作,出现问题也不太好解决;接触的还有一种屏就是裸屏,然后硬件电路这些全部都是自己完成,裸屏如果要显示中文,最好的办法就是加一块点阵字库芯片,当然你要是flash容量够大,你也可以直接将汉字点阵生成后全部存储到flash中;说到这里你应该明白为什么我要将所有OLED操作相关的接口全部留给开发者实现了,给开发者提供足够的自由度,可以很灵活的在这两类设计中随意切换。
还有一点不同的就是,此菜单设计并没有使用模拟显存,其中一个原因是如果你使用裸屏的方案,那么极有可能你就会使用模拟显存的方式来完成2D绘图等功能,如果再用一级缓存就会增大内存开销,另外一个原因就是与OLED的实时性特性有关。
由于我接触的OLED屏都是小屏,像分辨率是25664或者12832的,所以我采用的方案是全屏刷新,如果你觉得全屏刷新太慢了,你也可以增加回调函数方法,将静态内容和动态内容分开处理,这样对速度提升是非常大的,这也是OLED提供的便利。
今天的内容就讲这么多吧,下篇文章将会讲解此系列第一篇文章中视频演示的效果源码,以及移植的一些细节问题。
点击下面的链接查看系列其它文章:
单色OLED屏可移植多级菜单式GUI(1)-简介

评论区
登录后即可参与讨论
立即登录