修改了菜单按键的逻辑,使菜单可以根据按键动态跳转,但是还没有实现对应的操作逻辑

This commit is contained in:
2026-03-12 18:01:57 +08:00
parent fb1a28df00
commit c2ce221691
13 changed files with 4241 additions and 457 deletions

View File

@@ -22,6 +22,7 @@ add_executable(DTU-HMI
src/remoteDisplay.c src/remoteDisplay.c
src/Drv/menu.c src/Drv/menu.c
src/Drv/lcd.c src/Drv/lcd.c
src/Drv/key.c
src/Drv/Ascii.c src/Drv/Ascii.c
src/Drv/display.c src/Drv/display.c
src/TCP/tcp.c src/TCP/tcp.c

358
README.md
View File

@@ -1,10 +1,18 @@
# DTU-HMI # DTU-HMI
PC 端 HMI 菜单逻辑模拟程序,纯 C 实现,支持菜单树、LCD 显示、TCP 远程显示RemoDispBus 协议)及 UTF-8 汉字库 PC 端 HMI 菜单逻辑模拟程序,纯 C 实现,用于在电脑上模拟现场 DTU 设备的人机界面HMI行为
- **菜单树**:按真实装置菜单还原,支持多级嵌套、首尾成环遍历
- **LCD 显示**:模拟 128×64 单色 LCD点阵缓冲区 + 绘制函数)
- **键盘输入**:用 PC 键盘按键映射嵌入式按键(上/下/左/右/确认/退出)
- **TCP 远程显示**:实现 RemoDispBus 协议,可把 LCD 显示通过网络推送到上位机
- **UTF8 汉字库**:内置 12×12 点阵汉字库,可扩展
--- ---
## 快速开始 ## 1. 快速开始(构建 & 运行)
### 1.1 构建可执行程序
```powershell ```powershell
mkdir build mkdir build
@@ -13,11 +21,24 @@ cmake ..
cmake --build . cmake --build .
``` ```
生成可执行文件 `DTU-HMI.exe`Windows`DTU-HMI`Linux 生成可执行文件
--- - Windows`build/DTU-HMI.exe`
- Linux/macOS`build/DTU-HMI`
## 目录结构 ### 1.2 直接在本机运行
`build` 目录中执行:
```powershell
./DTU-HMI.exe # Windows PowerShell
./DTU-HMI # Linux / macOS
```
终端中会出现类似“LCD 文本界面 + 菜单内容”的输出,使用键盘控制菜单。
## 3. 目录结构
``` ```
DTU-HMI/ DTU-HMI/
@@ -42,7 +63,7 @@ DTU-HMI/
--- ---
## 环境要求 ## 4. 环境要求
- **CMake** 3.10+ - **CMake** 3.10+
- **编译器**Windows 默认 MSVCLinux 需 GCC/Clang - **编译器**Windows 默认 MSVCLinux 需 GCC/Clang
@@ -50,9 +71,9 @@ DTU-HMI/
--- ---
## 构建步骤 ## 5. 构建步骤
### Windows ### 5.1 Windows
```powershell ```powershell
mkdir build mkdir build
@@ -61,7 +82,7 @@ cmake ..
cmake --build . cmake --build .
``` ```
### Linux / macOS ### 5.2 Linux / macOS
```bash ```bash
mkdir build mkdir build
@@ -72,21 +93,47 @@ cmake --build .
--- ---
## TCP 通信 ## 6. TCP 远程显示RemoDispBus
程序监听端口 **7003**RemoDispBus 默认)。使用 `remo_disp_server.py` 连接后可实时查看 LCD 显存画面。协议支持 `CMD_INIT``CMD_LCDMEM``CMD_KEY``CMD_KEEPLIVE` 程序内部通过 `remoteDisplay.c` + `tcp.c` 实现 **RemoDispBus** 协议通讯,可把 LCD 显示内容实时推送给远程上位机
- 默认监听 **端口7003**
- 协议命令:
- `CMD_INIT`:初始化/握手
- `CMD_LCDMEM`:下发/上传 LCD 显存缓冲区
- `CMD_KEY`:上位机模拟按键下发给设备
- `CMD_KEEPLIVE`:心跳保持连接
### 6.1 使用 Python 远程显示服务端
在 PC 上运行模拟程序的同时,可以用仓库中的 `remo_disp_server.py` 来查看 LCD 的显示图像:
```bash
python remo_disp_server.py # 需要 Python 3 + 标准库,具体依赖参考脚本头部说明
```
脚本会作为 **“远程显示上位机”**
- 主动连接 DTU-HMI 程序监听的端口(默认 7003
- 接收并解析 `CMD_LCDMEM`,在一个窗口中实时绘制 LCD 内容
- 也可以把 GUI 上的按键事件转换为 `CMD_KEY` 下发给 DTU-HMI 程序
--- ---
## 菜单系统 ## 7. 菜单系统
### 1. 概述 ### 7.1 概述
菜单由**静态表** `g_tMenuModelTab` 定义,经 `Menu_Main_Creat_01` 构建为**可遍历树** `g_tMenuItem[]`每个结点有 `ptHigher/ptLower/ptBefore/ptBehind` 四个指针,同级首尾成环。 菜单由**静态表** `g_tMenuModelTab` 定义,经 `Menu_Main_Creat_01` 构建为**可遍历树** `g_tMenuItem[]`
运行时由 `menu.c` 负责:
### 2. 数据结构 - 把静态表解析成**树 + 循环双向链表**
- 根据按键事件更新当前指针 `g_tMenuCtrl.ptCurrent`
- 调用一系列“坐标计算函数”给每一级菜单计算矩形框位置
- 最终调用 `display.c` / `lcd.c` 完成绘制
#### 2.1 静态菜单定义tagMenuModel ### 7.2 数据结构
#### 7.2.1 静态菜单定义(`tagMenuModel`
```c ```c
typedef struct typedef struct
@@ -104,7 +151,7 @@ typedef struct
const tagMenuModel g_tMenuModelTab[] = { ... }; const tagMenuModel g_tMenuModelTab[] = { ... };
``` ```
#### 2.2 菜单遍历树tagMenuItem #### 7.2.2 菜单遍历树(`tagMenuItem`
```c ```c
/* 每个菜单包含:一、上下前后等级关系;二、属性与内容;三、显示坐标 */ /* 每个菜单包含:一、上下前后等级关系;二、属性与内容;三、显示坐标 */
@@ -130,9 +177,9 @@ typedef struct _MENU_ITEM_
tagMenuItem g_tMenuItem[300]; /* 所有菜单存储于此数组 */ tagMenuItem g_tMenuItem[300]; /* 所有菜单存储于此数组 */
``` ```
### 3. 菜单构建示例 ### 7.3 菜单构建示例
#### 3.1 静态表g_tMenuModelTab #### 7.3.1 静态表(`g_tMenuModelTab`
```c ```c
const tagMenuModel g_tMenuModelTab[] = const tagMenuModel g_tMenuModelTab[] =
@@ -201,7 +248,7 @@ const tagMenuModel g_tMenuModelTab[] =
}; };
``` ```
#### 3.2 解析后的树形结构 #### 7.3.2 解析后的树形结构
根据上表,`Menu_Main_Creat_01` 解析后的菜单树为: 根据上表,`Menu_Main_Creat_01` 解析后的菜单树为:
@@ -281,7 +328,7 @@ const tagMenuModel g_tMenuModelTab[] =
} }
``` ```
#### 3.3 单例解析示例:直流量 #### 7.3.3 单例解析示例:直流量
`{2,"直流量","查看遥测直流量",EN_MEA_DC,0x0000,EN_ANA_0,(FUNCPTR)MenuProc_See_YC}` 为例,构建后该菜单项的指针与属性为: `{2,"直流量","查看遥测直流量",EN_MEA_DC,0x0000,EN_ANA_0,(FUNCPTR)MenuProc_See_YC}` 为例,构建后该菜单项的指针与属性为:
@@ -302,7 +349,7 @@ const tagMenuModel g_tMenuModelTab[] =
} }
``` ```
#### 3.4 同级首尾成环 #### 7.3.4 同级首尾成环
同级菜单中,首尾通过 `ptBefore/ptBehind` 相连形成环。例如 2 级子菜单: 同级菜单中,首尾通过 `ptBefore/ptBehind` 相连形成环。例如 2 级子菜单:
@@ -322,11 +369,11 @@ const tagMenuModel g_tMenuModelTab[] =
- `2.虚拟遥信``ptBefore` 指向 `2.信号出口`(首的前一个是尾) - `2.虚拟遥信``ptBefore` 指向 `2.信号出口`(首的前一个是尾)
- `2.信号出口``ptBehind` 指向 `2.虚拟遥信`(尾的后一个是首) - `2.信号出口``ptBehind` 指向 `2.虚拟遥信`(尾的后一个是首)
### 4. 构建流程Menu_Main_Creat_01 ### 7.4 构建流程(`Menu_Main_Creat_01`
`g_tMenuModelTab` 的定义顺序逐项处理,通过比较 `byCurClass``byNextClass` 设置每个菜单的层级指针。 `g_tMenuModelTab` 的定义顺序逐项处理,通过比较 `byCurClass``byNextClass` 设置每个菜单的层级指针。
#### 情况 1byCurClass < byNextClass下一项更深进入子菜单 #### 7.4.1 情况 1`byCurClass < byNextClass`(下一项更深,进入子菜单)
```text ```text
ptCurrent ptNextNode ptCurrent ptNextNode
@@ -338,7 +385,7 @@ const tagMenuModel g_tMenuModelTab[] =
该级尾不变 新一级的 首=尾=ptNextNode 该级尾不变 新一级的 首=尾=ptNextNode
``` ```
#### 情况 2byCurClass == byNextClass同级并列 #### 7.4.2 情况 2`byCurClass == byNextClass`(同级,并列)
```text ```text
ptCurrent ─── ptBehind ──► ptNextNode ptCurrent ─── ptBehind ──► ptNextNode
@@ -349,7 +396,7 @@ const tagMenuModel g_tMenuModelTab[] =
该级 尾 更新为 ptNextNode 该级 尾 更新为 ptNextNode
``` ```
#### 情况 3byCurClass > byNextClass下一项更浅回到上层 #### 7.4.3 情况 3`byCurClass > byNextClass`(下一项更浅,回到上层)
```text ```text
... byCurClass 级 ... byNextClass 级 ... ... byCurClass 级 ... byNextClass 级 ...
@@ -363,7 +410,7 @@ const tagMenuModel g_tMenuModelTab[] =
ptLast[级]──►ptFirst[级]ptFirst[级]──►ptLast[级] ptLast[级]──►ptFirst[级]ptFirst[级]──►ptLast[级]
``` ```
#### 最后:各级首尾成环 #### 7.4.4 最后:各级首尾成环
表遍历完后,从 **0 级到当前结点所在级**,把该级首尾连成环: 表遍历完后,从 **0 级到当前结点所在级**,把该级首尾连成环:
@@ -373,3 +420,260 @@ const tagMenuModel g_tMenuModelTab[] =
└──── ptBehind ───────────────┘ └──── ptBehind ───────────────┘
◄──────── ptBefore ────────────┘ ◄──────── ptBefore ────────────┘
``` ```
### 7.5 `menu.c` 运行时整体逻辑
`menu.c` 在运行时主要做三件事:
1. **构建菜单树**`Menu_Main_Creat` / `Menu_0LevelNumCal`
2. **根据按键路由当前菜单**`Menu_Route`
3. **计算坐标并显示**`Menu_Show_Proc` + 一系列 `Menu_*PosCal` 函数
可以把主循环理解为下面这个“数据流”:
```text
键盘按键 (key.c) ──► 菜单路由 (Menu_Route)
当前菜单指针 g_tMenuCtrl.ptCurrent
坐标计算 (Menu_Sub1PosCal / Menu_PosCal_0Level ...)
显示绘制 (Menu_Show_Proc + display.c/lcd.c)
```
#### 7.5.1 主循环 & 菜单刷新时序
`main.c` 的主循环中,大致时序可抽象成:
```text
while (1) {
// 1. 采集按键状态key.c
Key_Scan();
// 2. 依据当前按键更新菜单当前位置menu.c
Menu_Route();
// 3. 根据当前位置和菜单树,计算各级菜单的坐标
Menu_Show_Proc();
// 4. 刷新 LCD 显示lcd.c / display.c
LCD_Refresh();
}
```
你可以把 **`Menu_Route` 看成“控制层”**,把 **`Menu_Show_Proc` 看成“视图层布局计算”**。
#### 7.5.2 按键如何驱动菜单移动(`Menu_Route`
`Menu_Route` 的核心思想是:
**根据按键,沿着当前层的循环链表移动 `ptCurrent`,或在父/子指针之间跳转。**
简化后的逻辑可以画成下面这样的**状态机图**
```text
┌─────────────┐
│ 当前菜单项 │ (g_tMenuCtrl.ptCurrent)
└─────┬───────┘
▲ 上键/W │ 下键/S ▼
│ │
│ │
ptBefore ptBehind
(同级上一个) (同级下一个)
左键/A 或 退出键/Esc 右键/D 或 Enter
│ │
▼ ▼
ptHigher(父菜单) ptLower(子菜单)
```
更具体地:
- **上键 / `W`**
- `ptCurrent = ptCurrent->ptBefore;`
- 由于同级首尾成环,**从首再往上就会回到尾**
- **下键 / `S`**
- `ptCurrent = ptCurrent->ptBehind;`
- 从尾再往下会回到首,实现循环菜单
- **右键 / `D``Enter`**
- 若当前有子菜单:`ptCurrent = ptCurrent->ptLower;`
- 同时更新 `g_tMenuCtrl.ptRoute[层级]`,记录层级路径
- **左键 / `A``Esc`**
- 若存在上级:`ptCurrent = ptCurrent->ptHigher;`
- 层级回退,路径栈 `ptRoute[]` 随之更新
伪代码示意(省略防抖、长按等细节):
```c
void Menu_Route(void)
{
tagPMenuItem ptCur = g_tMenuCtrl.ptCurrent;
if (KEY_UP_PRESSED()) {
ptCur = ptCur->ptBefore;
} else if (KEY_DOWN_PRESSED()) {
ptCur = ptCur->ptBehind;
} else if (KEY_RIGHT_PRESSED() || KEY_ENTER_PRESSED()) {
if (ptCur->ptLower != NULL) {
ptCur = ptCur->ptLower;
}
} else if (KEY_LEFT_PRESSED() || KEY_ESC_PRESSED()) {
if (ptCur->ptHigher != NULL) {
ptCur = ptCur->ptHigher;
}
}
g_tMenuCtrl.ptCurrent = ptCur;
}
```
实际代码里还会配合 `ptCurBak` 判断“是否移动了”,以决定是否需要重算坐标和刷新显示。
#### 7.5.3 坐标字段含义(`wSPosX/Y`、`wEPosX/Y`
`tagMenuItem` 中与坐标相关的字段:
- `wSPosX, wSPosY`**本菜单的下一级子菜单框的左上角坐标**
- `wEPosX, wEPosY`**本菜单的下一级子菜单框的右下角坐标**
可以类比成:
```text
┌──────────────── LCD 屏幕 ────────────────┐
│ │
│ ┌───── 本级菜单框 (父) ─────┐ │
│ │ │ │
│ │ ● 当前高亮菜单项 │ │
│ │ │ │
│ └───────────────────────────┘ │
│ ▲ wSPosY │
│ │ │
│ ▼ wEPosY │
│ ┌───── 子菜单框 (ptLower) ─────┐ │
│ │ ← wSPosX → wEPosX │ │
│ │ 子菜单项1 │ │
│ │ 子菜单项2 │ │
│ │ ... │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────┘
```
这样设计的好处是:
**每个菜单项都“携带”了自己的子菜单应该出现在哪一块区域的信息**,绘制时只需要看当前指针和这些坐标就能把所有框画出来。
#### 7.5.4 菜单位置如何计算(以 `Menu_Sub1PosCal` 为例)
用于计算二级菜单(以及更深层子菜单)矩形框位置的典型函数是 `Menu_Sub1PosCal`,高度概括为:
1. 统计当前菜单的**同级菜单数量**和**下一级菜单的最大标题长度**
2. 根据父菜单的框位置 + 当前菜单在本层中的序号,确定 Y 起点
3. 根据子菜单项数和行高 `LINE_HEIGHT`,确定 Y 终点,并做“是否超屏”判断
4. 若超出底部,则整体向上平移一段,避免越界
简化后的“计算流程图”:
```text
输入:
- bylevel 当前层级(例如 1 表示一级菜单)
- ptFirst[bylevel] 本级菜单首节点
- ptIndex[bylevel] 遍历用当前指针
步骤:
1) 统计下一级菜单:
byMaxLen = Menu_charLenCal(...)
→ 得到最长标题字符数 byMaxLen
→ 得到各层级菜单项数 byMenuNum[]
2) 计算 X 方向:
wSPosX = 父菜单 wEPosX
wEPosX = wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD
3) 计算 Y 起点:
byMenuPos = 当前菜单在本级中的位置 (wPos)
byItemNum = 本级菜单总项数 (byMenuNum[bylevel])
如本级菜单超过一屏,修正 byMenuPos 做翻页映射
wSPosY = 父菜单 wSPosY + (byMenuPos - 1) * LINE_HEIGHT
4) 计算 Y 终点:
byItemNum = 子菜单项数 (byMenuNum[bylevel + 1])
wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD
5) 越界修正:
如果 wEPosY > MENU_YMAX
a) 尝试从当前项往上展开,使尾部对齐当前项
b) 若仍越界,则限制在 [0, MENU_YMAX] 范围内
6) 写回结果:
ptIndex[bylevel]->wSPosX/Y = wSPosX/Y
ptIndex[bylevel]->wEPosX/Y = wEPosX/Y
```
可以把它想象成:
**“拿到父菜单的框,把子菜单框贴在它右侧,再根据当前高亮位置往上或往下展开一串子项,同时保证整块框不会跑出屏幕。”**
#### 7.5.5 多级菜单同时显示时的布局
当当前指针在三级菜单时,屏幕上通常会同时显示:
```text
┌──────── 0 级菜单框 ────────┐
│ ... │
└───────────────────────────┘
┌──────── 1 级菜单框 ────────┐
│ ... 当前所在的一级菜单 │
└───────────────────────────┘
┌── 2 级菜单框 ──┐
│ 子项1 │
│ 子项2(高亮) │ ← g_tMenuCtrl.ptCurrent
│ 子项3 │
└──────────────┘
```
`Menu_Show_Proc` 会依次遍历 `g_tMenuCtrl.ptRoute[]` 中记录的每一级菜单指针:
1. 对每一级调用对应的“位置计算函数”0 级用 `Menu_0LevelPosCal`1/2/3 级用 `Menu_Sub1PosCal` 等)
2. 根据计算出的矩形框坐标,在 `display.c` 中画出边框和文字内容
3. 对当前高亮项增加反显/反色效果
这样即可实现类似真实 HMI 上的“多级弹出菜单”效果。
---
## 9. UTF8 汉字库12×12 点阵)
项目中通过 `utf8_hz12_data.c/h` 存储 12×12 点阵的汉字库,供 LCD 绘制函数使用。
- 当需要**增加新的汉字**时,可以:
1. 修改或扩展 `gen_utf8_hz12.py` 中的字符集/输入文本
2. 运行脚本,重新生成 `utf8_hz12_data.c/h`
3. 重新编译项目
- LCD 显示模块会在绘制字符串时,根据 UTF8 编码在该表中查找对应点阵。
具体使用方式可以参考:
- `src/Drv/Ascii.c`ASCII/字母数字字符绘制
- `src/Drv/lcd.c`LCD 基础绘制接口
---
## 10. 调试与日志
- CMake 中提供了一个 `ENABLE_DEBUG` 选项,用于开启调试输出:
```bash
cmake -DENABLE_DEBUG=ON ..
cmake --build .
```
- 打开后会自动定义 `DEBUG` 宏,部分代码(例如 `Menu_Sub1PosCal` 等)会通过 `printf` 输出调试信息,方便观察菜单坐标计算、页滚动等细节。
如果你希望在调试时看到更多状态信息,可以:
- 在关键流程(如 `Menu_Route`、`Menu_Show_Proc`、`remoteDisplay` 收发函数)增加 `#ifdef DEBUG` 包裹的日志
- 使用 GDB / VS 调试器在 `main.c`/`menu.c` 等处打断点,单步查看菜单树和坐标计算过程

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ extern uint8_t g_abyASCII168[][16];
#define UTF8_HZ12_BYTES_PER_CHAR 24 #define UTF8_HZ12_BYTES_PER_CHAR 24
#define UTF8_HZ12_NUM_CHARS 4030 #define UTF8_HZ12_NUM_CHARS 7038
/* 按 Unicode 码点查找点阵,返回 24 字节指针,未找到返回 NULL */ /* 按 Unicode 码点查找点阵,返回 24 字节指针,未找到返回 NULL */
const uint8_t* utf8_hz12_get(uint32_t unicode); const uint8_t* utf8_hz12_get(uint32_t unicode);

View File

@@ -1,34 +1,46 @@
/* 远程按键驱动模块:
* 按键数据由外部(如远程显示、串口等)写入 g_tRemoteKey
* 本模块通过 Key_Read() 供菜单等上层逻辑读取。
*/
#include "key.h" #include "key.h"
tagRKeyCtrl g_tRemoteKey; /* 远程按键控制结构 */
/******************************************************************************
typedef struct{ // 按键控制结构 * 函数名: Key_Init
uint8_t byUsrFlg; // 用户标志0x55标志 有显示已刷新,供远程显示用 * 功能: 按键模块初始化,将远程按键控制结构体复位为默认状态
uint8_t byKeyValid; // 有效标志 <见 _KEY_VALID_FLAG_ 定义> * 参数: 无
uint8_t byKeyValue; // 键值 * 返回: 无
uint8_t bUseRkey; //是否启用远程按键 * 说明: 清除有效标志、键值置 KEY_NONE、关闭远程按键使能
}tagRKeyCtrl; *****************************************************************************/
void Key_Init(void)
tagRKeyCtrl g_tRemoteKey; //远程按键
uint8_t Key_Read()
{ {
uint8_t byKeyTmp; g_tRemoteKey.byKeyValid = EN_KEY_FLAG_NULL; /* 无新按键 */
g_tRemoteKey.byKeyValue = KEY_NONE; /* 键值清零 */
g_tRemoteKey.bUseRkey = 1; /* 不启用远程按键 */
}
/******************************************************************************
* 函数名: Key_Read
* 功能: 读取当前按键值(消费式读取)
* 参数: 无
* 返回: 按键值KEY_U/KEY_D/KEY_L/KEY_R/KEY_ENT/KEY_ESC/KEY_F1/KEY_F2 等),
* 无按键时返回 KEY_NONE
* 说明: 若存在新按键则返回键值并清除有效标志;每次调用最多消费一个按键事件
*****************************************************************************/
uint8_t Key_Read(void)
{
uint8_t byKeyTmp; /* 本次读取到的键值 */
byKeyTmp = KEY_NONE; byKeyTmp = KEY_NONE;
if(EN_KEY_FLAG_NEW == g_tRemoteKey.byKeyValid ) if (EN_KEY_FLAG_NEW == g_tRemoteKey.byKeyValid)
{ {
byKeyTmp = g_tRemoteKey.byKeyValue; byKeyTmp = g_tRemoteKey.byKeyValue; /* 取出键值 */
g_tRemoteKey.byKeyValid = EN_KEY_FLAG_NULL; g_tRemoteKey.byKeyValid = EN_KEY_FLAG_NULL; /* 消费后清除标志 */
} }
return byKeyTmp; return byKeyTmp;
} }

View File

@@ -25,7 +25,15 @@ enum _KEY_VALID_FLAG_ { // 按键是否有效标志
EN_KEY_FLAG_SAM, // 采样过程中(未确定) EN_KEY_FLAG_SAM, // 采样过程中(未确定)
}; };
typedef struct{ // 按键控制结构
uint8_t byUsrFlg; // 用户标志0x55标志 有显示已刷新,供远程显示用
uint8_t byKeyValid; // 有效标志 <见 _KEY_VALID_FLAG_ 定义>
uint8_t byKeyValue; // 键值
uint8_t bUseRkey; //是否启用远程按键
}tagRKeyCtrl;
uint8_t Key_Read(); extern tagRKeyCtrl g_tRemoteKey; //远程按键
uint8_t Key_Read(void);
void Key_Init(void);
#endif #endif

View File

@@ -1,7 +1,12 @@
#include "lcd.h" #include "lcd.h"
#include <string.h> #include <string.h>
#include "ascii.h" #include "ascii.h"
#include <math.h> #include <math.h>
#ifdef DEBUG
#include <stdio.h>
#endif
tagScreenControl g_tCVsr; // 当前界面结构指针 tagScreenControl g_tCVsr; // 当前界面结构指针
tagDspAttrib g_tDspAttrib; // 显示属性 tagDspAttrib g_tDspAttrib; // 显示属性
@@ -14,16 +19,17 @@ void Lcd_Init(void)
g_tCVsr.wBackColor = LCD_COLOR_BLACK; g_tCVsr.wBackColor = LCD_COLOR_BLACK;
/* 设置默认字体 */ /* 设置默认字体 */
//g_tCVsr.pwLibHZ = (uint16_t*)HZK12; //g_tCVsr.pwLibHZ = (uint16_t*)HZK12;
g_tCVsr.wGBFontWidth = 14; /* 字体的大小需要调试 */
g_tCVsr.wGBFontWidth = 13;
g_tCVsr.wGBFontHeight = 12; g_tCVsr.wGBFontHeight = 12;
/* 设置默认ASCII字体 */ /* 设置默认ASCII字体 */
g_tCVsr.pbyLibAscii = g_abyASCII126[0]; g_tCVsr.pbyLibAscii = g_abyASCII126[0];
g_tCVsr.wASCIIFontWidth = 8; g_tCVsr.wASCIIFontWidth = 7;
g_tCVsr.wASCIIFontHeight = 12; g_tCVsr.wASCIIFontHeight = 12;
g_tDspAttrib.rowSpace = 2; g_tDspAttrib.rowSpace = 2;
} }
void Lcd_SetPixel(uint16_t x, uint16_t y, uint16_t color) void Lcd_SetPixel(uint16_t x, uint16_t y, uint8_t color)
{ {
if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y) if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y)
{ {
@@ -86,7 +92,7 @@ void Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bo
* 2. 实际绘制区域为 [wXStart, wXEnd) × [wYStart, wYStart + wWidth) * 2. 实际绘制区域为 [wXStart, wXEnd) × [wYStart, wYStart + wWidth)
* 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor * 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor
*****************************************************************************/ *****************************************************************************/
void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint16_t color) void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color)
{ {
uint16_t wYEnd = wYStart + wWidth; /* 计算水平线在 Y 方向的结束位置 = 起始 Y + 线宽 */ uint16_t wYEnd = wYStart + wWidth; /* 计算水平线在 Y 方向的结束位置 = 起始 Y + 线宽 */
@@ -114,7 +120,7 @@ void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWid
* 2. 实际绘制区域为 [wXStart, wXStart + wWidth) × [wYStart, wYEnd) * 2. 实际绘制区域为 [wXStart, wXStart + wWidth) × [wYStart, wYEnd)
* 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor * 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor
*****************************************************************************/ *****************************************************************************/
void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint16_t color) void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color)
{ {
uint16_t wXEnd = wXStart + wWidth; uint16_t wXEnd = wXStart + wWidth;
@@ -146,7 +152,7 @@ void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWid
* 2. 其它情况使用类 Bresenham 算法,从两端向中间对称绘制,并按线宽加粗 * 2. 其它情况使用类 Bresenham 算法,从两端向中间对称绘制,并按线宽加粗
* 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor * 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) void Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color)
{ {
uint16_t wDX; /* X 方向差值(水平偏移量的绝对值) */ uint16_t wDX; /* X 方向差值(水平偏移量的绝对值) */
uint16_t wDY; /* Y 方向差值(垂直偏移量的绝对值) */ uint16_t wDY; /* Y 方向差值(垂直偏移量的绝对值) */
@@ -226,7 +232,6 @@ void Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend
wDecision = (wDY >> 1); /* 初始化判别值为一半的 wDY */ wDecision = (wDY >> 1); /* 初始化判别值为一半的 wDY */
for (; wCury <= wNexty; ) /* 从两端向中间画,直到在 Y 方向相遇 */ for (; wCury <= wNexty; ) /* 从两端向中间画,直到在 Y 方向相遇 */
{ {
/* 累积误差超过一条“格子高度”时,说明需要在 X 方向跨一格 */ /* 累积误差超过一条“格子高度”时,说明需要在 X 方向跨一格 */
if (wDecision >= wDY) if (wDecision >= wDY)
{ {
@@ -343,7 +348,7 @@ inline uint16_t Lcd_Pub_Ascii(uint16_t x, uint16_t y, uint8_t byAscii)
{ {
uint8_t i, j; uint8_t i, j;
uint8_t byLine, *pbyFontLib; uint8_t byLine, *pbyFontLib;
uint16_t on_color, off_color; uint8_t on_color, off_color;
/* 从 ASCII 字库中取得当前字符的点阵数据首地址 /* 从 ASCII 字库中取得当前字符的点阵数据首地址
每个字符占用 wASCIIFontHeight 个字节, 按行存储 */ 每个字符占用 wASCIIFontHeight 个字节, 按行存储 */
@@ -379,7 +384,7 @@ inline uint16_t Lcd_Pub_Ascii(uint16_t x, uint16_t y, uint8_t byAscii)
for (i = 0; i < g_tCVsr.wASCIIFontWidth; i++) for (i = 0; i < g_tCVsr.wASCIIFontWidth; i++)
{ {
uint8_t bit_on = ((byLine << i) & 0x80) != 0; uint8_t bit_on = ((byLine << i) & 0x80) != 0;
uint16_t color; uint8_t color;
uint16_t px, py; uint16_t px, py;
if (bit_on) if (bit_on)
@@ -423,7 +428,7 @@ inline uint16_t Lcd_Pub_Ascii(uint16_t x, uint16_t y, uint8_t byAscii)
* @param out_unicode 输出该字符的 Unicode 码点U+0000..U+FFFF * @param out_unicode 输出该字符的 Unicode 码点U+0000..U+FFFF
* @return 该字符占用的字节数 1/2/30 表示结束、无效或无法解析 * @return 该字符占用的字节数 1/2/30 表示结束、无效或无法解析
*/ */
static int utf8_next(const unsigned char *utf8, uint32_t *out_unicode) uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode)
{ {
unsigned char c = utf8[0]; unsigned char c = utf8[0];
@@ -477,8 +482,12 @@ void Lcd_Pub_UTF8(uint16_t x, uint16_t y, uint32_t unicode )
const uint8_t *bitmap = utf8_hz12_get(unicode); const uint8_t *bitmap = utf8_hz12_get(unicode);
uint16_t word = 0; uint16_t word = 0;
if (bitmap == NULL) if (bitmap == NULL)
{
#ifdef DEBUG
printf("Lcd_Pub_UTF8: unicode = %d, bitmap = NULL\n", unicode);
#endif
return; return;
}
for (uint8_t j = 0; j < g_tCVsr.wGBFontHeight; j++) for (uint8_t j = 0; j < g_tCVsr.wGBFontHeight; j++)
{ {
word = (uint16_t)((bitmap[j*2] << 8) | bitmap[j*2+1]); word = (uint16_t)((bitmap[j*2] << 8) | bitmap[j*2+1]);
@@ -509,8 +518,7 @@ int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString)
uint16_t bakx, baky; /* 当前行行首坐标,换行时 x 回到 bakx */ uint16_t bakx, baky; /* 当前行行首坐标,换行时 x 回到 bakx */
uint32_t unicode; /* utf8_next 解析出的当前字符码点 */ uint32_t unicode; /* utf8_next 解析出的当前字符码点 */
uint16_t index = 0; /* 当前字符在 pcString 中的字节下标 */ uint16_t index = 0; /* 当前字符在 pcString 中的字节下标 */
int8_t err; uint8_t n = 0;
bakx = x; bakx = x;
baky = y; baky = y;
@@ -519,11 +527,10 @@ int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString)
return -1; return -1;
if (y >= LCD_SIZE_Y - g_tCVsr.wGBFontHeight) if (y >= LCD_SIZE_Y - g_tCVsr.wGBFontHeight)
return -2; return -2;
while (pcString[index] != 0x0) while (pcString[index] != 0x0)
{ {
/* 解析当前字符n = 占用字节数1=ASCII2/3=多字节unicode = 码点 */ /* 解析当前字符n = 占用字节数1=ASCII2/3=多字节unicode = 码点 */
int n = utf8_next(pcString + index, &unicode); n = utf8_next(pcString + index, &unicode);
if (n <= 0) if (n <= 0)
break; break;

View File

@@ -24,12 +24,12 @@
typedef struct typedef struct
{ {
uint8_t pwbyLCDMemory[LCD_DISPLAYMEMORYSIZE]; //定义显存 uint8_t pwbyLCDMemory[LCD_DISPLAYMEMORYSIZE]; //定义显存
uint16_t wFontColor; // 字体颜色 uint8_t wFontColor; // 字体颜色
uint16_t wBackColor; // 字符显示背景颜色 uint8_t wBackColor; // 字符显示背景颜色
uint16_t wGBFontWidth; // 汉字字体宽度 uint8_t wGBFontWidth; // 汉字字体宽度
uint16_t wGBFontHeight; // 汉字字体高度 uint8_t wGBFontHeight; // 汉字字体高度
uint16_t wASCIIFontWidth; // 字符字体宽度 uint8_t wASCIIFontWidth; // 字符字体宽度
uint16_t wASCIIFontHeight; // 字符字体高度 uint8_t wASCIIFontHeight; // 字符字体高度
uint16_t *pwLibHZ; // 汉字库地址 uint16_t *pwLibHZ; // 汉字库地址
uint8_t *pbyLibAscii; // ASCII库地址 uint8_t *pbyLibAscii; // ASCII库地址
} tagScreenControl; } tagScreenControl;
@@ -48,15 +48,16 @@ typedef struct { // 显示属性数据结构
void Lcd_Init(void); void Lcd_Init(void);
void Lcd_SetPixel(uint16_t x, uint16_t y, uint16_t color); void Lcd_SetPixel(uint16_t x, uint16_t y, uint8_t color);
uint16_t Lcd_GetPixel(uint16_t x, uint16_t y); 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_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_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_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_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_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color);
void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint16_t color); void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color);
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode);
int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString); 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); int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString);
#endif #endif

View File

@@ -4,6 +4,7 @@
#include <string.h> #include <string.h>
#include "lcd.h" #include "lcd.h"
#include "display.h" #include "display.h"
#include "key.h"
/* 简单的静态菜单树: /* 简单的静态菜单树:
* *
@@ -28,13 +29,13 @@
uint16_t wPos; /* 当前菜单在本级中的位置 */ uint16_t wPos; /* 当前菜单在本级中的位置 */
uint16_t wNum; /* 下级菜单的总项数 */ uint16_t wNum; /* 下级菜单的总项数 */
uint16_t wSPosX; // 下级菜单起始坐标 uint16_t wSPosX; /*下级菜单框 x 轴的起始坐标*/
uint16_t wSPosY; // 下级菜单起始坐标 uint16_t wSPosY; /*下级菜单框 y 轴的起始坐标*/
uint16_t wEPosX; // 下级菜单对角坐标 uint16_t wEPosX; /*下级菜单框 x 轴的结束坐标*/
uint16_t wEPosY; // 下级菜单对角坐标 uint16_t wEPosY; /*下级菜单框 y 轴的结束坐标*/
}tagMenuItem,*tagPMenuItem; }tagMenuItem,*tagPMenuItem;
// 显示控制结构 // 显示控制结构
typedef struct typedef struct
{ {
tagPMenuItem ptMenuCur; // 菜单当前指针 tagPMenuItem ptMenuCur; // 菜单当前指针
@@ -73,15 +74,15 @@ typedef struct
typedef struct typedef struct
{ // 菜单控制结构 { // 菜单控制结构
uint8_t by0LevelNum; // 0级菜单项数 uint8_t by0LevelNum; // 0级菜单项数
uint8_t byLeftMove; // 菜单左移长度 uint8_t byLeftMove; // 菜单左移长度
uint8_t bySecPage; // 第二页菜单标志 uint8_t bySecPage; // 第二页菜单标志
FUNCPTR fnExecute; // 执行函数指针 FUNCPTR fnExecute; // 执行函数指针
tagPMenuItem ptHead; // 菜单链表头指针 tagPMenuItem ptHead; // 菜单链表头指针
tagPMenuItem ptCurrent; // 菜单链表当前指针 tagPMenuItem ptCurrent; /* 菜单链表当前指针 */
tagPMenuItem ptRoute[4]; // 0-3级菜单的指针路径 tagPMenuItem ptRoute[4]; // 0-3级菜单的指针路径
tagPMenuItem ptCurBak; // 菜单链表指针备份 tagPMenuItem ptCurBak; /* ptCurrent 的备份通过比较ptCurBak和ptCurrent是否相等来判断是否发生移动 */
tagPMenuItem pt0Level; // 0级菜单链表指针备份 tagPMenuItem pt0Level; // 0级菜单链表指针备份
tagPMenuItem ptSeeSetSoft; // 查看定值菜单结点 tagPMenuItem ptSeeSetSoft; // 查看定值菜单结点
@@ -92,8 +93,8 @@ typedef struct
tagPMenuItem ptSetValPara; // 整定定值菜单结点 tagPMenuItem ptSetValPara; // 整定定值菜单结点
}tagMenuCtrl,*tagPMenuCtrl; }tagMenuCtrl,*tagPMenuCtrl;
tagDspCtrl g_tDspCtrl; // 显示控制全局结构 tagDspCtrl g_tDspCtrl; /* 显示控制全局结构 */
tagMenuCtrl g_tMenuCtrl; // 菜单全局控制结构 tagMenuCtrl g_tMenuCtrl; /* 菜单全局控制结构 */
tagMenuItem g_tMenuItem[300]; // 菜单链表空间定义 tagMenuItem g_tMenuItem[300]; // 菜单链表空间定义
@@ -242,6 +243,28 @@ void Menu_Main_Creat(void)
ptFirst[wLoop1]->ptBefore = ptLast[wLoop1]; ptFirst[wLoop1]->ptBefore = ptLast[wLoop1];
} }
} }
uint8_t utf8_len_cal(uint8_t *str)
{
uint8_t strLen = 0;
uint32_t unicode;
uint8_t index = 0;
uint8_t n = 0;
while (str[index] != '\0')
{
n = utf8_next(str + index, &unicode);
if (n > 1)
{
strLen += 2;
}
else
{
strLen++;
}
index += n;
}
return strLen;
}
/****************************************************************************** /******************************************************************************
* 函数名: Menu_charLenCal * 函数名: Menu_charLenCal
* 功能: 1.计算指定级菜单下所有子项的最大显示长度 * 功能: 1.计算指定级菜单下所有子项的最大显示长度
@@ -262,6 +285,7 @@ uint8_t Menu_charLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFir
{ {
uint16_t wLoop; uint16_t wLoop;
uint8_t byStrLen; /* 当前菜单项名称长度(含可能追加的右箭头) */ uint8_t byStrLen; /* 当前菜单项名称长度(含可能追加的右箭头) */
uint8_t utf8Len;
uint8_t byMaxLen; /* 同级菜单中的最大显示长度 */ uint8_t byMaxLen; /* 同级菜单中的最大显示长度 */
/* 记录子级首项,供后续遍历与布局使用 */ /* 记录子级首项,供后续遍历与布局使用 */
@@ -277,19 +301,19 @@ uint8_t Menu_charLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFir
for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) for (wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++)
{ {
/* 计算当前菜单项名称长度 */ /* 计算当前菜单项名称长度 */
utf8Len = utf8_len_cal(ptIndex[bylevel]->byName);
byStrLen = (uint8_t)strlen((const char *)ptIndex[bylevel]->byName); byStrLen = (uint8_t)strlen((const char *)ptIndex[bylevel]->byName);
/* 若有下级且名称末尾尚无右箭头,则追加 '\x10' 并更新长度 */ /* 若有下级且名称末尾尚无右箭头,则追加 '\x10' 并更新长度 */
if ((ptIndex[bylevel]->ptLower != NULL) && ('\x10' != ptIndex[bylevel]->byName[byStrLen - 1])) if ((ptIndex[bylevel]->ptLower != NULL) && ('\x10' != ptIndex[bylevel]->byName[byStrLen - 1]))
{ {
ptIndex[bylevel]->byName[byStrLen] = '\x10'; ptIndex[bylevel]->byName[byStrLen] = '\x10';
ptIndex[bylevel]->byName[byStrLen + 1] = '\0'; ptIndex[bylevel]->byName[byStrLen + 1] = '\0';
byStrLen += 1; utf8Len += 1;
} }
if (byMaxLen < byStrLen) if (byMaxLen < utf8Len)
{ {
byMaxLen = byStrLen; byMaxLen = utf8Len;
} }
#ifdef DEBUG #ifdef DEBUG
printf("计算%d级菜单位置:%s,长度%d\n", bylevel, ptIndex[bylevel]->byName, byMaxLen); printf("计算%d级菜单位置:%s,长度%d\n", bylevel, ptIndex[bylevel]->byName, byMaxLen);
@@ -319,7 +343,7 @@ void Menu_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptInd
uint16_t wTemp; uint16_t wTemp;
uint8_t byMaxLen; uint8_t byMaxLen;
uint8_t byMaxNum; uint8_t byMaxNum;
uint8_t byMenuPos; uint16_t byMenuPos;
uint8_t byItemNum; uint8_t byItemNum;
uint8_t byMenuNum[4]; uint8_t byMenuNum[4];
@@ -392,55 +416,74 @@ void Menu_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptInd
} }
} }
/*
* 功能:根据当前菜单层级 bylevel计算该层级所有子菜单如二级菜单的矩形框位置。
* 主要负责:
* - 统计下一级菜单的最大字符长度用于确定菜单框的宽度X 方向)
* - 根据上一级菜单条目的位置以及当前条目在本列中的位置,计算菜单框的起始/结束 Y 坐标
* - 处理菜单项超过一屏时的翻页显示逻辑,保证菜单框不超出屏幕边界
*
* 参数说明:
* bylevel :当前要计算的菜单层级(例如 1 表示一级菜单2 表示二级菜单)
* ptFirst :保存各层级“同级菜单链表首节点”的指针数组
* ptIndex 在遍历过程中使用的“当前菜单指针数组”ptIndex[bylevel] 表示当前层级正在处理的菜单项
*/
void Menu_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex) void Menu_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex)
{ {
uint16_t wSPosX; uint16_t wSPosY; // 当前层级菜单框的纵向起始坐标Start Y
uint16_t wSPosY; uint16_t wEPosY; // 当前层级菜单框的纵向结束坐标End Y
uint16_t wEPosX; uint8_t byMenuNum[4]; // 各层级菜单项数量的临时缓存数组,供长度/行数计算使用
uint16_t wEPosY; uint8_t byMaxLen; // 下一级菜单条目中,最长标题的字符数,用来确定菜单框宽度
uint8_t byMenuNum[4]; uint8_t byMaxNum; // 在一屏竖直方向上最多能显示多少条菜单项
uint8_t byMaxLen; uint16_t byMenuPos; // 当前菜单在本层菜单链表中的序号位置(用于计算其在屏幕中的纵向位置)
uint8_t byMaxNum; uint8_t byItemNum; // 当前参与计算的“菜单项数量”(根据语境,既可能是当前层,也可能是子菜单层)
uint16_t byMenuPos; uint16_t wTemp; // 用于中间计算时的临时变量(例如交换起止坐标等)
uint8_t byItemNum;
uint16_t wTemp;
byMaxNum = (MENU_YMAX) / LINE_HEIGHT; // 页内纵向可显示菜单数 byMaxNum = (MENU_YMAX) / LINE_HEIGHT; // 在当前屏幕高度内,竖直方向上一页最多能显示菜单
ptIndex[bylevel] = ptFirst[bylevel]; ptIndex[bylevel] = ptFirst[bylevel]; // 从本层级菜单链表的首节点开始遍历
for (uint16_t wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) for (uint16_t wLoop = 0; wLoop < MENU_MAX_ITEM; wLoop++) // 遍历当前层级的所有菜单项(以最大项数为上限防止死循环)
{ {
if ((ptIndex[bylevel]->ptLower) != NULL) // 当前1级菜单有下级菜单 if ((ptIndex[bylevel]->ptLower) != NULL) // 如果当前菜单项存在下一级菜单(即有子菜单需要显示)
{ {
// 统计下一级菜单的条目数和每个条目的字符长度,返回该下一级中最长标题的字符数
// 同时,把各层级菜单数量写入 byMenuNum[] 中,用于后面计算 Y 坐标和分页
byMaxLen = Menu_charLenCal(bylevel, byMenuNum, ptFirst, ptIndex); // 计算2级菜单项数及字符数 byMaxLen = Menu_charLenCal(bylevel, byMenuNum, ptFirst, ptIndex); // 计算2级菜单项数及字符数
/*X坐标计算*/ /* X 坐标计算:二级(或更深层级)菜单的矩形框紧挨着上一级菜单的右侧展开 */
ptIndex[bylevel]->wSPosX = ptIndex[bylevel - 1]->wEPosX; // 2级菜单矩形框横坐标起始 ptIndex[bylevel]->wSPosX = ptIndex[bylevel - 1]->wEPosX; // 2级菜单矩形框横坐标起始 = 上一级菜单矩形框的结束 X
ptIndex[bylevel]->wEPosX = ptIndex[bylevel]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; // 2级菜单矩形框横坐标终止 ptIndex[bylevel]->wEPosX = ptIndex[bylevel]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; // 2级菜单矩形框横坐标终止 = 起始 X + 最大字符宽度 + 额外边框间距
byMenuPos = ptIndex[bylevel]->wPos; // 1级菜单在菜单列中的位置 byMenuPos = ptIndex[bylevel]->wPos; // 当前菜单在本层菜单链表中的逻辑位置(第几项)
byItemNum = byMenuNum[bylevel]; // 1级菜单项数 byItemNum = byMenuNum[bylevel]; // 本层菜单项数
// 当本层菜单总数超过一屏,且当前高亮菜单已经滚动到非第一页时,
// 需要对菜单在当前页中的显示位置进行折算,实现“↑/↓”翻页效果。
if ((byItemNum > byMaxNum) && (byMenuPos >= byMaxNum)) // 1级菜单项数大于一页且1级菜单当前位置不在第一页 if ((byItemNum > byMaxNum) && (byMenuPos >= byMaxNum)) // 1级菜单项数大于一页且1级菜单当前位置不在第一页
{ {
byMenuPos = byMaxNum - (byItemNum - byMenuPos); /* 第一页尾显示↓ 第二页头显示↑ */ byMenuPos = byMaxNum - (byItemNum - byMenuPos); /* 第一页尾显示↓ 第二页头显示↑ */
} }
// 根据“上一级菜单框的起始 Y 坐标”和“当前菜单位于本层中的序号”,
// 计算出当前子菜单框在 Y 方向上的起始位置,使子菜单在对应父菜单项的右侧对齐。
wSPosY = ptIndex[bylevel - 1]->wSPosY + (byMenuPos - 1) * LINE_HEIGHT; // 计算2级菜单框起始坐标 wSPosY = ptIndex[bylevel - 1]->wSPosY + (byMenuPos - 1) * LINE_HEIGHT; // 计算2级菜单框起始坐标
byItemNum = byMenuNum[bylevel + 1]; // 2级菜单项数 byItemNum = byMenuNum[bylevel + 1]; // 下一级(子菜单项数
wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 wEPosY = wSPosY + byItemNum * LINE_HEIGHT + MENU_YADD; // 计算2级菜单框终止坐标 = 起始 Y + 所有子项高度 + 边框间距
#ifdef DEBUG #ifdef DEBUG
printf("%d %d %d %d \n", bylevel, byItemNum, wSPosY, wEPosY); printf("%d %d %d %d \n", bylevel, byItemNum, wSPosY, wEPosY);
#endif #endif
// 情况 1子菜单框整体没有超过屏幕的最大 Y 边界,直接采用计算结果
if (wEPosY < MENU_YMAX) /* 菜单框没有超出边界 */ if (wEPosY < MENU_YMAX) /* 菜单框没有超出边界 */
{ {
ptIndex[bylevel]->wSPosY = wSPosY; // 2级菜单矩形框纵起始坐标定位 ptIndex[bylevel]->wSPosY = wSPosY; // 2级菜单矩形框纵起始坐标定位
ptIndex[bylevel]->wEPosY = wEPosY; // 2级菜单矩形框纵终止坐标定位 ptIndex[bylevel]->wEPosY = wEPosY; // 2级菜单矩形框纵终止坐标定位
} }
else /* 菜单框超出边界 */ else /* 情况 2子菜单框在向下展开时超出了屏幕底部需要整体向上“提”一段 */
{ {
// 反向计算:尝试让子菜单从当前高亮项“往上展开”,
// 使最后一项与当前父菜单项对齐,从而避免超出底部。
wEPosY = wSPosY - (byItemNum - 1) * LINE_HEIGHT - MENU_YADD; wEPosY = wSPosY - (byItemNum - 1) * LINE_HEIGHT - MENU_YADD;
if ((wEPosY > LINE_HEIGHT) && (wEPosY < MENU_YMAX)) // 菜单向上不越限 if ((wEPosY > LINE_HEIGHT) && (wEPosY < MENU_YMAX)) // 菜单向上不越限
{ {
@@ -497,6 +540,7 @@ void Menu_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptInd
* 3. 下拉框宽度由 Menu_charLenCal 返回的 byMaxLen 与字宽 CN_WITDTH 决定 * 3. 下拉框宽度由 Menu_charLenCal 返回的 byMaxLen 与字宽 CN_WITDTH 决定
* 4. 若下拉框超出屏幕顶部wEPosY < CN_YMAX则 wEPosY 取屏高,需分页显示 * 4. 若下拉框超出屏幕顶部wEPosY < CN_YMAX则 wEPosY 取屏高,需分页显示
* 5. 2 级及以下子菜单坐标由 Menu_Sub1PosCal 递归计算 * 5. 2 级及以下子菜单坐标由 Menu_Sub1PosCal 递归计算
* 注意:当前还有一个 bug没有检查坐标的上下界导致按照上下界清屏会清不干净
*****************************************************************************/ *****************************************************************************/
void Menu_PositionCal(tagPMenuItem ptMenuHead) void Menu_PositionCal(tagPMenuItem ptMenuHead)
{ {
@@ -521,18 +565,21 @@ void Menu_PositionCal(tagPMenuItem ptMenuHead)
if (ptIndex[0]->ptLower != NULL) /* 有下级菜单 */ if (ptIndex[0]->ptLower != NULL) /* 有下级菜单 */
{ {
/* 计算 1 级子项最大长度,并为其名称追加右箭头;更新 ptFirst/ptIndex/byMenuNum */ /* 计算 1 级子项最大长度,并为其名称追加右箭头;更新 ptFirst/ptIndex/byMenuNum */
byMaxLen = Menu_charLenCal(0, byMenuNum, ptFirst, ptIndex); byMaxLen = Menu_charLenCal(0, byMenuNum, ptFirst, ptIndex);
/* 左上角坐标计算 */ /* 左上角坐标计算 */
/* 1 级下拉框左上角:按 0 级序号横向定位,纵向从屏底向上预留多行高度 */ /* 1 级下拉框左上角:按 0 级序号横向定位,纵向从屏底向上预留多行高度 */
ptIndex[0]->wSPosX = (byMenuNum[0] - 1) * byInterval; ptIndex[0]->wSPosX = (byMenuNum[0] - 1) * byInterval;
ptIndex[0]->wSPosY = LCD_SIZE_Y - LINE_HEIGHT - byMenuNum[1] * LINE_HEIGHT - MENU_YADD; ptIndex[0]->wSPosY = LCD_SIZE_Y - LINE_HEIGHT - byMenuNum[1] * LINE_HEIGHT - MENU_YADD;
/* 右下角坐标计算 */ /* 右下角坐标计算 */
/* 下拉框右下角 X起始 + 最大字符数×字宽 + 边距 */ /* 下拉框右下角 X起始 + 最大字符数×字宽 + 边距 */
ptIndex[0]->wEPosX = ptIndex[0]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD; ptIndex[0]->wEPosX = ptIndex[0]->wSPosX + byMaxLen * MENU_WITDTH + MENU_XADD;
/* 下拉框右下角 Y与 0 级菜单底边齐平,若超出屏顶则取屏高(需分页) */ /* 下拉框右下角 Y与 0 级菜单底边齐平,若超出屏顶则取屏高(需分页) */
ptIndex[0]->wEPosY = LCD_SIZE_Y - LINE_HEIGHT; ptIndex[0]->wEPosY = LCD_SIZE_Y - LINE_HEIGHT;
#ifdef DEBUG
printf("Menu_PositionCal: wLoop = %d, byMaxLen = %d, ptIndex[0]->wEPosX = %d, ptIndex[0]->wEPosY = %d\n", wLoop, byMaxLen, ptIndex[0]->wEPosX, ptIndex[0]->wEPosY);
#endif
/* 递归计算 2 级及以下子菜单的显示坐标 */ /* 递归计算 2 级及以下子菜单的显示坐标 */
Menu_Sub1PosCal(1, ptFirst, ptIndex); Menu_Sub1PosCal(1, ptFirst, ptIndex);
@@ -563,9 +610,9 @@ void Menu_PositionCal(tagPMenuItem ptMenuHead)
void Menu_BoundaryBox(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y) 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-1X 从 left_x+1 到 right_x-1 */ /* 填充按钮内部区域Y 从 top_y+1 到 bottom_y-1X 从 left_x+1 到 right_x-1 */
for (uint16_t y = top_y + 1; y < bottom_y; y++) for (uint16_t y = top_y; y < bottom_y; y++)
{ {
for (uint16_t x = left_x + 1; x < right_x; x++) for (uint16_t x = left_x; x < right_x; x++)
{ {
Lcd_SetPixel(x, y, g_tCVsr.wBackColor); /* 设置按钮内部像素为前景色,实现实心效果 */ Lcd_SetPixel(x, y, g_tCVsr.wBackColor); /* 设置按钮内部像素为前景色,实现实心效果 */
} }
@@ -708,7 +755,6 @@ else if (byMenuNum > 20)
* 函数名: Menu_Show_Other * 函数名: Menu_Show_Other
* 功能: 显示除 0 级以外的其它级别菜单1 级 / 2 级 / 3 级)的下拉列表 * 功能: 显示除 0 级以外的其它级别菜单1 级 / 2 级 / 3 级)的下拉列表
* 参数: bylevel - 要显示的菜单层级1 表示 1 级菜单2 表示 2 级菜单等) * 参数: bylevel - 要显示的菜单层级1 表示 1 级菜单2 表示 2 级菜单等)
* byLeftMove - 菜单整体在 X 方向的左移像素数(用于横向滚动或对齐)
* 返回: 无 * 返回: 无
* 说明: 1. 使用 g_tMenuCtrl.ptRoute[bylevel] 获取本级菜单框坐标,并应用水平偏移 * 说明: 1. 使用 g_tMenuCtrl.ptRoute[bylevel] 获取本级菜单框坐标,并应用水平偏移
* 2. 调用 Menu_BoundaryBox 绘制当前级菜单的背景边框矩形 * 2. 调用 Menu_BoundaryBox 绘制当前级菜单的背景边框矩形
@@ -724,11 +770,9 @@ void Menu_Show_Other(uint8_t bylevel)
uint16_t wPosX; /* 菜单文本显示的起始 X 坐标 */ uint16_t wPosX; /* 菜单文本显示的起始 X 坐标 */
uint16_t wPosY; /* 菜单文本显示的起始 Y 坐标 */ uint16_t wPosY; /* 菜单文本显示的起始 Y 坐标 */
uint16_t byMenuNum; /* 本级菜单项总数,用于控制循环次数 */
uint16_t byMenuPos; /* 当前菜单项在本级中的序号1..byMenuNum */
uint8_t byName[50]; /* 临时缓冲区,用于拷贝菜单名称 */ uint8_t byName[50]; /* 临时缓冲区,用于拷贝菜单名称 */
/* 1. 基于当前层级路径结点,取得本级菜单框坐标并根据 byLeftMove 做水平偏移 */ /* 1. 获取当前层级路径结点,用于绘制本级菜单的背景边框矩形 */
ptIndex = g_tMenuCtrl.ptRoute[bylevel]; ptIndex = g_tMenuCtrl.ptRoute[bylevel];
/* 绘制本级菜单的背景边框矩形 */ /* 绘制本级菜单的背景边框矩形 */
Menu_BoundaryBox(ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY); Menu_BoundaryBox(ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY);
@@ -739,14 +783,16 @@ void Menu_Show_Other(uint8_t bylevel)
ptIndex = ptRoute; /* 从选中项开始向后遍历同级菜单 */ ptIndex = ptRoute; /* 从选中项开始向后遍历同级菜单 */
/* 3. 逐项绘制菜单文字,并对选中项所在行做反显处理 */ /* 3. 逐项绘制菜单文字,并对选中项所在行做反显处理 */
wPosX = ptIndex->wSPosX + 4; /* 文本相对左边框右移 4 像素,预留内边距 */ wPosX = g_tMenuCtrl.ptRoute[bylevel]->wSPosX + 4; /* 文本相对左边框右移 4 像素,预留内边距 */
for (uint16_t index = 0; index < g_tMenuCtrl.ptRoute[bylevel]->wNum; index++) 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 像素微调 */ wPosY = g_tMenuCtrl.ptRoute[bylevel]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3; /* 行顶坐标 + 行高 * 行号 + 3 像素微调 */
memcpy(byName, ptIndex->byName, 50); /* 将菜单名称拷贝到临时缓存 */ memcpy(byName, ptIndex->byName, 50); /* 将菜单名称拷贝到临时缓存 */
Lcd_ShowStr(wPosX, wPosY, byName); /* 在计算出的坐标位置显示菜单字符串 */ Lcd_ShowStr(wPosX, wPosY, byName); /* 在计算出的坐标位置显示菜单字符串 */
#ifdef DEBUG
if (ptRoute == ptIndex) /* 若当前绘制项为“选中项” */ printf("Menu_Show_Other: wPosX = %d, wPosY = %d, byName = %s\n", wPosX, wPosY, byName);
#endif
if(ptRoute == ptIndex) /* 若当前绘制项为“选中项” */
{ {
/* 对该行对应的矩形区域执行反显,用于高亮当前选择 */ /* 对该行对应的矩形区域执行反显,用于高亮当前选择 */
/* x轴起始位置+2结束位置-2产生内嵌的感觉 */ /* x轴起始位置+2结束位置-2产生内嵌的感觉 */
@@ -774,12 +820,222 @@ void Menu_Show_0Level()
Lcd_MeiTouPic(16, 2 ); Lcd_MeiTouPic(16, 2 );
Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" ); Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" );
} }
/******************************************************************************
* 函数名: Menu_Show_Proc
* 功能: 根据当前菜单状态刷新 0~3 级菜单显示(增量刷新或整体刷新)
* 情况一0 级路径发生变化(如 ESC 退回或跨列切换):需要整体刷新
* 情况二0 级路径未变,但当前结点发生变化
1. 同级内上下移动: 仅重画本级下拉菜单current->byClass-1 对应的菜单框索引)
2. 进层: 仅重画本级下拉菜单current->byClass-1 对应的菜单框索引)
3. 退层:清屏并重画 0~3 级菜单(这是最简单的操作,但是比较耗费资源)
所以1. 2. 的逻辑可以合并为仅重画本级下拉菜单current->byClass-1 对应的菜单框索引)
* 参数: 无(依赖全局 g_tMenuCtrl、g_tDspCtrl、g_tCVsr
* 返回: 无
* 说明: 该函数后续可以改进退层的逻辑,减少计算量
* 注意: 超出屏幕范围的逻辑还没有处理
*****************************************************************************/
void Menu_Show_Proc(void)
{
uint8_t bNeedFresh; /* 是否需要整体刷新 0~3 级菜单1=是0=否) */
#ifdef DEBUG
printf("Menu_Show_proc进入\n");
#endif
bNeedFresh = 0; /* 默认不整体刷新,仅增量更新 */
if (g_tDspCtrl.bFirst) /* 第一次进入菜单层标志 */
{
g_tDspCtrl.bFirst = 0; /* 清除首次标志 */
bNeedFresh = 1; /* 强制整体刷新 */
}
/* 情况一0 级路径发生变化(如 ESC 退回或跨列切换) */
if (g_tMenuCtrl.pt0Level != g_tMenuCtrl.ptRoute[0])
{
bNeedFresh = 1; /* 需要整体刷新 */
Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, g_tCVsr.wBackColor); /* 清除 1~3 级菜单区域 */
g_tMenuCtrl.pt0Level = g_tMenuCtrl.ptRoute[0]; /* 更新 0 级路径起点 */
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 更新备份指针 */
}
else if (g_tMenuCtrl.ptCurBak != g_tMenuCtrl.ptCurrent) /* 情况二0 级路径未变,但当前结点发生变化 */
{
if (g_tMenuCtrl.ptCurrent->byClass >= g_tMenuCtrl.ptCurBak->byClass) /* 2.1 同级内上下移动 或者 进层*/
{
#ifdef DEBUG
printf("同级内上下移动 或 进层:从 %d 级到 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass);
#endif
Menu_Show_Other(g_tMenuCtrl.ptCurrent->byClass - 1); /* 仅重画本级下拉菜单byClass-1 对应 1~3 级菜单框索引) */
/*由于刚开是已经判断了 0 层没有发生变化,因此 ptCurrent->byClass 一定是大于 1 的,所以不需要判断*/
}
else /* 2.2 级别减小(退层,如从 2 级退回 1 级) */
{
#ifdef DEBUG
printf("退层:从 %d 级退回 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass);
#endif
Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, g_tCVsr.wBackColor); /* 擦除 1~3 级区域(保留 0~32 像素标题栏) */
bNeedFresh = 1;
}
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 无论何种情况,更新备份指针为最新当前结点 */
}
/* 情况三:需要整体刷新 0~3 级菜单 */
if (bNeedFresh > 0)
{
Menu_Show_0Level(); /* 显示 0 级菜单 */
for (uint8_t index = 0; index < g_tMenuCtrl.ptCurrent->byClass; index++) /* 依次显示 1 级、2 级、3 级(直到当前层级) */
{
Menu_Show_Other(index);
}
}
#ifdef DEBUG
printf("Menu_Show_proc结束\n");
#endif
}
/******************************************************************************
* 函数名: Menu_Route
* 功能: 菜单层按键调度:读键并更新当前菜单结点与路径,必要时刷新显示
* 参数: 无
* 返回: 无
* 说明: 1. 首次进入时只调用 Menu_Show_Proc 显示当前路径
* 2. 上/下键同级移动;左键退上一级或上一列 0 级;右键/确认进入下级或执行叶子结点
* 3. ESC 在 1 级退回“主界面”并重算布局;在 2~3 级仅退上一级
* 4. 键值使用 key.h 中的 KEY_U/KEY_D/KEY_L/KEY_R/KEY_ENT/KEY_ESC/KEY_F1/KEY_F2
*****************************************************************************/
void Menu_Route(void)
{
tagPMenuItem ptCurrent; /* 当前光标所在菜单结点(可能被按键修改) */
tagPMenuItem ptHead; /* 0 级菜单头结点,用于 ESC 退回与路径重置 */
tagPMenuItem ptIndex; /* 路径回溯时的遍历指针 */
tagPMenuItem *ptRoute; /* 指向 g_tMenuCtrl.ptRoute便于按层级写入路径 */
uint8_t byKeyVal; /* 本次读到的键值KEY_U/KEY_D/KEY_L/KEY_R/KEY_ENT/KEY_ESC 等) */
uint8_t bNeedFresh; /* 本次按键是否导致需要重绘菜单1=是0=否) */
bNeedFresh = 0; /* 默认不刷新,仅当按键改变 ptCurrent 时置 1 */
ptCurrent = g_tMenuCtrl.ptCurrent; /* 取出当前选中的菜单结点 */
ptHead = g_tMenuCtrl.ptHead; /* 取出 0 级头结点 */
ptRoute = g_tMenuCtrl.ptRoute; /* 取出路径数组指针,用于更新 ptRoute[0..3] */
if (g_tDspCtrl.bFirst) /* 第一次进入菜单层:不读键,直接按当前路径刷新显示 */
{
Menu_Show_Proc(); /* 内部会清 bFirst 并绘制 0~当前级菜单 */
}
else /* 非首次:读键并根据键值更新 ptCurrent */
{
byKeyVal = Key_Read(); /* 从 Key_Read 获取键值(无键时为 KEY_NONE */
switch (byKeyVal)
{
case KEY_F1: /* F1 当前项目未使用,忽略 */
break;
case KEY_F2: /* F2 同上 */
break;
case KEY_U: /* 上键切换到同级上一项ptBefore */
ptCurrent = ptCurrent->ptBefore;
bNeedFresh = 1;
break;
case KEY_D: /* 下键切换到同级下一项ptBehind */
ptCurrent = ptCurrent->ptBehind;
bNeedFresh = 1;
break;
case KEY_L: /* 左键:退到上一级路径结点,若已在 0 级则退到上一列 0 级再进其下级 */
ptCurrent = ptRoute[ptCurrent->byClass - 1]; /* 先退到上一级路径结点 */
if (0 == ptCurrent->byClass) /* 若退完后是 0 级,则再退一列或两列并进下级 */
{
if (1 == ptCurrent->wPos) /* 当前在第 1 列:退两列再进下级 */
{
ptCurrent = ptCurrent->ptBefore->ptBefore;
ptCurrent = ptCurrent->ptLower;
}
else /* 其它列:退一列再进下级 */
{
ptCurrent = ptCurrent->ptBefore;
ptCurrent = ptCurrent->ptLower;
}
}
bNeedFresh = 1;
break;
case KEY_R: /* 右键与确认键统一处理 */
case KEY_ENT:
if (ptCurrent->ptLower != NULL) /* 有下级:进入下一层,光标移到下级首项 */
{
ptCurrent = ptCurrent->ptLower;
bNeedFresh = 1;
}
else /* 无下级(叶子结点):执行该菜单项绑定的处理函数 */
{
if (ptCurrent->pfnWinProc != NULL)
{
ptCurrent->pfnWinProc(); /* 如 MenuProc_See_AppInfo、MenuProc_See_YC 等 */
}
}
break;
case KEY_ESC: /* 退出键 */
if (1 == ptCurrent->byClass) /* 当前在 1 级:整体退出到“主界面”,重置 0 级路径与布局 */
{
g_tMenuCtrl.pt0Level = ptHead; /* 0 级路径起点设为头结点 */
g_tMenuCtrl.ptRoute[0] = ptHead; /* 路径[0] 指向 0 级头 */
g_tMenuCtrl.ptCurrent = ptHead->ptLower; /* 当前结点设为 0 级第一个子项 */
ptCurrent = g_tMenuCtrl.ptCurrent;
g_tMenuCtrl.ptCurBak = ptCurrent; /* 备份当前结点 */
g_tMenuCtrl.ptRoute[1] = ptCurrent; /* 路径[1] 指向该 1 级项 */
g_tMenuCtrl.byLeftMove = 0; /* 左移偏移清零 */
return; /* 直接返回,本次不再调用 Menu_Show_Proc由下次 Menu_Route 或外部刷新) */
}
else
{ /* 当前在 2 或 3 级:仅退到上一级路径结点 */
ptCurrent = ptRoute[ptCurrent->byClass - 1];
bNeedFresh = 1;
}
break;
default: /* 其它键值(含 KEY_NONE忽略 */
break;
}
}
if (bNeedFresh) /* 若按键导致菜单位置变化,则更新全局并刷新显示 */
{
g_tMenuCtrl.ptCurrent = ptCurrent; /* 更新当前选中的菜单项 */
if (g_tMenuCtrl.ptCurBak != ptCurrent) /* 当前菜单项相对上次备份有变化,需更新路径数组 */
{
ptIndex = g_tMenuCtrl.ptCurrent;
for (uint16_t index = 0; index < MENU_MAX_ITEM; index++) /* 从当前结点沿 ptHigher 回溯到 0 级 */
{
if (NULL == ptIndex->ptHigher) /* 无上级(不应出现,保险起见退到 ptBefore */
{
ptIndex = ptIndex->ptBefore;
}
else /* 有上级:先回到上级,再将该级路径记录到 ptRoute[byClass] */
{
ptIndex = ptIndex->ptHigher;
g_tMenuCtrl.ptRoute[ptIndex->byClass] = ptIndex;
}
if (0 == ptIndex->byClass) /* 已回溯到 0 级,结束 */
{
break;
}
}
g_tMenuCtrl.ptRoute[g_tMenuCtrl.ptCurrent->byClass] = ptCurrent; /* 当前所在层路径指向新结点 */
}
#ifdef DEBUG
printf("Menu_Route: g_tMenuCtrl.ptCurrent.byClass = %d\n", g_tMenuCtrl.ptCurrent->byClass);
printf("Menu_Route: g_tMenuCtrl.ptCurBak.byClass = %d\n", g_tMenuCtrl.ptCurBak->byClass);
for (uint16_t index = 0; index < 4; index++)
{
printf("Menu_Route: g_tMenuCtrl.ptRoute[%d].byName = %s\n", index, g_tMenuCtrl.ptRoute[index]->byName);
}
#endif
Menu_Show_Proc(); /* 按新路径刷新 0~当前级菜单显示 */
}
}
void Menu_Init(void) void Menu_Init(void)
{ {
Lcd_Init(); Lcd_Init();
/* 初始化显示控制结构 */ g_tDspCtrl.bFirst = 1;
memset(&g_tDspCtrl, 0, sizeof(g_tDspCtrl));
Menu_0LevelNumCal(); /* 统计 0 级菜单项个数 */ Menu_0LevelNumCal(); /* 统计 0 级菜单项个数 */
Menu_Main_Creat(); /* 创建菜单树 */ Menu_Main_Creat(); /* 创建菜单树 */
@@ -787,14 +1043,14 @@ void Menu_Init(void)
Menu_PositionCal(g_tMenuCtrl.ptHead); /* 菜单框位置计算 */ Menu_PositionCal(g_tMenuCtrl.ptHead); /* 菜单框位置计算 */
g_tMenuCtrl.ptRoute[0] = &g_tMenuItem[0]; // 0级路径初始化 g_tMenuCtrl.ptRoute[0] = &g_tMenuItem[0]; // 0级路径初始化
g_tMenuCtrl.ptCurrent = g_tMenuCtrl.ptHead->ptLower; // 当前指针初始化 g_tMenuCtrl.pt0Level = g_tMenuCtrl.ptRoute[0]; /* 供 Menu_Show_Proc 判断 0 级路径是否变化 */
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; g_tMenuCtrl.ptCurrent = g_tMenuCtrl.ptHead->ptLower; /* 菜单当前指针初始化为0级第一个子项 */
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 菜单备份指针初始化 */
g_tMenuCtrl.ptRoute[1] = g_tMenuCtrl.ptCurrent; // 1级路径初始化 g_tMenuCtrl.ptRoute[1] = g_tMenuCtrl.ptCurrent; // 1级路径初始化
g_tMenuCtrl.ptRoute[2] = g_tMenuCtrl.ptCurrent; // 1级路径初始化
g_tMenuCtrl.ptRoute[3] = g_tMenuCtrl.ptCurrent; // 1级路径初始化
g_tMenuCtrl.byLeftMove = 0; g_tMenuCtrl.byLeftMove = 0;
g_tMenuCtrl.bySecPage = 0; g_tMenuCtrl.bySecPage = 0;
Menu_Show_0Level(); /* 显示 0 级菜单 */
Menu_Show_Other(0);
} }

View File

@@ -12,43 +12,25 @@
#include <stdint.h> #include <stdint.h>
#include "lcd.h" /* MENU_YMAX 依赖 LCD_SIZE_Y */
#define CN_HEIGHT 12 /* 菜单汉字高 */
#define CN_HEIGHT 12 // 菜单汉字高
#define CN_ROWSPACE 2 // 菜单字符行间隔 #define CN_ROWSPACE 2 // 菜单字符行间隔
#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) // 字符行间隔 #define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) // 字符行间隔
#define MENU_XADD 7 // 菜单框横坐标增加 #define MENU_XADD 4 // 菜单框横坐标增加
#define MENU_YADD 4 // 菜单框纵坐标增加 #define MENU_YADD 4 // 菜单框纵坐标增加
#define MENU_WITDTH 7 // 菜单汉字13×12点阵 #define MENU_WITDTH 7 /*ASCII字体宽度 7因此单个字节显示的宽度是 7 个像素点*/
#define MENU_YMIN 0 // 菜单Y坐标的最小值 #define MENU_YMIN 0 // 菜单 Y 坐标的最小值 顶部边界 0
#define MENU_YMAX (LCD_SIZE_Y - 16) // 菜单Y坐标的最大值 #define MENU_YMAX (LCD_SIZE_Y - 12) // 菜单 Y 坐标的最大值 底部边界 LCD_SIZE_Y - 16
typedef struct MenuItem MenuItem;
struct MenuItem {
const char *name; /* 菜单名称 */
const char *tip; /* 底部提示信息 */
MenuItem *higher; /* 上级菜单 */
MenuItem *lower; /* 下级第一个菜单 */
MenuItem *before; /* 同级上一个 */
MenuItem *behind; /* 同级下一个 */
uint8_t level; /* 菜单级别0/1/2/... */
uint8_t pos; /* 在同级菜单中的序号(从 0 开始) */
};
typedef struct {
MenuItem *head0; /* 0 级菜单头结点 */
MenuItem *current; /* 当前选中的菜单结点 */
} MenuCtrl;
/* 初始化一棵简单的测试菜单树 */ /* 初始化一棵简单的测试菜单树 */
void Menu_Init(); void Menu_Init();
void Menu_Show_0Level(); void Menu_Route(void);
void Menu_Show_Proc(void);
/* 非功能键处理 */ /* 非功能键处理 */
void Menu_NonPfunc(); void Menu_NonPfunc();

View File

@@ -30,56 +30,18 @@ static int getch(void)
} }
#endif #endif
/* 菜单MenuItem、MenuCtrl、Menu_InitPC、Menu_LoopStep、Menu_MapKey、KEY_* */
#include "Drv/menu.h" #include "Drv/menu.h"
/* TCPTcp_Init/Cleanup、TcpServer_Listen/Accept/Close、TcpClient_Send/Recv/Close */
#include "TCP/tcp.h" #include "TCP/tcp.h"
/* 线程Thread_Create、Thread_Join、thread_handle_t */
#include "thread_utils.h"
/* 远程显示 / TCP 服务器线程server_thread_arg_t、tcp_server_thread_fn */
#include "remoteDisplay.h" #include "remoteDisplay.h"
#include "Drv/key.h"
/* ---------------------------------------------------------------------------- #include "thread_utils.h"
* 启动 TCP 服务与服务器线程Tcp_Init + 创建线程 + 短暂等待就绪)
* port: 监听端口(如 7070
* out_server_sock: 输出监听 socket供主线程退出时 TcpServer_Close
* out_server_quit: 输出退出标志,主线程置 1 通知服务器线程退出
* out_server_th: 输出线程句柄,供主线程 Thread_Join
* 返回0 成功1 失败(已调用 Tcp_Cleanup
* ---------------------------------------------------------------------------- */
static int StartTcpServerThread(uint16_t port,
int *out_server_sock,
volatile int *out_server_quit,
thread_handle_t *out_server_th)
{
server_thread_arg_t server_arg;
if (Tcp_Init() != 0) {
fprintf(stderr, "Tcp_Init failed\n");
return 1;
}
*out_server_sock = TCP_INVALID_SOCKET;
*out_server_quit = 0;
server_arg.port = port;
server_arg.p_server_sock = out_server_sock;
server_arg.p_quit = out_server_quit;
if (Thread_Create(tcp_server_thread_fn, &server_arg, out_server_th) != 0) {
fprintf(stderr, "Thread_Create(server) failed\n");
Tcp_Cleanup();
return 1;
}
Sleep(200);
return 0;
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* 程序入口 * 程序入口
* ---------------------------------------------------------------------------- */ * ---------------------------------------------------------------------------- */
int main(void) int main(void)
{ {
uint8_t count = 0;
#ifdef _WIN32 #ifdef _WIN32
/* Windows将控制台代码页设为 UTF-8避免中文乱码如“监听”“退出”等 */ /* Windows将控制台代码页设为 UTF-8避免中文乱码如“监听”“退出”等 */
@@ -89,18 +51,36 @@ int main(void)
int server_sock; int server_sock;
volatile int server_quit; volatile int server_quit;
thread_handle_t server_th; thread_handle_t server_th;
server_thread_arg_t server_arg;
server_sock = TCP_INVALID_SOCKET;
server_quit = 0;
server_arg.port = 7003;
server_arg.p_server_sock = &server_sock;
server_arg.p_quit = &server_quit;
printf("开始初始化菜单树...\n"); printf("开始初始化菜单树...\n");
Menu_Init(); /* 初始化菜单树(运行界面/定值设置/查看数据等) */ Menu_Init(); /* 初始化菜单树(运行界面/定值设置/查看数据等) */
Key_Init(); /* 初始化按键 */
printf("PC 端 HMI 菜单模拟启动TCP 服务在单独线程,端口 7003\n"); printf("PC 端 HMI 菜单模拟启动TCP 服务在单独线程,端口 7003\n");
/* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */ /* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */
if (StartTcpServerThread(7003, &server_sock, &server_quit, &server_th) != 0) if (StartTcpServerThread(&server_th, &server_arg) != 0)
{ {
return 1; return 1;
} }
while(1) { while(1)
{
Menu_Route();
Sleep(20);
count++;
if(count > 50)
{
count = 0;
Menu_Show_Proc();
}
} }
server_quit = 1; /* 通知服务器线程退出:下一轮 while 或 Accept 返回后会结束循环 */ server_quit = 1; /* 通知服务器线程退出:下一轮 while 或 Accept 返回后会结束循环 */

View File

@@ -8,11 +8,10 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include "remoteDisplay.h"
#include "Drv/lcd.h" #include "Drv/lcd.h"
#include "TCP/tcp.h" #include "TCP/tcp.h"
#include "Drv/key.h"
#include "remoteDisplay.h"
/* RemoDispBus 协议常量(与 remo_disp_server.py 一致) */ /* RemoDispBus 协议常量(与 remo_disp_server.py 一致) */
#define TAG_CLIENT 0xAAu #define TAG_CLIENT 0xAAu
@@ -81,7 +80,6 @@ static void handle_cmd_lcdmem(int client, const uint8_t* req_data, unsigned int
{ {
unsigned int start_addr = 0; unsigned int start_addr = 0;
printf("handle_cmd_lcdmem\n");
if (req_len >= 4) if (req_len >= 4)
start_addr = ((unsigned int)req_data[0] << 24) | ((unsigned int)req_data[1] << 16) start_addr = ((unsigned int)req_data[0] << 24) | ((unsigned int)req_data[1] << 16)
| ((unsigned int)req_data[2] << 8) | req_data[3]; | ((unsigned int)req_data[2] << 8) | req_data[3];
@@ -94,11 +92,14 @@ static void handle_cmd_lcdmem(int client, const uint8_t* req_data, unsigned int
payload[3] = start_addr & 0xFF; payload[3] = start_addr & 0xFF;
unsigned int copy_len = LCD_DISPLAYMEMORYSIZE; unsigned int copy_len = LCD_DISPLAYMEMORYSIZE;
if (start_addr < LCD_DISPLAYMEMORYSIZE) { if (start_addr < LCD_DISPLAYMEMORYSIZE)
{
unsigned int offset = start_addr; unsigned int offset = start_addr;
copy_len = LCD_DISPLAYMEMORYSIZE - offset; copy_len = LCD_DISPLAYMEMORYSIZE - offset;
memcpy(payload + 4, g_tCVsr.pwbyLCDMemory + offset, copy_len); memcpy(payload + 4, g_tCVsr.pwbyLCDMemory + offset, copy_len);
} else { }
else
{
copy_len = 0; copy_len = 0;
} }
send_reply(client, CMD_LCDMEM, payload, 4 + copy_len); send_reply(client, CMD_LCDMEM, payload, 4 + copy_len);
@@ -122,9 +123,11 @@ static void handle_cmd_init(int client)
/* 处理 CMD_KEY可选转交菜单此处仅回 ACK */ /* 处理 CMD_KEY可选转交菜单此处仅回 ACK */
static void handle_cmd_key(int client, const uint8_t* data, unsigned int len) static void handle_cmd_key(int client, const uint8_t* data, unsigned int len)
{ {
(void)data; #ifdef DEBUG
(void)len; printf("handle_cmd_key: 0x%02X\n", data[0]);
send_reply(client, CMD_KEY, (const uint8_t*)NULL, 0); #endif
g_tRemoteKey.byKeyValid = EN_KEY_FLAG_NEW;
g_tRemoteKey.byKeyValue = data[0];
} }
/* 处理 CMD_KEEPLIVE保活回空 */ /* 处理 CMD_KEEPLIVE保活回空 */
@@ -145,14 +148,15 @@ void tcp_server_thread_fn(void* arg)
printf("[TCP Server] RemoDispBus 监听端口 %d等待客户端连接...\n", ctx->port); printf("[TCP Server] RemoDispBus 监听端口 %d等待客户端连接...\n", ctx->port);
#define REMO_BUF_SIZE 4096 #define REMO_BUF_SIZE 4096
uint8_t recv_buf[REMO_BUF_SIZE]; uint8_t recv_buf[4096];
unsigned int recv_len = 0; unsigned int recv_len = 0;
/* ========== 外层循环:主线程未请求退出时,持续等待并接受新客户端 ========== */ /* ========== 外层循环:主线程未请求退出时,持续等待并接受新客户端 ========== */
while (!*ctx->p_quit) { while (!*ctx->p_quit) {
/* 阻塞等待一个客户端连接;主线程关闭 server_sock 时 Accept 会失败并返回 INVALID */ /* 阻塞等待一个客户端连接;主线程关闭 server_sock 时 Accept 会失败并返回 INVALID */
int client = TcpServer_Accept(server_sock); int client = TcpServer_Accept(server_sock);
if (client == TCP_INVALID_SOCKET) { if (client == TCP_INVALID_SOCKET)
{
if (*ctx->p_quit) if (*ctx->p_quit)
break; break;
continue; continue;
@@ -161,13 +165,16 @@ void tcp_server_thread_fn(void* arg)
recv_len = 0; /* 新连接对应新的接收缓冲区,避免混用上一连接的残留数据 */ recv_len = 0; /* 新连接对应新的接收缓冲区,避免混用上一连接的残留数据 */
/* ========== 内层循环:处理当前连接上的 RemoDispBus 请求,直到断开或退出 ========== */ /* ========== 内层循环:处理当前连接上的 RemoDispBus 请求,直到断开或退出 ========== */
while (!*ctx->p_quit) { while (!*ctx->p_quit)
{
/* ----- 1. 若缓冲区中已有一条完整且校验通过的帧,则解析并分发处理 ----- */ /* ----- 1. 若缓冲区中已有一条完整且校验通过的帧,则解析并分发处理 ----- */
uint8_t cmd; uint8_t cmd;
const uint8_t* pdata; const uint8_t* pdata;
unsigned int data_len, consume; unsigned int data_len, consume;
if (parse_frame(recv_buf, recv_len, &cmd, &pdata, &data_len, &consume)) { if (parse_frame(recv_buf, recv_len, &cmd, &pdata, &data_len, &consume))
switch (cmd) { {
switch (cmd)
{
case CMD_KEEPLIVE: case CMD_KEEPLIVE:
handle_cmd_keeplive(client); handle_cmd_keeplive(client);
break; break;
@@ -191,14 +198,17 @@ void tcp_server_thread_fn(void* arg)
} }
/* ----- 2. 缓冲区中尚无完整帧:先防止溢出,再收一批数据 ----- */ /* ----- 2. 缓冲区中尚无完整帧:先防止溢出,再收一批数据 ----- */
if (recv_len >= REMO_BUF_SIZE - 256) { if (recv_len >= REMO_BUF_SIZE - 256)
{
recv_len = 0; /* 异常情况:数据过多且始终不成帧,清空缓冲区防止越界 */ recv_len = 0; /* 异常情况:数据过多且始终不成帧,清空缓冲区防止越界 */
} }
int n = TcpClient_Recv(client, (char*)(recv_buf + recv_len), REMO_BUF_SIZE - recv_len - 1); int n = TcpClient_Recv(client, (char*)(recv_buf + recv_len), REMO_BUF_SIZE - recv_len - 1);
if (n > 0) { if (n > 0)
{
recv_len += (unsigned int)n; recv_len += (unsigned int)n;
printf("recv_len = %d\n", recv_len); }
} else { else
{
/* n==0 表示对方正常关闭连接n<0 表示读取出错;均退出本连接循环 */ /* n==0 表示对方正常关闭连接n<0 表示读取出错;均退出本连接循环 */
printf("[TCP Server] 客户端关闭连接\n"); printf("[TCP Server] 客户端关闭连接\n");
break; break;
@@ -212,3 +222,27 @@ void tcp_server_thread_fn(void* arg)
*ctx->p_server_sock = TCP_INVALID_SOCKET; *ctx->p_server_sock = TCP_INVALID_SOCKET;
printf("[TCP Server] 已退出\n"); printf("[TCP Server] 已退出\n");
} }
/* ----------------------------------------------------------------------------
* 启动 TCP 服务与服务器线程Tcp_Init + 创建线程 + 短暂等待就绪)
* port: 监听端口(如 7070
* out_server_sock: 输出监听 socket供主线程退出时 TcpServer_Close
* out_server_quit: 输出退出标志,主线程置 1 通知服务器线程退出
* out_server_th: 输出线程句柄,供主线程 Thread_Join
* 返回0 成功1 失败(已调用 Tcp_Cleanup
* ---------------------------------------------------------------------------- */
int StartTcpServerThread(thread_handle_t *out_server_th, server_thread_arg_t *io_server_arg)
{
if (Tcp_Init() != 0) {
fprintf(stderr, "Tcp_Init failed\n");
return 1;
}
if (Thread_Create(tcp_server_thread_fn, io_server_arg, out_server_th) != 0) {
fprintf(stderr, "Thread_Create(server) failed\n");
Tcp_Cleanup();
return 1;
}
Sleep(200);
return 0;
}

View File

@@ -1,5 +1,9 @@
#ifndef REMOTE_DISPLAY_H #ifndef __REMOTEDISPLAY_H
#define REMOTE_DISPLAY_H #define __REMOTEDISPLAY_H
#include "thread_utils.h"
/* /*
* 远程显示 / TCP 服务器相关接口 * 远程显示 / TCP 服务器相关接口
@@ -14,8 +18,7 @@ typedef struct {
volatile int* p_quit; /* 主线程置 1 通知线程退出 */ volatile int* p_quit; /* 主线程置 1 通知线程退出 */
} server_thread_arg_t; } server_thread_arg_t;
/* TCP 服务器线程函数:在独立线程中监听端口并处理客户端收/发 */ int StartTcpServerThread(thread_handle_t *out_server_th, server_thread_arg_t *io_server_arg);
void tcp_server_thread_fn(void* arg);
#endif /* REMOTE_DISPLAY_H */
#endif