触屏LCD菜单:链表+交互设计全解析
·
实现具有触屏功能的复杂 LCD 菜单时,链表结构的灵活性与触屏的直观交互结合,能构建层级丰富、操作便捷的界面。以下是详细实现方法,包含链表设计、触屏坐标映射、交互逻辑及优化技巧:
一、核心设计思路
- 链表结构:用双向链表 + 树形结构存储菜单,支持动态增删节点,适应复杂层级(如 “系统设置→显示→背光→亮度等级”)。
- 触屏交互:为每个菜单项分配矩形触摸区域,通过坐标检测判断用户操作(点击菜单项 / 返回按钮)。
- 视觉反馈:触摸时高亮显示选中项,执行操作后实时刷新界面,提升交互体验。
二、数据结构定义(链表 + 触摸区域)
#include "lcd.h"
#include "touch.h"
#include <stdlib.h>
#include <string.h>
// 菜单功能函数指针(指向具体业务逻辑)
typedef void (*MenuFunc)(void);
// 触摸区域结构体(定义菜单项在屏幕上的可点击范围)
typedef struct {
uint16_t x0; // 左上角x坐标
uint16_t y0; // 左上角y坐标
uint16_t x1; // 右下角x坐标
uint16_t y1; // 右下角y坐标
} TouchArea;
// 菜单节点结构体(链表核心)
typedef struct MenuNode {
char* name; // 菜单项名称(如"背光调节")
struct MenuNode* parent; // 父节点(上级菜单)
struct MenuNode* child; // 子节点(下级菜单首项)
struct MenuNode* prev; // 同级前节点(上一项)
struct MenuNode* next; // 同级后节点(下一项)
MenuFunc func; // 点击后执行的函数(无功能则为NULL)
TouchArea area; // 触摸区域
uint16_t normalColor; // 常态背景色
uint16_t pressColor; // 触摸时背景色
} MenuNode;
结构说明:
- 链表通过
parent(父子)、prev/next(同级)指针形成树形结构,支持无限层级。 TouchArea定义每个菜单项的可点击范围,用于判断触屏坐标是否落在该区域内。- 颜色属性用于触摸时的视觉反馈(如常态灰色→触摸时蓝色)。
三、链表菜单基础操作(创建与管理)
1. 创建菜单节点
// 创建新节点(参数:名称、功能函数、常态色、触摸色)
MenuNode* createNode(const char* name, MenuFunc func,
uint16_t normalColor, uint16_t pressColor) {
MenuNode* node = (MenuNode*)malloc(sizeof(MenuNode));
if (node == NULL) return NULL; // 内存分配检查
// 复制名称(避免字符串指针失效)
node->name = (char*)malloc(strlen(name) + 1);
strcpy(node->name, name);
// 初始化指针与属性
node->func = func;
node->normalColor = normalColor;
node->pressColor = pressColor;
node->parent = node->child = node->prev = node->next = NULL;
node->area = (TouchArea){0, 0, 0, 0}; // 初始触摸区域为空
return node;
}
2. 向父节点添加子菜单
// 为父节点添加子节点(子节点追加到同级末尾)
void addChild(MenuNode* parent, MenuNode* child) {
if (parent == NULL || child == NULL) return;
child->parent = parent; // 绑定父节点
if (parent->child == NULL) {
// 父节点无子菜单,直接作为首个子节点
parent->child = child;
} else {
// 遍历到同级最后一个节点,追加新节点
MenuNode* last = parent->child;
while (last->next != NULL) {
last = last->next;
}
last->next = child;
child->prev = last; // 双向关联
}
}
四、触屏交互核心实现
1. 计算菜单项触摸区域
根据屏幕尺寸和菜单位置,动态分配触摸区域(以 240×240 TFT 屏为例):
// 计算父节点下所有子菜单的触摸区域
void calcTouchAreas(MenuNode* parent) {
if (parent == NULL || parent->child == NULL) return;
MenuNode* current = parent->child;
uint8_t index = 0; // 子菜单索引(用于计算y坐标)
const uint16_t margin = 10; // 边距
const uint16_t itemHeight = 35; // 每项高度
while (current != NULL) {
// 计算区域:左=margin,右=屏宽-margin,上=标题栏高度+margin+索引×项高
current->area = (TouchArea){
margin,
50 + margin + index * itemHeight, // 标题栏占50像素
LCD_WIDTH - margin,
50 + margin + (index + 1) * itemHeight - 5 // 减5避免与下一项重叠
};
current = current->next;
index++;
}
}
2. 检测触摸坐标对应的菜单项
// 根据触屏坐标查找对应的菜单项
MenuNode* findNodeByTouch(MenuNode* parent, uint16_t x, uint16_t y) {
if (parent == NULL || parent->child == NULL) return NULL;
MenuNode* current = parent->child;
while (current != NULL) {
// 判断坐标是否在当前菜单项的触摸区域内
if (x >= current->area.x0 && x <= current->area.x1 &&
y >= current->area.y0 && y <= current->area.y1) {
return current; // 找到匹配项
}
current = current->next;
}
return NULL; // 未找到
}
3. 处理触屏输入(点击 / 返回)
// 全局变量:当前选中的节点
MenuNode* currentNode = NULL;
// 处理触屏事件(返回1=成功,0=无操作)
uint8_t processTouch() {
TouchPoint tp; // 触屏坐标结构体(x,y,状态)
if (Touch_GetState(&tp) != TOUCH_PRESSED) return 0; // 未触摸
// 坐标转换(将触屏原始坐标转为LCD屏幕坐标,需校准)
tp.x = Touch_ConvertX(tp.x);
tp.y = Touch_ConvertY(tp.y);
// 获取当前菜单的父节点(确定当前层级)
MenuNode* parent = (currentNode->parent != NULL) ? currentNode->parent : currentNode;
// 检测是否点击返回按钮(固定区域:左下角80×40像素)
if (parent->parent != NULL && // 非根菜单才显示返回按钮
tp.x >= 10 && tp.x <= 90 &&
tp.y >= LCD_HEIGHT - 50 && tp.y <= LCD_HEIGHT - 10) {
currentNode = parent->parent; // 返回上一级
return 1;
}
// 检测是否点击了菜单项
MenuNode* clicked = findNodeByTouch(parent, tp.x, tp.y);
if (clicked != NULL) {
if (clicked->child != NULL) {
// 有子菜单,进入子菜单(默认选中首个子项)
currentNode = clicked->child;
} else if (clicked->func != NULL) {
// 有功能函数,执行功能(如参数设置)
clicked->func();
} else {
// 无子菜单无功能,仅选中当前项
currentNode = clicked;
}
return 1;
}
return 0;
}
五、菜单绘制与视觉反馈
1. 绘制单个菜单项(带触摸反馈)
// 绘制菜单项(参数:节点、是否被触摸)
void drawMenuItem(MenuNode* node, uint8_t isTouched) {
if (node == NULL) return;
// 绘制背景(触摸时用pressColor,否则用normalColor)
LCD_Fill(node->area.x0, node->area.y0,
node->area.x1, node->area.y1,
isTouched ? node->pressColor : node->normalColor);
// 绘制边框(黑色)
LCD_DrawRect(node->area.x0, node->area.y0,
node->area.x1, node->area.y1, 0x0000);
// 居中显示文字(白色)
LCD_ShowString_Center(node->area.x0, node->area.y0,
node->area.x1, node->area.y1,
node->name, 16, 0xFFFF);
}
2. 刷新整个菜单界面
// 刷新当前菜单界面
void refreshMenu() {
if (currentNode == NULL) return;
// 清屏(白色背景)
LCD_Clear(0xFFFF);
// 绘制标题栏(蓝色背景,显示当前菜单路径)
LCD_Fill(0, 0, LCD_WIDTH, 50, 0x001F); // 标题栏高度50像素
char title[32] = {0};
strcat(title, currentNode->parent ? currentNode->parent->name : "Main Menu");
LCD_ShowString_Center(0, 0, LCD_WIDTH, 50, title, 20, 0xFFFF); // 白色文字
// 计算触摸区域(确保与显示位置匹配)
MenuNode* parent = (currentNode->parent != NULL) ? currentNode->parent : currentNode;
calcTouchAreas(parent);
// 绘制同级所有菜单项
MenuNode* first = parent->child;
while (first != NULL) {
// 若为当前选中项,用触摸色绘制(视觉反馈)
drawMenuItem(first, first == currentNode);
first = first->next;
}
// 绘制返回按钮(非根菜单)
if (parent->parent != NULL) {
LCD_Fill(10, LCD_HEIGHT - 50, 90, LCD_HEIGHT - 10, 0xF800); // 红色背景
LCD_ShowString_Center(10, LCD_HEIGHT - 50, 90, LCD_HEIGHT - 10,
"Back", 16, 0xFFFF); // 白色文字
}
}
六、完整菜单构建示例
// 示例功能函数(如显示系统信息)
void showSystemInfo() {
LCD_Clear(0xFFFF); // 清屏
LCD_ShowString(20, 50, "System Info", 24, 0x0000);
LCD_ShowString(20, 100, "Version: 2.1", 16, 0x0000);
LCD_ShowString(20, 150, "Touch to back", 16, 0x0000);
// 等待触摸返回
TouchPoint tp;
do {
Touch_GetState(&tp);
} while (tp.state != TOUCH_PRESSED); // 检测到触摸则返回
}
// 初始化菜单系统
void initMenuSystem() {
// 创建节点(名称,功能函数,常态色,触摸色)
MenuNode* root = createNode("Root", NULL, 0xE6E6, 0xC8C8); // 根节点(虚拟)
MenuNode* sysInfo = createNode("System Info", showSystemInfo, 0xE6E6, 0xC8C8);
MenuNode* display = createNode("Display", NULL, 0xE6E6, 0xC8C8);
MenuNode* backlight = createNode("Backlight", NULL, 0xE6E6, 0xC8C8);
MenuNode* brightHigh = createNode("High", NULL, 0xE6E6, 0xC8C8);
MenuNode* brightLow = createNode("Low", NULL, 0xE6E6, 0xC8C8);
// 构建菜单层级
addChild(root, sysInfo); // 根菜单→系统信息
addChild(root, display); // 根菜单→显示设置
addChild(display, backlight); // 显示设置→背光调节
addChild(backlight, brightHigh); // 背光调节→高亮度
addChild(backlight, brightLow); // 背光调节→低亮度
// 初始选中根菜单的第一个子项
currentNode = root->child;
// 初始化硬件
LCD_Init();
Touch_Init();
Touch_Calibrate(); // 触屏校准(首次使用需执行)
// 刷新初始界面
refreshMenu();
}
// 主循环
void mainLoop() {
while (1) {
if (processTouch()) { // 检测到有效触摸
refreshMenu(); // 刷新界面
}
// 其他任务(如传感器数据采集)
}
}
七、关键优化技巧
- 触屏校准:通过
Touch_Calibrate()生成坐标转换参数,解决触屏与 LCD 坐标偏移问题(存储校准参数到 EEPROM,避免每次上电校准)。 - 滑动翻页:当菜单项超过屏幕显示数量时,通过
scrollOffset变量控制显示范围,触摸屏幕上下边缘触发滑动。uint8_t scrollOffset = 0; // 滚动偏移量 // 下滑时:if (tp.y > LCD_HEIGHT - 50) scrollOffset++; - 触摸防抖:在
processTouch()中添加延时检测,避免误触(如连续 2 次检测到触摸才确认有效)。 - 动态加载:对层级极深的菜单,使用
loadChildMenu()函数动态创建子节点,减少初始内存占用。 - 图标支持:扩展
MenuNode结构体,增加icon成员,在菜单项左侧绘制图标(需 LCD 支持图片显示)。
总结
该方案通过链表实现菜单的灵活层级管理,结合触屏区域映射实现直观交互,核心优势在于:
- 动态扩展性:支持运行时增删菜单项(如根据设备状态显示不同选项)。
- 精准交互:每个菜单项有独立触摸区域,配合视觉反馈提升操作体验。
- 易于维护:驱动层(LCD / 触屏)与逻辑层分离,更换硬件时只需修改驱动接口。
适用于智能设备、工业控制终端等需要复杂菜单交互的场景,可根据屏幕尺寸和功能需求调整触摸区域和视觉样式。
更多推荐



所有评论(0)