LVGL实战训练——计算器实现
目录
一、简介
二、部件知识
2.1 按钮矩阵部件(lv_btnmatrix)
2.1.1 按钮矩阵部件的组成
2.1.2 按钮文本设置
2.1.3 按钮索引
2.1.4 按钮宽度
2.1.5 按钮属性
2.1.6 按钮互斥
2.1.7 按钮文本重着色
2.1.8 按钮矩阵部件的事件
2.1.9 按钮矩阵部件的 API 函数
2.2 文本区域部件(lv_textarea)
2.2.1 文本区域部件的组成
2.2.2 创建文本区域部件
2.2.3 添加与删除字符
2.2.4 占位符文本
2.2.5 移动光标
2.2.6 文本区域部件的特殊模式
2.2.7 限制输入的字符
2.2.8 文本区域部件的 API 函数
三、计算机实现
3.1 StrCalculate.c 计算器管理函数
3.2.1 文本区域初始化
3.2.2 矩阵按钮初始化
3.2.3 按钮初始化
3.2.4 按钮事件回调以及计算器逻辑
四、总结
一、简介
我们之前在我的LVGL专栏里面讲了部分控件(LVGL专栏),我们这里决定把部件知识用到实战中,在实战中来学习LVGL各个部件的使用,这里我们利用CodeBlock这个模拟器,使用LVGL来设计出一个计算器,我们看一下演示的视频,视频如下。
模拟器上用LVGL实现计算器功能
二、部件知识
2.1 按钮矩阵部件(lv_btnmatrix)
在 LVGL中,按钮矩阵部件相当于一系列伪按钮的集合,它按一定的序列来排布这些按钮。值得注意的是,这些伪按钮并不是真正的按钮部件(lv_btn),它们只是具有按钮外观的图形,但这些图形具有和按钮一样的点击效果。伪按钮所占的内存非常小,一个伪按钮大概占用 8 个字节,而一个普通按钮部件所占的内存大概为 100~150个字节,由此可见,如果我们 GUI界面中使用较多的按钮时,按钮矩阵的优势就尤为明显了。
2.1.1 按钮矩阵部件的组成

2.1.2 按钮文本设置
在 LVGL中,按钮矩阵部件中的每个按钮都可以设置文本,如果用户想设置这些按钮文本,则需要定义一个字符串数组(指针),并在该数组中传入所需的文本内容,最后通过 lv_btnmatrix_set_map 函数设置按钮文本,我们计算机矩阵按钮代码如下:
static const char *screen_btnm_1_text_map[] = {
"1","2","3","\xEF\xBC\x8B",
"\n","4","5","6",
"\xEF\xBC\x8D","\n","7","8",
"9","\xC3\x97","\n",".",
"0","\xEF\xBC\x9D","\xC3\xB7","",
};//这些十六进制转义序列使用的是 UTF-8 编码,将 Unicode 字符转换为多字节表示,本质就是+、-、×、÷、=,这五个中文字符,不过得确保字体里面包含这几个字符。
lv_btnmatrix_set_map(ui->screen_btnm_1, screen_btnm_1_text_map);
在上述源码中,我们首先定义了字符串数组,里面传入了 16个按钮的对应文本,注意:该数组最后一个元素必须为空,且如果需要在多一行按键的话,需要多加一个"\n",有了按钮数组后,再调用 lv_btnmatrix_create 函数创建按钮矩阵,最后通过 lv_btnmatrix_set_map 函数把字符串数组映射到按钮矩阵当中。
2.1.3 按钮索引
索引就相当于一个 ID,在按钮矩阵部件中,每一个按钮都有对应的索引。我们这个示例有16个按键,索引如下:
注意:索引对于按钮的属性设置非常重要,大家一定要理解它和实际按钮的对应关系。
2.1.4 按钮宽度
在默认情况下,按钮矩阵每一行按钮的宽度都是自动计算的,如果用户想改变按钮的宽度,可以调用 lv_btnmatrix_set_btn_width 函数来进行设置。值得注意的是,在按钮矩阵部件中,按钮只能设置相对宽度。
接下来,我们举一个例子,帮助大家理解按钮的相对宽度:假设按钮矩阵的某一行中存在3 个按钮(btn1~btn3),btn1~btn3 的相对宽度比为 1:1:2,此时,btn1 和 btn2 将各占该行25%的宽度,而 btn3 将占该行 50%的宽度,示意图如下:
2.1.5 按钮属性
用户可以调用 lv_btnmatrix_set_btn_ctrl 函数,为按钮添加、清除指定的属性,这些属性的相关枚举如下:
① LV_BTNMATRIX_CTRL_HIDDEN:将按钮隐藏;
② LV_BTNMATRIX_CTRL_NO_REPEAT:禁用长按;
③ LV_BTNMATRIX_CTRL_DISABLED:禁用按钮;
④ LV_BTNMATRIX_CTRL_CHECKABLE:启用按钮状态切换;
⑤ LV_BTNMATRIX_CTRL_CHECKED:选中按钮;
⑥ LV_BTNMATRIX_CTRL_POPOVER:按下此按钮时在弹出窗口中显示按钮标签;
⑦ LV_BTNMATRIX_CTRL_RECOLOR:启用按钮文本的重新着色功能。
接下来,我们以简单示例来理解按钮属性的设置,示例代码如下所示:
const char * map [] = { "btn1" , "btn2" , "btn3" , "" };void lv_mainstart (){lv_obj_t * btnm1 = lv_btnmatrix_create ( lv_scr_act ());lv_btnmatrix_set_map ( btnm1 , map );lv_obj_set_size ( btnm1 , 800 , 480 / 2 );lv_btnmatrix_set_btn_ctrl ( btnm1 , 0 , LV_BTNMATRIX_CTRL_HIDDEN );lv_btnmatrix_set_btn_ctrl ( btnm1 , 1 , LV_BTNMATRIX_CTRL_DISABLED );lv_btnmatrix_set_btn_ctrl ( btnm1 , 2 , LV_BTNMATRIX_CTRL_CHECKABLE );lv_btnmatrix_set_btn_width ( btnm1 , 2 , 2 );}

如果用户想要清除按钮的指定属性,可以调用 lv_btnmatrix_clear_btn_ctrl 函数。
2.1.6 按钮互斥
按钮互斥是指:在某一时刻,只允许有一个按钮处于按下不弹起状态(被选中),当我们选中一个按钮之后,其他的按钮将会自动清除选中属性,示意图如下:
用户可以调用 lv_btnmatrix_set_one_checked 函数,开启按钮互斥功能。
2.1.7 按钮文本重着色
在默认情况下,按钮矩阵中的按钮文本都是黑色的,如果用户需要设置文本为其他的颜色,则必须先调用 lv_btnmatrix_set_btn_ctrl 函数,为按钮添加文本重着色的属性。接下来,我们以简单示例来帮助大家理解按钮文本的重着色,示例代码如下所示:
const char * map [] = { "#FF0000 btn1#" , "btn2" , "btn3" , "" };void lv_mainstart (){lv_obj_t * btnm1 = lv_btnmatrix_create ( lv_scr_act (), NULL );lv_btnmatrix_set_map ( btnm1 , map );lv_btnmatrix_set_btn_ctrl ( btnm1 , 0 , LV_BTNMATRIX_CTRL_RECOLOR );}
由上述源码可知,我们在定义按钮数组时,为 btn1 的文本设置了颜色(红色),其通用的格式为:# + 16 进制颜色 + 按钮文本 + #,例如设置红色文本:#FF0000 btn1#。值得注意的是,在设置完文本颜色之后,我们还需要为按钮添加文本重着色的属性,其相关的枚举为LV_BTNMATRIX_CTRL_RECOLOR。示例代码可以在 PC 模拟器中运行,效果图如下所示:
2.1.8 按钮矩阵部件的事件
① LV_EVENT_VALUE_CHANGED:当一个按钮被按下、释放或长按时发送。
② LV_EVENT_DRAW_PART_BEGIN:开始绘制按钮。
2.1.9 按钮矩阵部件的 API 函数
LVGL 官方提供了一些与按钮矩阵部件相关 API,如下表所示:
函数名称 | 作用 | 参数 | 参数说明 |
---|---|---|---|
lv_btnmatrix_create() | 创建按钮矩阵部件 | (lv_obj_t *parent, const lv_obj_t *copy) | parent : 父对象;copy : 复制源(可选,通常为NULL ) |
lv_btnmatrix_set_map() | 设置按钮的文本映射(定义按钮布局和文本) | (lv_obj_t *btnm, const char *map[]) | btnm : 按钮矩阵对象;map[] : 文本数组(如screen_btnm_1_text_map ) |
lv_btnmatrix_set_ctrl_map() | 设置多个按钮的属性(如禁用、可切换等) | (lv_obj_t *btnm, const lv_btnmatrix_ctrl_t ctrl_map[]) | btnm : 按钮矩阵对象;ctrl_map[] : 控制属性数组(如LV_BTNMATRIX_CTRL_... ) |
lv_btnmatrix_set_selected_btn() | 设置当前选中的按钮(通过索引) | (lv_obj_t *btnm, uint16_t btn_id) | btnm : 按钮矩阵对象;btn_id : 按钮索引(从0开始) |
lv_btnmatrix_set_btn_ctrl() | 设置单个按钮的属性(如禁用、可切换等) | (lv_obj_t *btnm, uint16_t btn_id, lv_btnmatrix_ctrl_t ctrl) | btnm : 按钮矩阵对象;btn_id : 按钮索引;ctrl : 控制属性 |
lv_btnmatrix_clear_btn_ctrl() | 清除单个按钮的指定属性 | (lv_obj_t *btnm, uint16_t btn_id, lv_btnmatrix_ctrl_t ctrl) | 同上 |
lv_btnmatrix_set_btn_ctrl_all() | 设置所有按钮的同一属性 | (lv_obj_t *btnm, lv_btnmatrix_ctrl_t ctrl) | btnm : 按钮矩阵对象;ctrl : 控制属性 |
lv_btnmatrix_clear_btn_ctrl_all() | 清除所有按钮的同一属性 | (lv_obj_t *btnm, lv_btnmatrix_ctrl_t ctrl) | 同上 |
lv_btnmatrix_set_btn_width() | 设置单个按钮的相对宽度(如宽度为2表示占两列) | (lv_obj_t *btnm, uint16_t btn_id, uint8_t width) | btnm : 按钮矩阵对象;btn_id : 按钮索引;width : 宽度比例(1~16) |
lv_btnmatrix_set_one_checked() | 设置按钮互斥模式(同一时间只能选中一个按钮) | (lv_obj_t *btnm, bool one_checked) | btnm : 按钮矩阵对象;one_checked : true /false |
lv_btnmatrix_get_map() | 获取按钮的文本映射数组 | (const lv_obj_t *btnm) | btnm : 按钮矩阵对象;返回值:const char ** (文本数组) |
lv_btnmatrix_get_selected_btn() | 获取用户最后点击的按钮索引 | (const lv_obj_t *btnm) | btnm : 按钮矩阵对象;返回值:uint16_t (索引) |
lv_btnmatrix_get_btn_text() | 获取指定按钮的文本 | (const lv_obj_t *btnm, uint16_t btn_id) | btnm : 按钮矩阵对象;btn_id : 按钮索引;返回值:const char* |
lv_btnmatrix_has_btn_ctrl() | 检查按钮是否具有指定属性 | (const lv_obj_t *btnm, uint16_t btn_id, lv_btnmatrix_ctrl_t ctrl) | btnm : 按钮矩阵对象;btn_id : 按钮索引;ctrl : 控制属性;返回值:bool |
lv_btnmatrix_get_one_checked() | 判断是否启用了按钮互斥模式 | (const lv_obj_t *btnm) | btnm : 按钮矩阵对象;返回值:bool |
2.2 文本区域部件(lv_textarea)
2.2.1 文本区域部件的组成
文本区域部件由五个部分组成:
① 主体 LV_PART_MAIN:可设置背景属性以及文本样式属性;
② 滚动条 LV_PART_SCROLLBAR:可设置滚动条样式属性;
③ 所选文本 LV_PART_SELECTED:可设置所选文本的样式;
④ 光标 LV_PART_CURSOR:设置光标的位置、闪烁时间和样式属性;
⑤ 占位符 LV_PART_TEXTAREA_PLACEHOLDER:可设置占位符(提示文本)的样式。
2.2.2 创建文本区域部件
在 LVGL 中,用户需要创建文本区域部件,可调用以下函数:
lv_obj_t * lv_textarea_create ( lv_obj_t * parent );
上述函数只有一个形参,该形参指向文本区域部件的父类。
2.2.3 添加与删除字符
文本区域部件就是一个文本输入框, 用户可以在该部件的文本区域中输入字符和删除字符,下面我们分别介绍添加字符和删除字符的知识,如下所示:
1. 添加字符和文本
用户需要在文本区域中添加一个字符或者一段字符串,可分别调用 lv_textarea_add_char 和 lv_textarea_add_text 函数。接下来,我们以简单示例来理解字符和文本的添加,示例代码如下所示:
void lv_mainstart ( void ){lv_obj_t * textarea = lv_textarea_create ( lv_scr_act ()); /* 定义并创建文本框 */lv_textarea_add_char ( textarea , 'c' );/* 添加一个字符 */lv_textarea_add_text ( textarea , "insert this text" );/* 添加一个字符串 */lv_obj_center ( textarea );/* 中间对齐 */}
在上述源码中,我们创建先文本区域部件,调用 lv_textarea_add_char 函数在文本区域添加“c”字符,然后调用 lv_textarea_add_text 函数在文本区域中添加字符串“insert this text”,最后把文本区域部件居中对齐。示例代码可以在 PC 模拟器中运行,效果图如下所示:
2. 删除字符
在 LVGL 中,文本区域部件删除字符的方法有两种,如下所示:
① 调用 lv_textarea_del_char 函数,从光标位置的左侧删除一个字符;
② 调用 lv_textarea_del_char_forward 函数,从光标位置的右侧删除一个字符。
2.2.4 占位符文本
占位符(Placeholder)即默认显示的文本,常用于对用户进行默认的提示、说明或引导。在 LVGL 中,用户可调用 lv_textarea_set_placeholder_text 函数设置占位符。接下来,我们以简单示例来理解占位符文本的设置,示例代码如下所示:
在上述源码中,我们先创建文本区域部件,然后设置占位符提示文本为“Please enter text......”。示例代码可以在 PC 模拟器中运行,效果图如下所示:
从上图可知:占位符就是对用户进行提示和引导。
2.2.5 移动光标
在 LVGL中,文本区域的光标默认在左上角的位置。当我们添加一个字符时,该光标从左往右移动,默认情况下,光标会一直在文本最后一个字符的右侧。如果用户需要设置光标的位置,可调用 lv_textarea_set_cursor_pos 函数,该函数的第二个形参表示光标的移动位置,当该形参设置为 0,则光标在第一个字符之前;当该形参设置为 LV_TA_CURSOR_LAST,则光标在最后一个字符之后。
如果用户不想使用上述的方式设置光标的位置,可使用以下函数,单步移动光标:
上述的光标移动函数常用于按键手动控制光标,当用户按下某个按键时,光标将会向指定的方向移动。
2.2.6 文本区域部件的特殊模式
在 LVGL 中,文本区域部件的特殊模式有两个,如下所示:
① 单行模式:默认情况下,文本区域的文本超出它的宽度时,该文本将自动换行。如果将文本区域部件设置为单行模式,当它的文本超出其宽度后,文本并不会自动换行,超出的文本将水平滚动显示。单行模式可通过 lv_textarea_set_one_line 函数进行设置。
② 密码模式:为了保证文本的机密性,LVGL 的文本区域部件为用户提供了密码模式,当用户输入文本之后,这些文本内容将以“*”字符替代。用户需要设置密码模式,可调用
lv_textarea_set_password_mode 函数。
注意:当用户使用密码模式时,原始文本会先显示一段时间,然后隐藏,该显示时间可以在 lv_conf.h 文件的 LV_TEXTAREA_DEF_PWD_SHOW_TIME 宏定义中设置。如果用户想获取密码框的文本内容,可调用 lv_textarea_get_text 函数,返回的文本并不是“*”字符,而是原始的文本内容。
2.2.7 限制输入的字符
文本区域部件可以限制输入字符的范围,例如取款机的密码框,它只允许输入 0~9 的字符,且密码长度固定为 6。LVGL 文本区域部件限制输入字符的内容有两个:限制字符类型和限制字符长度。
① 限制字符类型,比如 ATM 的密码框,它只能输入数字 0~9。用户需要限制字符类型,
可以调用 lv_textarea_set_accepted_chars 函数进行设置。
② 限制字符长度,比如 ATM 的密码框,它只能输入 6 位密码。用户需要限制字符长度,
可以调用 lv_textarea_set_max_length 函数进行设置。
接下来,我们以简单示例来理解字符输入的限制,示例代码如下所示:
void lv_mainstart ( void ){/* 定义并创建文本框 */lv_obj_t * textarea = lv_textarea_create ( lv_scr_act ());/* 设置输入字符长度为 6 */lv_textarea_set_max_length ( textarea , 6 );/* 设置接收输入字符列表 */lv_textarea_set_accepted_chars ( textarea , "0123456789" );/* 一行模式 */lv_textarea_set_one_line ( textarea , true );/* 中间对齐 */lv_obj_center ( textarea );/* 输入的字符 */lv_textarea_add_char ( textarea , 'C' );lv_textarea_add_char ( textarea , 'a' );lv_textarea_add_char ( textarea , 'i' );lv_textarea_add_char ( textarea , 'X' );lv_textarea_add_char ( textarea , 'e' );lv_textarea_add_char ( textarea , 'u' );lv_textarea_add_char ( textarea , 'e' );lv_textarea_add_char ( textarea , 'F' );lv_textarea_add_char ( textarea , 'e' );lv_textarea_add_char ( textarea , 'n' );lv_textarea_add_char ( textarea , 'g' );lv_textarea_add_char ( textarea , '1' );lv_textarea_add_char ( textarea , '6' );lv_textarea_add_char ( textarea , '8' );lv_textarea_add_char ( textarea , '6' );lv_textarea_add_char ( textarea , '6' );lv_textarea_add_char ( textarea , '6' );}
在上述源码中,我们调用 lv_textarea_set_max_length 函数,限制输入字符长度为 6,然后调用 lv_textarea_set_accepted_chars函数,限制输入字符类型范围为“0123456789”,最后调用多个 lv_textarea_add_char函数添加文本,该文本为“CaiXueFeng168666”。上示例代码可以在PC 模拟器中运行,效果图如下所示:
由上图可知,文本区域部件最终显示的文本是“168666”,而我们输入的所有文本为“CaiXueFeng168666”,这说明我们设置的字符类型和长度限制生效了。
2.2.8 文本区域部件的 API 函数
函数名称 | 作用 | 参数 | 参数说明 |
---|---|---|---|
lv_textarea_create() | 创建文本区域对象 | (lv_obj_t *parent, const lv_obj_t *copy) | parent : 父对象;copy : 复制源(可选,通常为NULL ) |
lv_textarea_add_char() | 插入一个字符到当前光标位置 | (lv_obj_t *ta, uint32_t c) | ta : 文本区域对象;c : 要插入的字符(Unicode 编码) |
lv_textarea_add_text() | 将文本插入到当前光标位置 | (lv_obj_t *ta, const char *txt) | ta : 文本区域对象;txt : 要插入的文本 |
lv_textarea_del_char() | 删除当前光标左侧的字符 | (lv_obj_t *ta) | ta : 文本区域对象 |
lv_textarea_del_char_forward() | 删除当前光标右侧的字符 | (lv_obj_t *ta) | 同上 |
lv_textarea_set_text() | 设置文本区域的完整文本 | (lv_obj_t *ta, const char *txt) | ta : 文本区域对象;txt : 新文本 |
lv_textarea_set_placeholder_text() | 设置占位符文本(未输入时显示的提示文本) | (lv_obj_t *ta, const char *txt) | ta : 文本区域对象;txt : 占位符文本 |
lv_textarea_set_cursor_pos() | 设置光标位置(基于字符索引) | (lv_obj_t *ta, int32_t pos) | ta : 文本区域对象;pos : 字符索引(从0开始) |
lv_textarea_set_cursor_click_pos() | 启用/禁用通过点击移动光标 | (lv_obj_t *ta, bool en) | ta : 文本区域对象;en : true 启用,false 禁用 |
lv_textarea_set_password_mode() | 设置密码模式(输入显示为* 或其他字符) | (lv_obj_t *ta, bool en) | ta : 文本区域对象;en : true 启用,false 禁用 |
lv_textarea_set_one_line() | 设置单行模式(禁止换行) | (lv_obj_t *ta, bool en) | ta : 文本区域对象;en : true 启用单行模式 |
lv_textarea_set_accepted_chars() | 限制可输入的字符类型 | (lv_obj_t *ta, const char *list) | ta : 文本区域对象;list : 允许的字符列表(如"0123456789" ) |
lv_textarea_set_max_length() | 限制输入的最大字符数 | (lv_obj_t *ta, uint32_t num) | ta : 文本区域对象;num : 最大字符数 |
lv_textarea_set_insert_replace() | 设置输入时自动替换格式(如自动添加分隔符) | (lv_obj_t *ta, const char *txt) | ta : 文本区域对象;txt : 替换规则(如自动插入- ) |
lv_textarea_set_text_selection() | 启用/禁用文本选择模式 | (lv_obj_t *ta, bool en) | ta : 文本区域对象;en : true 启用选择模式 |
lv_textarea_set_password_show_time() | 设置密码显示明文的时间(单位:毫秒) | (lv_obj_t *ta, uint16_t time) | ta : 文本区域对象;time : 明文显示时间 |
lv_textarea_get_text() | 获取文本内容(密码模式下返回实际文本) | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:const char* |
lv_textarea_get_placeholder_text() | 获取占位符文本 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:const char* |
lv_textarea_get_label() | 获取文本区域的标签对象(用于自定义样式) | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:lv_obj_t* |
lv_textarea_get_cursor_pos() | 获取当前光标的字符索引位置 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:int32_t |
lv_textarea_get_cursor_click_pos() | 判断是否启用了点击移动光标 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:bool |
lv_textarea_get_password_mode() | 判断是否启用了密码模式 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:bool |
lv_textarea_get_one_line() | 判断是否启用单行模式 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:bool |
lv_textarea_get_accepted_chars() | 获取允许输入的字符列表 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:const char* |
lv_textarea_get_max_length() | 获取最大允许输入字符数 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:uint32_t |
lv_textarea_text_is_selected() | 检查当前是否有文本被选中 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:bool |
lv_textarea_get_text_selection() | 判断是否启用了文本选择模式 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:bool |
lv_textarea_get_password_show_time() | 获取密码明文显示时间 | (const lv_obj_t *ta) | ta : 文本区域对象;返回值:uint16_t |
lv_textarea_clear_selection() | 清除当前选中的文本 | (lv_obj_t *ta) | ta : 文本区域对象 |
lv_textarea_cursor_right() | 将光标向右移动一个字符 | (lv_obj_t *ta) | ta : 文本区域对象 |
lv_textarea_cursor_left() | 将光标向左移动一个字符 | (lv_obj_t *ta) | 同上 |
lv_textarea_cursor_down() | 将光标向下移动一行(多行模式下生效) | (lv_obj_t *ta) | 同上 |
lv_textarea_cursor_up() | 将光标向上移动一行(多行模式下生效) | (lv_obj_t *ta) | 同上 |
三、计算机实现
3.1 StrCalculate.c 计算器管理函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../Inc/StrCalculate.h"uint8_t strput(StrStack_t * st,char strin)
{if(st->Top_Point == 15 - 1){return -1;}st->strque[st->Top_Point++] = strin;return 0;
}uint8_t strdel(StrStack_t * st)
{if(st->Top_Point == 0){return -1;}st->strque[--st->Top_Point] = NULL;return 0;
}uint8_t strstack_isEmpty(StrStack_t* st)
{if(st->Top_Point == 0){return 1;}return 0;
}void strclear(StrStack_t* sq)
{while(!strstack_isEmpty(sq)){strdel(sq);}
}uint8_t NumStackPut(NumStack_t * st, float in)
{if(st->Top_Point == CAL_DEPTH - 1){return -1;}st->data[st->Top_Point++] = in;return 0;
}uint8_t NumStackDel(NumStack_t * st)
{if(st->Top_Point == 0){return -1;}st->data[st->Top_Point--] = 0;return 0;
}uint8_t NumStack_isEmpty(NumStack_t* st)
{if(st->Top_Point == 0){return 1;}return 0;
}void NumStackClear(NumStack_t* st)
{while(!NumStack_isEmpty(st)){NumStackDel(st);}
}uint8_t SymStackPut(SymStack_t * st, char in)
{if(st->Top_Point == CAL_DEPTH - 1){return -1;}st->data[st->Top_Point++] = in;return 0;
}uint8_t SymStackDel(SymStack_t * st)
{if(st->Top_Point == 0){return -1;}st->data[st->Top_Point--] = 0;return 0;
}uint8_t SymStack_isEmpty(SymStack_t* st)
{if(st->Top_Point == 0){return 1;}return 0;
}void SymStackClear(SymStack_t* st)
{while(!SymStack_isEmpty(st)){SymStackDel(st);}
}uint8_t SymisHighPriority(char top, char present)
{//乘除的优先级最大if(top == '*' || top == '/'){return 1;}else if(top == '+'){if(present == '-'){return 1;}else{return 0;}}else if(top == '-'){if(present == '+'){return 1;}else{return 0;}}
}void CalculateOne(NumStack_t * numstack, SymStack_t * symstack)
{caldata_t temp;temp.datatype = NUMBER_TYPE;temp.symbol = NULL;//计算数字栈中的顶部两数,结果存到temp中if(symstack->data[symstack->Top_Point-1] == '+')temp.number = (numstack->data[numstack->Top_Point-2]) + (numstack->data[numstack->Top_Point-1]);else if(symstack->data[symstack->Top_Point-1] == '-')temp.number = (numstack->data[numstack->Top_Point-2]) - (numstack->data[numstack->Top_Point-1]);else if(symstack->data[symstack->Top_Point-1] == '*')temp.number = (numstack->data[numstack->Top_Point-2]) * (numstack->data[numstack->Top_Point-1]);else if(symstack->data[symstack->Top_Point-1] == '/')temp.number = (numstack->data[numstack->Top_Point-2]) / (numstack->data[numstack->Top_Point-1]);//运算前两数出栈,运算结果数入栈NumStackDel(numstack);NumStackDel(numstack);NumStackPut(numstack,temp.number);SymStackDel(symstack);}uint8_t NumSymSeparate(char * str, uint8_t strlen, NumStack_t * NumStack, SymStack_t * SymStack)
{NumStackClear(NumStack);SymStackClear(SymStack);caldata_t temp,temp_pre;char NumBehindPoint_Flag = 0;//数字是否在小数点后,后多少位temp.datatype = NUMBER_TYPE;temp.number = 0;temp.symbol = NULL;temp_pre = temp;temp_pre.datatype = SYMBOL_TYPE;if(str[0]>'9' || str[0]<'0')return 1;//erroint i;for(i=0;i<strlen;i++){if(str[i]=='.'){temp.datatype = POINT_TYPE;if(temp_pre.datatype == NUMBER_TYPE){}else{return 2;}temp_pre = temp;}if(str[i]<='9' && str[i]>='0'){//溢出报错if(NumStack->Top_Point>CAL_DEPTH || SymStack->Top_Point>CAL_DEPTH){return 3;}//读取当前的字符到temp中temp.datatype = NUMBER_TYPE;temp.number = (str[i] - '0');temp.symbol = NULL;//如果为连续数字,需要进行进位,将数字栈顶读出进位,再加上现在位,再入栈if(temp_pre.datatype == NUMBER_TYPE){if(!NumBehindPoint_Flag){temp.number += NumStack->data[NumStack->Top_Point-1] * 10;}else{NumBehindPoint_Flag += 1;char i = NumBehindPoint_Flag;while(i--){temp.number /= 10;}temp.number += NumStack->data[NumStack->Top_Point-1];}NumStackDel(NumStack);NumStackPut(NumStack,temp.number);}//当前数字刚好是小数点后一位else if(temp_pre.datatype == POINT_TYPE){NumBehindPoint_Flag = 1;temp.number /= 10;temp.number += NumStack->data[NumStack->Top_Point-1];NumStackDel(NumStack);NumStackPut(NumStack,temp.number);}//前一位不是数字或小数点,现在读取的这一位是数字,直接入栈else{NumStackPut(NumStack,temp.number);}temp_pre = temp;}else if(str[i] == '+' || str[i] == '-' || str[i] == '*' || str[i] == '/'){//溢出报错if(NumStack->Top_Point>CAL_DEPTH || SymStack->Top_Point>CAL_DEPTH){return 4;}//读取当前的字符到temp中temp.datatype = SYMBOL_TYPE;temp.symbol = str[i];temp.number = 0;NumBehindPoint_Flag = 0;//小数点计算已经结束//重复输入了运算符号if(temp_pre.datatype == SYMBOL_TYPE){return 5 ;//erro}else{if((!SymStack_isEmpty(SymStack)) && SymisHighPriority(SymStack->data[SymStack->Top_Point-1],temp.symbol)){CalculateOne(NumStack, SymStack);SymStackPut(SymStack,temp.symbol);}else{//符号压入符号栈SymStackPut(SymStack,temp.symbol);}temp_pre = temp;}}}return 0;
}uint8_t StrCalculate(char * str,NumStack_t * NumStack, SymStack_t * SymStack)
{if(NumSymSeparate(str,strlen(str),NumStack,SymStack)){//erro, clear allNumStackClear(NumStack);SymStackClear(SymStack);return -1;}else{while(!SymStack_isEmpty(SymStack)){CalculateOne(NumStack,SymStack);}}return 0;
}uint8_t isIntNumber(float number)
{if(number == (int)number){return 1;}return 0;
}
计算器的逻辑就是很经典的计算器问题,经典的就是开两个栈,一个存放符号,一个存数字,然后进行出栈计算等等操作。
具体过程是:
1、遍历表达式,当遇到操作数,将其压入操作数栈。
2、遇到运算符时,如果运算符栈为空,则直接将其压入运算符栈。
3、如果运算符栈不为空,那就与运算符栈顶元素进行比较:如果当前运算符优先级比栈顶运算符高,则继续将其压入运算符栈,如果当前运算符优先级比栈顶运算符低或者相等,则从操作数符栈顶取两个元素,从栈顶取出运算符进行运算,并将运算结果压入操作数栈。
4、继续将当前运算符与运算符栈顶元素比较。
5、继续按照以上步骤进行遍历,当遍历结束之后,则将当前两个栈内元素取出来进行运算即可得到最终结果。
这里我简单的介绍一下这个算法:
2.4.1.1 数据结构
StrStack_t:字符栈
---用于临时存储输入字符
---供strput(入栈)、strdel(出栈)等操作
NumStack_t:数字栈(浮点数)
---存储运算中的数字
---深度为CAL_DEPTH(15)
SymStack_t:符号栈
---存储运算符(+-*/)
---同样具有栈操作函数
2.4.1.2 核心算法流程
uint8_t NumSymSeparate(...)
这个可以说是整个算法核心部分了,NumSymSeparate函数,它负责将输入的字符串分解为数字和运算符,并处理运算顺序的问题。这里需要特别注意数字的小数点处理和运算符优先级的判断。比如,当遇到小数点时,标记NumBehindPoint_Flag,并调整数字的位数。运算符处理时,通过SymisHighPriority函数比较栈顶运算符和当前运算符的优先级,决定是否立即进行计算,从而保持正确的运算顺序。另外,在NumSymSeparate函数中,当处理到运算符时,会检查前一个元素是否是符号类型,如果是则报错,这样处理连续的运算符(如"5++3")会被视为错误,这是正确的。但如果是负数的情况,这里就会导致错误,所以代码不支持负数的运算。前面做的所有都是为了这个函数进行铺垫,我们可以在函数调用关系看到:
优先级判断(SymisHighPriority函数)
uint8_t SymisHighPriority(...)
优先级规则:* / > + > -
栈顶运算符优先级 >= 当前运算符时返回1
例如:
栈顶+ vs 当前- → 同优先级,返回1
栈顶+ vs 当前* → 当前优先级高,返回0
void CalculateOne(NumStack_t * numstack, SymStack_t * symstack)
CalculateOne函数用于执行实际的运算操作,取出数字栈顶的两个数字和符号栈顶的运算符,计算结果后再将结果压回数字栈。这一步是实际计算的核心。
uint8_t StrCalculate(char * str,NumStack_t * NumStack, SymStack_t * SymStack)
1、调用NumSymSeparate进行表达式分解
2、循环执行CalculateOne直到符号栈为空
3、最终结果存储在数字栈顶
3.2 计算器UI编写
我们这里放部分关键的源码,关键源码如下:
3.2.1 文本区域初始化
//Write codes screen_ta_1ui->screen_ta_1 = lv_textarea_create(ui->screen);lv_obj_set_pos(ui->screen_ta_1, 4.5, 5);lv_obj_set_size(ui->screen_ta_1, 251, 51);lv_obj_set_scrollbar_mode(ui->screen_ta_1,LV_SCROLLBAR_MODE_OFF);lv_obj_clear_flag(ui->screen_ta_1, LV_OBJ_FLAG_SCROLLABLE);//Write style state: LV_STATE_DEFAULT for style_screen_ta_1_main_main_defaultstatic lv_style_t style_screen_ta_1_main_main_default;if (style_screen_ta_1_main_main_default.prop_cnt > 1)lv_style_reset(&style_screen_ta_1_main_main_default);elselv_style_init(&style_screen_ta_1_main_main_default);lv_style_set_radius(&style_screen_ta_1_main_main_default, 4);lv_style_set_bg_color(&style_screen_ta_1_main_main_default, lv_color_make(0xff, 0xff, 0xff));lv_style_set_bg_grad_color(&style_screen_ta_1_main_main_default, lv_color_make(0xff, 0xff, 0xff));lv_style_set_bg_grad_dir(&style_screen_ta_1_main_main_default, LV_GRAD_DIR_VER);lv_style_set_bg_opa(&style_screen_ta_1_main_main_default, 255);lv_style_set_border_color(&style_screen_ta_1_main_main_default, lv_color_make(0xe6, 0xe6, 0xe6));lv_style_set_border_width(&style_screen_ta_1_main_main_default, 2);lv_style_set_text_color(&style_screen_ta_1_main_main_default, lv_color_make(0x00, 0x00, 0x00));lv_style_set_text_font(&style_screen_ta_1_main_main_default, &ui_font_Cuyuan20);lv_style_set_text_letter_space(&style_screen_ta_1_main_main_default, 2);lv_style_set_text_align(&style_screen_ta_1_main_main_default, LV_TEXT_ALIGN_RIGHT);//lv_style_set_pad_left(&style_screen_ta_1_main_main_default, 2);//lv_style_set_pad_right(&style_screen_ta_1_main_main_default, 2);//lv_style_set_pad_top(&style_screen_ta_1_main_main_default, 2);//lv_style_set_pad_bottom(&style_screen_ta_1_main_main_default, 2);lv_obj_add_style(ui->screen_ta_1, &style_screen_ta_1_main_main_default, LV_PART_MAIN|LV_STATE_DEFAULT);lv_obj_clear_flag(ui->screen_ta_1,LV_OBJ_FLAG_CLICKABLE);//Write style state: LV_STATE_DEFAULT for style_screen_ta_1_main_scrollbar_defaultstatic lv_style_t style_screen_ta_1_main_scrollbar_default;if (style_screen_ta_1_main_scrollbar_default.prop_cnt > 1)lv_style_reset(&style_screen_ta_1_main_scrollbar_default);elselv_style_init(&style_screen_ta_1_main_scrollbar_default);lv_style_set_radius(&style_screen_ta_1_main_scrollbar_default, 0);lv_style_set_bg_color(&style_screen_ta_1_main_scrollbar_default, lv_color_make(0x21, 0x95, 0xf6));lv_style_set_bg_grad_color(&style_screen_ta_1_main_scrollbar_default, lv_color_make(0x21, 0x95, 0xf6));lv_style_set_bg_grad_dir(&style_screen_ta_1_main_scrollbar_default, LV_GRAD_DIR_VER);lv_style_set_bg_opa(&style_screen_ta_1_main_scrollbar_default, 255);lv_obj_add_style(ui->screen_ta_1, &style_screen_ta_1_main_scrollbar_default, LV_PART_SCROLLBAR|LV_STATE_DEFAULT);lv_textarea_set_text(ui->screen_ta_1,"\n");
3.2.2 矩阵按钮初始化
//Write codes screen_btnm_1ui->screen_btnm_1 = lv_btnmatrix_create(ui->screen);lv_obj_set_pos(ui->screen_btnm_1, 3, 61);lv_obj_set_size(ui->screen_btnm_1, 316, 176);static const char *screen_btnm_1_text_map[] = {"1","2","3","\xEF\xBC\x8B","\n","4","5","6","\xEF\xBC\x8D","\n","7","8","9","\xC3\x97","\n",".","0","\xEF\xBC\x9D","\xC3\xB7","",};lv_btnmatrix_set_map(ui->screen_btnm_1, screen_btnm_1_text_map);//Write style state: LV_STATE_DEFAULT for style_screen_btnm_1_main_main_defaultstatic lv_style_t style_screen_btnm_1_main_main_default;if (style_screen_btnm_1_main_main_default.prop_cnt > 1)lv_style_reset(&style_screen_btnm_1_main_main_default);elselv_style_init(&style_screen_btnm_1_main_main_default);lv_style_set_radius(&style_screen_btnm_1_main_main_default, 4);lv_style_set_bg_color(&style_screen_btnm_1_main_main_default, lv_color_make(0xff, 0xff, 0xff));lv_style_set_bg_grad_color(&style_screen_btnm_1_main_main_default, lv_color_make(0xff, 0xff, 0xff));lv_style_set_bg_grad_dir(&style_screen_btnm_1_main_main_default, LV_GRAD_DIR_VER);lv_style_set_bg_opa(&style_screen_btnm_1_main_main_default, 255);lv_style_set_border_color(&style_screen_btnm_1_main_main_default, lv_color_make(0xff, 0xff, 0xff));lv_style_set_border_width(&style_screen_btnm_1_main_main_default, 1);lv_style_set_pad_left(&style_screen_btnm_1_main_main_default, 2);lv_style_set_pad_right(&style_screen_btnm_1_main_main_default, 2);lv_style_set_pad_top(&style_screen_btnm_1_main_main_default, 2);lv_style_set_pad_bottom(&style_screen_btnm_1_main_main_default, 2);lv_style_set_pad_row(&style_screen_btnm_1_main_main_default, 8);lv_style_set_pad_column(&style_screen_btnm_1_main_main_default, 8);lv_obj_add_style(ui->screen_btnm_1, &style_screen_btnm_1_main_main_default, LV_PART_MAIN|LV_STATE_DEFAULT);//Write style state: LV_STATE_DEFAULT for style_screen_btnm_1_main_items_defaultstatic lv_style_t style_screen_btnm_1_main_items_default;if (style_screen_btnm_1_main_items_default.prop_cnt > 1)lv_style_reset(&style_screen_btnm_1_main_items_default);elselv_style_init(&style_screen_btnm_1_main_items_default);lv_style_set_radius(&style_screen_btnm_1_main_items_default, 4);lv_style_set_bg_color(&style_screen_btnm_1_main_items_default, lv_color_make(0xe6, 0xe6, 0xe6));lv_style_set_bg_grad_color(&style_screen_btnm_1_main_items_default, lv_color_make(0xe6, 0xe6, 0xe6));lv_style_set_bg_grad_dir(&style_screen_btnm_1_main_items_default, LV_GRAD_DIR_VER);lv_style_set_bg_opa(&style_screen_btnm_1_main_items_default, 255);lv_style_set_border_color(&style_screen_btnm_1_main_items_default, lv_color_make(0xd6, 0xdd, 0xe3));lv_style_set_border_width(&style_screen_btnm_1_main_items_default, 1);lv_style_set_text_color(&style_screen_btnm_1_main_items_default, lv_color_make(0x00, 0x00, 0x00));lv_style_set_text_font(&style_screen_btnm_1_main_items_default, &ui_font_Cuyuan20);lv_obj_add_style(ui->screen_btnm_1, &style_screen_btnm_1_main_items_default, LV_PART_ITEMS|LV_STATE_DEFAULT);
3.2.3 按钮初始化
ui_CompageBackBtn=lv_btn_create(ui->screen);lv_obj_align(ui_CompageBackBtn,LV_ALIGN_RIGHT_MID,-10,-90);lv_obj_set_width(ui_CompageBackBtn,40);lv_obj_set_height(ui_CompageBackBtn,30);lv_obj_set_style_bg_color(ui_CompageBackBtn,lv_color_make(0xAD, 0xD8, 0xE6), LV_PART_MAIN | LV_STATE_DEFAULT);lv_obj_set_style_bg_opa(ui_CompageBackBtn, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT); // 设置背景不透明lv_obj_t * btnlabel = lv_label_create(ui_CompageBackBtn);lv_label_set_text(btnlabel, LV_SYMBOL_BACKSPACE);lv_obj_set_style_text_font(btnlabel, &lv_font_montserrat_24, 0);lv_obj_center(btnlabel);
3.2.4 按钮事件回调以及计算器逻辑
#define TEXT_FULL 100StrStack_t CalStr;
NumStack_t NumStack;
SymStack_t SymStack;
extern lv_obj_t * ui_CompageBackBtn;static void my_event_handle(lv_event_t* e)
{lv_event_code_t code = lv_event_get_code(e);lv_obj_t * obj = lv_event_get_target(e);if (code == LV_EVENT_DRAW_PART_BEGIN){lv_obj_draw_part_dsc_t * dsc = lv_event_get_param(e);if (dsc->id == 3 || dsc->id == 7 || dsc->id == 11 || dsc->id == 14 || dsc->id == 15){dsc->rect_dsc->radius = LV_RADIUS_CIRCLE;if (lv_btnmatrix_get_selected_btn(obj) == dsc->id){dsc->rect_dsc->bg_color = lv_palette_darken(LV_PALETTE_BLUE, 3);// lv_btnmatrix_set_selected_btn(ui_CompageBtnM, NULL);}else{dsc->rect_dsc->bg_color = lv_palette_main(LV_PALETTE_BLUE);}}}if (code == LV_EVENT_DRAW_PART_END){lv_obj_draw_part_dsc_t * dsc = lv_event_get_param(e);}if (code == LV_EVENT_VALUE_CHANGED){uint16_t btn_id = lv_btnmatrix_get_selected_btn(obj); // 获取当前选中的按键的idconst char * txt = lv_btnmatrix_get_btn_text(obj, btn_id); // 获取当前按键的文本if (txt != NULL){if (guider_ui.screen_ta_1 != NULL){if (lv_textarea_get_cursor_pos(guider_ui.screen_ta_1) <= TEXT_FULL){lv_textarea_add_text(guider_ui.screen_ta_1, txt); // 文本框追加字符switch (btn_id){case 0:strput(&CalStr, '1');break;case 1:strput(&CalStr, '2');break;case 2:strput(&CalStr, '3');break;case 3:strput(&CalStr, '+');break;case 4:strput(&CalStr, '4');break;case 5:strput(&CalStr, '5');break;case 6:strput(&CalStr, '6');break;case 7:strput(&CalStr, '-');break;case 8:strput(&CalStr, '7');break;case 9:strput(&CalStr, '8');break;case 10:strput(&CalStr, '9');break;case 11:strput(&CalStr, '*');break;case 12:strput(&CalStr, '.');break;case 13:strput(&CalStr, '0');break;case 14:strput(&CalStr, '=');lv_textarea_add_text(guider_ui.screen_ta_1, "\n");strput(&CalStr, '\n');break;case 15:strput(&CalStr, '/');break;}}}}if (lv_btnmatrix_get_selected_btn(obj) == 14){// calculateif (StrCalculate(CalStr.strque, &NumStack, &SymStack)){lv_textarea_add_text(guider_ui.screen_ta_1, "erro");}else{char strout[10];if (isIntNumber(NumStack.data[NumStack.Top_Point - 1])){sprintf(strout, "%.0f", NumStack.data[NumStack.Top_Point - 1]);}else{sprintf(strout, "%.4f", NumStack.data[NumStack.Top_Point - 1]);}lv_textarea_add_text(guider_ui.screen_ta_1, strout);}strclear(&CalStr);lv_obj_clear_flag(guider_ui.screen_btnm_1, LV_OBJ_FLAG_CLICKABLE);}}
}void ui_CompageBackBtn_event_cb(lv_event_t * e)
{lv_event_code_t code = lv_event_get_code(e);lv_obj_t * obj = lv_event_get_target(e);if(code == LV_EVENT_CLICKED){if (guider_ui.screen_ta_1 != NULL){if(!strstack_isEmpty(&CalStr)){lv_textarea_del_char(guider_ui.screen_ta_1);strdel(&CalStr);}else{int i = 0;for (i = 0; i < (TEXT_FULL*2); i++){lv_textarea_del_char(guider_ui.screen_ta_1);}lv_obj_add_flag(guider_ui.screen_ta_1,LV_OBJ_FLAG_CLICKABLE);}}}if(code == LV_EVENT_LONG_PRESSED){if (guider_ui.screen_ta_1 != NULL){if(!strstack_isEmpty(&CalStr)){strclear(&CalStr);int i = 0;for (i = 0; i < (TEXT_FULL*2); i++){lv_textarea_del_char(guider_ui.screen_ta_1);}}lv_obj_add_flag(guider_ui.screen_btnm_1,LV_OBJ_FLAG_CLICKABLE);}}
}void custom_init(lv_ui *ui)
{strclear(&CalStr);NumStackClear(&NumStack);SymStackClear(&SymStack);lv_obj_add_event_cb(ui->screen_btnm_1, my_event_handle, LV_EVENT_ALL, ui);lv_obj_add_event_cb(ui_CompageBackBtn, ui_CompageBackBtn_event_cb, LV_EVENT_ALL, NULL);}
四、总结
这个计算器涉及了多个部件以及计算器管理的算法,个人觉得非常有意思,大家可以跟着讲解来进行复刻,需要模拟器工程的,在评论区留下邮箱即可。