本次介绍一款中景园带字库的OLED显示屏,并基于该模块描述一种菜单操作方法,能够极大的减少显示界面开发工作量。
使用的2.08寸OLED显示屏,字库芯片为GT30L32S4W,支持多种字号中英文。
官方提供了很完善的参考资料,包括不同芯片的操作例程,也就是说点亮显示屏显示字符等操作官方已经帮你完成了,所以接下来要讲的就是具体的应用。
首先我们从例程中可以得到如下类型的操作函数,这些函数基本具备了菜单操作的一切,只需要填充坐标和显示内容就可以了。
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 sizey,u8 mode);
void OLED_ShowString(u8 x,u8 y,u8 *dp,u8 sizey,u8 mode);
void OLED_ShowNum(u8 x,u8 y,u32 num,u16 len,u8 sizey,u8 mode);
void OLED_DrawBMP(u8 x,u8 y,u16 length,u8 width,const u8 BMP[],u8 mode);
void OLED_Clear(void);
void OLED_Fill(u16 x1,u8 y1,u16 x2,u8 y2,u8 color);
如果只是单页面的话,这样直接调用函数去操作也是没什么问题的,毕竟应用本身就不复杂。但是如果我们需要实现多种菜单页面,而且页面内容还支持滚动查看,那么就不太方便了。因此我们需要一套GUI操作方法,能够实现多页面,长页面,页面上下滚动,甚至还有加下划线或高亮指示等功能。
下面就来说说我所使用的一套GUI操作方法,以本次所述带中文字库的OLED为例。
首先说一下OLED显示字符的方式,OLED可以显示不同字号字体以及图片等,那么它是如何实现的呢。事实上,OLED厂家会提供一个取模软件,通过这个软件,你可以输入想要的文字或图片,设置字体和字号,最终输出一个字节数组。而OLED在显示的时候就是读取字节数组实现的,因此不同字体字号的文字得使用不同的字节数组,使用不同的操作函数,也就是说想要显示的类型越多,就需要越大的内存空间来存储。
以上所述是常规的OLED显示方法,针对带字库芯片的OLED有额外的显示方法。首先,不需要再通过取模软件去生成字节数组了,如有需要,直接从字库芯片获取就可以了,当然,受限于字库芯片,你也只能得到有限种的显示效果。比如本次使用的这款OLED,它的字库芯片所支持的内容如下:
好的,不说那么多,开始讲如何实现动态菜单吧。首先我们得知道关于这个屏的一些基本参数,分辨率256*64,那么根据所选的字体,也就可以计算出每行每列最大支持几个字符显示了。在这里我为了简单只使用了固定大小的字体16,所以可以知道每行最多显示32个字符,每列最多显示4个字符。
有时候我们需要设置菜单标题和菜单项,并且在滚动菜单时需要冻结菜单标题的显示。如果需要在一行显示多个内容,需要设置左右间隔(事实上设置一边就够了)。如果要设置参数,需要一个索引,这里使用一个箭头作指示。我们把这些常量写到宏里并放到头文件里。
#define DISPLAY_MAX_SCREEN_WIDTH (256)
#define DISPLAY_MAX_SCREEN_HEIGHT (64)
#define DISPLAY_LINENUM_MAX (4)
#define DISPLAY_LINENUM_WITHOUT_HEAD_MAX (DISPLAY_LINENUM_MAX - 1)
#define Label_TEXT_LENGTH_MAX (32)
#define Label_LEFT_MARGIN (16) // 定义label左边的留白宽度
#define MENU_TOP_MARGIN (0)
#define MENU_ARROW_X (0)
#define Label_DEFAULT_FONT_SIZE (16)
#define Label_DEFAULT_FONT_SIZE_WIDTH (16)
#define Label_DEFAULT_FONT_SIZE_HEIGHT (16)
接下来我们开始构建一个显示结构体,可以看到很全面,要显示的内容,长度,坐标,是否有下划线等都可以设置。
typedef struct
{
uint8_t text[KLabel_TEXT_LENGTH_MAX]; // 需要显示的字符串
uint8_t textLength; // 字符串的实际长度
POS_typedef pos; // 需要显示的位置(左上角)
POS_typedef size; // 当前label的大小
uint8_t fontSize; // 字符显示的大小
uint8_t head; // 是否是head
UNDERLINE_enum underline; // 是否含有下划线
}Label_typedef;
typedef struct
{
uint8_t x;
uint8_t y;
}POS_typedef;
typedef enum
{
UNDERLINE_NO = 0,
UNDERLINE_YES = 1
} UNDERLINE_enum;
接下来定义操作函数,在这里先对显示标签初始化,后面如果要调整再使用下面的操作函数。
void Label_Init(Label_typedef *label, uint8_t *text)
{
memset(label->text, 0x00, sizeof(uint8_t) * Label_TEXT_LENGTH_MAX);
label->textLength = 0;
label->pos.x = 0;
label->pos.y = 0;
label->size.x = 0;
label->size.y = 0;
label->fontSize = Label_DEFAULT_FONT_SIZE;
label->head = 0;
label->underline = UNDERLINE_NO;
Label_SetText(label, text);
}
void Label_SetText(Label_typedef *label, uint8_t *text)
{
uint8_t index = 0;
if (strlen((char*)text) >= Label_TEXT_LENGTH_MAX)
{
return;
}
memset(label->text, 0x00, sizeof(uint8_t) * Label_TEXT_LENGTH_MAX);
while (*(text + index))
{
label->text[index] = text[index];
index++;
}
label->textLength = index;
}
void Label_SetPos(Label_typedef *label, POS_typedef pos)
{
label->pos.x = pos.x;
label->pos.y = pos.y;
}
void Label_SetSize(Label_typedef *label, POS_typedef size)
{
label->size.x = size.x;
label->size.y = size.y;
}
void Label_SetFontSize(Label_typedef *label, uint8_t fontSize)
{
label->fontSize = fontSize;
}
void Label_SetUnderline(Label_typedef *label, UNDERLINE_enum underline)
{
label->underline = underline;
}
void Label_SetHead(Label_typedef *label, uint8_t isHead)
{
label->head = isHead;
}
接下里就是核心,真正的显示函数。在这里首先判断了是否是菜单标题,如果是,则肯定会得到显示。如果不是的话,就得根据坐标去判断是否要显示了,比如菜单总共10行,但每次算上标题也只能显示4行,那么后面7行菜单项自然是不需要显示的。最后,如果要加高亮指示等,可以叠加下划线或设置反色等都可以。
void Label_Display(Label_typedef *label)
{
if (label->head)
{
OLED_ShowString(label->pos.y, label->pos.x, (char*)label->text, label->fontSize);
}
else
{
if (label->pos.y >=Label_DEFAULT_FONT_SIZE_HEIGHT && (label->pos.y+label->size.y) <= DISPLAY_MAX_SCREEN_HEIGHT && (label->pos.x+label->size.x+Label_DEFAULT_FONT_SIZE_WIDTH*label->textLength) <= DISPLAY_MAX_SCREEN_WIDTH)
{
OLED_ShowString(label->pos.y, label->pos.x, (char*)label->text, label->fontSize);
}
if (label->underline == UNDERLINE_YES)
{
OLED_ShowLine(label->pos.y + label->size.y - 2,label->pos.x,label->size.x, 1);
}
else
{
OLED_ShowLine(label->pos.y + label->size.y - 2,label->pos.x,label->size.x, 0);
}
}
}
有了上面这些,要实现菜单就很容易了。比如要实现一个带标题的菜单,菜单含有4个菜单项,那么可以按如下方式去构造菜单。
typedef struct
{
KLabel_typedef headLabel; // 菜单头
KLabel_typedef menuitem1Label;
KLabel_typedef menuitem1ValueLabel;
KLabel_typedef menuitem2Label;
KLabel_typedef menuitem2ValueLabel;
KLabel_typedef menuitem3Label;
KLabel_typedef menuitem3ValueLabel;
KLabel_typedef menuitem4Label;
KLabel_typedef menuitem4ValueLabel;
uint8_t firstLineIndex;
uint8_t lastLineIndex;
uint8_t currentIndex;
} TESTWidget_typedef;
菜单结构定义完毕,接下来就是具体的显示实现了。首先是菜单的初始化,这里先对菜单项赋了初值,然后调用布局函数。
void TESTWidget_Init(TESTWidget_typedef *widget)
{
KLabel_Init(&widget->headLabel, "TEST WIDGET");
KLabel_Init(&widget->menuitem1Label, "MENU1:");
KLabel_Init (&widget->menuitem1ValueLabel, "MENU1_VALUE");
KLabel_Init(&widget->menuitem2Label, "MENU2:");
KLabel_Init (&widget->menuitem2ValueLabel, "MENU2_VALUE");
KLabel_Init(&widget->menuitem3Label, "MENU3:");
KLabel_Init (&widget->menuitem3ValueLabel, "MENU3_VALUE");
KLabel_Init(&widget->menuitem4Label, "MENU4:");
KLabel_Init (&widget->menuitem4ValueLabel, "MENU4_VALUE");
KLabel_SetHead(&widget->headLabel, 1);
widget->firstLineIndex = 1;
widget->lastLineIndex = DISPLAY_LINENUM_WITHOUT_HEAD_MAX;
TESTWidget_Layout(widget);
}
现在看看布局函数是什么样的,可以看到除了标题外,其余菜单项的坐标与索引widget->firstLineIndex有关,之前说的比如菜单总共10行,但每次算上标题也只能显示4行,如果此时widget->firstLineIndex==0,那么会显示2-4行,如果widget->firstLineIndex==3,那么会显示5-7行,这样只需调整widget->firstLineIndex的值,就能实现菜单的滚动显示。
static void TESTWidget_Layout(TESTWidget_typedef *widget)
{
POS_typedef pos;
POS_typedef posValue;
POS_typedef size;
size.x = 0;
size.y = Label_DEFAULT_FONT_SIZE_HEIGHT;
TESTWidget_LayoutHeadRow(&widget->headLabel, &pos);
pos.x = TESTWIDGET_LEFT_MARGIN;
pos.y = widget->headLabel.pos.y + widget->headLabel.size.y
- (widget->firstLineIndex - 1) * Label_DEFAULT_FONT_SIZE_HEIGHT;
KLabel_SetPos (&widget->menuitem1Label, pos);
KLabel_SetSize (&widget->menuitem1Label, size);
KLabel_SetAlignment (&widget->menuitem1Label, ALIGNMENT_MIDDLE);
KLabel_SetFontSize (&widget->menuitem1Label, Label_DEFAULT_FONT_SIZE);
KLabel_SetUnderline (&widget->menuitem1Label, UNDERLINE_NO);
posValue.x = pos.x + 2*Label_DEFAULT_FONT_SIZE_HEIGHT;
posValue.y = pos.y;
KLabel_SetPos (&widget->menuitem1ValueLabel, posValue);
KLabel_SetSize (&widget->menuitem1ValueLabel, size);
KLabel_SetAlignment (&widget->menuitem1ValueLabel, ALIGNMENT_MIDDLE);
KLabel_SetFontSize (&widget->menuitem1ValueLabel, KLabel_DEFAULT_FONT_SIZE);
KLabel_SetUnderline (&widget->menuitem1ValueLabel, UNDERLINE_NO);
pos.y = pos.y + widget->menuitem1ValueLabel.size.y;
KLabel_SetPos (&widget->menuitem2Label, pos);
KLabel_SetSize (&widget->menuitem2Label, size);
KLabel_SetAlignment (&widget->menuitem2Label, ALIGNMENT_MIDDLE);
KLabel_SetFontSize (&widget->menuitem2Label, KLabel_DEFAULT_FONT_SIZE);
KLabel_SetUnderline (&widget->menuitem2Label, UNDERLINE_NO);
posValue.x = pos.x + 2*Label_DEFAULT_FONT_SIZE_HEIGHT;
posValue.y = pos.y;
KLabel_SetPos (&widget->menuitem2ValueLabel, posValue);
KLabel_SetSize (&widget->menuitem2ValueLabel, size);
KLabel_SetAlignment (&widget->menuitem2ValueLabel, ALIGNMENT_MIDDLE);
KLabel_SetFontSize (&widget->menuitem2ValueLabel, KLabel_DEFAULT_FONT_SIZE);
KLabel_SetUnderline (&widget->menuitem2ValueLabel, UNDERLINE_NO);
/*后边类似,使用简写代替*/
TESTWidget_LayoutAppendedRow(&widget->menuitem3Label, &pos);
TESTWidget_LayoutAppendedColumn(&widget->menuitem3ValueLabel, &pos, 2 * Label_DEFAULT_FONT_SIZE_HEIGHT);
}
我们可以使用按键或旋转编码器来用作菜单的滚动,可以设置往上滚动为-1,往下滚动为+1,那么可以得到下面的程序。根据当前所处的索引widget->currentIndex和总的菜单项数目计算widget->firstLineIndex和widget->lastLineIndex,由此决定当前需要显示的内容。
void TESTWidgetItemIndexScrewed(TESTWidget_typedef* widget,int8_t value)
{
int8_t tempIndex = 0;
tempIndex = widget->currentIndex + value;
if (tempIndex < 1)
{
tempIndex = 1;
}
else if (tempIndex >= TESTWIDGET_MENU_LINE_NUM)
{
tempIndex = TESTWIDGET_MENU_LINE_NUM- 1;
}
widget->currentIndex = tempIndex;
if (widget->currentIndex < widget->firstLineIndex)
{
widget->firstLineIndex = widget->currentIndex;
widget->lastLineIndex = widget->firstLineIndex + (DISPLAY_LINENUM_WITHOUT_HEAD_MAX - 1);
}
if (widget->currentIndex > widget->lastLineIndex)
{
widget->lastLineIndex = widget->currentIndex;
widget->firstLineIndex = widget->lastLineIndex - (DISPLAY_LINENUM_WITHOUT_HEAD_MAX - 1);
}
}
如果要修改菜单参数,可以参照TESTWidgetItemIndexScrewed函数编写,这样就有两个功能,一个用于菜单翻页(选择具体菜单项),另一个用作调整参数值,基本满足菜单操作的所有需要。