单色OLED屏可移植多级菜单式GUI(2)-源码
在上篇文章中对多级菜单式GUI进行了简单的介绍,也给出了实际的演示视频,可以看出实际效果中我使用的文字作为选项,但不管是文字选项还是图标选项都是一样的,只不过是显示内容不一样而已,只需要更改一个函数就可以随意转换,今天讲讲源码实现相关的内容吧。
先贴上源码:(注意绘线、绘框、绘制位图提供的方法不一定可用,最好自己提供)
#ifndef __M_GUI_H
#define __M_GUI_H
#include
#include
#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 {
y += dyabs;
if( y >= dxabs )
{
y -= dxabs;
drawy += sgndy;
}
drawx += sgndx;
gui->ops->pset(drawx, drawy,c);
}
}
else
{
for( n=0; 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屏都是小屏,像分辨率是256*64或者128*32的,所以我采用的方案是全屏刷新,如果你觉得全屏刷新太慢了,你也可以增加回调函数方法,将静态内容和动态内容分开处理,这样对速度提升是非常大的,这也是OLED提供的便利。
今天的内容就讲这么多吧,下篇文章将会讲解此系列第一篇文章中视频演示的效果源码,以及移植的一些细节问题。





