九宫格多级OLED(LCD)菜单

目录

函数指针

函数指针的定义方式

使用举例

菜单索引

索引格式

索引列表

按键获取

UI刷新

光标

页面函数


        OLED菜单实现的方法有很多,本文介绍的方法也很常见。相对来说比较特殊的地方就是九宫格,但实现起来难度也不是特别大。本文只介绍了该菜单的基础实现方法,并没有将菜单做的特别花哨,大家学会后,可以自己添加东西在上面。下面开始介绍

函数指针

        在实现这种菜的之前,有必要先复习一下函数指针(不是指针函数)。众所周知。指针在C语言里面指向一个地址,既然如此,指针也应该可以指向函数的地址。

函数指针的定义方式

数据类型 (*指针名)();

        数据类型表示函数返回值的类型,*表示指针,第一个括号不能省略,省略后就变成指针函数,意味着函数的返回值是指针。第二个括号里面可以放数据类型,例如int、char,代表指针所指函数的形参类型。

使用举例

int max(int a,int b);      //函数声明  
int (*p_fun)(int,int);
p_fun = max;              //指向目标函数
(*p_fun)(x,y);           //调用

        如果目标函数没有参数的话,可以什么都不写,或者用void。

菜单索引

        菜单索引的作用,就好像一个目录。设置索引的目的就是要帮助我们根据索引的内容,判断如果某个按键按下的话,下一步应该显示什么页面执行什么动作。也就是说,索引里面要包含整个菜单的框架。所以说按键要和页面在索引里面对应起来,一个按键对应一个页面,一个页面要对应一个动作(函数),当然也会有特殊情况。

        既然索引里面要包括这么多内容,而且不单单是一个页面。如果是九宫格的话,就会有九个主页面,每一个页面下,都应当包含对应的动作,和在当前页面下,某个按键对应的页面。

        为了方便,描述有标号来代表对应的页面,使用的按键是五向开关,实际上就是五个按键,分别是上下左右和中间的按键。

         假设当前页面在0号页面,若果此时我们按下左键,那下一个要显示的页面就是1号页面,下一个执行的动作就是1号页面对应的动作。

索引格式

        首先说明,本文描述的菜单相当于是一个二级菜单,第一级是9个页面,页面之间是互通的,可以通过按键相互转换,按下确定按键后进入第二级菜单页面,但是每一个页面对应自己的二级菜单页面,也就是说二级菜单页面是相互独立的,不能通过左右来转换到另一个页面。

根据以上的描述,得到以下索引

typedef struct
{
	u8 Cur_Index;					 //当前索引项
	u8 left;						 //左面
	u8 right;						 //右面
	u8 up;							 //上面
	u8 down;						 //下面
	u8 enter;						 //确认
	void (*current_operation)(void); //当前索引执行的函数(界面)
} Main_Menu;

Cur_Index表示当前显示页面,left、right、up、down、enter分别表示对应按键按下后的要显示的页面。最后一个是函数指针,指向当前页面要执行的函数。

如果使用变量来代表页面的话,最方便的方式就是枚举。如下所示

enum
{
	_P0_Option = 0, //默认页面
	_P1_Option,
	_P2_Option, 
	_P3_Option, 
	_P4_Option, 
	_P5_Option,
	_P6_Option,
	_P7_Option,
	_P8_Option,

	_P11_Option,
	_P22_Option,
	_P33_Option,
	_P44_Option,
	_P55_Option,
	_P66_Option,
	_P77_Option,
	_P88_Option

};

索引列表

如果有多个页面的话,就要使用结构体数组来实现。

Main_Menu table[20] =
{

        //当前索引页  ,    左面    ,    右面    ,     上面  ,  下面      , 确认       ,执行函数。
        {_P0_Option, _P1_Option, _P2_Option, _P3_Option, _P4_Option, _P0_Option, _P0_Option_task}, //  主 界面

        {_P1_Option, _P1_Option, _P0_Option, _P5_Option, _P7_Option, _P11_Option, _P1_Option_task}, //   1 界面

        {_P2_Option, _P0_Option, _P2_Option, _P6_Option, _P8_Option, _P22_Option, _P2_Option_task}, //   2 界面

        {_P3_Option, _P5_Option, _P6_Option, _P3_Option, _P0_Option, _P33_Option, _P3_Option_task}, //   3 界面

        {_P4_Option, _P7_Option, _P8_Option, _P0_Option, _P4_Option, _P44_Option, _P4_Option_task}, //   4 界面

        {_P5_Option, _P5_Option, _P3_Option, _P5_Option, _P1_Option, _P55_Option, _P5_Option_task}, //   5 界面

        {_P6_Option, _P3_Option, _P6_Option, _P6_Option, _P2_Option, _P66_Option, _P6_Option_task}, //   6 界面

        {_P7_Option, _P7_Option, _P4_Option, _P1_Option, _P7_Option, _P77_Option, _P7_Option_task}, //   7 界面

        {_P8_Option, _P4_Option, _P8_Option, _P2_Option, _P8_Option, _P88_Option, _P8_Option_task}, //   8 界面

        //    5   3   6
        //    1   0   2
        //    7   4   8
        //子级菜单
        //当前索引页  ,    左面    ,    右面    ,     上面  ,  下面      , 确认       ,执行函数。
        {_P11_Option, _P1_Option, _P1_Option, _P11_Option, _P11_Option, _P11_Option, _P11_Option_task}, //   11 界面

        {_P22_Option, _P2_Option, _P2_Option, _P22_Option, _P22_Option, _P22_Option, _P22_Option_task}, //   22 界面

        {_P33_Option, _P3_Option, _P3_Option, _P33_Option, _P33_Option, _P33_Option, _P33_Option_task}, //   33 界面

        {_P44_Option, _P4_Option, _P4_Option, _P44_Option, _P44_Option, _P44_Option, _P44_Option_task}, //   44 界面

        {_P55_Option, _P5_Option, _P5_Option, _P55_Option, _P55_Option, _P55_Option, _P55_Option_task}, //   55 界面

        {_P66_Option, _P6_Option, _P6_Option, _P66_Option, _P66_Option, _P66_Option, _P8_Option_task}, //   66 界面

        {_P77_Option, _P7_Option, _P7_Option, _P77_Option, _P77_Option, _P77_Option, _P77_Option_task}, //   77 界面

        {_P88_Option, _P8_Option, _P8_Option, _P88_Option, _P88_Option, _P88_Option, _P88_Option_task}, //   88 界面

};

        可以看到,_Px_Option代表x号页面,_Pxx_Option代表x号页面下的子级页面,而0号页面没有自己的子级页面。_Px_Option_task表示在x号页面下需要执行的函数,_Pxx_Option_task表示在x号页面的子级页面下需要执行的函数。

        下面来分析一下,以_P0_Option也就是0号页面为例,当在0号页面时,按下左键后,需要显示的就是1号页面,用_P1_Option表示,其余方向同理。0号页面比较特殊,按下确定按键后还是当前页面,其余的跳转到子级页面。最后一项是函数指针,指向对应页面的函数,里面一边放一些屏幕显示函数和自己编写的程序。

按键获取

如下图所示,获取按键值,是一种比较常规的写法(部分参考学长)。

//按键索引值
enum
{
	KEY_PREVIOUS    = 0,
	KEY_ENTER_PRESS,
	KEY_U_PRESS,
	KEY_D_PRESS,
	KEY_L_PRESS,
	KEY_R_PRESS,
	NOTHING
};

#define KEY_ENTER  Key_filter(C0, 0)
#define KEY_U      Key_filter(F9, 0)
#define KEY_D      Key_filter(C1, 0)
#define KEY_L      Key_filter(C2, 0)
#define KEY_R      Key_filter(A0, 0)

// 软件按键滤波
u8 Key_filter(PIN_enum pin, u8 aim)
{
  u8 count = 0, i;
  for (i = 0; i < 5; i++)
    if (gpio_get(pin) != aim)
      return 0;
  return 1;
}

u8 Get_KEY_Value(void)
{
  static u8 key_up = 0;
  // if(c_begin)key_up=0;
  if (!key_up && (KEY_ENTER || KEY_L || KEY_R || KEY_U || KEY_D))
  {
    key_up = 1;
    if (KEY_ENTER)
      return KEY_ENTER_PRESS;
    if (KEY_L)
      return KEY_L_PRESS;
    if (KEY_R)
      return KEY_R_PRESS;
    if (KEY_U)
      return KEY_U_PRESS;
    if (KEY_D)
      return KEY_D_PRESS;
  }
  if (key_up && !KEY_ENTER && !KEY_L && !KEY_R && !KEY_U && !KEY_D)
    key_up = 0;
  return NOTHING;
}

UI刷新

void GUI_Refresh(void)
{
  key_val = Get_KEY_Value();
  if (key_val != NOTHING) //只有按键按下才刷屏
  {
    switch (key_val)
    {
    case KEY_ENTER_PRESS:
      func_index = table[func_index].enter; //更新索引值
      break;
    case KEY_L_PRESS:
      func_index = table[func_index].left; //更新索引值
      break;
    case KEY_R_PRESS:
      func_index = table[func_index].right; //更新索引值
      break;
    case KEY_U_PRESS:
      func_index = table[func_index].up;
      break;
    case KEY_D_PRESS:
      func_index = table[func_index].down;
      break;
    default:
      break;
    }
    if (!c_begin || key_val == KEY_L_PRESS || key_val == KEY_R_PRESS)
      lcd_clear(WHITE);
  }
  current_operation_func = table[func_index].current_operation;
  (*current_operation_func)(); //执行当前索引对应的函数
  last_index = func_index; //更新上一界面索引值
}

        需要说明的是,func_index表示当前的页面,last_index表示上一次的页面。根据按键的不同,进行幅值。修改页面后,将本页面的对应的函数指针赋值给current_operation_func,然后执行对应函数。为了防止卡屏,中间加了条件进行刷屏,这个可以根据自己需求修改。

光标

        很多菜单里面都是用的反色来显示光标,我习惯用箭头来显示,但是画箭头的方式有点笨拙就不在这里展示了。跟大家分享一下光标的移动。

void guangbiao(u8 min, u8 max)
{
  if (!c_begin)
  {
    if (key_val == KEY_U_PRESS)
      m_opt--;
    else if (key_val == KEY_D_PRESS)
      m_opt++;
  }
  if (m_opt < min)
    m_opt = max;
  else if (m_opt > max)
    m_opt = min;

  if (c_begin)
    jiantou(m_opt, GRAY);
  else
    jiantou(m_opt, BLUE);
}

        C_begin用来表示是否选中某一项,m_opt表示光标位置。因为在不同的页面光标移动的范围可能是不一样的的,所以写了这个函数

页面函数

        以1号页面为例,简要说明一下页面函数。这个页面用来调节参数。

void _P1_Option_task(void)
{
  if (last_index != func_index)
    m_opt = 1;
  if (key_val != NOTHING)          //固定显示,不用时刻刷新
  {
    lcd_showstr(40, 0, "balance  ");
    lcd_showstr(20, 1, "AngleZero");
    lcd_showstr(20, 2, "Pitch_P  ");
    lcd_showstr(20, 3, "Pitch_D  ");
    lcd_showstr(20, 4, "Gyro_P   ");
    lcd_showstr(20, 5, "Gyro_I   ");
    lcd_showstr(20, 6, "Gyro_D   ");
  }
  lcd_showfloat(100, 1, AngleZero, 2, 2);
  lcd_showfloat(100, 2, Pitch_P, 2, 2);
  lcd_showfloat(100, 3, Pitch_D, 2, 2);
  lcd_showfloat(100, 4, Gyro_P, 2, 2);
  lcd_showfloat(100, 5, Gyro_I, 2, 2);
  lcd_showfloat(100, 6, Gyro_D, 2, 2);
}

        这里很简单,除了显示什么的,就只有一个判断,如果上一个页面不等于这个页面,说明是第一次进入,光标复位。

void _P11_Option_task(void)
{
  _P1_Option_task();
  guangbiao(1, 6);

  if (key_val == KEY_ENTER_PRESS && last_index == func_index)
    c_begin = !c_begin;

  if (c_begin)
    switch (m_opt)
    {
    case 1:
    {
      if (key_val == KEY_U_PRESS)
        AngleZero += 0.1;
      else if (key_val == KEY_D_PRESS)
        AngleZero -= 0.1;
      break;
    }
    case 2:
    {
      if (key_val == KEY_U_PRESS)
        Pitch_P += 1;
      else if (key_val == KEY_D_PRESS)
        Pitch_P -= 1;
      break;
    }
    case 3:
    {
      if (key_val == KEY_U_PRESS)
        Pitch_D += 1;
      else if (key_val == KEY_D_PRESS)
        Pitch_D -= 1;
      break;
    }
    case 4:
    {
      if (key_val == KEY_U_PRESS)
        Gyro_P += 0.02;
      else if (key_val == KEY_D_PRESS)
        Gyro_P -= 0.02;
      break;
    }
    case 5:
    {
      if (key_val == KEY_U_PRESS)
        Gyro_I += 0.01;
      else if (key_val == KEY_D_PRESS)
        Gyro_I -= 0.01;
      break;
    }
    case 6:
    {
      if (key_val == KEY_U_PRESS)
        Gyro_D += 0.01;
      else if (key_val == KEY_D_PRESS)
        Gyro_D -= 0.01;
      break;
    }
    }
}

在子级页面,继续调用上一级函数,用于显示参数,然后对光标进限制,但这个写的不太好,重复限制好像没什么用。然后是判断是都选中某一项,选中后根据光标位置,修改相应的参数。

结语

参考了网上的写法,仍有进步空间。

Arduino OLED菜单教程通常是用于指导如何在基于Arduino的硬件平台上,利用OLED显示屏(如SSD1306、I2C OLED等)构建一个交互式的菜单系统。以下是简单的步骤: 1. **设置环境**: - 首先,你需要确保已经安装了Arduino IDE并连接了一个支持OLED的库,比如Adafruit库或SSD1306库。 2. **硬件准备**: - 将OLED显示屏连接到Arduino,通常数据线连接SCL和SDA,电源线连接VCC和GND。 3. **编写代码**: - 使用`#include`包含OLED库文件,例如 `#include <Adafruit_SSD1306.h>`。 - 创建一个OLED对象实例,并初始化显示尺寸和I2C通信(如果是I2C连接)。 ```cpp Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT); display.begin(); ``` 4. **菜单结构**: - 创建菜单项数组,并设置它们的文本和对应的动作函数。 ```cpp const char* menuItems[] = {"Home", "Settings", "About"}; void(*menuActions[])(void) = {homeMenu, settingsMenu, aboutMenu}; ``` 5. **主循环和菜单操作**: - 主循环中检查用户输入(如按键),选择相应的菜单项并调用相应函数。 ```cpp while (true) { selectMenuItem(menuActions); // 调用函数选择当前菜单项 display.clearDisplay(); // 清除屏幕 display.display(); // 更新显示 delay(UPDATE_DELAY); // 等待一定时间再显示下一行 } ``` 6. **函数实现**: - 对于每个动作函数(如`homeMenu`, `settingsMenu`, `aboutMenu`),你可以编写相应的代码来处理对应的界面展示或功能。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苦瓜人生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值