diff --git a/CMakeLists.txt b/CMakeLists.txt index 6652249..cc7543c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,13 +10,20 @@ if(MSVC) add_compile_options(/utf-8) endif() +# 可选:启用 DEBUG 宏,用于条件编译输出调试信息(cmake -DENABLE_DEBUG=ON ..) +option(ENABLE_DEBUG "Enable debug printf output" OFF) +if(ENABLE_DEBUG) + add_compile_definitions(DEBUG) +endif() + add_executable(DTU-HMI src/main.c + src/thread_utils.c + src/remoteDisplay.c src/Drv/menu.c src/Drv/lcd.c src/Drv/Ascii.c - src/thread_utils.c - src/remoteDisplay.c + src/Drv/display.c src/TCP/tcp.c ) diff --git a/README.md b/README.md index 12b647b..a6b4340 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,375 @@ -# PC_HMI – 本地 HMI 菜单逻辑的 PC 端模拟 +# DTU-HMI -本目录提供一个 **纯 C 语言的控制台程序**,在 PC 上模拟嵌入式 HMI 的菜单交互逻辑,方便调试和理解 `Menu_Route` / `Menu_Show_Proc` 的行为。 +PC 端 HMI 菜单逻辑模拟程序,纯 C 实现,支持菜单树、LCD 显示、TCP 远程显示(RemoDispBus 协议)及 UTF-8 汉字库。 -## 环境搭建 +--- -### 1. 安装 CMake - -1. 访问 CMake 官网下载页:[`https://cmake.org/download/`](https://cmake.org/download/) -2. 下载适用于 Windows 的安装包(通常为 `cmake-*-windows-x86_64.msi`),安装时**勾选将 CMake 添加到 PATH**。 -3. 安装完成后打开终端(PowerShell / CMD),执行: - - ```powershell - cmake --version - ``` - - 能看到版本信息即表示安装成功。 - -### 2. 编译器与编码说明 - -- Windows 下默认使用 **Visual Studio / MSVC** 工具链进行编译。 -- 源文件采用 **UTF‑8 编码并包含中文注释**,`CMakeLists.txt` 中已经为 MSVC 打开 `/utf-8` 选项,无需额外配置即可正常编译。 - - -## 目录结构 - -- `CMakeLists.txt` – CMake 构建配置 -- `include/menu.h` – 菜单数据结构与接口声明 -- `include/tcp.h` – TCP 通信接口(客户端/服务端) -- `src/menu.c` – 简单菜单树与菜单调度逻辑(PC 版) -- `src/main.c` – 主程序入口,从键盘读取按键并驱动菜单 -- `src/tcp.c` – TCP 实现(Windows Winsock / Linux socket) - -## 构建步骤 - -### 1. Windows 示例(当前工程默认方式) - -在仓库根目录下执行(注意路径中有空格时要加引号): +## 快速开始 ```powershell mkdir build -cd "D:\Code\DTU 程序\DTU-HMI\build" +cd build cmake .. cmake --build . ``` -### 2. 通用(Linux / macOS 等) +生成可执行文件 `DTU-HMI.exe`(Windows)或 `DTU-HMI`(Linux)。 + +--- + +## 目录结构 + +``` +DTU-HMI/ +├── CMakeLists.txt +├── gen_utf8_hz12.py # 12×12 UTF-8 汉字库生成脚本 +├── remo_disp_server.py # 远程显示 Python 服务端 +├── include/ +│ └── types.h +├── src/ +│ ├── main.c +│ ├── thread_utils.c/h +│ ├── remoteDisplay.c/h +│ ├── TCP/tcp.c, tcp.h +│ └── Drv/ +│ ├── menu.c/h +│ ├── display.c/h +│ ├── lcd.c/h +│ ├── Ascii.c/h +│ └── utf8_hz12_data.c/h # 由脚本生成 +└── build/ +``` + +--- + +## 环境要求 + +- **CMake** 3.10+ +- **编译器**:Windows 默认 MSVC;Linux 需 GCC/Clang +- **编码**:源文件 UTF-8,CMake 已配置 MSVC `/utf-8` + +--- + +## 构建步骤 + +### Windows + +```powershell +mkdir build +cd build +cmake .. +cmake --build . +``` + +### Linux / macOS ```bash mkdir build cd build -cmake ../ +cmake .. cmake --build . ``` -生成的可执行文件名为: - -```text -pc_hmi # Windows / Linux 下均可 -``` +--- ## TCP 通信 +程序监听端口 **7003**(RemoDispBus 默认)。使用 `remo_disp_server.py` 连接后可实时查看 LCD 显存画面。协议支持 `CMD_INIT`、`CMD_LCDMEM`、`CMD_KEY`、`CMD_KEEPLIVE`。 +--- + +## 菜单系统 + +### 1. 概述 + +菜单由**静态表** `g_tMenuModelTab` 定义,经 `Menu_Main_Creat_01` 构建为**可遍历树** `g_tMenuItem[]`。每个结点有 `ptHigher/ptLower/ptBefore/ptBehind` 四个指针,同级首尾成环。 + +### 2. 数据结构 + +#### 2.1 静态菜单定义(tagMenuModel) + +```c +typedef struct +{ + uint8_t byClass; // 菜单分级标志 0/1/2/3 + uint8_t byName[50]; // 菜单字符串 + uint8_t byTip[50]; // 菜单提示文本 + uint8_t byAttrib; // 菜单属性 + uint16_t wPassword; // 访问密码,0x0000 表示无密码 + uint16_t wPara; // 菜单执行函数参数 + FUNCPTR pfnWinProc; // 界面执行函数指针 +} tagMenuModel, *tagPMenuModel; + +/* 注意:表定义时顺序不能乱,需从 0 级开始,一级一级按顺序写入 */ +const tagMenuModel g_tMenuModelTab[] = { ... }; +``` + +#### 2.2 菜单遍历树(tagMenuItem) + +```c +/* 每个菜单包含:一、上下前后等级关系;二、属性与内容;三、显示坐标 */ +typedef struct _MENU_ITEM_ +{ + struct _MENU_ITEM_ *ptHigher; // 上级菜单指针 + struct _MENU_ITEM_ *ptLower; // 下级菜单指针 + struct _MENU_ITEM_ *ptBefore; // 同级上方指针 + struct _MENU_ITEM_ *ptBehind; // 同级下方指针 + + uint8_t byClass; + uint8_t byName[50]; + uint8_t byTip[50]; + uint8_t byAttrib; + uint16_t wPassword; + uint16_t wPara; + FUNCPTR pfnWinProc; + + uint16_t wPos, wNum; + uint16_t wSPosX, wSPosY, wEPosX, wEPosY; +} tagMenuItem, *tagPMenuItem; + +tagMenuItem g_tMenuItem[300]; /* 所有菜单存储于此数组 */ +``` + +### 3. 菜单构建示例 + +#### 3.1 静态表(g_tMenuModelTab) + +```c +const tagMenuModel g_tMenuModelTab[] = +{ + { 0, " ", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo }, + { 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, + { 2, "直流量", "查看遥测直流量", EN_MEA_DC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, + { 2, "遥信量", "查看遥信开入量", EN_INPUT_RLY_ALL, 0x0000, EN_INPUT_0, (FUNCPTR)MenuProc_See_Input }, + { 1, "参数定值", "保护参数查看与修改", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "整定", "整定装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 3, "参数", "查看设备参数定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_BASIC, (FUNCPTR)MenuProc_Set_Value }, + { 3, "定值", "设置装置数值定值", EN_FIGURE_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, + { 3, "控制字", "设置装置控制字", EN_SOFT_SET, CN_USER_PWD, EN_SIDE_DEF, (FUNCPTR)MenuProc_Set_Value }, + { 3, "软压板", "设置软压板", 0, CN_USER_PWD, EN_SOFT_PRO, (FUNCPTR)MenuProc_Set_Soft }, + { 2, "查看", "查看装置保护参数", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 3, "参数", "设置设备参数定值", EN_FIGURE_SET, 0x0000, EN_SIDE_BASIC, (FUNCPTR)MenuProc_See_Set }, + { 3, "定值", "查看数值型定值", EN_FIGURE_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, + { 3, "控制字", "查看控制字定值", EN_SOFT_SET, 0x0000, EN_SIDE_DEF, (FUNCPTR)MenuProc_See_Set }, + { 3, "软压板", "查看软压板", 0, 0x0000, EN_SOFT_PRO, (FUNCPTR)MenuProc_See_Soft }, + { 1, "三遥设置", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "遥测死区", "设置遥测量死区门槛", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetSqValue }, + { 2, "遥测系数", "设置遥测量微调系数", EN_MEA_ADJ, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YC_SetAdjCoe }, + { 2, "遥信类型", "设置遥信类型", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetCommType }, + { 2, "遥信防抖", "设置遥信防抖时间", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetWidth }, + { 2, "双点遥信", "设置双点遥信虚端子", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_YX_SetTwin }, + { 1, "装置维护", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "时钟设置", "设置系统时钟", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_Time }, + { 2, "强制复归", "可复归未返回事件", EN_REV_FORCE, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_RevEvent }, + { 2, "手动录波", "启动手动录波", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ManualWave }, + { 2, "清除记录", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ClrRec }, + { 1, "通讯参数", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "通讯设置", "外部通讯设置", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_ComPara }, + { 2, "网口设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditIP }, + { 2, "SNTP设置", "", 0, CN_USER_PWD, 0, (FUNCPTR)MenuProc_Cfg_EditSntp }, + { 1, "记录查询", "查看各种装置记录", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "SOE记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecSOE }, + { 2, "事故记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAct }, + { 2, "操作记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecOpt }, + { 2, "保护告警", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecAlm }, + { 2, "保护启动", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecStart }, + { 2, "遥控记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecYK }, + { 2, "自检记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecChk }, + { 2, "运行记录", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecRun }, + { 2, "运行报告", "", 0, 0x0000, 0, (FUNCPTR)MenuProc_See_RecFault }, + { 0, "厂家设置", "设置装置相关参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, + { 1, "元件配置", "配置元件配置", 0, CN_COP_PWD, EN_FACTORY_PASSWORD,(FUNCPTR)MenuProc_Cfg_CellConf }, + { 1, "恢复默认", "恢复默认元件定值参数", 0, CN_COP_PWD, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "全部恢复", "全部参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_AllInf_Default }, + { 2, "默认参数", "当前参数恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Para_Default }, + { 2, "默认定值", "当前定值区恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Set_Default }, + { 2, "软压板", "当前软压板恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Resume_Soft }, + { 2, "元件配置", "元件配置恢复默认", 0, CN_COP_PWD, EN_NO_USER_PWD, (FUNCPTR)MenuProc_Cfg_CellDef }, + { 1, "交流显示", "交流显示方式设置", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Cfg_ShowAnaType }, + { 1, "装置调试", "调试装置", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 2, "虚拟遥信", "设置虚拟遥信值", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuYX }, + { 2, "交流虚遥测", "设置虚拟交流遥测值", EN_MEA_AC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, + { 2, "直流虚遥测", "设置虚拟直流遥测值", EN_MEA_DC, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, + { 2, "电度虚遥测", "设置虚拟电度遥测值", EN_MEA_POWER, CN_COP_PWD, EN_ANA_0, (FUNCPTR)MenuProc_Dbg_XuYC }, + { 2, "动作虚事件", "设置虚拟动作事件", EN_ACT_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, + { 2, "告警虚事件", "设置虚拟告警事件", EN_ALM_REC, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_XuEvent }, + { 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay }, + { 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay }, + { 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard }, +}; +``` + +#### 3.2 解析后的树形结构 + +根据上表,`Menu_Main_Creat_01` 解析后的菜单树为: + +```text +0 级目录 { + 1.装置信息 + 1.实时数据 { + 2.交流量 + 2.直流量 + 2.遥信量 + } + 1.参数定值 { + 2.整定 { + 3.参数 + 3.定值 + 3.控制字 + 3.软压板 + } + 2.查看 { + 3.参数 + 3.定值 + 3.控制字 + 3.软压板 + } + } + 1.三遥设置 { + 2.遥测死区 + 2.遥测系数 + 2.遥信类型 + 2.遥信防抖 + 2.双点遥信 + } + 1.装置维护 { + 2.时钟设置 + 2.强制复归 + 2.手动录波 + 2.清除记录 + 2.通讯参数 + 2.通讯设置 + 2.网口设置 + 2.SNTP设置 + } + 1.记录查询 { + 2.SOE记录 + 2.事故记录 + 2.操作记录 + 2.保护告警 + 2.保护启动 + 2.遥控记录 + 2.自检记录 + 2.运行记录 + 2.运行报告 + } +} + +0.厂家设置 { + 1.元件配置 + 1.恢复默认 { + 2.全部恢复 + 2.默认参数 + 2.默认定值 + 2.软压板 + 2.元件配置 + } + 1.交流显示 + 1.装置调试 { + 2.虚拟遥信 + 2.交流虚遥测 + 2.直流虚遥测 + 2.电度虚遥测 + 2.动作虚事件 + 2.告警虚事件 + 2.动作出口 + 2.信号出口 + } + 1.版本信息 +} +``` + +#### 3.3 单例解析示例:直流量 + +以 `{2,"直流量","查看遥测直流量",EN_MEA_DC,0x0000,EN_ANA_0,(FUNCPTR)MenuProc_See_YC}` 为例,构建后该菜单项的指针与属性为: + +```text +{ + ptHigher = 1.实时数据; + ptLower = NULL; + ptBefore = 2.交流量; + ptBehind = 2.遥信量; + + byClass = 2; + byName = 直流量; + byTip = 查看遥测直流量; + byAttrib = EN_MEA_DC; + wPassword = 0x0000; + wPara = 0; + pfnWinProc = MenuProc_See_YC; +} +``` + +#### 3.4 同级首尾成环 + +同级菜单中,首尾通过 `ptBefore/ptBehind` 相连形成环。例如 2 级子菜单: + +```text +2.虚拟遥信 +2.交流虚遥测 +2.直流虚遥测 +2.电度虚遥测 +2.动作虚事件 +2.告警虚事件 +2.动作出口 +2.信号出口 +``` + +其中: + +- `2.虚拟遥信` 的 `ptBefore` 指向 `2.信号出口`(首的前一个是尾) +- `2.信号出口` 的 `ptBehind` 指向 `2.虚拟遥信`(尾的后一个是首) + +### 4. 构建流程(Menu_Main_Creat_01) + +按 `g_tMenuModelTab` 的定义顺序逐项处理,通过比较 `byCurClass` 与 `byNextClass` 设置每个菜单的层级指针。 + +#### 情况 1:byCurClass < byNextClass(下一项更深,进入子菜单) + +```text + ptCurrent ptNextNode + (当前) (下一项,更深一级) + │ │ + │ ptLower ──────────────────►│ + │◄───────────────── ptHigher │ + │ │ + 该级尾不变 新一级的 首=尾=ptNextNode +``` + +#### 情况 2:byCurClass == byNextClass(同级,并列) + +```text + ptCurrent ─── ptBehind ──► ptNextNode + │ │ + │ ptBefore ◄───────────┤ + │ │ + └──── ptHigher (同) ────────┘ + 该级 尾 更新为 ptNextNode +``` + +#### 情况 3:byCurClass > byNextClass(下一项更浅,回到上层) + +```text + ... byCurClass 级 ... byNextClass 级 ... + ptLast[byNextClass] 已存在 + │ + │ ptBehind ──► ptNextNode(新) + │ │ + │◄── ptBefore ────┤ + │ ptHigher = 该级尾的 ptHigher + 同时:从 byCurClass 到 byNextClass+1 各级首尾成环 + ptLast[级]──►ptFirst[级],ptFirst[级]──►ptLast[级] +``` + +#### 最后:各级首尾成环 + +表遍历完后,从 **0 级到当前结点所在级**,把该级首尾连成环: + +```text + ptFirst[级] ◄──────────────► ptLast[级] + │ │ + └──── ptBehind ───────────────┘ + ◄──────── ptBefore ────────────┘ +``` diff --git a/src/Drv/display.c b/src/Drv/display.c index c95c649..f0738f8 100644 --- a/src/Drv/display.c +++ b/src/Drv/display.c @@ -79,20 +79,12 @@ enum _STR_FLAG_ { } enumStrType; -// 主菜单定义 -//============================================================ -// byMenuClass; 菜单分级标志(最大级数不能超过3级); -// byName[50]; 菜单字符串; -// byTip[36]; 菜单提示文本; -// byAttrib; 菜单属性,设置菜单特殊显示效果; -// wPassword; 访问密码,0x0000表示没有密码; -// wPara; 菜单执行函数参数; -// pfnWinProc; 界面执行函数指针; -// 如果CN_HAVE_HIDE_MENU 为 TRUE ,0 级菜单的最后一组菜单为隐藏菜单 -//=========================================================== +/* 静态菜单表 +注意:该表定义时顺序不能乱,需要从 0 级开始,一级一经按顺序写入 +*/ const tagMenuModel g_tMenuModelTab[] = { - { 0, " ", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, + { 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo }, { 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc }, { 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC }, @@ -153,4 +145,5 @@ const tagMenuModel g_tMenuModelTab[] = { 2, "动作出口", "进入此菜单保护退出", EN_OUTPUT_TRIP, CN_COP_PWD, EN_INPUT_0, (FUNCPTR)MenuProc_Dbg_Relay }, { 2, "信号出口", "进入此菜单保护退出", EN_OUTPUT_SIGN, CN_COP_PWD, 0, (FUNCPTR)MenuProc_Dbg_Relay }, { 1, "版本信息", "查看板件版本信息", 0, CN_COP_PWD, 0, (FUNCPTR)MenuProc_See_VersionBoard }, -}; \ No newline at end of file +}; +const uint32_t MENU_MAX_ITEM = sizeof(g_tMenuModelTab) / sizeof(tagMenuModel); \ No newline at end of file diff --git a/src/Drv/display.h b/src/Drv/display.h index 021d307..d49a08a 100644 --- a/src/Drv/display.h +++ b/src/Drv/display.h @@ -4,7 +4,7 @@ #include "../../include/types.h" -// 菜单常量表定义 +/* 静态菜定义需要的属性 */ typedef struct { uint8_t byClass; // 菜单分级标志; @@ -16,6 +16,12 @@ typedef struct FUNCPTR pfnWinProc; // 界面执行函数指针; }tagMenuModel,*tagPMenuModel; +extern const tagMenuModel g_tMenuModelTab[]; +extern const uint32_t MENU_MAX_ITEM; + + + + //菜单密码设置 //============================================================================== diff --git a/src/Drv/lcd.c b/src/Drv/lcd.c index 5d0571b..c10125a 100644 --- a/src/Drv/lcd.c +++ b/src/Drv/lcd.c @@ -1,7 +1,7 @@ #include "lcd.h" #include #include "ascii.h" - +#include tagScreenControl g_tCVsr; // 当前界面结构指针 tagDspAttrib g_tDspAttrib; // 显示属性 @@ -23,26 +23,6 @@ void Lcd_Init(void) g_tCVsr.wASCIIFontHeight = 12; g_tDspAttrib.rowSpace = 2; } - -/* 填充矩形区域 */ -void Lcd_FillRect(uint16_t lx, uint16_t ty, uint16_t rx, uint16_t by, uint32_t dwRGB) -{ - uint16_t h, l, err; /* 行/列循环变量;CheckPosi 返回值(本函数未使用) */ - uint32_t dwRRGB; /* 保存进入前的字体色,用于退出时恢复 */ - - dwRRGB = g_tCVsr.wFontColor; - g_tCVsr.wFontColor = dwRGB; - - /* 按行扫描矩形区域,逐点用当前颜色绘制(__Lcd_PixelFT 使用当前字体色作为像素色) */ - for ( h = ty; h <= by; h++ ) - { - for ( l = lx; l <= rx; l++ ) - { - Lcd_SetPixel(l, h, g_tCVsr.wFontColor); - } - } -} - void Lcd_SetPixel(uint16_t x, uint16_t y, uint16_t color) { if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y) @@ -65,8 +45,287 @@ uint16_t Lcd_GetPixel(uint16_t x, uint16_t y) /* 一个字节一个像素点 */ return g_tCVsr.pwbyLCDMemory[y * LCD_LINE_SIZE + x]; } +/****************************************************************************** + * 函数名: Lcd_FillRect + * 功能: 在显存中用指定颜色填充一个矩形区域 + * 参数: lx - 左上角 X 坐标 (left x) + * ty - 左上角 Y 坐标 (top y) + * rx - 右下角 X 坐标 (right x) + * by - 右下角 Y 坐标 (bottom y) + * color- 填充颜色 + * 返回: 无 + * 说明: 对 [left_x, right_x] × [top_y, bottom_y] 区域内的每个像素逐点调用 Lcd_SetPixel 进行填充, + * 仅操作显存,不负责刷屏输出,由上层根据需要统一刷新。 + *****************************************************************************/ +void Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color) +{ + for(uint16_t y = top_y; y <= bottom_y; y++ ) + { + for(uint16_t x = left_x; x <= right_x; x++ ) + { + Lcd_SetPixel(x, y, color); + } + } +} +/****************************************************************************** + * 宏名: M_GuiSwap + * 功能: 交换两个整型变量的值(按位异或方式) + * 说明: 要求 a、b 为同一类型的可写左值,且 a、b 不能是同一地址 + *****************************************************************************/ +#define M_GuiSwap(a, b) { a^=b; b^=a; a^=b; } + +/****************************************************************************** + * 函数名: Lcd_LineH + * 功能: 在显存中绘制一条水平实线(可指定线宽) + * 参数: wXStart - 线段起始 X 坐标(左端) + * wXEnd - 线段结束 X 坐标(右端,若小于起始则自动交换) + * wYStart - 线段上边缘 Y 坐标 + * wWidth - 线宽(沿 Y 方向的像素高度) + * 返回: 无 + * 说明: 1. 若 wXEnd < wXStart,会先交换,保证从左向右绘制 + * 2. 实际绘制区域为 [wXStart, wXEnd) × [wYStart, wYStart + wWidth) + * 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor + *****************************************************************************/ +void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint16_t color) +{ + uint16_t wYEnd = wYStart + wWidth; /* 计算水平线在 Y 方向的结束位置 = 起始 Y + 线宽 */ + + if( wXEnd < wXStart ) /* 若结束 X 小于起始 X,说明调用参数顺序反了,需要交换 */ + { + M_GuiSwap(wXEnd, wXStart); /* 使用异或交换宏,将 wXStart 与 wXEnd 的值互换 */ + } + for(uint16_t x = wXStart; x < wXEnd; x++ ) /* 从左到右,遍历线段覆盖的每一列 X 坐标 */ + { + for(uint16_t y = wYStart; y < wYEnd; y++ ) /* 在当前 X 列内,从上到下按线宽遍历每个像素行 */ + { + Lcd_SetPixel(x, y, color); /* 将当前像素点设置为当前字体颜色,实现实心线绘制 */ + } + } +} +/****************************************************************************** + * 函数名: Lcd_LineV + * 功能: 在显存中绘制一条垂直实线(可指定线宽) + * 参数: wYStart - 线段起始 Y 坐标(上端) + * wYEnd - 线段结束 Y 坐标(下端,若小于起始则自动交换) + * wXStart - 线段左边缘 X 坐标 + * wWidth - 线宽(沿 X 方向的像素宽度) + * 返回: 无 + * 说明: 1. 若 wYEnd < wYStart,会先交换,保证从上向下绘制 + * 2. 实际绘制区域为 [wXStart, wXStart + wWidth) × [wYStart, wYEnd) + * 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor + *****************************************************************************/ +void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint16_t color) +{ + uint16_t wXEnd = wXStart + wWidth; + + if( wXEnd < wXStart ) /* 若结束 X 小于起始 X,说明调用参数顺序反了,需要交换 */ + { + M_GuiSwap(wYEnd, wYStart); /* 使用异或交换宏,将 wYStart 与 wYEnd 的值互换 */ + } + + for(uint16_t x = wXStart; x < wXEnd; x++) /* 从左到右,遍历线段覆盖的每一列 X 坐标 */ + { + for(uint16_t y = wYStart; y < wYEnd; y++) /* 从上到下,遍历线段覆盖的每一行 Y 坐标 */ + { + Lcd_SetPixel(x, y, color); /* 将当前像素点设置为当前字体颜色,实现实心线绘制 */ + } + } +} +/****************************************************************************** + * 函数名: Lcd_Line + * 功能: 在显存中绘制一条任意斜率的直线,并支持指定线宽 + * 参数: wXstart - 起点 X 坐标 + * wYstart - 起点 Y 坐标 + * wXend - 终点 X 坐标 + * wYend - 终点 Y 坐标 + * wWidth - 线宽(像素),以线段中轴为中心向两侧扩展 + * 返回: 无 + * 说明: 1. 对水平线/垂直线分别调用 Lcd_LineH / Lcd_LineV 进行优化绘制 + * 2. 其它情况使用类 Bresenham 算法,从两端向中间对称绘制,并按线宽加粗 + * 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor + *****************************************************************************/ +void Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint16_t color) +{ + uint16_t wDX; /* X 方向差值(水平偏移量的绝对值) */ + uint16_t wDY; /* Y 方向差值(垂直偏移量的绝对值) */ + uint16_t wSignY; /* 纵向增量符号(+1 或 -1),决定向上/向下绘制 */ + uint16_t wSignX; /* 横向增量符号(+1 或 -1),决定向左/向右绘制 */ + uint16_t wDecision; /* 误差累积量,类似 Bresenham 算法中的判别值 */ + uint16_t wCurx, wCury, wNextx, wNexty, wPY, wPX; /* 当前端点、对称端点以及循环中使用的临时坐标 */ + + /* 若起点和终点 Y 相同,说明是水平直线,直接调用水平画线函数即可 */ + if( wYstart == wYend ) + { + Lcd_LineH(wXstart, wXend, wYstart, wWidth, color); + return; + } + /* 若起点和终点 X 相同,说明是垂直直线,直接调用垂直画线函数即可 */ + if( wXstart == wXend ) + { + Lcd_LineV(wYstart, wYend, wXstart, wWidth, color); + return; + } + + /* 计算水平和垂直方向的距离,后续根据 wDX / wDY 决定主增量方向 */ + wDX = abs(wXstart - wXend); + wDY = abs(wYstart - wYend); + /* 为了统一从“左到右 / 上到下”的绘制方向,对某些象限的线段进行起终点交换 */ + if (((wDX >= wDY && (wXstart > wXend)) || /* 以 X 为主增量,且起点在右侧,则交换 */ + ((wDY > wDX) && (wYstart > wYend)))) /* 以 Y 为主增量,且起点在下侧,则交换 */ + { + M_GuiSwap(wXend, wXstart); /* 交换 X 坐标,使起点在左/上 */ + M_GuiSwap(wYend, wYstart); /* 交换 Y 坐标,对应调整终点 */ + } + /* 计算每一步在 Y 方向上的符号:向下(+1) 或向上(-1) */ + wSignY = (wYend - wYstart) / wDY; + /* 计算每一步在 X 方向上的符号:向右(+1) 或向左(-1) */ + wSignX = (wXend - wXstart) / wDX; + + /* 情况一:X 方向偏移大于等于 Y 方向(线更“横向”) */ + if (wDX >= wDY) + { + wCurx = wXstart; /* 当前点 X 从起点开始 */ + wCury = wYstart; /* 当前点 Y 从起点开始 */ + wNextx = wXend; /* 对称点 X 从终点开始(用于加粗两端) */ + wNexty = wYend; /* 对称点 Y 从终点开始 */ + wDecision = (wDX >> 1); /* 初始化判别值为一半的 wDX */ + for (; wCurx <= wNextx; ) /* 从两端向中间画,直到两个端点相遇或交叉 */ + { + /* 累积的误差超过一条“格子宽度”时,说明需要在 Y 方向跨一格 */ + if (wDecision >= wDX) + { + wDecision -= wDX; /* 误差回退一个 wDX,保持在合理范围内 */ + wCury += wSignY; /* 当前端点 Y 沿着符号方向移动一格 */ + wNexty -= wSignY; /* 对称端点 Y 反向移动一格,保持对称 */ + } + /* 以当前端点 (wCurx, wCury) 为中心,按线宽在 Y 方向“扩粗”填充像素 */ + for (wPY = wCury - wWidth / 2; wPY <= wCury + wWidth / 2; wPY++) + { + Lcd_SetPixel(wCurx, wPY, color); + } + + /* 以对称端点 (wNextx, wNexty) 为中心,同样按线宽在 Y 方向填充,实现两端对称绘制 */ + for (wPY = wNexty - wWidth / 2; wPY <= wNexty + wWidth / 2; wPY++) + { + Lcd_SetPixel(wNextx, wPY, color); + } + wCurx++; /* 当前端点 X 向右移动一格 */ + wNextx--; /* 对称端点 X 向左移动一格 */ + wDecision += wDY; /* 增加误差值,下一轮判断是否需要在 Y 方向跨格 */ + } + } + /* 情况二:Y 方向偏移大于 X 方向(线更“竖向”) */ + else + { + wCurx = wXstart; /* 当前点 X 从起点开始 */ + wCury = wYstart; /* 当前点 Y 从起点开始 */ + wNextx = wXend; /* 对称点 X 从终点开始 */ + wNexty = wYend; /* 对称点 Y 从终点开始 */ + wDecision = (wDY >> 1); /* 初始化判别值为一半的 wDY */ + for (; wCury <= wNexty; ) /* 从两端向中间画,直到在 Y 方向相遇 */ + { + + /* 累积误差超过一条“格子高度”时,说明需要在 X 方向跨一格 */ + if (wDecision >= wDY) + { + wDecision -= wDY; /* 回退一个 wDY,避免误差过大 */ + wCurx += wSignX; /* 当前端点 X 沿符号方向移动一格 */ + wNextx -= wSignX; /* 对称端点 X 反向移动一格 */ + } + /* 以当前端点 (wCurx, wCury) 为中心,在 X 方向按线宽“扩粗”填充像素 */ + for (wPX = wCurx - wWidth / 2; wPX <= wCurx + wWidth / 2; wPX++) + { + Lcd_SetPixel(wPX, wCury, color); + } + + /* 以对称端点 (wNextx, wNexty) 为中心,同样在 X 方向扩粗填充,实现两端对称绘制 */ + for (wPX = wNextx - wWidth / 2; wPX <= wNextx + wWidth / 2; wPX++) + { + Lcd_SetPixel(wPX, wNexty, color); + } + wCury++; /* 当前端点 Y 向下移动一格 */ + wNexty--; /* 对称端点 Y 向上移动一格 */ + wDecision += wDX; /* 增加误差值,下一轮判断是否需要在 X 方向跨格 */ + } + } +} +/****************************************************************************** + * 函数名: Lcd_MeiTouPic + * 功能: 在指定 Y 位置绘制一条带左右“眉头”装饰的水平线(中间直线 + 两端斜线) + * 参数: wYStart - 中间水平线的 Y 坐标 + * wWidth - 线宽(像素),传递给 Lcd_LineH / Lcd_Line + * 返回: 无 + * 说明: 1. 中间部分为 X=16..144 的水平粗线 + * 2. 左端在 (16, wYStart) 位置向左上方连一条斜线到 (8, wYStart-8) + * 3. 右端在 (144, wYStart) 位置向右上方连一条斜线到 (152, wYStart-8) + *****************************************************************************/ +void Lcd_MeiTouPic(uint16_t wYStart, uint16_t wWidth) +{ + Lcd_LineH(16, 144, wYStart, wWidth, g_tCVsr.wFontColor); /* 中间水平粗线段 */ + Lcd_Line(8, wYStart - 8, 16, wYStart, wWidth, g_tCVsr.wFontColor);/* 左端向上的斜线“眉头” */ + Lcd_Line(144, wYStart, 152, wYStart - 8, wWidth, g_tCVsr.wFontColor);/* 右端向上的斜线“眉头” */ +} + + + +void Lcd_ButtonPush(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y) +{ + Lcd_LineH(left_x, right_x,top_y, 1, g_tCVsr.wFontColor); + Lcd_LineV(top_y, bottom_y,left_x, 1, g_tCVsr.wFontColor); + Lcd_LineH(left_x, right_x,bottom_y, 1, g_tCVsr.wFontColor); + Lcd_LineV(top_y, bottom_y,right_x, 1, g_tCVsr.wFontColor); +} +/****************************************************************************** + * 宏名: M_Max / M_Min + * 功能: 返回两个数中的较大值 / 较小值(简单三目运算宏) + *****************************************************************************/ +#define M_Max( a, b ) ( ((a) > (b)) ? (a) : (b) ) +#define M_Min( a, b ) ( ((a) < (b)) ? (a) : (b) ) + +/****************************************************************************** + * 函数名: Lcd_Invert + * 功能: 对指定矩形区域内的像素进行“反相”操作(黑变白、白变黑) + * 参数: wXstart - 矩形左上角 X 坐标 + * wYstart - 矩形左上角 Y 坐标 + * wXend - 矩形右下角 X 坐标 + * wYend - 矩形右下角 Y 坐标 + * 返回: 无 + * 说明: 1. 首先进行边界检查,防止坐标越界访问显存 + * 2. 使用 M_Min/M_Max 规范化矩形对角坐标为 (xMin,yMin)-(xMax,yMax) + * 3. 遍历矩形区域,每个像素值按位取反写回,实现反白/反显效果 + *****************************************************************************/ +void Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend) +{ + uint16_t xMin, xMax, yMin, yMax; /* 归一化后的矩形边界坐标 */ + uint8_t wColor; /* 当前像素原始色值(8bit 单色) */ + + /* 边界检查:若任意一端 X 超出屏幕宽度,则直接返回,不做处理 */ + if ((wXstart > LCD_SIZE_X) || (wXend > LCD_SIZE_X)) + return; + /* 边界检查:若任意一端 Y 超出屏幕高度,则直接返回 */ + if ((wYstart > LCD_SIZE_Y) || (wYend > LCD_SIZE_Y)) + return; + + /* 规范化矩形:xMin/xMax 为左右边界,yMin/yMax 为上下边界(不关心调用时 start/end 的大小关系) */ + xMin = M_Min(wXstart, wXend); + yMin = M_Min(wYstart, wYend); + xMax = M_Max(wXstart, wXend); + yMax = M_Max(wYstart, wYend); + + /* 双重循环遍历矩形区域内的每个像素点(不包含 xMax/yMax 边界本身) */ + for (uint16_t y = yMin; y < yMax; y++) + { + for (uint16_t x = xMin; x < xMax; x++) + { + /* 从显存中读取当前像素值,逐位取反后写回,实现黑白反转/反显效果 */ + wColor = g_tCVsr.pwbyLCDMemory[y * LCD_SIZE_X + x]; + g_tCVsr.pwbyLCDMemory[y * LCD_SIZE_X + x] = (uint8_t)~wColor; + } + } + return; +} //============================================================================== // 功能说明 : 在指定屏幕坐标处显示一个 ASCII 字符 // 设计说明 : 从 ASCII 字库中取出点阵, 按当前显示属性(正显/反显、旋转 90° 与否) diff --git a/src/Drv/lcd.h b/src/Drv/lcd.h index c1d8c48..a685684 100644 --- a/src/Drv/lcd.h +++ b/src/Drv/lcd.h @@ -50,6 +50,13 @@ typedef struct { // 显示属性数据结构 void Lcd_Init(void); void Lcd_SetPixel(uint16_t x, uint16_t y, uint16_t color); uint16_t Lcd_GetPixel(uint16_t x, uint16_t y); +void Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color); +void Lcd_MeiTouPic(uint16_t wYStart, uint16_t wWidth); +void Lcd_ButtonPush(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y); +void Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend); +void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint16_t color); +void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint16_t color); + int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString); int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString); #endif \ No newline at end of file diff --git a/src/Drv/menu.c b/src/Drv/menu.c index 857766d..5c449df 100644 --- a/src/Drv/menu.c +++ b/src/Drv/menu.c @@ -1,8 +1,9 @@ #include "menu.h" +#include #include #include #include "lcd.h" -#include "key.h" +#include "display.h" /* 简单的静态菜单树: * @@ -25,8 +26,8 @@ uint16_t wPara; // 菜单执行函数参数; FUNCPTR pfnWinProc; // 界面执行函数指针; - uint16_t wPos; // 当前菜单位置,相对于本级菜单。 - uint16_t wNum; // 下级菜单项数 + uint16_t wPos; /* 当前菜单在本级中的位置 */ + uint16_t wNum; /* 下级菜单的总项数 */ uint16_t wSPosX; // 下级菜单起始坐标 uint16_t wSPosY; // 下级菜单起始坐标 uint16_t wEPosX; // 下级菜单对角坐标 @@ -70,41 +71,731 @@ typedef struct unsigned bRes:1; }tagDspCtrl,*tagPDspCtrl; +typedef struct +{ // 菜单控制结构 + uint8_t by0LevelNum; // 0级菜单项数 + uint8_t byLeftMove; // 菜单左移长度 + uint8_t bySecPage; // 第二页菜单标志 + FUNCPTR fnExecute; // 执行函数指针 + + tagPMenuItem ptHead; // 菜单链表头指针 + tagPMenuItem ptCurrent; // 菜单链表当前指针 + tagPMenuItem ptRoute[4]; // 0-3级菜单的指针路径 + tagPMenuItem ptCurBak; // 菜单链表指针备份 + tagPMenuItem pt0Level; // 0级菜单链表指针备份 + + tagPMenuItem ptSeeSetSoft; // 查看定值菜单结点 + tagPMenuItem ptSetValSoft; // 整定定值菜单结点 + tagPMenuItem ptSeeSetFigure; // 查看定值菜单结点 + tagPMenuItem ptSetValFigure; // 整定定值菜单结点 + tagPMenuItem ptSeeSetPara; // 查看定值菜单结点 + tagPMenuItem ptSetValPara; // 整定定值菜单结点 +}tagMenuCtrl,*tagPMenuCtrl; tagDspCtrl g_tDspCtrl; // 显示控制全局结构 +tagMenuCtrl g_tMenuCtrl; // 菜单全局控制结构 -static MenuItem g_menuRun; -static MenuItem g_menuSet; -static MenuItem g_menuSee; -static MenuItem g_menuSet1; -static MenuItem g_menuSet2; +tagMenuItem g_tMenuItem[300]; // 菜单链表空间定义 -void Menu_Init() +void Menu_0LevelNumCal(void) { - Lcd_Init(); + tagPMenuModel ptModelIndex; /* 当前遍历到的菜单表项指针 */ + uint16_t wLoop; /* 表项下标,0 .. MENU_MAX_ITEM-1 */ + uint8_t by0LevelNum; /* 0 级菜单项计数 */ - /* 初始化显示控制结构 */ - memset(&g_tDspCtrl, 0, sizeof(g_tDspCtrl)); + by0LevelNum = 0; + for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) { + ptModelIndex = (tagPMenuModel)&g_tMenuModelTab[wLoop]; + if (0 == ptModelIndex->byClass) { + by0LevelNum++; + } + } + g_tMenuCtrl.by0LevelNum = by0LevelNum; +} +/****************************************************************************** + * 函数名: Menu_Main_Creat + * 功能: 根据静态菜单表 g_tMenuModelTab 在 g_tMenuItem 中建立可遍历的菜单树 + * (双向链表 + 父子关系,同级首尾成环) + * 参数: 无 + * 返回: 无 + * 说明: 1. 表内按深度优先顺序排列:先 0 级,再其下 1 级,再 2 级… 同级按表顺序 + * 2. 通过 byClass(0/1/2/3)区分层级 + * 3. 用 ptFirst[级]、ptLast[级] 记录每级首尾,便于链接与最后首尾成环 + *****************************************************************************/ +void Menu_Main_Creat(void) +{ + tagPMenuItem ptFirst[4]; /* 各级(0..3)当前“首”结点,用于成环与回退时链接 */ + tagPMenuItem ptLast[4]; /* 各级当前“尾”结点 */ + tagPMenuItem ptCurrent; /* 表顺序上的“当前”结点(刚处理完的) */ + tagPMenuItem ptNextNode; /* 本轮要接入的“下一”结点 */ + + tagPMenuModel ptModelIndex; + uint16_t wLoop1, wLoop2; + uint8_t byCurClass; /* 当前结点层级 */ + uint8_t byNextClass; /* 下一结点层级 */ + + /* ---------- 第 0 项:作为 0 级首项,初始化并记为 0 级首/尾 ---------- */ + ptFirst[0] = &g_tMenuItem[0]; + ptLast[0] = ptFirst[0]; + + ptCurrent = &g_tMenuItem[0]; + ptModelIndex = (tagPMenuModel)&g_tMenuModelTab[0]; + ptCurrent->wPos = 0; + ptCurrent->wNum = 0; + ptCurrent->wSPosX = 0; + ptCurrent->wSPosY = 0; + ptCurrent->wEPosX = 0; + ptCurrent->wEPosY = 0; + + ptCurrent->ptHigher = NULL; + ptCurrent->ptLower = NULL; + ptCurrent->ptBefore = NULL; + ptCurrent->ptBehind = NULL; + + ptCurrent->byClass = ptModelIndex->byClass; + ptCurrent->byAttrib = ptModelIndex->byAttrib; + ptCurrent->wPassword = ptModelIndex->wPassword; + ptCurrent->wPara = ptModelIndex->wPara; + ptCurrent->pfnWinProc = ptModelIndex->pfnWinProc; + memcpy(ptCurrent->byName, ptModelIndex->byName, 50); + memcpy(ptCurrent->byTip, ptModelIndex->byTip, 50); +#ifdef DEBUG + printf("Menu_Main_Creat:%d级菜单:%s\n", ptCurrent->byClass, ptCurrent->byName); +#endif + + /* ---------- 从第 1 项起,按表顺序逐个接入树 ---------- */ + for (wLoop1 = 1; wLoop1 < MENU_MAX_ITEM; wLoop1++) + { + /* 获取下一项菜单项 */ + ptNextNode = &g_tMenuItem[wLoop1]; + /* 获取下一项菜单项的模型索引 */ + ptModelIndex = (tagPMenuModel)&g_tMenuModelTab[wLoop1]; + + /* 初始化下一项菜单项的各项属性 */ + ptNextNode->wPos = 0; + ptNextNode->wNum = 0; + ptNextNode->wSPosX = 0; + ptNextNode->wSPosY = 0; + ptNextNode->wEPosX = 0; + ptNextNode->wEPosY = 0; + ptNextNode->ptHigher = NULL; + ptNextNode->ptLower = NULL; + ptNextNode->ptBefore = NULL; + ptNextNode->ptBehind = NULL; + + ptNextNode->byClass = ptModelIndex->byClass; + ptNextNode->byAttrib = ptModelIndex->byAttrib; + ptNextNode->wPassword = ptModelIndex->wPassword; + ptNextNode->wPara = ptModelIndex->wPara; + ptNextNode->pfnWinProc = ptModelIndex->pfnWinProc; + memcpy(ptNextNode->byName, ptModelIndex->byName, 50); + memcpy(ptNextNode->byTip, ptModelIndex->byTip, 50); +#ifdef DEBUG + printf("Menu_Main_Creat:%d级菜单:%s\n", ptNextNode->byClass, ptNextNode->byName); +#endif + + byCurClass = ptCurrent->byClass; + byNextClass = ptNextNode->byClass; + + /* 根据当前菜单项和下一菜单项的层级关系,更新菜单链表 */ + if (byCurClass < byNextClass) + { + ptFirst[byNextClass] = ptNextNode; + ptLast[byNextClass] = ptFirst[byNextClass]; + + ptCurrent->ptLower = ptNextNode; + ptNextNode->ptHigher = ptCurrent; + } + else if (byCurClass == byNextClass) + { + ptLast[byNextClass] = ptNextNode; + ptCurrent->ptBehind = ptNextNode; + + ptNextNode->ptBefore = ptCurrent; + ptNextNode->ptHigher = ptCurrent->ptHigher; + } + else if (byCurClass > byNextClass) /* 下一项更浅 */ + { + /* 下一项更浅:下一项接到 byNextClass 级当前尾的后面*/ + ptLast[byNextClass]->ptBehind = ptNextNode; + ptNextNode->ptBefore = ptLast[byNextClass]; + ptNextNode->ptHigher = ptLast[byNextClass]->ptHigher; + /* 更新 byNextClass 级当前尾 */ + ptLast[byNextClass] = ptNextNode; + + /* 当层级回退时,更新各级首尾成环 */ + for (wLoop2 = byCurClass; wLoop2 > byNextClass; wLoop2--) + { + ptLast[wLoop2]->ptBehind = ptFirst[wLoop2]; /* 尾的下一个是首 */ + ptFirst[wLoop2]->ptBefore = ptLast[wLoop2]; /* 首的前一个是尾 */ + } + } + ptCurrent = ptNextNode; + } + + /* ---------- 从当前所在级到 0 级,各级首尾成环(循环链表) ---------- */ + byCurClass = ptCurrent->byClass; + for (wLoop1 = 0; wLoop1 <= byCurClass; wLoop1++) + { + ptLast[wLoop1]->ptBehind = ptFirst[wLoop1]; + ptFirst[wLoop1]->ptBefore = ptLast[wLoop1]; + } +} +/****************************************************************************** + * 函数名: Menu_charLenCal + * 功能: 1.计算指定级菜单下所有子项的最大显示长度 + * 2.为有下级的菜单名追加右箭头符号 '\x10' + * 3.更新 wPos 字段,表示当前菜单在本级中的位置 + * 4. wNum 指定级菜单的下级级菜数量 + * 参数: bylevel - 当前层级(调用方一般为 0,表示从 0 级菜单的子级开始) + * byMenuNum - 各级菜单项数量数组 [0..3],本函数会更新 bylevel+1 级计数 + * ptFirst - 各级首项指针数组,本函数会写入 ptFirst[bylevel+1] + * ptIndex - 各级当前项指针数组,本函数会更新 ptIndex[bylevel] 及子级 + * 返回: 该级所有菜单项中的最大字符长度(含可能追加的右箭头 '\x10') + * 说明: 1. 从 ptIndex[bylevel] 的子级(ptLower)开始,沿 ptBehind 遍历同级所有项 + * 2. 若有下级菜单且名称末尾无右箭头,则在 byName 末尾追加 '\x10' 并补 '\0' + * 3. 同级菜单首尾成环,遍历回 ptFirst 时结束 + * 4. 供 Menu_PositionCal 用于计算下拉菜单矩形框宽度(byMaxLen * 字宽) + *****************************************************************************/ +uint8_t Menu_charLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) +{ + uint16_t wLoop; + uint8_t byStrLen; /* 当前菜单项名称长度(含可能追加的右箭头) */ + uint8_t byMaxLen; /* 同级菜单中的最大显示长度 */ + + /* 记录子级首项,供后续遍历与布局使用 */ + ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower; + + bylevel = bylevel + 1; + ptIndex[bylevel] = ptFirst[bylevel]; + + byMenuNum[bylevel] = 1; + ptIndex[bylevel]->wPos = 1; + + byMaxLen = 0; + for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) + { + /* 计算当前菜单项名称长度 */ + byStrLen = (uint8_t)strlen((const char *)ptIndex[bylevel]->byName); + + /* 若有下级且名称末尾尚无右箭头,则追加 '\x10' 并更新长度 */ + if ((ptIndex[bylevel]->ptLower != NULL) && ('\x10' != ptIndex[bylevel]->byName[byStrLen - 1])) + { + ptIndex[bylevel]->byName[byStrLen] = '\x10'; + ptIndex[bylevel]->byName[byStrLen + 1] = '\0'; + byStrLen += 1; + } + + if (byMaxLen < byStrLen) + { + byMaxLen = byStrLen; + } +#ifdef DEBUG + printf("计算%d级菜单位置:%s,长度%d\n", bylevel, ptIndex[bylevel]->byName, byMaxLen); +#endif + + /* 移至同级下一项(ptBehind),同级首尾成环 */ + ptIndex[bylevel] = ptIndex[bylevel]->ptBehind; + if (ptIndex[bylevel] == ptFirst[bylevel]) /* 同级首尾成环,遍历回 ptFirst 时说明遍历完了当前级的所有菜单,结束 */ + { + break; + } + byMenuNum[bylevel]++; /* 同级菜单项数量+1 */ + ptIndex[bylevel]->wPos = byMenuNum[bylevel]; /* 同级菜单项序号 + 1 */ + } + + /* 将子级菜单总数回填到父项 wNum */ + ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel]; + return byMaxLen; +} +void Menu_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) +{ + uint16_t wLoop; + uint16_t wSPosX; + uint16_t wSPosY; + uint16_t wEPosX; + uint16_t wEPosY; + uint16_t wTemp; + uint8_t byMaxLen; + uint8_t byMaxNum; + uint8_t byMenuPos; + uint8_t byItemNum; + uint8_t byMenuNum[4]; + + byMaxNum = (MENU_YMAX - MENU_YMIN - 6) / LINE_HEIGHT; // 页内纵向可显示菜单数 + + ptIndex[bylevel] = ptFirst[bylevel]; + for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) + { + if ((ptIndex[bylevel]->ptLower) != NULL) // 当前2级菜单有下级菜单 + { + byMaxLen = Menu_charLenCal(bylevel, byMenuNum, ptFirst, ptIndex); // 计算3级菜单项数及字符数 + + wSPosX = ptIndex[bylevel - 1]->wEPosX; // 3级菜单矩形框横坐标起始 + ptIndex[bylevel]->wSPosX = wSPosX; + + wEPosX = + wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; // 3级菜单矩形框横坐标终止 + ptIndex[bylevel]->wEPosX = wEPosX; + + byMenuPos = ptIndex[bylevel]->wPos; // 2级菜单在菜单列中的位置 + byItemNum = byMenuNum[bylevel]; // 2级菜单项数 + + if ((byItemNum > byMaxNum) && (byMenuPos >(byMaxNum - 1))) // 2级菜单项数大于一页且2级菜单当前位置不在第一页 + { + byMenuPos = byMaxNum - (byItemNum - byMenuPos); // 第一页尾显示↓ 第二页头显示↑ + } + + wSPosY = ptIndex[bylevel - 1]->wSPosY; + wSPosY = + wSPosY + (byMenuPos - 1) * LINE_HEIGHT; // 计算3级菜单框起始坐标 + + byItemNum = byMenuNum[bylevel + 1]; // 3级菜单项数 + wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算3级菜单框终止坐标 + + if (wEPosY < MENU_YMAX) // 3级菜单框与2级对应位置不越限 + { + ptIndex[bylevel]->wSPosY = wSPosY; // 3级菜单矩形框纵起始坐标定位 + ptIndex[bylevel]->wEPosY = wEPosY; // 3级菜单矩形框纵终止坐标定位 + } + else + { + wEPosY = wSPosY - (byItemNum - 1) * LINE_HEIGHT - MENU_YADD; + if ((wEPosY > MENU_YMIN) && (wEPosY < MENU_YMAX)) // 菜单向上不越限 + { + wTemp = wSPosY; + wSPosY = wEPosY; + wEPosY = wTemp + LINE_HEIGHT; + } + else // 菜单显示超过一页 + { + wSPosY = ptIndex[0]->wSPosY; // 3级菜单框起始坐标同1级菜单 + wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算3级菜单框终止坐标 + + if (wEPosY > MENU_YMAX) + { + wEPosY = MENU_YMAX; // 菜单纵向一屏可显示 + } + } + ptIndex[bylevel]->wSPosY = wSPosY; // 3级菜单矩形框纵起始坐标定位 + ptIndex[bylevel]->wEPosY = wEPosY; // 3级菜单矩形框纵终止坐标定位 + } + } + /* 移至同级下一项 */ + ptIndex[bylevel] = ptIndex[bylevel]->ptBehind; + /*如果移至同级下一项后,发现回到了同级菜单的第一个菜单,则说明遍历完了同级菜单的所有菜单,结束*/ + if (ptIndex[bylevel] == ptFirst[bylevel]) + { + break; + } + } } +void Menu_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) +{ + uint16_t wSPosX; + uint16_t wSPosY; + uint16_t wEPosX; + uint16_t wEPosY; + uint8_t byMenuNum[4]; + uint8_t byMaxLen; + uint8_t byMaxNum; + uint16_t byMenuPos; + uint8_t byItemNum; + uint16_t wTemp; + + byMaxNum = (MENU_YMAX) / LINE_HEIGHT; // 页内纵向可显示菜单数 + + ptIndex[bylevel] = ptFirst[bylevel]; + for (uint16_t wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) + { + if ((ptIndex[bylevel]->ptLower) != NULL) // 当前1级菜单有下级菜单 + { + byMaxLen = Menu_charLenCal(bylevel, byMenuNum, ptFirst, ptIndex); // 计算2级菜单项数及字符数 + + /*X坐标计算*/ + ptIndex[bylevel]->wSPosX = ptIndex[bylevel - 1]->wEPosX; // 2级菜单矩形框横坐标起始 + ptIndex[bylevel]->wEPosX = ptIndex[bylevel]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; // 2级菜单矩形框横坐标终止 + + + byMenuPos = ptIndex[bylevel]->wPos; // 1级菜单在菜单列中的位置 + byItemNum = byMenuNum[bylevel]; // 1级菜单项数 + + if ((byItemNum > byMaxNum) && (byMenuPos >= byMaxNum)) // 1级菜单项数大于一页且1级菜单当前位置不在第一页 + { + byMenuPos = byMaxNum - (byItemNum - byMenuPos); /* 第一页尾显示↓ 第二页头显示↑ */ + } + + wSPosY = ptIndex[bylevel - 1]->wSPosY + (byMenuPos - 1) * LINE_HEIGHT; // 计算2级菜单框起始坐标 + + byItemNum = byMenuNum[bylevel + 1]; // 2级菜单项数 + wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 +#ifdef DEBUG + printf("%d %d %d %d \n", bylevel, byItemNum, wSPosY, wEPosY); +#endif + if (wEPosY < MENU_YMAX) /* 菜单框没有超出边界 */ + { + ptIndex[bylevel]->wSPosY = wSPosY; // 2级菜单矩形框纵起始坐标定位 + ptIndex[bylevel]->wEPosY = wEPosY; // 2级菜单矩形框纵终止坐标定位 + } + else /* 菜单框超出边界 */ + { + wEPosY = wSPosY - (byItemNum - 1) * LINE_HEIGHT - MENU_YADD; + if ((wEPosY > LINE_HEIGHT) && (wEPosY < MENU_YMAX)) // 菜单向上不越限 + { + wTemp = wSPosY; + wSPosY = wEPosY; + wEPosY = wTemp + LINE_HEIGHT; + } + else + { + wSPosY = ptIndex[0]->wSPosY; // 2级菜单框起始坐标同1级菜单 + wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 + for (uint16_t i = 1; i < (wSPosY / LINE_HEIGHT + 1); i++) + { + if (wEPosY > MENU_YMAX) // wEPosY = CN_YMAX; // 菜单纵向一屏可显示 + { + wSPosY = ptIndex[0]->wSPosY - LINE_HEIGHT * i; + if (wSPosY < LINE_HEIGHT) + { + wSPosY = 0; + } + wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 +#ifdef DEBUG + printf("Menu_Sub1PosCal: %d %d %d %d\n", bylevel, byItemNum, wSPosY, wEPosY); +#endif + } + else + { + break; /* 菜单向上不越限,结束 */ + } + } + } + ptIndex[bylevel]->wSPosY = wSPosY; /* 2级菜单矩形框纵起始坐标定位 */ + ptIndex[bylevel]->wEPosY = wEPosY; /* 2级菜单矩形框纵终止坐标定位 */ + } + Menu_Sub2PosCal(2, ptFirst, ptIndex); // 3级菜单框坐标计算 + } + /* 移至同级下一项 */ + ptIndex[bylevel] = ptIndex[bylevel]->ptBehind; + /*如果移至同级下一项后,发现回到了同级菜单的第一个菜单,则说明遍历完了同级菜单的所有菜单,结束*/ + if (ptIndex[bylevel] == ptFirst[bylevel]) + { + break; + } + } +} +/****************************************************************************** + * 函数名: Menu_PositionCal + * 功能: 为菜单树中所有有下级的 0 级菜单项计算并填充其下拉框的显示坐标 + * (wSPosX/Y、wEPosX/Y),供后续绘制矩形框与文字使用 + * 参数: ptMenuHead - 0 级菜单链表头指针(通常为 g_tMenuCtrl.ptHead) + * 返回: 无 + * 说明: 1. 0 级菜单在屏幕顶部横向均分,有下级的项在其下方弹出 1 级下拉框 + * 2. 下拉框左上角 (wSPosX, wSPosY) 从屏幕底部向上推算,避免超出屏幕 + * 3. 下拉框宽度由 Menu_charLenCal 返回的 byMaxLen 与字宽 CN_WITDTH 决定 + * 4. 若下拉框超出屏幕顶部(wEPosY < CN_YMAX),则 wEPosY 取屏高,需分页显示 + * 5. 2 级及以下子菜单坐标由 Menu_Sub1PosCal 递归计算 + *****************************************************************************/ +void Menu_PositionCal(tagPMenuItem ptMenuHead) +{ + tagPMenuItem ptFirst[4]; /* 各级首项指针 [0..3] */ + tagPMenuItem ptIndex[4]; /* 各级当前遍历项指针 */ + + uint8_t byMenuNum[4]; /* 各级菜单项数量 */ + uint8_t byMaxLen; /* 1 级菜单项最大字符长度(含右箭头) */ + uint8_t byInterval; /* 0 级菜单项在 X 方向的间隔像素 */ + uint8_t by0LevelNum; /* 0 级菜单项总数 */ + + ptFirst[0] = ptMenuHead; + ptIndex[0] = ptFirst[0]; + byMenuNum[0] = 1; + ptIndex[0]->wPos = 1; + + byInterval = LCD_SIZE_X / g_tMenuCtrl.by0LevelNum; /* 平均分布 */ + by0LevelNum = g_tMenuCtrl.by0LevelNum; + + for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++) /* 遍历 0 级菜单项 */ + { + if (ptIndex[0]->ptLower != NULL) /* 有下级菜单 */ + { + /* 计算 1 级子项最大长度,并为其名称追加右箭头;更新 ptFirst/ptIndex/byMenuNum */ + byMaxLen = Menu_charLenCal(0, byMenuNum, ptFirst, ptIndex); + + /* 左上角坐标计算 */ + /* 1 级下拉框左上角:按 0 级序号横向定位,纵向从屏底向上预留多行高度 */ + ptIndex[0]->wSPosX = (byMenuNum[0] - 1) * byInterval; + ptIndex[0]->wSPosY = LCD_SIZE_Y - LINE_HEIGHT - byMenuNum[1] * LINE_HEIGHT - MENU_YADD; + + /* 右下角坐标计算 */ + /* 下拉框右下角 X:起始 + 最大字符数×字宽 + 边距 */ + ptIndex[0]->wEPosX = ptIndex[0]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; + /* 下拉框右下角 Y:与 0 级菜单底边齐平,若超出屏顶则取屏高(需分页) */ + ptIndex[0]->wEPosY = LCD_SIZE_Y - LINE_HEIGHT; + + /* 递归计算 2 级及以下子菜单的显示坐标 */ + Menu_Sub1PosCal(1, ptFirst, ptIndex); + } + /* 移至 0 级下一项*/ + ptIndex[0] = ptIndex[0]->ptBehind; + /*如果移至 0 级下一项后,发现回到了0级菜单的第一个菜单,则说明遍历完了0级菜单的所有菜单,结束*/ + if (ptIndex[0] == ptFirst[0]) + { + break; + } + byMenuNum[0]++; /* 0级菜单项数量+1 */ + ptIndex[0]->wPos = byMenuNum[0]; /* 0级菜单项序号 + 1 */ + } +} +/****************************************************************************** + * 函数名: Lcd_Black_Button + * 功能: 在指定矩形区域绘制一个带边框的实心“按钮”效果 + * 参数: left_x - 按钮左上角 X 坐标 + * top_y - 按钮左上角 Y 坐标 + * right_x - 按钮右下角 X 坐标 + * bottom_y - 按钮右下角 Y 坐标 + * 返回: 无 + * 说明: 1. 先用当前前景色填充内部区域(边框内减 1 像素) + * 2. 再用 1 像素宽的水平/垂直线绘制上、下、左、右边框 + * 3. 颜色均使用 g_tCVsr.wFontColor(当前字体/前景颜色) + *****************************************************************************/ + void Menu_BoundaryBox(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y) + { + /* 填充按钮内部区域:Y 从 top_y+1 到 bottom_y-1,X 从 left_x+1 到 right_x-1 */ + for (uint16_t y = top_y + 1; y < bottom_y; y++) + { + for (uint16_t x = left_x + 1; x < right_x; x++) + { + Lcd_SetPixel(x, y, g_tCVsr.wBackColor); /* 设置按钮内部像素为前景色,实现实心效果 */ + } + } + /* 绘制上边框:从 (left_x, top_y) 到 (right_x, top_y),线宽 1 像素 */ + Lcd_LineH(left_x, right_x, top_y, 1, g_tCVsr.wFontColor); + /* 绘制左边框:从 (left_x, top_y) 到 (left_x, bottom_y),线宽 1 像素 */ + Lcd_LineV(top_y, bottom_y, left_x, 1, g_tCVsr.wFontColor); + /* 绘制下边框:从 (left_x, bottom_y) 到 (right_x+1, bottom_y),稍微向右多画 1 像素防止边界漏点 */ + Lcd_LineH(left_x, right_x + 1, bottom_y, 1, g_tCVsr.wFontColor); + /* 绘制右边框:从 (right_x, top_y) 到 (right_x, bottom_y),线宽 1 像素 */ + Lcd_LineV(top_y, bottom_y, right_x, 1, g_tCVsr.wFontColor); +} +/* +这是分页的逻辑,需要保存,不能删除 +if (byMenuNum <= 10) +{ + byPage = 1; +} +else if (byMenuNum <= 20) +{ + byPage = 2; +} +else if (byMenuNum > 20) +{ + byPage = 3; +} + if (byMenuNum > byMaxNum) // 菜单项数大于一页 + { + wPosX = (wSPosX + wEPosX) / 2 - 5; + wLoopMax = byMaxNum - 1; + + if (byMenuPos < byMenuNum - (byMaxNum - 1) + 1) // 由第二页返回第一页 + { + bySecPage = 0; + g_tMenuCtrl.bySecPage = bySecPage; + } + + if ((byMenuPos < byMaxNum) && (0 == bySecPage)) // 当前位置在第一页 + { + byFirstPos = 1; + wPosY = wEPosY - LINE_HEIGHT; + + Lcd_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14); + Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓ + } + else if ((byMenuPos < (byMaxNum * 2 - 2)) && (byPage > 2)) // 当前位置在第二页 + { + bySecPage = 0xCC; + g_tMenuCtrl.bySecPage = bySecPage; + + byFirstPos = byMaxNum; + wPosY = wSPosY + 2; + + Lcd_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13); + Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑ + + wPosY = wEPosY - LINE_HEIGHT; + + Lcd_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14); + Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓ + } + else + { + bySecPage = 0x77; + g_tMenuCtrl.bySecPage = bySecPage; + + if (byPage > 2) + { + byFirstPos = byMaxNum * 2 - 2; + } + else + { + byFirstPos = byMenuNum - (byMaxNum - 1) + 1; + } + wPosY = wSPosY + 2; + + Lcd_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13); + Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑ + } + + for (wLoop = 0; wLoop < byMenuNum; wLoop++) + { + if (byFirstPos == ptIndex->wPos) + { + break; + } + ptIndex = ptIndex->ptBehind; + } + } + else + { + bySecPage = 0; + g_tMenuCtrl.bySecPage = bySecPage; + } + if (0xCC == bySecPage) + { + for (wLoop = 0; wLoop < (wLoopMax - 1); wLoop++) + { + byMenuPos = ptIndex->wPos - (byFirstPos - 1); + wPosY = wSPosY + byMenuPos * LINE_HEIGHT + 3; // 计算显示字符位置 + + memcpy(byName, ptIndex->byName, 50); + Lcd_ShowStr(wPosX, wPosY, byName); // 显示菜单字符串 + + if (ptRoute == ptIndex) // 当前菜单设置反显 + { + Lcd_Invert(wSPosX + 2, wPosY - 1, wEPosX - 2, wPosY + 14); + } + ptIndex = ptIndex->ptBehind; // 取下一菜单结点 + } + } + else + { + wPosX = wSPosX + 4; + for (wLoop = 0; wLoop < wLoopMax; wLoop++) + { + if (0x77 == bySecPage) // 显示第二页菜单 + { + byMenuPos = ptIndex->wPos - (byFirstPos - 1); + } + else // 显示第一页菜单 + { + byMenuPos = ptIndex->wPos - 1; + } + wPosY = wSPosY + byMenuPos * LINE_HEIGHT + 3; // 计算显示字符位置 + memcpy(byName, ptIndex->byName, 50); + Lcd_ShowStr(wPosX, wPosY, byName); // 显示菜单字符串 + + if (ptRoute == ptIndex) // 当前菜单设置反显 + { + Lcd_Invert(wSPosX + 2, wPosY - 1, wEPosX - 2, wPosY + 14); + } + ptIndex = ptIndex->ptBehind; // 取下一菜单结点 + } + } +*/ + +/****************************************************************************** + * 函数名: Menu_Show_Other + * 功能: 显示除 0 级以外的其它级别菜单(1 级 / 2 级 / 3 级)的下拉列表 + * 参数: bylevel - 要显示的菜单层级(1 表示 1 级菜单,2 表示 2 级菜单等) + * byLeftMove - 菜单整体在 X 方向的左移像素数(用于横向滚动或对齐) + * 返回: 无 + * 说明: 1. 使用 g_tMenuCtrl.ptRoute[bylevel] 获取本级菜单框坐标,并应用水平偏移 + * 2. 调用 Menu_BoundaryBox 绘制当前级菜单的背景边框矩形 + * 3. 以 g_tMenuCtrl.ptRoute[bylevel+1] 为当前“选中项”,逐项绘制所有本级菜单文字 + * 4. 对选中项对应的那一行调用 Lcd_Invert 进行反显高亮 + * 注意: 分页逻辑还没有完成,如果有分页的情况会显示错误 + *****************************************************************************/ +void Menu_Show_Other(uint8_t bylevel) +{ + tagPMenuItem ptIndex; /* 本级菜单遍历指针 */ + tagPMenuItem ptRoute; /* 下一级“选中项”指针(用于确定当前高亮项) */ + + uint16_t wPosX; /* 菜单文本显示的起始 X 坐标 */ + uint16_t wPosY; /* 菜单文本显示的起始 Y 坐标 */ + + uint16_t byMenuNum; /* 本级菜单项总数,用于控制循环次数 */ + uint16_t byMenuPos; /* 当前菜单项在本级中的序号(1..byMenuNum) */ + uint8_t byName[50]; /* 临时缓冲区,用于拷贝菜单名称 */ + + /* 1. 基于当前层级路径结点,取得本级菜单框坐标并根据 byLeftMove 做水平偏移 */ + ptIndex = g_tMenuCtrl.ptRoute[bylevel]; + /* 绘制本级菜单的背景边框矩形 */ + Menu_BoundaryBox(ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY); + + /* 2. 取得下一级路径中的当前结点,用于确定哪一项需要高亮 */ + ptRoute = g_tMenuCtrl.ptRoute[bylevel + 1]; /* 进入下一级菜单的选中项 */ + + ptIndex = ptRoute; /* 从选中项开始向后遍历同级菜单 */ + + /* 3. 逐项绘制菜单文字,并对选中项所在行做反显处理 */ + wPosX = ptIndex->wSPosX + 4; /* 文本相对左边框右移 4 像素,预留内边距 */ + for (uint16_t index = 0; index < g_tMenuCtrl.ptRoute[bylevel]->wNum; index++) + { + wPosY = g_tMenuCtrl.ptRoute[bylevel]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3; /* 行顶坐标 + 行高 * 行号 + 3 像素微调 */ + memcpy(byName, ptIndex->byName, 50); /* 将菜单名称拷贝到临时缓存 */ + Lcd_ShowStr(wPosX, wPosY, byName); /* 在计算出的坐标位置显示菜单字符串 */ + + if (ptRoute == ptIndex) /* 若当前绘制项为“选中项” */ + { + /* 对该行对应的矩形区域执行反显,用于高亮当前选择 */ + /* x轴:起始位置+2,结束位置-2,产生内嵌的感觉 */ + Lcd_Invert(g_tMenuCtrl.ptRoute[bylevel]->wSPosX + 2, wPosY - 1, g_tMenuCtrl.ptRoute[bylevel]->wEPosX - 2, wPosY + 14); + } + ptIndex = ptIndex->ptBehind; /* 沿同级链表向后移动,处理下一项 */ + } +} +/****************************************************************************** + * 函数名: Menu_Show_0Level + * 功能: 绘制 0 级主菜单栏(顶部标题区域),按当前菜单指针依次显示各个 0 级菜单项 + * 参数: 无(依赖全局菜单控制结构 g_tMenuCtrl 与 LCD 显存控制 g_tCVsr) + * 返回: 无 + * 说明: 1. 所有 0 级菜单在 X 方向上按等间距分布 + * 2. 每次循环仅在菜单项仍在屏幕可见区域时才绘制对应标题文本 + * 3. 顶部 0~32 像素区域作为 0 级菜单标题栏背景,会被统一清屏并重绘 + * 4. 调用 Lcd_MeiTouPic 绘制“眉头”装饰线条,增强标题栏的视觉效果 + * 5. 示例中固定显示 “当前模式: 无模式”,后续可替换为实际运行模式文本 + *****************************************************************************/ void Menu_Show_0Level() { - tagPMenuItem ptIndex; /* 遍历 0 级菜单链表用的指针(从表头开始依次后移) */ - uint16_t wSPosX; /* 当前要显示的 0 级菜单项左上角 X 坐标(屏幕坐标) */ - uint16_t wSPosY; /* 0 级菜单“下陷框”左上角 Y 坐标 */ - uint16_t wEPosX; /* 0 级菜单“下陷框”右下角 X 坐标 */ - uint16_t wEPosY; /* 0 级菜单“下陷框”右下角 Y 坐标 */ - uint16_t wLoop; /* 循环计数:第几个 0 级菜单项(从 0 开始) */ - uint8_t byName[50]; /* 临时缓冲区:存放当前 0 级菜单项名称 */ - uint8_t byInterval; /* 每个 0 级菜单在 X 方向上的间隔宽度(用于平均分布) */ - uint8_t byLeftMove; /* 当前 0 级菜单整体向左平移的像素数(超出一屏时产生“滚动”效果) */ - uint8_t by0LevelNum; /* 实际参与显示的 0 级菜单个数(是否显示“隐藏/显示”项会影响个数) */ - - - Lcd_ShowStr(16, 20, (uint8_t*)"你好!\nabcsadfasdfasdfsdfasdf"); /* 你好! */ + /* 先清除顶部 0~32 像素高度区域,作为 0 级菜单标题栏背景 */ + Lcd_FillRect(0, 0, LCD_SIZE_X, 32, g_tCVsr.wBackColor); + /* 绘制“眉头”装饰,使 0 级菜单栏更加立体 */ + Lcd_MeiTouPic(16, 2 ); + Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" ); } +void Menu_Init(void) +{ + Lcd_Init(); + /* 初始化显示控制结构 */ + memset(&g_tDspCtrl, 0, sizeof(g_tDspCtrl)); + + Menu_0LevelNumCal(); /* 统计 0 级菜单项个数 */ + Menu_Main_Creat(); /* 创建菜单树 */ + g_tMenuCtrl.ptHead = &g_tMenuItem[0]; // 取菜单头结点 + Menu_PositionCal(g_tMenuCtrl.ptHead); /* 菜单框位置计算 */ + + g_tMenuCtrl.ptRoute[0] = &g_tMenuItem[0]; // 0级路径初始化 + g_tMenuCtrl.ptCurrent = g_tMenuCtrl.ptHead->ptLower; // 当前指针初始化 + g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; + g_tMenuCtrl.ptRoute[1] = g_tMenuCtrl.ptCurrent; // 1级路径初始化 + g_tMenuCtrl.byLeftMove = 0; + g_tMenuCtrl.bySecPage = 0; + + Menu_Show_0Level(); /* 显示 0 级菜单 */ + Menu_Show_Other(0); +} void MenuProc_See_AppInfo() @@ -165,14 +856,33 @@ void MenuProc_YX_SetTwin() printf("MenuProc_YX_SetTwin\n"); } -int Menu_MapKey(int ch) -{ - if (ch == 'w' || ch == 'W') return KEY_U; - if (ch == 's' || ch == 'S') return KEY_D; - if (ch == 'a' || ch == 'A') return KEY_L; - if (ch == 'd' || ch == 'D') return KEY_R; - if (ch == '\r' || ch == '\n') return KEY_ENT; - if (ch == 27) return KEY_ESC; /* ESC */ - return KEY_NONE; -} +/* ---------- 以下为 display.c 菜单表引用、暂无具体实现的桩函数,避免链接未定义符号 ---------- */ +void Menu_NonPfunc(void) { printf("Menu_NonPfunc\n"); } +void MenuProc_AllInf_Default(void) { printf("MenuProc_AllInf_Default\n"); } +void MenuProc_Para_Default(void) { printf("MenuProc_Para_Default\n"); } +void MenuProc_Set_Default(void) { printf("MenuProc_Set_Default\n"); } +void MenuProc_Resume_Soft(void) { printf("MenuProc_Resume_Soft\n"); } +void MenuProc_Cfg_CellDef(void) { printf("MenuProc_Cfg_CellDef\n"); } +void MenuProc_Cfg_ShowAnaType(void) { printf("MenuProc_Cfg_ShowAnaType\n"); } +void MenuProc_Dbg_XuYX(void) { printf("MenuProc_Dbg_XuYX\n"); } +void MenuProc_Dbg_XuYC(void) { printf("MenuProc_Dbg_XuYC\n"); } +void MenuProc_Dbg_XuEvent(void) { printf("MenuProc_Dbg_XuEvent\n"); } +void MenuProc_Dbg_Relay(void) { printf("MenuProc_Dbg_Relay\n"); } +void MenuProc_See_VersionBoard(void){ printf("MenuProc_See_VersionBoard\n"); } +void MenuProc_Cfg_ClrRec(void) { printf("MenuProc_Cfg_ClrRec\n"); } +void MenuProc_Cfg_ComPara(void) { printf("MenuProc_Cfg_ComPara\n"); } +void MenuProc_Cfg_EditIP(void) { printf("MenuProc_Cfg_EditIP\n"); } +void MenuProc_Cfg_EditSntp(void) { printf("MenuProc_Cfg_EditSntp\n"); } +void MenuProc_Cfg_CellConf(void) { printf("MenuProc_Cfg_CellConf\n"); } +void MenuProc_See_RecSOE(void) { printf("MenuProc_See_RecSOE\n"); } +void MenuProc_See_RecAct(void) { printf("MenuProc_See_RecAct\n"); } +void MenuProc_See_RecOpt(void) { printf("MenuProc_See_RecOpt\n"); } +void MenuProc_See_RecAlm(void) { printf("MenuProc_See_RecAlm\n"); } +void MenuProc_See_RecStart(void) { printf("MenuProc_See_RecStart\n"); } +void MenuProc_See_RecYK(void) { printf("MenuProc_See_RecYK\n"); } +void MenuProc_See_RecChk(void) { printf("MenuProc_See_RecChk\n"); } +void MenuProc_See_RecRun(void) { printf("MenuProc_See_RecRun\n"); } +void MenuProc_See_RecFault(void) { printf("MenuProc_See_RecFault\n"); } +void MenuProc_YC_SetSqValue(void) { printf("MenuProc_YC_SetSqValue\n"); } +void MenuProc_YC_SetAdjCoe(void) { printf("MenuProc_YC_SetAdjCoe\n"); } diff --git a/src/Drv/menu.h b/src/Drv/menu.h index 785a749..b96301e 100644 --- a/src/Drv/menu.h +++ b/src/Drv/menu.h @@ -13,6 +13,19 @@ #include + +#define CN_HEIGHT 12 // 菜单汉字高 +#define CN_ROWSPACE 2 // 菜单字符行间隔 +#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) // 字符行间隔 +#define MENU_XADD 7 // 菜单框横坐标增加 +#define MENU_YADD 4 // 菜单框纵坐标增加 +#define MENU_WITDTH 7 // 菜单汉字13×12点阵 + + + +#define MENU_YMIN 0 // 菜单Y坐标的最小值 +#define MENU_YMAX (LCD_SIZE_Y - 16) // 菜单Y坐标的最大值 + typedef struct MenuItem MenuItem; struct MenuItem { diff --git a/src/main.c b/src/main.c index 40e9baf..fcb56d0 100644 --- a/src/main.c +++ b/src/main.c @@ -93,7 +93,6 @@ int main(void) printf("开始初始化菜单树...\n"); Menu_Init(); /* 初始化菜单树(运行界面/定值设置/查看数据等) */ - Menu_Show_0Level(); printf("PC 端 HMI 菜单模拟启动(TCP 服务在单独线程,端口 7003)。\n"); /* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */ if (StartTcpServerThread(7003, &server_sock, &server_quit, &server_th) != 0)