完成了,菜单内容的初级显示

This commit is contained in:
2026-03-09 22:59:38 +08:00
parent 9da748efb8
commit fb1a28df00
9 changed files with 1421 additions and 116 deletions

View File

@@ -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
)

397
README.md
View File

@@ -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** 工具链进行编译。
- 源文件采用 **UTF8 编码并包含中文注释**`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 默认 MSVCLinux 需 GCC/Clang
- **编码**:源文件 UTF-8CMake 已配置 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` 设置每个菜单的层级指针。
#### 情况 1byCurClass < byNextClass下一项更深进入子菜单
```text
ptCurrent ptNextNode
(当前) (下一项,更深一级)
│ │
│ ptLower ──────────────────►│
│◄───────────────── ptHigher │
│ │
该级尾不变 新一级的 首=尾=ptNextNode
```
#### 情况 2byCurClass == byNextClass同级并列
```text
ptCurrent ─── ptBehind ──► ptNextNode
│ │
│ ptBefore ◄───────────┤
│ │
└──── ptHigher (同) ────────┘
该级 尾 更新为 ptNextNode
```
#### 情况 3byCurClass > byNextClass下一项更浅回到上层
```text
... byCurClass 级 ... byNextClass 级 ...
ptLast[byNextClass] 已存在
│ ptBehind ──► ptNextNode
│ │
│◄── ptBefore ────┤
│ ptHigher = 该级尾的 ptHigher
同时:从 byCurClass 到 byNextClass+1 各级首尾成环
ptLast[级]──►ptFirst[级]ptFirst[级]──►ptLast[级]
```
#### 最后:各级首尾成环
表遍历完后,从 **0 级到当前结点所在级**,把该级首尾连成环:
```text
ptFirst[级] ◄──────────────► ptLast[级]
│ │
└──── ptBehind ───────────────┘
◄──────── ptBefore ────────────┘
```

View File

@@ -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 },
};
};
const uint32_t MENU_MAX_ITEM = sizeof(g_tMenuModelTab) / sizeof(tagMenuModel);

View File

@@ -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;
//菜单密码设置
//==============================================================================

View File

@@ -1,7 +1,7 @@
#include "lcd.h"
#include <string.h>
#include "ascii.h"
#include <math.h>
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° 与否)

View File

@@ -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

View File

@@ -1,8 +1,9 @@
#include "menu.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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. 通过 byClass0/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-1X 从 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"); }

View File

@@ -13,6 +13,19 @@
#include <stdint.h>
#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 {

View File

@@ -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)