实现具有触屏功能的复杂 LCD 菜单时,链表结构的灵活性与触屏的直观交互结合,能构建层级丰富、操作便捷的界面。以下是详细实现方法,包含链表设计、触屏坐标映射、交互逻辑及优化技巧:

一、核心设计思路

  1. 链表结构:用双向链表 + 树形结构存储菜单,支持动态增删节点,适应复杂层级(如 “系统设置→显示→背光→亮度等级”)。
  2. 触屏交互:为每个菜单项分配矩形触摸区域,通过坐标检测判断用户操作(点击菜单项 / 返回按钮)。
  3. 视觉反馈:触摸时高亮显示选中项,执行操作后实时刷新界面,提升交互体验。

二、数据结构定义(链表 + 触摸区域)

#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();     // 刷新界面
        }
        // 其他任务(如传感器数据采集)
    }
}

七、关键优化技巧

  1. 触屏校准:通过Touch_Calibrate()生成坐标转换参数,解决触屏与 LCD 坐标偏移问题(存储校准参数到 EEPROM,避免每次上电校准)。
  2. 滑动翻页:当菜单项超过屏幕显示数量时,通过scrollOffset变量控制显示范围,触摸屏幕上下边缘触发滑动。
    uint8_t scrollOffset = 0;  // 滚动偏移量
    // 下滑时:if (tp.y > LCD_HEIGHT - 50) scrollOffset++;
    
  3. 触摸防抖:在processTouch()中添加延时检测,避免误触(如连续 2 次检测到触摸才确认有效)。
  4. 动态加载:对层级极深的菜单,使用loadChildMenu()函数动态创建子节点,减少初始内存占用。
  5. 图标支持:扩展MenuNode结构体,增加icon成员,在菜单项左侧绘制图标(需 LCD 支持图片显示)。

总结

该方案通过链表实现菜单的灵活层级管理,结合触屏区域映射实现直观交互,核心优势在于:

  • 动态扩展性:支持运行时增删菜单项(如根据设备状态显示不同选项)。
  • 精准交互:每个菜单项有独立触摸区域,配合视觉反馈提升操作体验。
  • 易于维护:驱动层(LCD / 触屏)与逻辑层分离,更换硬件时只需修改驱动接口。

适用于智能设备、工业控制终端等需要复杂菜单交互的场景,可根据屏幕尺寸和功能需求调整触摸区域和视觉样式。

更多推荐