Просмотр исходного кода

add:exEasyUI UI扩展库,exlcd显示扩展库、extp触摸扩展库

江访 3 месяцев назад
Родитель
Сommit
8eed5dc3a3

+ 353 - 0
script/libs/exeasyui/exEasyUI版本管理.md

@@ -0,0 +1,353 @@
+# exEasyUI 版本管理
+
+## 版本历史
+
+### v1.0.0 - 初始版本 (SimpleUI)
+**发布日期**: 2025年9月
+
+**新增功能**:
+- 基础UI框架设计
+- 硬件解耦设计(LCD/TP独立初始化)
+- 事件驱动架构
+- 基础组件:Button、MessageBox
+- MVC编程模式支持
+- 触摸事件分发系统
+
+**核心特性**:
+- 组件化设计,统一 `draw/handleEvent` 接口
+- 基于 `sys.publish/sys.subscribe` 的事件系统
+- 支持硬件助手初始化或手动初始化
+- 清晰的API设计,快速上手
+
+---
+
+### v1.1.0 - 扩展版本 (exSimpleUI)
+**发布日期**: 2025年9月
+
+**新增功能**:
+- 库名更改为 `exSimpleUI`(扩展SimpleUI)
+- 新增 CheckBox 复选框组件
+- 新增 Icon 图标组件
+- 引入 SimpleWindow 窗口容器概念
+- 支持窗口背景图片设置
+- 子组件管理功能
+
+**删除功能**:
+- 无
+
+**改进功能**:
+- 保持与v1.0的API兼容性
+- 提供迁移建议和过渡支持
+- 优化组件组织结构
+
+---
+
+### v1.2.0 - 字体系统扩展
+**发布日期**: 2025年9月
+
+**新增功能**:
+- 统一字体适配层(FontAdapter)
+- 支持三种字体后端:
+  - 默认字体(内置位图字库)
+  - 矢量字体(gtfont高通字体芯片)
+  - 客户自定义bin字体(预留接口)
+- 字体配置系统
+- 文本测量和渲染优化
+- 灰度字体渲染支持
+
+**删除功能**:
+- 无
+
+**改进功能**:
+- 组件文本渲染统一化
+- 字体切换无感化
+- 文本宽度测量准确性提升
+- 支持多字号和字体样式
+
+---
+
+### v1.3.0 - 滑动窗口功能
+**发布日期**: 2025年9月
+
+**新增功能**:
+- 窗口滑动支持(纵向/横向/双向)
+- 惯性滑动效果
+- 边界回弹效果
+- 分页切换功能(横向桌面效果)
+- 滚动条显示
+- 拖拽阈值配置
+- 分页吸附功能
+
+**删除功能**:
+- 无
+
+**改进功能**:
+- 触摸事件处理优化
+- 点击与拖拽冲突处理
+- 窗口容器功能增强
+- 坐标变换系统
+
+---
+
+### v1.4.0 - 组件增强
+**发布日期**: 2025年9月
+
+**新增功能**:
+- ToolButton 工具按钮组件(带图标的按键)
+- Picture 图片组件(支持单图和轮播)
+- 图片自动轮播功能
+- 图片切换控制(播放/暂停/上一张/下一张)
+
+**删除功能**:
+- 无
+
+**改进功能**:
+- 组件功能更加丰富
+- 图片显示能力增强
+- 交互体验提升
+
+---
+
+### v1.5.0 - 稳定版本
+**发布日期**: 2025年9月
+
+**包含功能**:
+
+#### 核心系统
+- 硬件抽象层(hw模块)
+- 组件管理系统(core模块)
+- 事件分发系统(event模块)
+- 主题支持(深色/浅色主题)
+
+#### 组件库(8个组件)
+1. **Button** - 基础按钮组件
+   - 支持按下/抬起状态
+   - 点击回调
+   - 自定义颜色和文本
+
+2. **ToolButton** - 工具按钮组件
+   - 支持图标显示
+   - 普通/按下/切换状态
+   - 图片按钮功能
+
+3. **CheckBox** - 复选框组件
+   - 二态切换(选中/未选中)
+   - 支持文本标签
+   - 状态变化回调
+
+4. **Label** - 标签组件
+   - 文本显示
+   - 自动尺寸计算
+   - 字体样式支持
+
+5. **Picture** - 图片组件
+   - 单图显示
+   - 多图轮播
+   - 自动播放控制
+
+6. **MessageBox** - 消息框组件
+   - 标题和消息显示
+   - 自定义按钮组
+   - 模态对话框
+
+7. **Window** - 窗口组件
+   - 子组件管理
+   - 子页面导航
+   - 滚动支持
+   - 背景设置
+
+8. **ProgressBar** - 进度条组件
+   - 百分比显示
+   - 进度设置
+   - 自定义文本
+
+#### 高级功能
+- **字体系统**: 支持默认字体、gtfont矢量字体
+- **滑动窗口**: 支持纵向/横向滚动、惯性滑动、分页切换
+- **主题系统**: 深色/浅色主题自动适配
+- **事件系统**: 完整的触摸事件分发
+- **硬件支持**: 支持多种LCD和触摸屏
+
+#### 技术特性
+- 单文件架构,便于集成
+- 零依赖设计(仅依赖LuatOS基础模块)
+- 内存占用优化
+- 高性能渲染
+- 完整的错误处理
+
+---
+
+### v1.6.0 - 当前版本
+**发布日期**: 2025年10月7日
+
+**更新概述**
+
+1. **智能文本换行** - Label和MessageBox支持自动换行(`wordWrap`参数),英文按单词、中文按字符智能换行,高度自适应
+
+2. **Button功能整合** - 合并ToolButton,统一支持文本/图片/Toggle三种模式(`src`/`toggle`参数),简化组件体系
+
+3. **Windows 11风格配色** - Light/Dark双主题默认配色,Window/Button自动应用现代化视觉效果,提供6个新颜色常量
+
+4. **代码质量提升** - UTF-8字符正确处理、向后兼容、删除冗余组件、所有示例添加版本标识
+
+**新增功能**:
+- Label组件支持自动换行
+  - 新增 `wordWrap` 参数控制是否启用换行
+  - 支持指定最大宽度 `w`,超出部分截断或换行
+  - 换行时高度自动调整以容纳所有文本行
+  - **智能换行**:英文按单词换行,中文按字符换行
+- MessageBox组件支持自动换行
+  - 新增 `wordWrap` 参数控制是否启用换行
+  - 在固定高度内自动分行显示,超出部分截断
+  - **智能换行**:英文按单词换行,中文按字符换行
+- Button组件合并ToolButton功能
+  - 新增图片按钮支持:`src`、`src_pressed`、`src_toggled` 参数
+  - 新增Toggle模式:`toggle`、`toggled`、`onToggle` 参数
+  - 图片显示优先于文本显示
+- 所有示例代码添加版本标识注释
+- **Windows 11 风格配色**(Light + Dark模式)
+  - Light模式:
+    - `COLOR_WIN11_LIGHT_DIALOG_BG`:RGB(243, 243, 243) - 对话框背景色
+    - `COLOR_WIN11_LIGHT_BUTTON_BG`:RGB(251, 251, 252) - 按钮背景色
+    - `COLOR_WIN11_LIGHT_BUTTON_BORDER`:RGB(229, 229, 229) - 按钮边框色
+  - Dark模式:
+    - `COLOR_WIN11_DARK_DIALOG_BG`:RGB(32, 32, 32) - 对话框背景色
+    - `COLOR_WIN11_DARK_BUTTON_BG`:RGB(51, 51, 51) - 按钮背景色
+    - `COLOR_WIN11_DARK_BUTTON_BORDER`:RGB(76, 76, 76) - 按钮边框色
+
+**删除功能**:
+- 移除 ToolButton 组件(功能已合并到Button)
+- 删除 ToolButton 相关示例代码
+
+**改进功能**:
+- 文本处理能力增强,支持长文本自动换行
+- 组件体系简化,减少代码冗余
+- 按钮组件功能更加完整和统一
+- **Windows 11 风格默认配色改进**(Light + Dark)
+  - Light模式(浅色主题):
+    - Window 默认背景色:RGB(243, 243, 243)
+    - Button 默认背景色:RGB(251, 251, 252)
+    - Button 默认边框色:RGB(229, 229, 229)
+  - Dark模式(深色主题):
+    - Window 默认背景色:RGB(32, 32, 32)
+    - Button 默认背景色:RGB(51, 51, 51)
+    - Button 默认边框色:RGB(76, 76, 76)
+  - 整体视觉效果更加现代化,与 Windows 11 UI 风格完全一致
+
+**技术改进**:
+- UTF-8字符正确处理,按完整字符边界换行
+- 文本宽度测量和换行算法优化
+- RGB565颜色精确转换,保证色彩准确度
+- 主题适配增强,深色/浅色模式自动切换合适配色
+- 保持向后兼容性,现有代码无需修改
+
+---
+
+### v1.6.1 - 稳定性修复版
+**发布日期**: 2025年10月9日
+
+**更新概述**  
+1. **交互体验提升** - 按钮防移出机制修复,滚动和非滚动窗口下按钮交互更自然,避免误触和颜色异常。  
+2. **MessageBox复用与易用性增强** - MessageBox新增了动态显示/隐藏和内容更新的子方法,默认自动换行。
+3. **图片加载与占位符优化** - 新增图片占位符样式,图片加载失败有清晰提示,I/O检查更高效,开发调试体验提升。
+
+**修复问题**:
+1. **按钮防移出机制修复**
+   - 修复滚动Window中按钮移出范围后颜色异常
+   - 修复非滚动Window中缺少_capture检查的问题
+   - 在Window滚动观望期转发MOVE事件给已捕获的子组件
+   - 添加_capture检查让已捕获组件能收到移出范围的MOVE事件
+
+2. **MessageBox复用功能完善**
+   - 修复点击按钮后enabled被禁用导致无法复用的问题
+   - 新增`show()`方法:显示MessageBox
+   - 新增`hide()`方法:隐藏MessageBox
+   - 新增`setTitle(title)`方法:动态更新标题
+   - 新增`setMessage(message)`方法:动态更新消息内容
+   - 修复无按钮MessageBox文本区域被挤占的问题(增加约38px可用空间)
+   - 修复visible参数不生效的问题,支持从opts读取
+   - 修复wordWrap默认关闭的问题,现在默认启用自动换行
+
+3. **图片占位符优化**
+   - 新增`draw_image_placeholder()`函数绘制改进的占位符
+   - 占位符样式:灰色背景+白色边框+双层X叉(尺寸≥20px时)
+   - 添加文件存在性检查(io.exists)
+   - 添加图片加载状态缓存(_imageCache),每个路径只检查一次
+   - 文件不存在时输出警告日志(每个路径只警告一次)
+   - Button和Picture组件统一使用新的占位符样式
+   - 性能优化:避免每帧重复I/O检查(从每秒33次降为1次)
+
+**改进功能**:
+- MessageBox自动换行默认开启,无需显式传入wordWrap=true
+- 图片加载失败时提供清晰的视觉反馈和日志提示
+- 优化开发调试体验,快速定位图片路径问题
+
+**示例更新**:
+- 更新`msgbox/main.lua`示例,展示MessageBox复用功能
+- 添加1秒循环任务演示动态更新时间
+- 更新`msgbox_page.lua`示例,使用新的复用API
+
+**技术改进**:
+- 事件分发机制优化,修复Window事件转发逻辑
+- 缓存机制优化,减少不必要的文件I/O操作
+- 代码注释增强,添加v1.6.1修复标记
+
+**兼容性**:
+- ✅ 保持向后兼容,现有代码无需修改
+- ✅ 新增API不影响现有功能
+- ⚠️ MessageBox自动换行默认开启,如需关闭请显式传入wordWrap=false
+
+**使用建议**:
+- Picture组件建议使用正方形或接近正方形的尺寸(如120x120、160x160等)
+- MessageBox推荐使用复用模式:创建时设置visible=false,使用时调用show()
+
+---
+
+## 版本规划
+
+### v1.7.0 - 计划中
+**预计功能**:
+- 更多基础组件(Input输入框、List列表等)
+- 布局系统
+- 动画支持
+- 性能优化
+
+### v2.0.0 - 长期规划
+**预计功能**:
+- 完整的UI框架
+- 更多高级组件
+- 主题定制系统
+- 国际化支持
+- 开发工具支持
+
+---
+
+## 使用建议
+
+- **新项目**: 建议使用最新版本v1.6.1
+- **现有项目**: 建议升级到v1.6.1以获得稳定性修复
+- **功能选择**: 根据项目需求选择合适的组件和功能
+- **性能考虑**: 在资源受限的设备上建议关闭不必要的功能
+
+---
+
+## 更新日志格式
+
+每个版本更新时,请按照以下格式记录:
+
+```markdown
+### vX.X.X - 版本名称
+**发布日期**: YYYY年MM月
+
+**新增功能**:
+- 功能描述
+
+**删除功能**:
+- 功能描述
+
+**改进功能**:
+- 功能描述
+
+**修复问题**:
+- 问题描述
+```

+ 1484 - 0
script/libs/exeasyui/exEasyUI的API手册.md

@@ -0,0 +1,1484 @@
+> 当前exEasyUI的版本为1.6.1  
+## 一、概述
+- exEasyUI 是基于 LuatOS 打造的简化 UI 组件库,内聚了组件管理、渲染与触摸事件分发能力。
+- 触摸层基于扩展库 extp(触摸识别/手势统一为 baseTouchEvent),UI 内部统一订阅并转发到组件。
+- 主要组件:Button、CheckBox、Label、Picture、MessageBox、Window、ProgressBar。
+- 设计目标:开箱即用、少依赖、跨屏易配置,支持滑动窗口(纵向滚动、横向平移/分页吸附)。
+- v1.6.0新特性:Label/MessageBox智能换行、Button合并图片和Toggle功能、Windows 11风格配色。
+- v1.6.1修复与增强:按钮防移出机制修复、MessageBox复用功能(show/hide/setTitle/setMessage)、图片占位符优化。
+
+注意:工程需提供 `screen_data_table.lua`(LCD 与 TP 参数),`exeasyui.lua` 会自动从该文件读取并初始化硬件与触摸。
+
+## 二、核心示例(滑动窗口从上到下展示各组件)
+以下示例演示:创建一个窗口,开启纵向滚动,将各个组件的最小可用示例纵向排布,滑动浏览。
+
+```
+PROJECT = "exEasyUI_demo"
+VERSION = "1.0.0"
+
+sys = require("sys")
+local ui = require("exeasyui")
+
+sys.taskInit(function()
+    sys.wait(500)
+    -- 依赖 screen_data_table.lua 内的 lcdargs/touch 参数
+    ui.hw.init({})
+    ui.init({ theme = "light" })
+
+    local win = ui.Window({ backgroundColor = ui.COLOR_WHITE })
+    -- 内容高度较大,启用纵向滚动
+    win:enableScroll({ direction = "vertical", contentHeight = 1000, threshold = 8 })
+
+    local y = 20
+    local function place(h)
+        local cur = y; y = y + h + 16; return cur
+    end
+
+    -- 1) Button
+    local btn = ui.Button({ x = 20, y = place(44), w = 280, h = 44, text = "Button", onClick = function(self)
+        log.info("demo", "button clicked")
+    end })
+    win:add(btn)
+
+    -- 2) Toggle按钮(v1.6.0:Button支持toggle功能)
+    local tb = ui.Button({ x = 20, y = place(64), w = 64, h = 64, 
+        src = "/luadb/icon.jpg", toggle = true, 
+        onToggle = function(t) log.info("demo", "toggled", t) end 
+    })
+    win:add(tb)
+
+    -- 3) CheckBox
+    local cb = ui.CheckBox({ x = 20, y = place(24), text = "Check me", checked = false, onChange = function(v)
+        log.info("demo", "checkbox", v)
+    end })
+    win:add(cb)
+
+    -- 4) Label(v1.6.0:支持自动换行)
+    local lbl = ui.Label({ x = 20, y = place(60), w = 280, wordWrap = true,
+        text = "This is a long text that will wrap automatically within the specified width." })
+    win:add(lbl)
+
+    -- 5) Picture(示例:无实际图片时会显示占位框)
+    local pic = ui.Picture({ x = 20, y = place(120), w = 160, h = 120, autoplay = false, src = "/luadb/sample.jpg" })
+    win:add(pic)
+
+    -- 6) MessageBox(作为静态面板展示)
+    local box = ui.MessageBox({ x = 20, y = place(120), w = 280, h = 120, title = "MessageBox", message = "Info Panel", buttons = {"OK"}, onResult = function(r)
+        log.info("demo", "msgbox", r)
+    end })
+    win:add(box)
+
+    -- 7) ProgressBar
+    local pb = ui.ProgressBar({ x = 20, y = place(26), w = 280, h = 26, progress = 35, text = "35%" })
+    win:add(pb)
+
+    ui.add(win)
+
+    while true do
+        ui.clear()
+        ui.render()
+        sys.wait(30)
+    end
+end)
+
+sys.run()
+```
+
+## 三、常量详解
+
+### 3.1 基础颜色常量
+
+| 常量名           | 说明         | RGB565值      |
+|------------------|--------------|---------------|
+| COLOR_WHITE      | 白色         | 0xFFFF        |
+| COLOR_BLACK      | 黑色         | 0x0000        |
+| COLOR_GRAY       | 灰色         | 0x8410        |
+| COLOR_BLUE       | 蓝色         | 0x001F        |
+| COLOR_RED        | 红色         | 0xF800        |
+| COLOR_GREEN      | 绿色         | 0x07E0        |
+| COLOR_YELLOW     | 黄色         | 0xFFE0        |
+| COLOR_CYAN       | 青色         | 0x07FF        |
+| COLOR_MAGENTA    | 品红         | 0xF81F        |
+| COLOR_ORANGE     | 橙色         | 0xFC00        |
+| COLOR_PINK       | 粉色         | 0xF81F        |
+
+### 3.2 Windows 11 风格颜色(v1.6.0新增)
+
+**Light模式**:
+| 常量名                        | 说明           | RGB值              | RGB565值 |
+|-------------------------------|----------------|--------------------|----------|
+| COLOR_WIN11_LIGHT_DIALOG_BG   | 对话框背景     | RGB(243,243,243)   | 0xF79E   |
+| COLOR_WIN11_LIGHT_BUTTON_BG   | 按钮背景       | RGB(251,251,252)   | 0xFFDF   |
+| COLOR_WIN11_LIGHT_BUTTON_BORDER| 按钮边框      | RGB(229,229,229)   | 0xE73C   |
+
+**Dark模式**:
+| 常量名                        | 说明           | RGB值              | RGB565值 |
+|-------------------------------|----------------|--------------------|----------|
+| COLOR_WIN11_DARK_DIALOG_BG    | 对话框背景     | RGB(32,32,32)      | 0x2104   |
+| COLOR_WIN11_DARK_BUTTON_BG    | 按钮背景       | RGB(51,51,51)      | 0x3186   |
+| COLOR_WIN11_DARK_BUTTON_BORDER| 按钮边框       | RGB(76,76,76)      | 0x4A69   |
+
+> 以上颜色常量均由 `ui` 导出,可直接通过 `ui.COLOR_WHITE` 等方式使用。  
+> Window和Button组件在light主题下默认使用Windows 11 Light配色,dark主题下使用Dark配色。
+
+## 四、组件 API 详解
+本节对每个组件提供:功能说明、构造参数、方法、事件、示例。
+
+### 4.1 Button(按钮)
+**功能**
+
+多功能按钮组件,支持文本按钮、图片按钮、Toggle切换按钮(v1.6.0合并ToolButton功能)。
+
+**构造**:`ui.Button(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = ,
+    -- 参数含义:左上角坐标;
+    -- 数据类型:number;
+    -- 是否必选:可选,默认0;
+
+    w = , h = ,
+    -- 参数含义:宽/高;
+    -- 数据类型:number;
+    -- 是否必选:可选,默认 w=100, h=36;
+
+    -- 文本模式参数
+    text = ,
+    -- 参数含义:按钮文字;
+    -- 数据类型:string;
+    -- 是否必选:可选,默认"Button";
+
+    textSize = ,
+    -- 参数含义:文字字号;
+    -- 数据类型:number;
+    -- 是否必选:可选;
+
+    bgColor = , textColor = , borderColor = ,
+    -- 参数含义:背景/文本/边框颜色;
+    -- 数据类型:number(RGB565);
+    -- 是否必选:可选,浅色主题默认Windows 11配色;
+
+    -- 图片模式参数(v1.6.0新增)
+    src = ,
+    -- 参数含义:普通态图片路径;
+    -- 数据类型:string;
+    -- 是否必选:可选,有此参数时按钮显示为图片;
+    -- 说明:v1.6.1起会检查文件是否存在,不存在时显示占位符并输出警告日志;
+
+    src_pressed = ,
+    -- 参数含义:按下态图片路径;
+    -- 数据类型:string;
+    -- 是否必选:可选;
+
+    src_toggled = ,
+    -- 参数含义:切换态图片路径(toggle=true时使用);
+    -- 数据类型:string;
+    -- 是否必选:可选;
+
+    -- Toggle模式参数(v1.6.0新增)
+    toggle = ,
+    -- 参数含义:是否为Toggle切换按钮;
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认false;
+
+    toggled = ,
+    -- 参数含义:初始切换状态;
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认false;
+
+    onToggle = ,
+    -- 参数含义:切换回调;
+    -- 数据类型:function(toggled, self);
+    -- 是否必选:可选;
+
+    onClick = 
+    -- 参数含义:点击回调;
+    -- 数据类型:function(self);
+    -- 是否必选:可选;
+}
+```
+
+**方法**
+```plain
+setText(newText)         -- 设置按钮文字
+```
+
+**事件**
+```plain
+SINGLE_TAP: 调用 onClick(self)
+```
+
+**示例**
+```plain
+-- 1. 文本按钮(使用默认配色)
+local btn1 = ui.Button({
+    x = 20, y = 40, w = 120, h = 44,
+    text = "确定",
+    onClick = function() log.info("btn", "clicked") end
+})
+
+-- 2. 图片按钮(有按下态)
+local btn2 = ui.Button({
+    x = 20, y = 100, w = 64, h = 64,
+    src = "/luadb/icon.jpg",
+    src_pressed = "/luadb/icon_pressed.jpg",
+    onClick = function() log.info("img btn", "clicked") end
+})
+
+-- 3. Toggle按钮
+local btn3 = ui.Button({
+    x = 20, y = 180, w = 64, h = 64,
+    src = "/luadb/off.jpg",
+    src_toggled = "/luadb/on.jpg",
+    toggle = true,
+    onToggle = function(state)
+        log.info("toggle", state and "ON" or "OFF")
+    end
+})
+```
+
+#### Button.setText
+**功能**
+
+设置按钮文字。
+
+**签名**
+```plain
+Button:setText(newText)
+```
+
+**参数**
+```plain
+newText
+-- 参数含义:新的按钮文本
+-- 数据类型:string
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+local b = ui.Button({ x = 20, y = 40, text = "原文" })
+b:setText("确定")
+```
+
+### 4.2 CheckBox(复选框)
+**功能**
+
+布尔选择控件。
+
+**构造**:`ui.CheckBox(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = ,
+    -- 参数含义:位置;
+    -- 数据类型:number;
+
+    boxSize = ,
+    -- 参数含义:复选框边长;
+    -- 数据类型:number;
+    -- 是否必选:可选,默认16;
+
+    text = ,
+    -- 参数含义:右侧文本;
+    -- 数据类型:string;
+
+    checked = ,
+    -- 参数含义:初始选中状态;
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认false;
+
+    onChange = ,
+    -- 参数含义:状态改变回调;
+    -- 数据类型:function(checked);
+
+    textColor = , borderColor = , bgColor = , tickColor = ,
+    -- 参数含义:颜色(文本/边框/背景/选中块);
+    -- 数据类型:number;
+}
+```
+
+**方法**
+```plain
+setChecked(v)
+toggle()
+```
+
+#### CheckBox.setChecked
+**功能**
+
+设置复选框选中状态。
+
+**签名**
+```plain
+CheckBox:setChecked(v)
+```
+
+**参数**
+```plain
+v
+-- 参数含义:目标选中状态
+-- 数据类型:boolean
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+local cb = ui.CheckBox({ x=20, y=160, text="启用" })
+cb:setChecked(true)
+```
+
+#### CheckBox.toggle
+**功能**
+
+切换当前选中状态。
+
+**签名**
+```plain
+CheckBox:toggle()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+cb:toggle()
+```
+
+**事件**
+```plain
+SINGLE_TAP: 点击区域内部时切换并调用 onChange(checked)
+```
+
+**示例**
+```plain
+local cb = ui.CheckBox({ 
+    x = 20, y = 160, text = "启用", 
+    onChange = function(v) log.info("cb", v) end 
+})
+```
+
+### 4.3 Label(文本标签)
+**功能**
+
+静态文本显示,支持自动换行(v1.6.0新增)。
+
+**构造**:`ui.Label(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = ,
+    -- 参数含义:位置;
+    -- 数据类型:number;
+
+    text = ,
+    -- 参数含义:文本内容;
+    -- 数据类型:string;
+
+    w = ,
+    -- 参数含义:最大宽度(v1.6.0);
+    -- 数据类型:number;
+    -- 是否必选:可选,未指定时自动计算;
+    -- 说明:指定后,无换行时超出部分截断,有换行时在此宽度内换行;
+
+    wordWrap = ,
+    -- 参数含义:是否启用自动换行(v1.6.0);
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认false;
+    -- 说明:启用后在w宽度内智能换行(英文按单词,中文按字符);
+
+    color = ,
+    -- 参数含义:文本颜色;
+    -- 数据类型:number;
+    -- 是否必选:可选,默认随主题;
+
+    size = ,
+    -- 参数含义:字号;
+    -- 数据类型:number;
+    -- 是否必选:可选;
+
+    font = 
+    -- 参数含义:自定义字体句柄(默认后端时生效);
+    -- 数据类型:userdata;
+}
+```
+
+**方法**
+```plain
+setText(t)
+setSize(sz)
+```
+
+#### Label.setText
+**功能**
+
+设置文本内容。
+
+**签名**
+```plain
+Label:setText(t)
+```
+
+**参数**
+```plain
+t
+-- 参数含义:新文本
+-- 数据类型:string
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+local lbl = ui.Label({ x = 20, y = 200, text = "Hello" })
+lbl:setText("World")
+```
+
+#### Label.setSize
+**功能**
+
+设置字号,并按需调整宽高。
+
+**签名**
+```plain
+Label:setSize(sz)
+```
+
+**参数**
+```plain
+sz
+-- 参数含义:字号
+-- 数据类型:number
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+lbl:setSize(20)
+```
+
+**示例**
+```plain
+-- 1. 基础文本
+local lbl1 = ui.Label({ x = 20, y = 200, text = "Hello exEasyUI" })
+
+-- 2. 文本截断(超出w则截断)
+local lbl2 = ui.Label({ 
+    x = 20, y = 230, w = 200,
+    text = "这是一段很长的文本会被截断" 
+})
+
+-- 3. 自动换行(v1.6.0)
+local lbl3 = ui.Label({ 
+    x = 20, y = 260, w = 280, wordWrap = true,
+    text = "This is a long text that demonstrates automatic word wrapping feature in exEasyUI v1.6.0."
+})
+```
+
+### 4.4 Picture(图片/轮播)
+**功能**
+
+显示单图或多图轮播。v1.6.1起,图片文件不存在时会显示占位符(灰色背景+白色边框+X叉)。
+
+**构造**:`ui.Picture(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = , w = , h = ,
+    -- 参数含义:位置与尺寸;
+    -- 数据类型:number;
+    -- 说明:v1.6.1建议使用正方形或接近正方形尺寸以获得最佳占位符显示效果;
+
+    src = ,
+    -- 参数含义:单张图片路径;
+    -- 数据类型:string;
+    -- 说明:v1.6.1起会检查文件是否存在,不存在时显示占位符并输出警告日志;
+
+    sources = ,
+    -- 参数含义:多张图片路径列表;
+    -- 数据类型:table,如 { "/luadb/a.jpg", "/luadb/b.jpg" };
+
+    autoplay = ,
+    -- 参数含义:是否自动轮播(sources 有多图时生效);
+    -- 数据类型:boolean,默认false;
+
+    interval = 
+    -- 参数含义:轮播间隔(毫秒);
+    -- 数据类型:number,默认1000;
+}
+```
+
+**方法**
+```plain
+setSources(list)
+next()
+prev()
+play()
+pause()
+```
+
+#### Picture.setSources
+**功能**
+
+设置图片列表并重置索引。
+
+**签名**
+```plain
+Picture:setSources(list)
+```
+
+**参数**
+```plain
+list
+-- 参数含义:图片路径数组
+-- 数据类型:table,如 {"/luadb/a.jpg", "/luadb/b.jpg"}
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+pic:setSources({ "/luadb/1.jpg", "/luadb/2.jpg" })
+```
+
+#### Picture.next / Picture.prev
+**功能**
+
+切换到下一张 / 上一张(在 sources 存在时)。
+
+**签名**
+```plain
+Picture:next()
+Picture:prev()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+pic:next()
+pic:prev()
+```
+
+#### Picture.play / Picture.pause
+**功能**
+
+开启 / 暂停自动轮播。
+
+**签名**
+```plain
+Picture:play()
+Picture:pause()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+pic:play()
+sys.wait(2000)
+pic:pause()
+```
+
+**示例**
+```plain
+-- 单图显示
+local pic1 = ui.Picture({ 
+    x = 20, y = 240, w = 120, h = 90,
+    src = "/luadb/image.jpg" 
+})
+
+-- 自动轮播
+local pic2 = ui.Picture({ 
+    x = 20, y = 350, w = 120, h = 90,
+    sources = {"/luadb/a.jpg", "/luadb/b.jpg"}, 
+    autoplay = true 
+})
+```
+
+### 4.5 MessageBox(消息框/面板)
+**功能**
+
+展示标题、内容与按钮组,支持消息自动换行(v1.6.0新增),支持复用与动态更新(v1.6.1新增)。
+
+**构造**:`ui.MessageBox(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = , w = , h = ,
+    -- 参数含义:位置与尺寸;
+    -- 数据类型:number;
+
+    title = ,
+    -- 参数含义:标题文本;
+    -- 数据类型:string,默认"Info";
+
+    message = ,
+    -- 参数含义:内容文本;
+    -- 数据类型:string;
+
+    wordWrap = ,
+    -- 参数含义:消息是否自动换行(v1.6.0);
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认true(v1.6.1起);
+    -- 说明:启用后消息在固定高度内智能换行,超出部分截断;
+
+    visible = ,
+    -- 参数含义:初始是否可见(v1.6.1修复);
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认true;
+
+    enabled = ,
+    -- 参数含义:初始是否可交互(v1.6.1修复);
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认true;
+
+    textSize = ,
+    -- 参数含义:文本字号;
+    -- 数据类型:number;
+    -- 是否必选:可选;
+
+    buttons = ,
+    -- 参数含义:按钮文本数组;
+    -- 数据类型:table,默认 {"OK"};
+    -- 说明:可传入空数组 {} 创建无按钮的MessageBox;
+
+    onResult = ,
+    -- 参数含义:按钮点击回调;
+    -- 数据类型:function(label);
+
+    borderColor = , textColor = , bgColor = ,
+    -- 参数含义:边框/文本/背景颜色;
+    -- 数据类型:number;
+}
+```
+
+**方法**
+```plain
+show()                   -- 显示MessageBox(v1.6.1新增)
+hide()                   -- 隐藏MessageBox(v1.6.1新增)
+setTitle(title)          -- 动态更新标题(v1.6.1新增)
+setMessage(message)      -- 动态更新消息内容(v1.6.1新增)
+```
+
+**事件**
+```plain
+内部按钮 SINGLE_TAP: 调用 onResult(label)
+```
+
+#### MessageBox 子方法
+
+#### MessageBox.show
+**功能**
+
+显示MessageBox(设置visible和enabled为true)。v1.6.1新增,用于MessageBox复用。
+
+**签名**
+```plain
+MessageBox:show()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+msgbox:show()
+```
+
+#### MessageBox.hide
+**功能**
+
+隐藏MessageBox(设置visible为false)。v1.6.1新增,用于MessageBox复用。
+
+**签名**
+```plain
+MessageBox:hide()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+msgbox:hide()
+```
+
+#### MessageBox.setTitle
+**功能**
+
+动态更新MessageBox标题。v1.6.1新增。
+
+**签名**
+```plain
+MessageBox:setTitle(title)
+```
+
+**参数**
+```plain
+title
+-- 参数含义:新标题文本
+-- 数据类型:string
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+msgbox:setTitle("警告")
+```
+
+#### MessageBox.setMessage
+**功能**
+
+动态更新MessageBox消息内容。v1.6.1新增,如启用了自动换行会自动重新计算行数。
+
+**签名**
+```plain
+MessageBox:setMessage(message)
+```
+
+**参数**
+```plain
+message
+-- 参数含义:新消息文本
+-- 数据类型:string
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+msgbox:setMessage("更新后的消息内容")
+```
+
+**示例**
+```plain
+-- 1. 简单提示
+local box1 = ui.MessageBox({ 
+    x = 20, y = 340, w = 280, h = 120,
+    title = "提示", message = "操作成功", 
+    buttons = {"确定"},
+    onResult = function(r) log.info("msg", r) end 
+})
+
+-- 2. 长消息自动换行(v1.6.0,v1.6.1起默认开启)
+local box2 = ui.MessageBox({ 
+    x = 20, y = 340, w = 280, h = 160,
+    title = "提示",
+    message = "这是一段很长的提示消息,启用自动换行后会在固定宽度内智能分行显示,超出高度的部分会被截断。",
+    buttons = {"知道了"}
+})
+
+-- 3. MessageBox复用(v1.6.1推荐方式)
+-- 创建时设置为初始隐藏
+local msgbox = ui.MessageBox({ 
+    x = 20, y = 340, w = 280, h = 120,
+    title = "提示", 
+    message = "初始消息", 
+    visible = false,  -- 初始隐藏
+    buttons = {"确定"},
+    onResult = function(r) 
+        msgbox:hide()  -- 点击后隐藏而非禁用
+    end 
+})
+win:add(msgbox)
+
+-- 多次复用同一个MessageBox
+local btn1 = ui.Button({ 
+    x = 20, y = 40, w = 120, h = 44, 
+    text = "显示消息1",
+    onClick = function()
+        msgbox:setTitle("通知")
+        msgbox:setMessage("这是第一条消息")
+        msgbox:show()
+    end 
+})
+
+local btn2 = ui.Button({ 
+    x = 160, y = 40, w = 120, h = 44, 
+    text = "显示消息2",
+    onClick = function()
+        msgbox:setTitle("警告")
+        msgbox:setMessage("这是第二条消息")
+        msgbox:show()
+    end 
+})
+
+-- 4. 无按钮MessageBox(信息面板)
+local infoPanel = ui.MessageBox({ 
+    x = 20, y = 480, w = 280, h = 100,
+    title = "系统状态",
+    message = "运行正常\n内存使用: 45%",
+    buttons = {}  -- 无按钮
+})
+
+-- 5. 动态更新示例(如实时显示时间)
+sys.taskInit(function()
+    while true do
+        local time = os.date("%H:%M:%S")
+        msgbox:setMessage("当前时间: " .. time)
+        sys.wait(1000)
+    end
+end)
+```
+
+### 4.6 ProgressBar(进度条)
+**功能**
+
+显示百分比进度,可附带文本。
+
+**构造**:`ui.ProgressBar(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = , w = , h = ,
+    -- 参数含义:位置与尺寸;
+    -- 数据类型:number;
+
+    progress = ,
+    -- 参数含义:进度百分比 0~100;
+    -- 数据类型:number,默认0;
+
+    text = , textSize = ,
+    -- 参数含义:文本与字号;
+    -- 数据类型:string/number;
+
+    backgroundColor = , progressColor = , borderColor = , textColor = ,
+    -- 参数含义:配色(背景/进度/边框/文本);
+    -- 数据类型:number;
+
+    showPercentage = 
+    -- 参数含义:是否显示百分比文本;
+    -- 数据类型:boolean,默认true;
+}
+```
+
+**方法**
+```plain
+setProgress(v)
+getProgress()
+setText(text)
+```
+
+#### ProgressBar.setProgress
+**功能**
+
+设置进度百分比。
+
+**签名**
+```plain
+ProgressBar:setProgress(v)
+```
+
+**参数**
+```plain
+v
+-- 参数含义:目标进度 0~100
+-- 数据类型:number
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+pb:setProgress(75)
+```
+
+#### ProgressBar.getProgress
+**功能**
+
+获取当前进度值。
+
+**签名**
+```plain
+ProgressBar:getProgress()
+```
+
+**参数**
+```plain
+无
+```
+
+**返回值**
+```plain
+number -- 当前进度 0~100
+```
+
+**示例**
+```plain
+local v = pb:getProgress()
+log.info("pb", v)
+```
+
+#### ProgressBar.setText
+**功能**
+
+设置显示文本(可覆盖百分比显示)。
+
+**签名**
+```plain
+ProgressBar:setText(text)
+```
+
+**参数**
+```plain
+text
+-- 参数含义:文本内容
+-- 数据类型:string
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+pb:setText("下载中...")
+```
+
+**示例**
+```plain
+-- 基础进度条
+local pb = ui.ProgressBar({ 
+    x = 20, y = 480, w = 280, h = 26, 
+    progress = 40 
+})
+
+-- 带自定义文本
+local pb2 = ui.ProgressBar({ 
+    x = 20, y = 520, w = 280, h = 26, 
+    progress = 65, text = "下载中..." 
+})
+```
+
+## 五、Window 使用模式说明
+本节采用与上文一致的结构,分别介绍 Window 的创建与参数、子方法以及子页面(子窗口)使用方式。
+
+### 5.1 Window(窗口容器)创建与参数
+**功能**
+
+窗口容器,用作页面根节点或子页面载体。支持背景色/背景图、子组件管理、滚动与分页、子页面导航等。
+
+**构造**:`ui.Window(args)`
+
+**参数** :**args**
+```plain
+{
+    x = , y = ,
+    -- 参数含义:窗口左上角坐标;
+    -- 数据类型:number;
+    -- 是否必选:可选,默认0;
+
+    w = , h = ,
+    -- 参数含义:窗口宽/高;
+    -- 数据类型:number;
+    -- 是否必选:可选,默认填满屏幕(lcd.getSize());
+
+    backgroundImage = ,
+    -- 参数含义:背景图片路径(.jpg);
+    -- 数据类型:string;
+    -- 是否必选:可选;
+
+    backgroundColor = ,
+    -- 参数含义:背景颜色(RGB565);
+    -- 数据类型:number;
+    -- 是否必选:可选,浅色主题默认Windows 11配色;
+
+    visible = , enabled = ,
+    -- 参数含义:可见/可交互;
+    -- 数据类型:boolean;
+    -- 是否必选:可选,默认 true;
+}
+```
+
+**示例**
+```plain
+-- 使用默认配色(v1.6.0:浅色主题自动使用Windows 11配色)
+local win = ui.Window()
+ui.add(win)
+```
+
+### 5.2 Window 子方法
+**方法**
+```plain
+add(child)                    -- 添加子组件
+remove(child)                 -- 移除子组件
+clear()                       -- 清空子组件
+
+setBackgroundImage(path)      -- 设置背景图
+setBackgroundColor(color)     -- 设置背景色(并清除背景图)
+
+enableScroll(opts)            -- 启用滚动/分页(纵向/横向/双向)
+setContentSize(w, h)          -- 设置内容区尺寸(影响滚动边界)
+
+enableSubpageManager(opts)    -- 启用子页面管理(一次开启即可)
+configureSubpages(factories)  -- 注册子页面工厂{name->function}
+showSubpage(name[, factory])  -- 显示子页面;如未缓存则调用工厂创建
+back()                        -- 关闭当前子页面,返回父窗口
+closeSubpage(name, opts)      -- 关闭指定子页面;opts.destroy=true 彻底销毁
+```
+
+**enableScroll(opts)**
+```plain
+opts = {
+    direction = "vertical" | "horizontal" | "both", -- 滚动方向,默认 "vertical"
+    contentWidth  = <number>,  -- 内容宽度(默认等于窗口宽度)
+    contentHeight = <number>,  -- 内容高度(默认等于窗口高度)
+    threshold = <number>,      -- 判定拖拽门限像素,默认10
+    pagingEnabled = <boolean>, -- 横向分页吸附开关(横向/双向时生效)
+    pageWidth = <number>,      -- 分页宽度,默认窗口宽度
+}
+```
+
+**示例:纵向滚动列表**
+```plain
+local win = ui.Window({ backgroundColor = ui.COLOR_WHITE })
+win:enableScroll({ direction = "vertical", contentHeight = 1200, threshold = 8 })
+-- 之后 add 的子组件会随滚动偏移显示
+```
+
+**示例:横向分页(桌面式)**
+```plain
+local win = ui.Window({ backgroundColor = ui.COLOR_WHITE })
+win:enableScroll({ direction = "horizontal", pagingEnabled = true, pageWidth = 320, contentWidth = 320 * 3 })
+```
+
+### 5.2.1 Window.add
+**功能**
+
+向窗口添加一个子组件(任何实现了 draw/handleEvent 的组件)。
+
+**签名**
+```plain
+Window:add(child)
+```
+
+**参数**
+```plain
+child
+-- 参数含义:要添加的组件实例
+-- 数据类型:table(组件对象)
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+local btn = ui.Button({ x=20, y=40, w=120, h=44, text="OK" })
+win:add(btn)
+```
+
+#### 5.2.2 Window.remove
+**功能**
+
+从窗口中移除一个已添加的子组件。
+
+**签名**
+```plain
+Window:remove(child)
+```
+
+**参数**
+```plain
+child
+-- 参数含义:要移除的组件实例
+-- 数据类型:table(组件对象)
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+boolean -- 是否成功移除
+```
+
+**示例**
+```plain
+win:remove(btn)
+```
+
+#### 5.2.3 Window.clear
+**功能**
+
+清空窗口的所有子组件。
+
+**签名**
+```plain
+Window:clear()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+win:clear()
+```
+
+#### 5.2.4 Window.setBackgroundImage
+**功能**
+
+设置窗口背景图。
+
+**签名**
+```plain
+Window:setBackgroundImage(path)
+```
+
+**参数**
+```plain
+path
+-- 参数含义:.jpg 图片路径
+-- 数据类型:string
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+win:setBackgroundImage("/luadb/wallpaper.jpg")
+```
+
+#### 5.2.5 Window.setBackgroundColor
+**功能**
+
+设置窗口背景色,并清除已设置的背景图。
+
+**签名**
+```plain
+Window:setBackgroundColor(color)
+```
+
+**参数**
+```plain
+color
+-- 参数含义:RGB565 颜色值
+-- 数据类型:number
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+win:setBackgroundColor(ui.COLOR_WHITE)
+```
+
+#### 5.2.6 Window.enableScroll
+**功能**
+
+启用窗口内容的拖拽滚动与可选的横向分页吸附。
+
+**签名**
+```plain
+Window:enableScroll(opts)
+```
+
+**参数**
+```plain
+opts
+-- 参数含义:滚动/分页配置
+-- 数据类型:table(见上文 enableScroll(opts) 配置项)
+-- 是否必选:否
+```
+
+**返回值**
+```plain
+Window -- 返回自身,便于链式调用
+```
+
+**示例**
+```plain
+win:enableScroll({ direction = "vertical", contentHeight = 1200 })
+```
+
+#### 5.2.7 Window.setContentSize
+**功能**
+
+设置内容区尺寸(影响滚动边界的计算),通常在 enableScroll 后调用。
+
+**签名**
+```plain
+Window:setContentSize(w, h)
+```
+
+**参数**
+```plain
+w, h
+-- 参数含义:内容宽/高
+-- 数据类型:number
+-- 是否必选:至少提供其一
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+win:setContentSize(320, 1200)
+```
+
+### 5.3 子页面(Subpage)使用
+Window 内置子页面管理能力,便于在单窗口内组织多级页面与导航返回。
+
+**启用与注册**
+```plain
+local home = ui.Window({ backgroundColor = 0xFFFF })
+-- 一次启用(可省略,首次调用会自动启用)
+home:enableSubpageManager({
+    backEventName = "NAV.BACK",          -- 可选;默认 "NAV.BACK"
+    onBack = function() log.info("nav", "back pressed") end, -- 可选
+})
+
+-- 注册子页面工厂(返回一个 Window 实例)
+home:configureSubpages({
+    settings = function() return require("settings_page").create(ui) end,
+    about    = function() return require("about_page").create(ui) end,
+})
+```
+
+#### 5.3.1 Window.enableSubpageManager
+**功能**
+
+启用子页面管理能力,注册返回事件及可选回调;通常调用一次即可。
+
+**签名**
+```plain
+Window:enableSubpageManager(opts)
+```
+
+**参数**
+```plain
+opts
+-- 参数含义:可选项
+-- 数据类型:table
+-- 字段:
+--   backEventName: string,返回事件名,默认 "NAV.BACK"
+--   onBack: function(),收到返回事件时回调(在父窗口范围)
+```
+
+**返回值**
+```plain
+Window -- 返回自身
+```
+
+**示例**
+```plain
+home:enableSubpageManager({ backEventName = "NAV.BACK" })
+```
+
+#### 5.3.2 Window.configureSubpages
+**功能**
+
+注册子页面工厂表;当使用 showSubpage 时按名称创建并缓存子页面。
+
+**签名**
+```plain
+Window:configureSubpages(factories)
+```
+
+**参数**
+```plain
+factories
+-- 参数含义:工厂表 { name -> function() return Window end }
+-- 数据类型:table
+-- 是否必选:是
+```
+
+**返回值**
+```plain
+Window -- 返回自身
+```
+
+**示例**
+```plain
+home:configureSubpages({ settings = function() return require("settings_page").create(ui) end })
+```
+
+#### 5.3.3 Window.showSubpage
+**功能**
+
+显示指定名称的子页面;如未创建则调用工厂创建并缓存;显示子页面时会隐藏当前窗口。
+
+**签名**
+```plain
+Window:showSubpage(name[, factory])
+```
+
+**参数**
+```plain
+name
+-- 参数含义:子页面名称
+-- 数据类型:string
+-- 是否必选:是
+
+factory
+-- 参数含义:备用工厂(当未通过 configureSubpages 注册时提供)
+-- 数据类型:function() -> Window
+-- 是否必选:否
+```
+
+**返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+home:showSubpage("settings")
+```
+
+#### 5.3.4 Window.back
+**功能**
+
+在子页面中调用,使当前子页面隐藏,并在父窗口无其他子页面可见时恢复父窗口可见/可交互。
+
+**签名**
+```plain
+Window:back()
+```
+
+**参数 / 返回值**
+```plain
+无
+```
+
+**示例**
+```plain
+self:back()
+```
+
+#### 5.3.5 Window.closeSubpage
+**功能**
+
+关闭指定名称的子页面;当 opts.destroy=true 时从缓存中删除并触发垃圾回收。
+
+**签名**
+```plain
+Window:closeSubpage(name, opts)
+```
+
+**参数**
+```plain
+name
+-- 参数含义:子页面名称
+-- 数据类型:string
+-- 是否必选:是
+
+opts
+-- 参数含义:关闭选项
+-- 数据类型:table
+-- 字段:
+--   destroy: boolean,是否销毁缓存
+```
+
+**返回值**
+```plain
+boolean -- 是否存在并处理该子页面
+```
+
+**示例**
+```plain
+home:closeSubpage("settings", { destroy = true })
+```
+
+#### 5.3.6 返回事件(NAV.BACK)
+**功能**
+
+通过发布返回事件触发 onBack 回调;若没有任一子页面可见,则自动恢复父窗口可见。
+
+**签名**
+```plain
+sys.publish("NAV.BACK")
+```
+
+**示例**
+```plain
+sys.publish("NAV.BACK")
+```
+
+**完整示例**
+```plain
+local home = ui.Window({ backgroundColor = 0xFFFF })
+home:configureSubpages({
+    checkbox = function() return require("checkbox_page").create(ui) end,
+    msgbox   = function() return require("msgbox_page").create(ui) end,
+})
+
+local btn = ui.Button({ x = 20, y = 60, w = 280, h = 50, text = "进入子页", onClick = function()
+    home:showSubpage("checkbox")
+end })
+home:add(btn)
+ui.add(home)
+```

+ 1740 - 0
script/libs/exeasyui/exeasyui.lua

@@ -0,0 +1,1740 @@
+--[[
+exEasyUI - 简化的UI组件库
+版本号:1.6.1
+作者: zengshuai 
+日期:2025-10-09
+=====================================
+
+结构说明:
+1. 常量定义 - UI颜色常量和调试配置
+2. 硬件依赖 - 使用exlcd/extp初始化LCD和TP,并使用gtfont初始化字体(可选)
+3. 核心部分 - 组件管理、事件分发、渲染系统
+4. 组件部分 - 目前有6个组件
+    - Button:按钮组件
+    - CheckBox:复选框组件
+    - Label:标签组件
+    - Picture:图片组件
+    - MessageBox:消息框组件
+    - Window:窗口组件
+    - ProgressBar:进度条组件
+
+基于原exSimpleUI重构,将所有代码合并为单个文件,便于使用和维护。
+支持触摸事件分发、组件渲染、主题切换等核心功能。
+]]
+
+local screen_data = require "screen_data_table"     -- 唯一引入屏幕配置参数的地方
+local exlcd = require "exlcd"             -- 显示驱动模块
+local extp = require "extp"                    -- 触摸驱动模块
+
+gtfont_dev = gtfont_dev or nil                  -- 全局SPI设备句柄,避免被GC
+
+-- ================================
+-- 1. 常量定义
+-- ================================
+
+-- UI颜色常量
+local COLOR_WHITE = 0xFFFF
+local COLOR_BLACK = 0x0000
+local COLOR_GRAY  = 0x8410
+local COLOR_BLUE  = 0x001F
+local COLOR_RED   = 0xF800
+local COLOR_GREEN = 0x07E0
+local COLOR_YELLOW = 0xFFE0
+local COLOR_CYAN = 0x07FF
+local COLOR_MAGENTA = 0xF81F
+local COLOR_ORANGE = 0xFC00
+local COLOR_PINK = 0xF81F
+
+-- Windows 11 风格颜色(v1.6.0新增)
+-- Light模式
+local COLOR_WIN11_LIGHT_DIALOG_BG = 0xF79E      -- RGB(243, 243, 243) - 对话框背景
+local COLOR_WIN11_LIGHT_BUTTON_BG = 0xFFDF      -- RGB(251, 251, 252) - 按钮背景
+local COLOR_WIN11_LIGHT_BUTTON_BORDER = 0xE73C  -- RGB(229, 229, 229) - 按钮边框
+-- Dark模式
+local COLOR_WIN11_DARK_DIALOG_BG = 0x2104      -- RGB(32, 32, 32) - 对话框背景
+local COLOR_WIN11_DARK_BUTTON_BG = 0x3186      -- RGB(51, 51, 51) - 按钮背景
+local COLOR_WIN11_DARK_BUTTON_BORDER = 0x4A69  -- RGB(76, 76, 76) - 按钮边框
+
+-- ================================
+-- 2. 硬件依赖部分 (hw)
+-- ================================
+
+local hw = {}
+local FontAdapter = { _backend = "default", _size = 12, _gray = false, _name = nil }
+
+-- 硬件初始化入口
+function hw.init(opts)
+	-- 初始化显示屏
+    -- 使用screen_data配置表中的参数初始化LCD,在配置表中修改即可
+    local lcd_init_success exlcd.init(screen_data.lcdargs)
+	
+
+    -- 检查LCD初始化是否成功
+    if lcd_init_success then
+        log.error("ui_main", "LCD初始化失败")
+        return  -- 初始化失败,退出任务
+    end
+
+	
+
+	-- 通用显示设置
+    lcd.setupBuff(nil, false)     -- 设置帧缓冲区
+    lcd.autoFlush(false)          -- 禁止自动刷新
+
+    -- -- 设置字体为模组自带的opposansm12中文字体
+    -- lcd.setFont(lcd.font_opposansm12_chinese)
+
+    -- 初始化触摸IC
+    -- 使用配置表中的参数初始化触摸
+    extp.init(screen_data.touch)
+    extp.setPublishEnabled("all", true) -- 发布所有消息
+
+    -- 自定义配置
+    extp.setSlideThreshold(40)        -- 设置滑动阈值为40像素
+    extp.setLongPressThreshold(600)   -- 设置长按阈值为600毫秒
+
+    -- 字体后端装配(保持原逻辑,可选)
+    local fcfg = opts.font or {}
+    if fcfg.type == "gtfont" then
+        local spi_id = (fcfg.spi and fcfg.spi.id) or 0
+        local spi_cs = (fcfg.spi and fcfg.spi.cs) or 8
+        local spi_clk = (fcfg.spi and fcfg.spi.clock) or (20 * 1000 * 1000)
+        gtfont_dev = spi.deviceSetup(spi_id or 1, spi_cs or 12, 0, 0, 8, spi_clk or (20*1000*1000), spi.MSB, 1, 0)
+        log.error("exEasyUI.gtfont", "spi.deviceSetup", type(gtfont_dev))
+        if type(gtfont_dev) ~= "userdata" then
+            log.error("exEasyUI.gtfont", "spi.deviceSetup error", type(gtfont_dev))
+            gtfont_dev = nil
+        end
+        local gtfont_ok = gtfont.init(gtfont_dev)
+        if gtfont_ok then
+            FontAdapter._backend = "gtfont"
+            FontAdapter._size = tonumber(fcfg.size or 16)
+            FontAdapter._gray = not not fcfg.gray
+            log.info("exEasyUI", "gtfont enabled", spi_id, spi_cs, FontAdapter._size)
+        else
+            FontAdapter._backend = "default"
+            FontAdapter._size = 12
+            FontAdapter._gray = false
+            log.warn("exEasyUI", "gtfont init failed, fallback to default font")
+        end
+    else
+        FontAdapter._backend = "default"
+        FontAdapter._size = 12
+        FontAdapter._gray = false
+        FontAdapter._name = (fcfg and fcfg.name) or nil
+        if lcd and lcd.setFont and lcd.font_opposansm12_chinese then
+            lcd.setFont(lcd.font_opposansm12_chinese)
+        end
+    end
+    return true
+end
+
+-- ================================
+-- 3. 核心部分 (core + event)
+-- ================================
+
+local core = {}
+local event = {}
+
+-- 组件注册表
+local registry = {}
+local last_action = nil
+local current_theme = "dark"
+
+-- 调试开关
+core.debug_touch = true
+
+-- 调试配置函数
+function core.debug(v)
+    if v == nil then return { touch = not not core.debug_touch } end
+    if type(v) == "boolean" then
+        core.debug_touch = v
+        return
+    end
+    if type(v) == "table" then
+        if v.touch ~= nil then core.debug_touch = not not v.touch end
+        return
+    end
+end
+
+-- 添加组件到渲染队列
+function core.add(component)
+    registry[#registry + 1] = component
+end
+
+-- 从注册表移除组件
+function core.remove(component)
+    for i = #registry, 1, -1 do
+        if registry[i] == component then
+            table.remove(registry, i)
+            return true
+        end
+    end
+    return false
+end
+
+-- 清屏
+function core.clear(color)
+    lcd.clear(color or COLOR_BLACK)
+end
+
+-- 渲染所有可见组件
+function core.render()
+    for i = 1, #registry do
+        local c = registry[i]
+        if c and c.visible ~= false and c.draw then c:draw() end
+    end
+    lcd.flush()
+end
+
+-- 获取当前时间戳
+local function now_ms()
+    if mcu and mcu.ticks then return mcu.ticks() end
+    return (os.clock() or 0) * 1000
+end
+
+-- 命中测试
+local function hit_test(x, y, r)
+    return x >= r.x and y >= r.y and x <= (r.x + r.w) and y <= (r.y + r.h)
+end
+
+-- 触摸事件分发(extp事件)
+function core.handleTouchEvent(evt, x, y)
+    local start_ms
+    if core.debug_touch then
+        start_ms = now_ms()
+    end
+    for i = #registry, 1, -1 do
+        local c = registry[i]
+        if c and c.enabled ~= false and c.handleEvent and 
+           (c._capture == true or hit_test(x, y, { x = c.x, y = c.y, w = c.w, h = c.h })) then
+            if c:handleEvent(evt, x, y) then
+                if core.debug_touch and start_ms then
+                    local dt = now_ms() - start_ms
+                    log.info("exEasyUI", "consumed_by", tostring(c.__name or "component"), string.format("%.2fms", dt))
+                end
+                return true
+            end
+        end
+    end
+    if core.debug_touch and start_ms then
+        local dt = now_ms() - start_ms
+        if evt ~= "MOVE_X" and evt ~= "MOVE_Y" then
+            log.info("exEasyUI", "not_consumed_cost", string.format("%.2fms", dt))
+        end
+    end
+    return false
+end
+
+-- 系统初始化
+function core.init(opts)
+    opts = opts or {}
+    
+    -- 主题设置:根据传入参数设置当前主题(light/dark)
+    if opts.theme == "light" or opts.theme == "dark" then
+        current_theme = opts.theme
+    end
+
+    -- 触摸事件订阅与转发(extp)
+    -- extp_down_x, extp_down_y 记录按下时的原始坐标
+    -- extp_curr_x, extp_curr_y 记录当前触摸点坐标(用于MOVE/滑动等)
+    local extp_down_x, extp_down_y
+    local extp_curr_x, extp_curr_y
+
+    -- extp_dispatch: 触摸事件分发函数,负责将底层触摸事件转换为UI事件
+    -- evt: 事件类型(如TOUCH_DOWN、MOVE_X、SINGLE_TAP等)
+    -- a, b: 事件参数(如坐标或偏移量)
+    local function extp_dispatch(evt, a, b)
+        if evt == "TOUCH_DOWN" then
+            -- 记录触摸按下时的原始坐标,a和b通常为触摸点的x、y坐标,若无法转换为数字则默认为0
+            extp_down_x, extp_down_y = tonumber(a) or 0, tonumber(b) or 0
+            extp_curr_x, extp_curr_y = extp_down_x, extp_down_y
+            if core.debug_touch then log.info("exEasyUI", "extp", "TOUCH_DOWN", extp_curr_x, extp_curr_y) end
+            -- 分发TOUCH_DOWN事件
+            core.handleTouchEvent("TOUCH_DOWN", extp_curr_x, extp_curr_y)
+            last_action = "TOUCH_DOWN"
+            return
+        end
+
+        -- 若未按下则忽略后续事件
+        if not extp_down_x or not extp_down_y then return end
+
+        if evt == "MOVE_X" then
+            -- 处理横向滑动,a为x方向偏移
+            local dx = tonumber(a) or 0
+            extp_curr_x = extp_down_x + dx
+            if core.debug_touch then log.info("exEasyUI", "extp", "MOVE_X", extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent("MOVE_X", extp_curr_x, extp_curr_y)
+            last_action = "MOVE_X"
+            return
+        elseif evt == "MOVE_Y" then
+            -- 处理纵向滑动,b为y方向偏移
+            local dy = tonumber(b) or 0
+            extp_curr_y = extp_down_y + dy
+            if core.debug_touch then log.info("exEasyUI", "extp", "MOVE_Y", extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent("MOVE_Y", extp_curr_x, extp_curr_y)
+            last_action = "MOVE_Y"
+            return
+        elseif evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" then
+            -- 处理左右滑动手势,a为x方向偏移
+            local dx = tonumber(a) or 0
+            extp_curr_x = extp_down_x + dx
+            if core.debug_touch then log.info("exEasyUI", "extp", evt, extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent(evt, extp_curr_x, extp_curr_y)
+            last_action = evt
+        elseif evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
+            -- 处理上下滑动手势,b为y方向偏移
+            local dy = tonumber(b) or 0
+            extp_curr_y = extp_down_y + dy
+            if core.debug_touch then log.info("exEasyUI", "extp", evt, extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent(evt, extp_curr_x, extp_curr_y)
+            last_action = evt
+        elseif evt == "SINGLE_TAP" or evt == "LONG_PRESS" then
+            -- 处理单击/长按事件,a/b为最终坐标(若无则用当前坐标)
+            local ux = tonumber(a) or extp_curr_x or 0
+            local uy = tonumber(b) or extp_curr_y or 0
+            if core.debug_touch then log.info("exEasyUI", "extp", evt, ux, uy) end
+            core.handleTouchEvent(evt, ux, uy)
+            last_action = evt
+        end
+
+        -- 触摸序列结束后,清空坐标状态
+        if last_action == "SINGLE_TAP" or last_action == "LONG_PRESS" or
+           last_action == "SWIPE_LEFT" or last_action == "SWIPE_RIGHT" or
+           last_action == "SWIPE_UP" or last_action == "SWIPE_DOWN" then
+            extp_down_x, extp_down_y = nil, nil
+            extp_curr_x, extp_curr_y = nil, nil
+        end
+    end
+
+    -- 订阅底层触摸事件(baseTouchEvent),由extp_dispatch处理
+    sys.subscribe("baseTouchEvent", extp_dispatch)
+end
+
+-- 获取当前主题
+function core.getTheme()
+    return current_theme
+end
+
+-- 事件系统:订阅事件
+function event.on(name, cb)
+    return sys.subscribe(name, cb)
+end
+
+-- 事件系统:发送事件
+function event.emit(name, ...)
+    return sys.publish(name, ...)
+end
+
+-- ================================
+-- 4. 组件部分
+-- ================================
+
+-- 通用绘图函数
+local function fill_rect(x1, y1, x2, y2, color)
+    lcd.fill(x1, y1, x2, y2 + 1, color) -- 右下边界为不含区间, y2需要+1
+end
+
+local function stroke_rect(x1, y1, x2, y2, color)
+    lcd.drawLine(x1, y1, x2, y1, color)
+    lcd.drawLine(x2, y1, x2, y2, color)
+    lcd.drawLine(x2, y2, x1, y2, color)
+    lcd.drawLine(x1, y2, x1, y1, color)
+end
+
+-- v1.6.1新增:绘制图片占位符(方框+X叉)
+local function draw_image_placeholder(x, y, w, h, bg_color, border_color)
+    bg_color = bg_color or 0x8410  -- 默认灰色
+    border_color = border_color or COLOR_WHITE
+    
+    -- 填充背景
+    fill_rect(x, y, x + w - 1, y + h - 1, bg_color)
+    
+    -- 绘制边框
+    stroke_rect(x, y, x + w - 1, y + h - 1, border_color)
+    
+    -- 绘制X叉(对角线)
+    lcd.drawLine(x, y, x + w - 1, y + h - 1, border_color)
+    lcd.drawLine(x + w - 1, y, x, y + h - 1, border_color)
+    
+    -- 如果尺寸足够大,绘制内缩的X叉使其更明显
+    if w >= 20 and h >= 20 then
+        local margin = math.min(w, h) // 8  -- 内缩边距
+        lcd.drawLine(x + margin, y + margin, x + w - 1 - margin, y + h - 1 - margin, border_color)
+        lcd.drawLine(x + w - 1 - margin, y + margin, x + margin, y + h - 1 - margin, border_color)
+    end
+end
+
+-- FontAdapter 实现
+local function font_line_height(style)
+    if FontAdapter._backend == "gtfont" then
+        local sz = (style and style.size) or FontAdapter._size or 16
+        return sz
+    end
+    -- default backend:优先使用样式中的 size,没有则回退 12
+    if style and style.size then
+        return tonumber(style.size) or 12
+    end
+    return 12
+end
+
+local function font_set(style)
+    style = style or {}
+    if FontAdapter._backend == "gtfont" then
+        FontAdapter._size = tonumber(style.size or FontAdapter._size or 16)
+        FontAdapter._gray = (style.gray ~= nil) and not not style.gray or FontAdapter._gray
+        if lcd and lcd.setFont and lcd.drawGtfontUtf8 then
+            lcd.setFont(lcd.drawGtfontUtf8)
+        end
+        return
+    end
+    -- default backend
+    FontAdapter._name = style.name or FontAdapter._name
+    if lcd and lcd.setFont then
+        -- 优先按 name,其次按 size 猜测常见字体名,最后回退
+        if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
+            lcd.setFont(lcd["font_" .. FontAdapter._name])
+        elseif style and style.size then
+            local size_num = tonumber(style.size)
+            if size_num then
+                local guess = "font_opposansm" .. tostring(size_num) .. "_chinese"
+                if lcd[guess] then
+                    lcd.setFont(lcd[guess])
+                elseif lcd.font_opposansm12_chinese then
+                    lcd.setFont(lcd.font_opposansm12_chinese)
+                end
+            elseif lcd.font_opposansm12_chinese then
+                lcd.setFont(lcd.font_opposansm12_chinese)
+            end
+        elseif lcd.font_opposansm12_chinese then
+            lcd.setFont(lcd.font_opposansm12_chinese)
+        end
+    end
+end
+
+local function font_draw(text, x, y, color, style)
+    color = color or COLOR_WHITE
+    style = style or {}
+    if FontAdapter._backend == "gtfont" then
+        local sz = tonumber(style.size or FontAdapter._size or 16)
+        if FontAdapter._gray and lcd.drawGtfontUtf8Gray then
+            -- 固件灰度级目前不可调,传固定值4
+            lcd.drawGtfontUtf8Gray(text, sz, 4, x, y, color)
+        elseif lcd.drawGtfontUtf8 then
+            lcd.drawGtfontUtf8(text, sz, x, y, color)
+        else
+            -- 回退:不应触达
+            if lcd.drawStr then
+                lcd.drawStr(x, y + 12, text, color)
+            end
+        end
+        return
+    end
+    -- default backend:y 为顶部坐标,内部转换为基线
+    if lcd and lcd.setFont then
+        if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
+            lcd.setFont(lcd["font_" .. FontAdapter._name])
+        else
+            -- 尝试根据 size 选择合适字体
+            local used = false
+            if style and style.size then
+                local guess = "font_opposansm" .. tostring(style.size) .. "_chinese"
+                if lcd[guess] then
+                    lcd.setFont(lcd[guess])
+                    used = true
+                end
+            end
+            if not used and lcd.font_opposansm12_chinese then
+                lcd.setFont(lcd.font_opposansm12_chinese)
+            end
+        end
+    end
+    local lh = font_line_height(style)
+    lcd.drawStr(x, y + lh, text, color)
+end
+
+-- 文本宽度测量
+local function font_measure(text, style)
+    if not text or text == "" then return 0 end
+    style = style or {}
+    if FontAdapter._backend == "gtfont" then
+        local sz = tonumber(style.size or FontAdapter._size or 16)
+        local w = 0
+        local i = 1
+        while i <= #text do
+            local b = string.byte(text, i)
+            if b == 32 then -- space
+                w = w + math.ceil(sz / 2)
+                i = i + 1
+            elseif b < 128 then
+                w = w + math.ceil(sz / 2)
+                i = i + 1
+            else
+                w = w + sz
+                -- 简化处理UTF-8宽字节
+                if i + 2 <= #text then i = i + 3 else i = i + 1 end
+            end
+        end
+        return w
+    end
+    -- default backend,尽量使用原生接口
+    if lcd and lcd.setFont then
+        -- 尝试在测量前设置到期望字体,以匹配绘制
+        if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
+            lcd.setFont(lcd["font_" .. FontAdapter._name])
+        elseif style and style.size then
+            local guess = "font_opposansm" .. tostring(style.size) .. "_chinese"
+            if lcd[guess] then
+                lcd.setFont(lcd[guess])
+            elseif lcd.font_opposansm12_chinese then
+                lcd.setFont(lcd.font_opposansm12_chinese)
+            end
+        elseif lcd.font_opposansm12_chinese then
+            lcd.setFont(lcd.font_opposansm12_chinese)
+        end
+    end
+    if lcd and lcd.getStrWidth then return lcd.getStrWidth(text) end
+    if lcd and lcd.strWidth then return lcd.strWidth(text) end
+    if lcd and lcd.get_string_width then return lcd.get_string_width(text) end
+    -- 估算:英文约为 size/2,中文约为 size
+    local width = 0
+    local i = 1
+    while i <= #text do
+        local byte = string.byte(text, i)
+        if byte < 128 then
+            width = width + math.ceil((tonumber(style.size) or 12) / 2)
+            i = i + 1
+        else
+            width = width + (tonumber(style.size) or 12)
+            i = i + 3
+        end
+    end
+    return width
+end
+
+-- UTF-8字符获取(返回字符和字节长度)
+local function get_utf8_char(text, i)
+    if not text or i > #text then return "", 0 end
+    local byte = string.byte(text, i)
+    if byte < 128 then
+        -- ASCII字符(1字节)
+        return string.sub(text, i, i), 1
+    elseif byte >= 224 and byte < 240 then
+        -- 3字节UTF-8字符(中文等)
+        if i + 2 <= #text then
+            return string.sub(text, i, i + 2), 3
+        else
+            return string.sub(text, i, i), 1
+        end
+    elseif byte >= 192 and byte < 224 then
+        -- 2字节UTF-8字符
+        if i + 1 <= #text then
+            return string.sub(text, i, i + 1), 2
+        else
+            return string.sub(text, i, i), 1
+        end
+    elseif byte >= 240 then
+        -- 4字节UTF-8字符
+        if i + 3 <= #text then
+            return string.sub(text, i, i + 3), 4
+        else
+            return string.sub(text, i, i), 1
+        end
+    else
+        -- 其他情况
+        return string.sub(text, i, i), 1
+    end
+end
+
+-- 文本换行处理(返回行数组)
+-- 支持英文按单词换行,中文按字符换行
+local function wrap_text_lines(text, maxWidth, style)
+    if not text or text == "" then return {""} end
+    if not maxWidth or maxWidth <= 0 then return {text} end
+    
+    local lines = {}
+    local currentLine = ""
+    local currentWidth = 0
+    local wordBuffer = ""  -- 当前英文单词缓冲
+    local wordWidth = 0    -- 当前单词宽度
+    local i = 1
+    
+    while i <= #text do
+        local char, charLen = get_utf8_char(text, i)
+        local charWidth = font_measure(char, style)
+        local byte = string.byte(text, i)
+        
+        -- 判断是否为英文字母或数字
+        local isAlphaNum = (byte >= 48 and byte <= 57) or   -- 0-9
+                          (byte >= 65 and byte <= 90) or    -- A-Z
+                          (byte >= 97 and byte <= 122)      -- a-z
+        
+        if isAlphaNum then
+            -- 英文字符或数字,加入单词缓冲
+            wordBuffer = wordBuffer .. char
+            wordWidth = wordWidth + charWidth
+            i = i + charLen
+        else
+            -- 非英文字符(空格、标点、中文等)
+            -- 先处理缓冲的单词
+            if wordBuffer ~= "" then
+                if currentWidth + wordWidth > maxWidth then
+                    -- 单词放不下
+                    if currentLine ~= "" then
+                        -- 当前行有内容,换行后放单词
+                        table.insert(lines, currentLine)
+                        currentLine = wordBuffer
+                        currentWidth = wordWidth
+                    else
+                        -- 单词本身超长,强制显示
+                        currentLine = wordBuffer
+                        currentWidth = wordWidth
+                    end
+                else
+                    -- 单词可以放下
+                    currentLine = currentLine .. wordBuffer
+                    currentWidth = currentWidth + wordWidth
+                end
+                wordBuffer = ""
+                wordWidth = 0
+            end
+            
+            -- 处理当前字符(空格、标点、中文等)
+            if char == " " then
+                -- 空格:尝试加入当前行
+                if currentWidth + charWidth <= maxWidth then
+                    currentLine = currentLine .. char
+                    currentWidth = currentWidth + charWidth
+                else
+                    -- 空格放不下,换行(空格不放到下一行开头)
+                    if currentLine ~= "" then
+                        table.insert(lines, currentLine)
+                    end
+                    currentLine = ""
+                    currentWidth = 0
+                end
+            else
+                -- 标点或中文
+                if currentWidth + charWidth > maxWidth then
+                    -- 字符放不下,换行
+                    if currentLine ~= "" then
+                        table.insert(lines, currentLine)
+                    end
+                    currentLine = char
+                    currentWidth = charWidth
+                else
+                    -- 字符可以放下
+                    currentLine = currentLine .. char
+                    currentWidth = currentWidth + charWidth
+                end
+            end
+            i = i + charLen
+        end
+    end
+    
+    -- 处理剩余的单词
+    if wordBuffer ~= "" then
+        if currentWidth + wordWidth > maxWidth and currentLine ~= "" then
+            -- 单词放不下,换行
+            table.insert(lines, currentLine)
+            currentLine = wordBuffer
+        else
+            currentLine = currentLine .. wordBuffer
+        end
+    end
+    
+    -- 添加最后一行
+    if currentLine ~= "" then
+        table.insert(lines, currentLine)
+    end
+    
+    -- 至少返回一个空行
+    if #lines == 0 then
+        lines = {""}
+    end
+    
+    return lines
+end
+
+-- 兼容旧接口已移除,统一使用下方两个新 API
+
+-- 新增:直接绘制文本 API(支持 style.size/name/gray)
+local function draw_text_direct(x, y, text, opts)
+    opts = opts or {}
+    local color = opts.color or COLOR_WHITE
+    local style = opts.style or {}
+    font_draw(text or "", x, y, color, style)
+end
+
+-- 新增:在矩形内自适应(仅居中与边界约束,不缩放、不换行)
+local function draw_text_in_rect_centered(x, y, w, h, text, opts)
+    opts = opts or {}
+    local color = opts.color or COLOR_WHITE
+    local style = opts.style or {}
+    local padding = opts.padding or 0
+
+    local tw = font_measure(text or "", style)
+    local lh = font_line_height(style)
+
+    local inner_x = x + padding
+    local inner_y = y + padding
+    local inner_w = w - padding * 2
+    local inner_h = h - padding * 2
+
+    local tx = inner_x + (inner_w - tw) // 2
+    local ty = inner_y + (inner_h - lh) // 2
+
+    -- 夹紧,避免越界
+    tx = math.max(inner_x, tx)
+    tx = math.min(inner_x + inner_w - tw, tx)
+
+    font_draw(text or "", tx, ty, color, style)
+end
+
+-- 旧的宽度测量包装已移除,请直接使用 font_measure(text, style)
+
+-- Button组件 - 基础按钮,支持文本/图片、按下/抬起与点击回调,支持toggle模式
+local Button = {}
+Button.__index = Button
+
+function Button:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.width or opts.w or 100
+    o.h = opts.height or opts.h or 36
+    
+    -- 文本模式参数
+    o.text = opts.text or "Button"
+    o.textSize = opts.textSize or opts.size
+    local dark = (current_theme == "dark")
+    o.bgColor = opts.bgColor or (dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG)
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.borderColor = opts.borderColor or (dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER)
+    
+    -- 图片模式参数(v1.6.0新增,合并自ToolButton)
+    o.src = opts.src
+    o.src_pressed = opts.src_pressed
+    o.src_toggled = opts.src_toggled
+    
+    -- Toggle模式参数(v1.6.0新增)
+    o.toggle = opts.toggle or false
+    o.toggled = opts.toggled or false
+    o.onToggle = opts.onToggle
+    
+    -- 状态
+    o.pressed = false
+    o.onClick = opts.onClick
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    o._imageCache = {}  -- v1.6.1:缓存图片加载状态,避免重复检查和重复打印警告
+    return o
+end
+
+function Button:draw()
+    if not self.visible then return end
+    
+    -- 图片模式:优先显示图片
+    if self.src then
+        local path
+        if self.toggle and self.toggled then
+            path = self.src_toggled or self.src
+        elseif self.pressed then
+            path = self.src_pressed or self.src
+        else
+            path = self.src
+        end
+        
+        -- v1.6.1修复:检查文件是否存在,并改进占位符显示(使用缓存避免重复检查)
+        if type(path) == "string" and path ~= "" and path:lower():sub(-4) == ".jpg" then
+            -- 检查缓存
+            if self._imageCache[path] == nil then
+                -- 未缓存,首次检查文件是否存在
+                if io and io.exists and io.exists(path) then
+                    self._imageCache[path] = true  -- 缓存:文件存在
+                else
+                    self._imageCache[path] = false  -- 缓存:文件不存在
+                    log.warn("Button", "图片文件不存在:", path)
+                end
+            end
+            
+            -- 根据缓存状态处理
+            if self._imageCache[path] == true then
+                lcd.showImage(self.x, self.y, path)
+            else
+                -- 文件不存在(已缓存),直接显示占位符,不再重复警告
+                draw_image_placeholder(self.x, self.y, self.w, self.h, COLOR_GRAY, COLOR_WHITE)
+            end
+        else
+            -- path无效或不是jpg,显示占位符
+            draw_image_placeholder(self.x, self.y, self.w, self.h, COLOR_GRAY, COLOR_WHITE)
+        end
+        return
+    end
+    
+    -- 文本模式:绘制文本按钮
+    local bg = self.pressed and COLOR_GRAY or self.bgColor
+    fill_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, bg)
+    stroke_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.borderColor)
+    draw_text_in_rect_centered(self.x, self.y, self.w, self.h, self.text, {
+        color = self.textColor,
+        style = { size = self.textSize },
+        padding = 2
+    })
+end
+
+function Button:setText(newText)
+    self.text = tostring(newText or "")
+end
+
+function Button:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    local inside = hit_test(x, y, { x = self.x, y = self.y, w = self.w, h = self.h })
+    
+    if evt == "TOUCH_DOWN" and inside then
+        self.pressed = true
+        self._capture = true
+        return true
+    elseif evt == "MOVE_X" or evt == "MOVE_Y" then
+        if self._capture then
+            self.pressed = inside
+            return true
+        end
+    elseif evt == "SINGLE_TAP" then
+        local was_pressed = self.pressed
+        self.pressed = false
+        self._capture = false
+        if was_pressed and inside then
+            -- Toggle模式处理
+            if self.toggle then
+                self.toggled = not self.toggled
+                if self.onToggle then self.onToggle(self.toggled, self) end
+            end
+            -- 触发点击回调
+            if self.onClick then self.onClick(self) end
+            return true
+        end
+        return true
+    elseif evt == "LONG_PRESS" or evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" or evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
+        self.pressed = false
+        self._capture = false
+        return true
+    end
+    return false
+end
+
+-- CheckBox组件 - 复选框,支持选中状态切换
+local CheckBox = {}
+CheckBox.__index = CheckBox
+
+function CheckBox:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.boxSize = opts.boxSize or 16
+    o.text = opts.text
+    local dark = (current_theme == "dark")
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.borderColor = opts.borderColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.bgColor = opts.bgColor or (dark and COLOR_BLACK or COLOR_WHITE)
+    o.tickColor = opts.tickColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.checked = opts.checked or false
+    o.enabled = opts.enabled ~= false
+    o.visible = opts.visible ~= false
+    o.onChange = opts.onChange
+    local text_w = (o.text and (#o.text * 6 + 6) or 0)
+    o.w = o.boxSize + text_w
+    o.h = math.max(o.boxSize, 16)
+    return o
+end
+
+function CheckBox:draw()
+    local x2 = self.x + self.boxSize - 1
+    local y2 = self.y + self.boxSize - 1
+    stroke_rect(self.x, self.y, x2, y2, self.borderColor)
+    fill_rect(self.x + 2, self.y + 2, x2 - 2, y2 - 2, self.bgColor)
+    if self.checked then
+        local pad = 2
+        fill_rect(self.x + pad, self.y + pad, x2 - pad, y2 - pad, self.tickColor)
+    end
+    if self.text then
+        local lh = font_line_height(nil)
+        local ty = self.y + (self.h - lh) // 2
+        draw_text_direct(self.x + self.boxSize + 10, ty, self.text, { color = self.textColor })
+    end
+end
+
+function CheckBox:setChecked(v)
+    local nv = not not v
+    if nv ~= self.checked then
+        self.checked = nv
+        if self.onChange then self.onChange(self.checked) end
+    end
+end
+
+function CheckBox:toggle()
+    self:setChecked(not self.checked)
+end
+
+function CheckBox:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    if evt == "SINGLE_TAP" then
+        if x >= self.x and y >= self.y and x <= (self.x + self.w) and y <= (self.y + self.h) then
+            self:toggle()
+            return true
+        end
+    end
+    return false
+end
+
+-- Label组件 - 文本标签,仅显示不响应事件
+local Label = {}
+Label.__index = Label
+
+function Label:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.text = tostring(opts.text or "")
+    local dark = (current_theme == "dark")
+    o.color = opts.color or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.font = opts.font
+    o.size = opts.size or opts.textSize
+    o.wordWrap = not not opts.wordWrap  -- 是否启用换行
+    o._autoW = (opts.w == nil)
+    o._autoH = true  -- 高度始终自动计算
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    
+    -- 宽度处理
+    local style = { size = o.size }
+    if opts.w then
+        o.w = opts.w
+        o._autoW = false
+    else
+        o.w = font_measure(o.text, style)
+        o._autoW = true
+    end
+    
+    -- 高度处理(根据是否换行)
+    local lh = font_line_height(style)
+    if o.wordWrap and not o._autoW then
+        -- 启用换行且指定了宽度,计算多行高度
+        local lines = wrap_text_lines(o.text, o.w, style)
+        o.h = #lines * lh
+        o._lines = lines
+    else
+        -- 单行高度
+        o.h = lh
+        o._lines = nil
+    end
+    
+    return o
+end
+
+function Label:setText(t)
+    self.text = tostring(t or "")
+    local style = { size = self.size }
+    local lh = font_line_height(style)
+    
+    -- 更新宽度(如果自动)
+    if self._autoW then
+        self.w = font_measure(self.text, style)
+    end
+    
+    -- 更新高度和行缓存
+    if self.wordWrap and not self._autoW then
+        local lines = wrap_text_lines(self.text, self.w, style)
+        self.h = #lines * lh
+        self._lines = lines
+    else
+        self.h = lh
+        self._lines = nil
+    end
+end
+
+function Label:setSize(sz)
+    self.size = tonumber(sz) or self.size
+    local style = { size = self.size }
+    local lh = font_line_height(style)
+    
+    -- 更新宽度(如果自动)
+    if self._autoW then
+        self.w = font_measure(self.text or "", style)
+    end
+    
+    -- 更新高度和行缓存
+    if self.wordWrap and not self._autoW then
+        local lines = wrap_text_lines(self.text or "", self.w, style)
+        self.h = #lines * lh
+        self._lines = lines
+    else
+        self.h = lh
+        self._lines = nil
+    end
+end
+
+function Label:draw()
+    if not self.visible then return end
+    
+    local style = { size = self.size }
+    
+    -- 若指定自定义字体指针,走默认后端路径(不支持换行)
+    if self.font and lcd and lcd.setFont then
+        lcd.setFont(self.font)
+        local lh = font_line_height(nil)
+        lcd.drawStr(self.x, self.y + lh, self.text, self.color)
+        return
+    end
+    
+    -- 换行模式
+    if self.wordWrap and not self._autoW then
+        local lines = self._lines or wrap_text_lines(self.text, self.w, style)
+        local lh = font_line_height(style)
+        for i = 1, #lines do
+            local yPos = self.y + (i - 1) * lh
+            draw_text_direct(self.x, yPos, lines[i], { color = self.color, style = style })
+        end
+        return
+    end
+    
+    -- 无换行模式:截断显示
+    if not self._autoW then
+        -- 有指定宽度限制,需要截断
+        local displayText = self.text
+        local tw = font_measure(displayText, style)
+        if tw > self.w then
+            -- 逐字符截断,直到宽度合适
+            local truncated = ""
+            local i = 1
+            while i <= #displayText do
+                local char, charLen = get_utf8_char(displayText, i)
+                local testText = truncated .. char
+                if font_measure(testText, style) <= self.w then
+                    truncated = testText
+                    i = i + charLen
+                else
+                    break
+                end
+            end
+            displayText = truncated
+        end
+        draw_text_direct(self.x, self.y, displayText, { color = self.color, style = style })
+    else
+        -- 自动宽度,直接显示
+        draw_text_direct(self.x, self.y, self.text, { color = self.color, style = style })
+    end
+end
+
+function Label:handleEvent(evt, x, y)
+    return false -- 文本不拦截事件
+end
+
+-- Picture组件 - 显示单图或轮播多图
+local Picture = {}
+Picture.__index = Picture
+
+function Picture:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.w or 80
+    o.h = opts.h or 80
+    o.src = opts.src
+    o.sources = opts.sources
+    o.index = opts.index or 1
+    o.autoplay = not not opts.autoplay
+    o.interval = opts.interval or 1000
+    o._last_switch = now_ms()
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    o._imageCache = {}  -- v1.6.1:缓存图片加载状态,避免重复检查和重复打印警告
+    return o
+end
+
+function Picture:setSources(list)
+    self.sources = list
+    self.index = 1
+end
+
+function Picture:next()
+    if self.sources and #self.sources > 0 then
+        self.index = self.index % #self.sources + 1
+    end
+end
+
+function Picture:prev()
+    if self.sources and #self.sources > 0 then
+        self.index = (self.index - 2) % #self.sources + 1
+    end
+end
+
+function Picture:play() 
+    self.autoplay = true 
+end
+
+function Picture:pause() 
+    self.autoplay = false 
+end
+
+function Picture:draw()
+    if not self.visible then return end
+    -- 自动轮播
+    if self.autoplay and self.sources and #self.sources > 1 then
+        local t = now_ms()
+        if (t - self._last_switch) >= self.interval then
+            self:next()
+            self._last_switch = t
+        end
+    end
+    -- 选择当前图片路径
+    local path
+    if self.sources and #self.sources > 0 then
+        path = self.sources[self.index]
+    else
+        path = self.src
+    end
+    
+    -- v1.6.1修复:检查文件是否存在,并改进占位符显示(使用缓存避免重复检查)
+    if type(path) == "string" and path ~= "" and path:lower():sub(-4) == ".jpg" then
+        -- 检查缓存
+        if self._imageCache[path] == nil then
+            -- 未缓存,首次检查文件是否存在
+            if io and io.exists and io.exists(path) then
+                self._imageCache[path] = true  -- 缓存:文件存在
+            else
+                self._imageCache[path] = false  -- 缓存:文件不存在
+                log.warn("Picture", "图片文件不存在:", path)
+            end
+        end
+        
+        -- 根据缓存状态处理
+        if self._imageCache[path] == true then
+            lcd.showImage(self.x, self.y, path)
+        else
+            -- 文件不存在(已缓存),显示占位符
+            draw_image_placeholder(self.x, self.y, self.w, self.h, 0x4208, COLOR_WHITE)
+        end
+    elseif path then
+        -- path不是jpg或无效路径,显示占位符
+        draw_image_placeholder(self.x, self.y, self.w, self.h, 0x4208, COLOR_WHITE)
+    end
+    -- 如果path为nil,不显示任何内容(不绘制占位符)
+end
+
+function Picture:handleEvent(evt, x, y)
+    return false -- 默认不消费事件
+end
+
+-- MessageBox组件 - 消息框,包含标题、文本和按钮组
+local MessageBox = {}
+MessageBox.__index = MessageBox
+
+function MessageBox:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 20
+    o.y = opts.y or 40
+    o.w = opts.width or opts.w or 280
+    o.h = opts.height or opts.h or 160
+    o.title = opts.title or "Info"
+    o.message = opts.message or ""
+    o.wordWrap = opts.wordWrap ~= false  -- v1.6.1修复:默认启用自动换行,除非显式传入false
+    o.textSize = opts.textSize or opts.size  -- 文本字号
+    local dark = (current_theme == "dark")
+    o.borderColor = opts.borderColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.bgColor = opts.bgColor or (dark and COLOR_BLACK or COLOR_WHITE)
+    o.buttons = opts.buttons or { "OK" }
+    o.onResult = opts.onResult
+    o.visible = opts.visible ~= false  -- v1.6.1修复:支持从opts读取visible参数,默认true
+    o.enabled = opts.enabled ~= false  -- v1.6.1修复:支持从opts读取enabled参数,默认true
+
+    -- 内部按钮布局
+    o._btns = {}
+    local btn_w = 80
+    local gap = 12
+    local total_w = #o.buttons * btn_w + (#o.buttons - 1) * gap
+    local bx = o.x + (o.w - total_w) // 2
+    local by = o.y + o.h - 12 - 36
+    for i = 1, #o.buttons do
+        local label = o.buttons[i]
+        local b = Button:new({ x = bx, y = by, w = btn_w, h = 36, text = label })
+        b.onClick = function()
+            if o.onResult then o.onResult(label) end
+            o.visible = false
+            -- v1.6.1修复:不再禁用enabled,允许MessageBox复用
+        end
+        o._btns[#o._btns + 1] = b
+        bx = bx + btn_w + gap
+    end
+    
+    -- 计算message文本可用区域
+    o._msgPadding = 10  -- 左右内边距
+    o._msgMaxWidth = o.w - o._msgPadding * 2
+    o._msgStartY = 36  -- message文本起始Y(相对于MessageBox)
+    -- v1.6.1修复:根据是否有按钮动态计算可用高度
+    if #o.buttons > 0 then
+        o._msgMaxHeight = o.h - 12 - 36 - o._msgStartY  -- 有按钮:预留底部边距12 + 按钮高度36
+    else
+        o._msgMaxHeight = o.h - 10 - o._msgStartY  -- 无按钮:只保留底部边距10
+    end
+    
+    return o
+end
+
+function MessageBox:draw()
+    if not self.visible then return end
+    fill_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.bgColor)
+    stroke_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.borderColor)
+    
+    -- 绘制标题
+    draw_text_direct(self.x + 10, self.y + 8, self.title, { color = self.textColor, style = { size = self.textSize } })
+    
+    -- 绘制message文本
+    local msgX = self.x + self._msgPadding
+    local msgY = self.y + self._msgStartY
+    local style = { size = self.textSize }
+    
+    if self.wordWrap then
+        -- 换行模式:在固定高度内显示多行,超出截断
+        local lines = wrap_text_lines(self.message, self._msgMaxWidth, style)
+        local lh = font_line_height(style)
+        local maxLines = math.floor(self._msgMaxHeight / lh)
+        
+        for i = 1, math.min(#lines, maxLines) do
+            local yPos = msgY + (i - 1) * lh
+            draw_text_direct(msgX, yPos, lines[i], { color = self.textColor, style = style })
+        end
+    else
+        -- 无换行模式:单行显示
+        draw_text_direct(msgX, msgY, self.message, { color = self.textColor, style = style })
+    end
+    
+    -- 绘制按钮
+    for i = 1, #self._btns do 
+        self._btns[i]:draw() 
+    end
+end
+
+function MessageBox:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    for i = 1, #self._btns do
+        local b = self._btns[i]
+        if hit_test(x, y, { x = b.x, y = b.y, w = b.w, h = b.h }) then
+            return b:handleEvent(evt, x, y)
+        end
+    end
+    return true -- 拦截其它事件
+end
+
+-- v1.6.1新增:MessageBox复用方法
+function MessageBox:show()
+    self.visible = true
+    self.enabled = true
+end
+
+function MessageBox:hide()
+    self.visible = false
+end
+
+function MessageBox:setTitle(title)
+    self.title = tostring(title or "")
+end
+
+function MessageBox:setMessage(message)
+    self.message = tostring(message or "")
+    -- 如果启用了换行,更新行缓存
+    if self.wordWrap then
+        local style = { size = self.textSize }
+        self._lines = wrap_text_lines(self.message, self._msgMaxWidth, style)
+    end
+end
+
+-- Window组件 - 窗口容器,支持子组件管理和子页面导航
+local Window = {}
+Window.__index = Window
+
+function Window:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    local sw, sh = lcd.getSize()
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.w or sw
+    o.h = opts.h or sh
+    o.backgroundImage = opts.backgroundImage
+    local dark = (current_theme == "dark")
+    o.backgroundColor = opts.backgroundColor or (dark and COLOR_WIN11_DARK_DIALOG_BG or COLOR_WIN11_LIGHT_DIALOG_BG)
+    o.children = {}
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    o._managed = nil
+    o:enableSubpageManager()
+    -- 滚动配置(0.1 版:纵向/横向)
+    o._scroll = nil
+    return o
+end
+
+function Window:add(child)
+    self.children[#self.children + 1] = child
+end
+
+function Window:remove(child)
+    for i = #self.children, 1, -1 do
+        if self.children[i] == child then 
+            table.remove(self.children, i) 
+            return true 
+        end
+    end
+    return false
+end
+
+function Window:clear()
+    self.children = {}
+end
+
+function Window:setBackgroundImage(path)
+    self.backgroundImage = path
+end
+
+function Window:setBackgroundColor(color)
+    self.backgroundColor = color
+    self.backgroundImage = nil
+end
+
+function Window:draw()
+    -- 背景
+    if self.backgroundImage then
+        lcd.showImage(self.x, self.y, self.backgroundImage)
+    else
+        lcd.fill(self.x, self.y, self.x + self.w, self.y + self.h, self.backgroundColor)
+    end
+    -- 子组件
+    local offX, offY = 0, 0
+    if self._scroll and self._scroll.enabled then
+        if self._scroll.direction == "vertical" then
+            offY = self._scroll.offsetY or 0
+        elseif self._scroll.direction == "horizontal" then
+            offX = self._scroll.offsetX or 0
+        elseif self._scroll.direction == "both" then
+            offX = self._scroll.offsetX or 0
+            offY = self._scroll.offsetY or 0
+        end
+    end
+    for i = 1, #self.children do
+        local c = self.children[i]
+        if c and c.visible ~= false and c.draw then
+            local ox, oy = c.x, c.y
+            if self._scroll and self._scroll.enabled then c.x = ox + offX c.y = oy + offY end
+            c:draw()
+            if self._scroll and self._scroll.enabled then c.x, c.y = ox, oy end
+        end
+    end
+end
+
+function Window:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    if not hit_test(x, y, { x = self.x, y = self.y, w = self.w, h = self.h }) then return false end
+    -- 简易滚动(0.1):vertical/horizontal
+    if self._scroll and self._scroll.enabled then
+        local sc = self._scroll
+        local contentW = sc.contentWidth or self.w
+        local contentH = sc.contentHeight or self.h
+        local minX = math.min(0, self.w - (contentW or self.w))
+        local maxX = 0
+        local minY = math.min(0, self.h - (contentH or self.h))
+        local maxY = 0
+        if evt == "TOUCH_DOWN" then
+            sc.startX = x
+            sc.startY = y
+            sc.baseOffsetX = sc.offsetX or 0
+            sc.baseOffsetY = sc.offsetY or 0
+            sc.dragging = false
+            sc.captured = false
+            -- 透传按下给命中的子组件,便于组件进入按下态;若后续进入拖拽会被取消
+            local tx = x - (sc.offsetX or 0)
+            local ty = y - (sc.offsetY or 0)
+            sc.downTarget = nil
+            for i = #self.children, 1, -1 do
+                local c = self.children[i]
+                if c and c.enabled ~= false and c.handleEvent and 
+                   hit_test(tx, ty, { x = c.x, y = c.y, w = c.w, h = c.h }) then
+                    sc.downTarget = c
+                    c:handleEvent("TOUCH_DOWN", tx, ty)
+                    break
+                end
+            end
+            return true
+        elseif evt == "MOVE_Y" or evt == "MOVE_X" then
+            local dx = (x - (sc.startX or x))
+            local dy = (y - (sc.startY or y))
+            if not sc.dragging then
+                local m = math.max(math.abs(dx), math.abs(dy))
+                if m >= (sc.threshold or 10) then
+                    sc.dragging = true
+                    sc.captured = true
+                    -- 进入拖拽,取消先前按下态
+                    if sc.downTarget and sc.downTarget.handleEvent then
+                        local tx = x - (sc.offsetX or 0)
+                        local ty = y - (sc.offsetY or 0)
+                        sc.downTarget:handleEvent("LONG_PRESS", tx, ty)
+                    end
+                    sc.downTarget = nil
+                else
+                    -- v1.6.1修复:未达拖拽阈值时(观望期),转发MOVE给downTarget让其实时更新状态
+                    if sc.downTarget and sc.downTarget.handleEvent then
+                        local tx = x - (sc.offsetX or 0)
+                        local ty = y - (sc.offsetY or 0)
+                        sc.downTarget:handleEvent(evt, tx, ty)
+                    end
+                end
+            end
+            if sc.dragging then
+                local nx = sc.baseOffsetX + dx
+                local ny = sc.baseOffsetY + dy
+                if sc.direction == "vertical" then
+                    if ny < minY then ny = minY end
+                    if ny > maxY then ny = maxY end
+                    sc.offsetY = ny
+                elseif sc.direction == "horizontal" then
+                    if nx < minX then nx = minX end
+                    if nx > maxX then nx = maxX end
+                    sc.offsetX = nx
+                else -- both
+                    if nx < minX then nx = minX end
+                    if nx > maxX then nx = maxX end
+                    if ny < minY then ny = minY end
+                    if ny > maxY then ny = maxY end
+                    sc.offsetX, sc.offsetY = nx, ny
+                end
+                return true
+            end
+            return true
+        elseif evt == "SINGLE_TAP" or evt == "LONG_PRESS" then
+            if sc.dragging then
+                -- 滑动期间禁用点击;若启用分页,则在抬手时做“就近吸附”(无论是否触发 SWIPE)
+                if sc.pagingEnabled and (sc.direction == "horizontal" or sc.direction == "both") then
+                    local pageW = sc.pageWidth or self.w
+                    local totalW = sc.contentWidth or self.w
+                    local pages = math.max(1, math.floor((totalW + pageW - 1) / pageW))
+                    local cur = math.floor((-(sc.offsetX or 0) + pageW / 2) / pageW)
+                    if cur < 0 then cur = 0 end
+                    if cur > pages - 1 then cur = pages - 1 end
+                    sc.offsetX = -cur * pageW
+                end
+                sc.dragging = false
+                sc.captured = false
+                sc.downTarget = nil
+                return true
+            end
+            -- 未拖拽:将事件分发给子组件(坐标转内容坐标)
+            local tx = x - (sc.offsetX or 0)
+            local ty = y - (sc.offsetY or 0)
+            for i = #self.children, 1, -1 do
+                local c = self.children[i]
+                if c and c.enabled ~= false and c.handleEvent and 
+                   hit_test(tx, ty, { x = c.x, y = c.y, w = c.w, h = c.h }) then
+                    if c:handleEvent(evt, tx, ty) then sc.downTarget = nil return true end
+                end
+            end
+            sc.downTarget = nil
+            return true
+        elseif evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" or evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
+            -- 抬手后的滑动手势:结束拖拽,并在需要时做分页吸附(仅横向)
+            sc.dragging = false
+            sc.captured = false
+            if sc.pagingEnabled and (sc.direction == "horizontal" or sc.direction == "both") then
+                local pageW = sc.pageWidth or self.w
+                local totalW = sc.contentWidth or self.w
+                local pages = math.max(1, math.floor((totalW + pageW - 1) / pageW))
+                -- 当前页(offsetX 为负值向右移动内容)
+                local cur = math.floor((-(sc.offsetX or 0) + pageW / 2) / pageW)
+                if evt == "SWIPE_LEFT" then cur = cur + 1 elseif evt == "SWIPE_RIGHT" then cur = cur - 1 end
+                if cur < 0 then cur = 0 end
+                if cur > pages - 1 then cur = pages - 1 end
+                sc.offsetX = -cur * pageW
+            end
+            return true
+        else
+            -- 其他事件(如 MOVE_X/SWIPE_*)在 0.1 版忽略或按需拦截
+            return true
+        end
+    end
+    -- 非滚动窗口:正常分发
+    for i = #self.children, 1, -1 do
+        local c = self.children[i]
+        -- v1.6.1修复:添加_capture检查,让已捕获的组件(如按下的Button)能收到移出范围的MOVE事件
+        if c and c.enabled ~= false and c.handleEvent and 
+           (c._capture == true or hit_test(x, y, { x = c.x, y = c.y, w = c.w, h = c.h })) then
+            if c:handleEvent(evt, x, y) then return true end
+        end
+    end
+    return true -- 拦截窗口区域内未被子组件消费的事件
+end
+
+-- 启用简易滚动(0.1)
+function Window:enableScroll(opts)
+    opts = opts or {}
+    self._scroll = {
+        enabled = true,
+        direction = opts.direction or "vertical",
+        contentWidth = tonumber(opts.contentWidth or self.w) or self.w,
+        contentHeight = tonumber(opts.contentHeight or self.h) or self.h,
+        offsetX = 0,
+        offsetY = 0,
+        threshold = tonumber(opts.threshold or 10) or 10,
+        pagingEnabled = not not opts.pagingEnabled,
+        pageWidth = tonumber(opts.pageWidth or self.w) or self.w,
+        dragging = false,
+        captured = false,
+    }
+    return self
+end
+
+function Window:setContentSize(w, h)
+    if not self._scroll then return end
+    if w then self._scroll.contentWidth = tonumber(w) or self._scroll.contentWidth end
+    if h then self._scroll.contentHeight = tonumber(h) or self._scroll.contentHeight end
+end
+
+-- 启用子页面管理
+function Window:enableSubpageManager(opts)
+    opts = opts or {}
+    if not self._managed then
+        self._managed = { 
+            pages = {}, 
+            backEventName = opts.backEventName or "NAV.BACK", 
+            onBack = opts.onBack 
+        }
+        sys.subscribe(self._managed.backEventName, function()
+            if self._managed.onBack then pcall(self._managed.onBack) end
+            local anyVisible = false
+            for _, pg in pairs(self._managed.pages) do
+                if pg and pg.visible ~= false then anyVisible = true break end
+            end
+            if not anyVisible then
+                self.visible = true
+                self.enabled = true
+            end
+        end)
+    end
+    return self
+end
+
+-- 配置子页面工厂
+function Window:configureSubpages(factories)
+    if not self._managed then self:enableSubpageManager() end
+    self._managed.factories = self._managed.factories or {}
+    for k, v in pairs(factories or {}) do
+        self._managed.factories[k] = v
+    end
+    return self
+end
+
+-- 显示子页面
+function Window:showSubpage(name, factory)
+    if not self._managed then error("enableSubpageManager must be called before showSubpage") end
+    for key, pg in pairs(self._managed.pages) do
+        if pg and pg.visible ~= false then
+            pg.visible = false
+            pg.enabled = false
+        end
+    end
+    if not self._managed.pages[name] then
+        local f = factory
+        if not f and self._managed.factories then f = self._managed.factories[name] end
+        if not f then error("no factory for subpage '" .. tostring(name) .. "'") end
+        self._managed.pages[name] = f()
+        self._managed.pages[name]._parentWindow = self
+        core.add(self._managed.pages[name])
+    end
+    self.visible = false
+    self._managed.pages[name].visible = true
+    self._managed.pages[name].enabled = true
+end
+
+-- 返回上级页面
+function Window:back()
+    if self._parentWindow then
+        self.visible = false
+        self.enabled = false
+        local parent = self._parentWindow
+        local anyVisible = false
+        if parent._managed and parent._managed.pages then
+            for _, pg in pairs(parent._managed.pages) do
+                if pg and pg.visible ~= false then anyVisible = true break end
+            end
+        end
+        if not anyVisible then
+            parent.visible = true
+            parent.enabled = true
+        end
+    end
+end
+
+-- 关闭子页面
+function Window:closeSubpage(name, opts)
+    if not self._managed or not self._managed.pages then return false end
+    opts = opts or {}
+    local pg = self._managed.pages[name]
+    if not pg then return false end
+    pg.visible = false
+    pg.enabled = false
+    if opts.destroy == true then
+        core.remove(pg)
+        self._managed.pages[name] = nil
+        collectgarbage("collect")
+    end
+    local anyVisible = false
+    for _, p in pairs(self._managed.pages) do
+        if p and p.visible ~= false then anyVisible = true break end
+    end
+    if not anyVisible then
+        self.visible = true
+        self.enabled = true
+    end
+    return true
+end
+
+-- ProgressBar组件 - 进度条,支持百分比显示和主题适配
+local ProgressBar = {}
+ProgressBar.__index = ProgressBar
+
+function ProgressBar:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.width or opts.w or 200
+    o.h = opts.height or opts.h or 24
+    o.progress = math.max(0, math.min(100, opts.progress or 0))
+    o.showPercentage = opts.showPercentage ~= false
+    o.text = opts.text
+    o.textSize = opts.textSize or opts.size
+    local dark = (current_theme == "dark")
+    o.backgroundColor = opts.backgroundColor or (dark and COLOR_GRAY or 0xC618)
+    o.progressColor = opts.progressColor or (dark and COLOR_BLUE or 0x001F)
+    o.borderColor = opts.borderColor or (dark and COLOR_WHITE or 0x8410)
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    return o
+end
+
+function ProgressBar:setProgress(value)
+    self.progress = math.max(0, math.min(100, value))
+end
+
+function ProgressBar:getProgress()
+    return self.progress
+end
+
+function ProgressBar:setText(text)
+    self.text = text
+end
+
+function ProgressBar:draw()
+    if not self.visible then return end
+    
+    -- 绘制背景(可选:只绘制内区,避免与边框重复像素)
+    local padding = 1
+    fill_rect(self.x + padding, self.y + padding, self.x + self.w - padding, self.y + self.h - padding, self.backgroundColor)
+    
+    -- 绘制边框
+    stroke_rect(self.x, self.y, self.x + self.w, self.y + self.h, self.borderColor)
+    
+    -- 计算进度条填充
+    padding = 1
+    local inner_left = self.x + padding
+    local inner_top = self.y + padding
+    local inner_right = self.x + self.w - padding  -- 包含式
+    local inner_bottom = self.y + self.h - padding -- 包含式
+    local inner_width = inner_right - inner_left
+    local fill_width = math.floor(inner_width * (self.progress / 100))
+    if fill_width > 0 then
+        local x1 = inner_left
+        local x2 = inner_left + fill_width
+        fill_rect(x1, inner_top, x2, inner_bottom, self.progressColor)
+    end
+    
+    -- 绘制文本
+    if self.showPercentage or self.text then
+        local display_text = self.text or (self.progress .. "%")
+        draw_text_in_rect_centered(self.x, self.y, self.w, self.h, display_text, {
+            color = self.textColor,
+            style = { size = self.textSize },
+            padding = 2
+        })
+    end
+end
+
+function ProgressBar:handleEvent(evt, x, y)
+    return false -- 进度条默认不处理触摸事件
+end
+
+
+-- ================================
+-- 主模块导出
+-- ================================
+
+local M = {}
+
+-- 核心API导出
+M.init = core.init
+M.add = core.add
+M.remove = core.remove
+M.clear = core.clear
+M.render = core.render
+M.handleTouchEvent = core.handleTouchEvent
+M.debug = core.debug
+M.getTheme = core.getTheme
+
+-- 硬件支持
+M.hw = hw
+
+-- 事件系统
+M.event = event
+
+-- 组件构造函数
+M.Button = function(opts) return Button:new(opts) end
+M.CheckBox = function(opts) return CheckBox:new(opts) end
+M.Label = function(opts) return Label:new(opts) end
+M.Picture = function(opts) return Picture:new(opts) end
+M.MessageBox = function(opts) return MessageBox:new(opts) end
+M.Window = function(opts) return Window:new(opts) end
+M.ProgressBar = function(opts) return ProgressBar:new(opts) end
+
+-- 字体 API 导出
+M.font = {
+    set = function(style) return font_set(style) end,
+    measure = function(text, style) return font_measure(text, style) end,
+    lineHeight = function(style) return font_line_height(style) end
+}
+
+-- 对外导出常用颜色常量,便于在业务侧直接使用
+M.COLOR_WHITE = COLOR_WHITE
+M.COLOR_BLACK = COLOR_BLACK
+M.COLOR_GRAY  = COLOR_GRAY
+M.COLOR_BLUE  = COLOR_BLUE
+M.COLOR_RED   = COLOR_RED
+M.COLOR_GREEN = COLOR_GREEN
+M.COLOR_YELLOW = COLOR_YELLOW
+M.COLOR_CYAN = COLOR_CYAN
+M.COLOR_MAGENTA = COLOR_MAGENTA
+M.COLOR_ORANGE = COLOR_ORANGE
+M.COLOR_PINK = COLOR_PINK
+-- Windows 11 Light模式颜色
+M.COLOR_WIN11_LIGHT_DIALOG_BG = COLOR_WIN11_LIGHT_DIALOG_BG
+M.COLOR_WIN11_LIGHT_BUTTON_BG = COLOR_WIN11_LIGHT_BUTTON_BG
+M.COLOR_WIN11_LIGHT_BUTTON_BORDER = COLOR_WIN11_LIGHT_BUTTON_BORDER
+-- Windows 11 Dark模式颜色
+M.COLOR_WIN11_DARK_DIALOG_BG = COLOR_WIN11_DARK_DIALOG_BG
+M.COLOR_WIN11_DARK_BUTTON_BG = COLOR_WIN11_DARK_BUTTON_BG
+M.COLOR_WIN11_DARK_BUTTON_BORDER = COLOR_WIN11_DARK_BUTTON_BORDER
+
+return M

+ 170 - 0
script/libs/exeasyui/exlcd.lua

@@ -0,0 +1,170 @@
+-- exlcd.lua
+--[[
+@module  exlcd
+@summary LCD显示拓展库
+@version 1.0
+@date    2025.09.17
+@author  江访
+@usage
+核心业务逻辑为:
+1、初始化LCD显示屏,支持多种显示芯片
+2、管理屏幕背光亮度及开关状态
+3、管理屏幕休眠和唤醒状态
+4、提供屏幕状态管理功能
+
+本文件的对外接口有4个:
+1、exlcd.init(args)   -- LCD初始化函数
+2、exlcd.bkl(level)   -- 设置背光亮度接口,level 亮度级别(0-100)
+3、exlcd.sleep()      -- 屏幕休眠
+4、exlcd.wakeup()     -- 屏幕唤醒
+]]
+
+local exlcd = {}
+
+-- 屏幕状态管理表
+local screen_state = {
+    is_sleeping = false,   -- 是否休眠中标识
+    last_brightness = 100,  -- 默认亮度100%
+    backlight_on = true,   -- 背光默认开启
+    lcd_config = nil       -- 存储LCD配置
+}
+
+-- LCD初始化函数
+-- @param args LCD参数配置表
+-- @return 初始化成功状态
+function exlcd.init(args)
+
+    if type(args) ~= "table" then
+        log.error("exlcd", "参数必须为表")
+        return false
+    end
+
+    -- 检查必要参数
+    if not args.LCD_MODEL then
+        log.error("exlcd", "缺少必要参数: LCD_MODEL")
+        return false
+    end
+
+    -- LCD型号映射表
+    local lcd_models = {
+        AirLCD_1000 = "st7796",
+        AirLCD_1001 = "st7796",
+        Air780EHM_LCD_1 = "st7796",
+        Air780EHM_LCD_2 = "st7796",
+        Air780EHM_LCD_3 = "st7796",
+        Air780EHM_LCD_4 = "st7796",
+        AirLCD_1020 = "custom"
+    }
+
+    -- 确定LCD型号
+    local lcd_model = lcd_models[args.LCD_MODEL] or args.LCD_MODEL
+
+    -- 存储LCD配置供其他函数使用
+    screen_state.lcd_config = {
+        pin_pwr = args.pin_pwr,
+        pin_pwm = args.pin_pwm,
+        model = lcd_model
+    }
+
+    -- 设置电源引脚 (可选)
+    if args.pin_vcc then
+        gpio.setup(args.pin_vcc, 1, gpio.PULLUP)
+        gpio.set(args.pin_vcc, 1)
+    end
+
+    -- 设置背光电源引脚 (可选)
+    if args.pin_pwr then
+        gpio.setup(args.pin_pwr, 1, gpio.PULLUP)
+        gpio.set(args.pin_pwr, 1)  -- 默认开启背光
+    end
+
+    -- 设置PWM背光引脚 (可选)
+    if args.pin_pwm then
+        pwm.setup(args.pin_pwm, 1000, screen_state.last_brightness)
+        pwm.open(args.pin_pwm, 1000, screen_state.last_brightness)
+    end
+
+    -- 屏幕初始化 (spi_dev和init_in_service为可选参数)
+    local lcd_init = lcd.init(
+        lcd_model,
+        args,
+        args.spi_dev and args.spi_dev or nil,
+        args.init_in_service and args.init_in_service or nil
+    )
+
+    log.info("exlcd", "LCD初始化", lcd_init)
+    return lcd_init
+end
+
+-- 设置背光亮度接口
+-- 使用背光PWM模式控制亮度
+-- @param level 亮度级别(0-100)
+function exlcd.bkl(level)
+    -- 检查屏幕状态
+    if screen_state.is_sleeping then
+        log.warn("exlcd", "屏幕处于休眠状态,无法调节背光")
+        return
+    end
+
+    -- 确保亮度在有效范围内
+    level = level or screen_state.last_brightness
+    level = math.max(0, math.min(100, level or 0))
+
+    -- 设置PWM背光 (如果配置了PWM引脚)
+    if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+        pwm.setup(screen_state.lcd_config.pin_pwm, 1000, level)
+        pwm.open(screen_state.lcd_config.pin_pwm, 1000, level)
+    -- 如果没有PWM但配置了电源引脚,则通过开关控制
+    elseif screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+        gpio.set(screen_state.lcd_config.pin_pwr, level > 0 and 1 or 0)
+    end
+
+    screen_state.last_brightness = level
+    screen_state.backlight_on = (level > 0)
+    log.info("exlcd", "背光设置为", level, "%")
+end
+
+-- 屏幕休眠
+function exlcd.sleep()
+    if not screen_state.is_sleeping then
+        -- 关闭PWM背光 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+            pwm.close(screen_state.lcd_config.pin_pwm)
+        end
+
+        -- 关闭背光电源 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+            gpio.set(screen_state.lcd_config.pin_pwr, 0)
+        end
+
+        -- 执行LCD睡眠
+        lcd.sleep()
+        screen_state.is_sleeping = true
+        log.info("exlcd", "LCD进入休眠状态")
+    end
+end
+
+-- 屏幕唤醒
+function exlcd.wakeup()
+    if screen_state.is_sleeping then
+        -- 开启背光电源 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+            gpio.set(screen_state.lcd_config.pin_pwr, 1)
+        end
+
+        -- 唤醒LCD
+        lcd.wakeup()
+        sys.wait(100)  -- 等待100ms稳定
+
+        -- 恢复背光设置 (如果配置了PWM引脚)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+            pwm.setup(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
+            pwm.open(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
+        end
+
+        screen_state.is_sleeping = false
+        log.info("exlcd", "LCD唤醒")
+    end
+end
+
+return exlcd

+ 294 - 0
script/libs/exeasyui/extp.lua

@@ -0,0 +1,294 @@
+-- extp.lua - 触摸系统模块
+--[[
+@module  extp
+@summary 触摸系统拓展库
+@version 1.0
+@date    2025.09.17
+@author  江访
+@usage
+核心业务逻辑为:
+1、初始化触摸设备,支持多种触摸芯片
+2、处理原始触摸数据并解析为各种手势事件
+3、通过统一消息接口发布触摸事件
+4、提供消息发布控制功能
+5、提供滑动和长按阈值配置功能
+
+支持的触摸事件类型包括:
+1、RAW_DATA - 原始触摸数据
+2、TOUCH_DOWN - 按下事件
+3、MOVE_X - 水平移动
+4、MOVE_Y - 垂直移动
+5、SWIPE_LEFT - 向左滑动
+6、SWIPE_RIGHT - 向右滑动
+7、SWIPE_UP - 向上滑动
+8、SWIPE_DOWN - 向下滑动
+9、SINGLE_TAP - 单击
+10、LONG_PRESS - 长按
+
+本文件的对外接口有5个:
+1、extp.init(args)                      -- 触摸设备初始化函数
+2、extp.setPublishEnabled(msg_type, enabled) -- 设置消息发布状态
+3、extp.getPublishEnabled(msg_type)          -- 获取消息发布状态
+4、extp.setSlideThreshold(threshold)         -- 设置滑动判定阈值
+5、extp.setLongPressThreshold(threshold)     -- 设置长按判定阈值
+
+所有触摸事件均通过sys.publish("baseTouchEvent", event_type, ...)发布
+]]
+local extp = {}
+
+-- 触摸状态变量
+local state = "IDLE"            -- 当前状态:IDLE(空闲), DOWN(按下), MOVE(移动)
+local touch_down_x = 0          -- 按下时的X坐标
+local touch_down_y = 0          -- 按下时的Y坐标
+local touch_down_time = 0       -- 按下时的时间戳
+local slide_threshold = 45      -- 滑动判定阈值(像素)
+local long_press_threshold = 500 -- 长按判定阈值(毫秒)
+local slide_direction = nil     -- 滑动方向(用于MOVE状态)
+
+-- 消息发布控制表,默认全部打开
+local publish_control = {
+    RAW_DATA = true,            -- 原始触摸数据
+    TOUCH_DOWN = true,          -- 按下事件
+    MOVE_X = true,              -- 水平移动
+    MOVE_Y = true,              -- 垂直移动
+    SWIPE_LEFT = true,          -- 向左滑动
+    SWIPE_RIGHT = true,         -- 向右滑动
+    SWIPE_UP = true,            -- 向上滑动
+    SWIPE_DOWN = true,          -- 向下滑动
+    SINGLE_TAP = true,          -- 单击
+    LONG_PRESS = true           -- 长按
+}
+
+-- 定义支持的触摸芯片配置
+local tp_configs = {
+    cst820 = { i2c_speed = i2c.FAST, tp_model = "cst820" },
+    cst9220 = { i2c_speed = i2c.SLOW, tp_model = "cst9220" },
+    gt9157 = { i2c_speed = i2c.FAST, tp_model = "gt9157" },
+    jd9261t = { i2c_speed = i2c.FAST, tp_model = "jd9261t" },
+    AirLCD_1001 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    Air780EHM_LCD_3 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    Air780EHM_LCD_4 = { i2c_speed = i2c.SLOW, tp_model = "gt911" }
+}
+
+-- 设置消息发布状态
+-- @param msg_type 消息类型 ("RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS", 或 "all")
+-- @param enabled 是否启用 (true/false)
+-- @return boolean 操作是否成功
+function extp.setPublishEnabled(msg_type, enabled)
+    if msg_type == "all" then
+        for k, _ in pairs(publish_control) do
+            publish_control[k] = enabled
+        end
+        log.info("extp", "所有消息发布", enabled and "启用" or "禁用")
+        return true
+    elseif publish_control[msg_type] ~= nil then
+        publish_control[msg_type] = enabled
+        log.info("extp", msg_type, "消息发布", enabled and "启用" or "禁用")
+        return true
+    else
+        log.error("extp", "未知的消息类型:", msg_type)
+        return false
+    end
+end
+
+-- 获取消息发布状态
+-- @param msg_type 消息类型 ("RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS")
+-- @return boolean|table 发布状态 (true/false) 或所有状态表(当msg_type为nil时)
+function extp.getPublishEnabled(msg_type)
+    if msg_type == nil then
+        return publish_control
+    elseif publish_control[msg_type] ~= nil then
+        return publish_control[msg_type]
+    else
+        log.error("extp", "未知的消息类型:", msg_type)
+        return false
+    end
+end
+
+-- 设置滑动判定阈值
+-- @param threshold number 滑动判定阈值(像素)
+-- @return boolean 操作是否成功
+function extp.setSlideThreshold(threshold)
+    if type(threshold) == "number" and threshold > 0 then
+        slide_threshold = threshold
+        log.info("extp", "滑动判定阈值设置为:", threshold)
+        return true
+    else
+        log.error("extp", "无效的滑动阈值:", threshold)
+        return false
+    end
+end
+
+-- 设置长按判定阈值
+-- @param threshold number 长按判定阈值(毫秒)
+-- @return boolean 操作是否成功
+function extp.setLongPressThreshold(threshold)
+    if type(threshold) == "number" and threshold > 0 then
+        long_press_threshold = threshold
+        log.info("extp", "长按判定阈值设置为:", threshold)
+        return true
+    else
+        log.error("extp", "无效的长按阈值:", threshold)
+        return false
+    end
+end
+
+-- 触摸回调函数
+-- 参数: tp_device-触摸设备对象, tp_data-触摸数据
+local function tp_callback(tp_device, tp_data)
+    -- 发布原始数据(如果启用)
+    if publish_control.RAW_DATA then
+        sys.publish("TP", tp_device, tp_data)  --当前消息
+        -- sys.publish("baseTouchEvent", "RAW_DATA", moveX, 0) 需要适配此条消息
+    end
+
+    -- 兼容多种数据结构:数组[1]或直接单点表;字段名兼容 x/x_coordinate, y/y_coordinate
+    local p = nil
+    if type(tp_data) == "table" then
+        p = tp_data[1] or tp_data
+    end
+    if type(p) ~= "table" then return end
+
+    local event_type = p.event or p.type or p.evt
+    local x = p.x or p.x_coordinate or 0
+    local y = p.y or p.y_coordinate or 0
+    local timestamp = p.timestamp or p.ts or 0
+    if not event_type then return end
+
+    if event_type == 2 then  -- 抬手事件
+        if state == "DOWN" or state == "MOVE" then
+            local moveX = x - touch_down_x
+            local moveY = y - touch_down_y
+
+            if moveX < -slide_threshold then
+                if publish_control.SWIPE_LEFT then
+                    sys.publish("baseTouchEvent", "SWIPE_LEFT", moveX, 0)
+                end
+            elseif moveX > slide_threshold then
+                if publish_control.SWIPE_RIGHT then
+                    sys.publish("baseTouchEvent", "SWIPE_RIGHT", moveX, 0)
+                end
+            elseif moveY < -slide_threshold then
+                if publish_control.SWIPE_UP then
+                    sys.publish("baseTouchEvent", "SWIPE_UP", 0, moveY)
+                end
+            elseif moveY > slide_threshold then
+                if publish_control.SWIPE_DOWN then
+                    sys.publish("baseTouchEvent", "SWIPE_DOWN", 0, moveY)
+                end
+            else
+                -- 计算按下时间
+                local press_time = timestamp - touch_down_time
+
+                -- 判断是单击还是长按
+                if press_time < long_press_threshold then
+                    if publish_control.SINGLE_TAP then
+                        sys.publish("baseTouchEvent", "SINGLE_TAP", touch_down_x, touch_down_y)
+                    end
+                else
+                    if publish_control.LONG_PRESS then
+                        sys.publish("baseTouchEvent", "LONG_PRESS", touch_down_x, touch_down_y)
+                    end
+                end
+            end
+            state = "IDLE"
+        end
+    elseif event_type == 1 or event_type == 3 then  -- 按下或移动事件
+        if state == "IDLE" and event_type == 1 then
+            -- 从空闲状态接收到按下事件
+            state = "DOWN"
+            touch_down_x = x
+            touch_down_y = y
+            touch_down_time = timestamp
+            slide_direction = nil
+
+            -- 发布按下事件
+            if publish_control.TOUCH_DOWN then
+                sys.publish("baseTouchEvent", "TOUCH_DOWN", x, y)
+            end
+        elseif state == "DOWN" and event_type == 3 then
+            -- 在按下状态下接收到移动事件
+            if math.abs(x - touch_down_x) >= slide_threshold or math.abs(y - touch_down_y) >= slide_threshold then
+                state = "MOVE"
+                -- 确定滑动方向
+                if math.abs(x - touch_down_x) > math.abs(y - touch_down_y) then
+                    -- 水平滑动
+                    if x - touch_down_x < 0 then
+                        slide_direction = "LEFT"
+                    else
+                        slide_direction = "RIGHT"
+                    end
+                else
+                    -- 垂直滑动
+                    if y - touch_down_y < 0 then
+                        slide_direction = "UP"
+                    else
+                        slide_direction = "DOWN"
+                    end
+                end
+            end
+        elseif state == "MOVE" and event_type == 3 then
+            -- 在移动状态下接收到移动事件
+            -- 根据滑动方向发布相应的移动事件
+            if slide_direction == "LEFT" or slide_direction == "RIGHT" then
+                -- 水平滑动,发布MOVE_X事件
+                if publish_control.MOVE_X then
+                    sys.publish("baseTouchEvent", "MOVE_X", x - touch_down_x, 0)
+                end
+            else
+                -- 垂直滑动,发布MOVE_Y事件
+                if publish_control.MOVE_Y then
+                    sys.publish("baseTouchEvent", "MOVE_Y", 0, y - touch_down_y)
+                end
+            end
+        end
+    end
+end
+
+-- 初始化触摸功能
+-- @param args table 初始化参数表,包含以下字段:
+--   TP_MODEL: string 触摸芯片型号 ("cst820", "cst9220", "gt9157", "jd9261t", "AirLCD_1001", "Air780EHM_LCD_3", "Air780EHM_LCD_4")
+--   i2c_id: number I2C总线ID
+--   pin_rst: number 复位引脚
+--   pin_int: number 中断引脚
+-- @return boolean 初始化是否成功
+function extp.init(args)
+    if type(args) ~= "table" then
+        log.error("extp", "参数必须为表")
+        return false
+    end
+
+    -- 检查必要参数
+    if not args.TP_MODEL then
+        log.error("extp", "缺少必要参数: TP_MODEL")
+        return false
+    end
+
+    local TP_MODEL, tp_i2c_id, tp_pin_rst, tp_pin_int = args.TP_MODEL, args.i2c_id, args.pin_rst, args.pin_int
+
+    -- 检查是否支持该型号
+    local config = tp_configs[TP_MODEL]
+    if not config then
+        log.error("extp", "不支持的触摸型号:", TP_MODEL)
+        return false
+    end
+
+    -- 统一初始化流程
+    if config.i2c_speed ~= nil then
+        i2c.setup(tp_i2c_id, config.i2c_speed)
+    end
+    local tp_device = tp.init(config.tp_model, {port=tp_i2c_id, pin_rst=tp_pin_rst, pin_int=tp_pin_int}, tp_callback)
+    if tp_device ~= nil then return true end
+
+    -- 若硬件触摸初始化失败,尝试PC触摸回退
+    log.warn("extp", "触摸初始化失败,尝试PC触摸回退")
+    local ok_pc, dev_pc = pcall(tp.init, "pc", { port = 0 }, tp_callback)
+    if ok_pc and dev_pc then
+        log.info("extp", "PC触摸回退成功")
+        return true
+    end
+    log.error("extp", "PC触摸回退失败")
+    return false
+end
+
+return extp

+ 212 - 0
script/libs/exlcd/exlcd.lua

@@ -0,0 +1,212 @@
+-- exlcd.lua
+--[[
+@module  exlcd
+@summary LCD显示拓展库
+@version 1.0
+@date    2025.09.17
+@author  江访
+@usage
+核心业务逻辑为:
+1、初始化LCD显示屏,支持多种显示芯片
+2、管理屏幕背光亮度及开关状态
+3、管理屏幕休眠和唤醒状态
+4、提供屏幕状态管理功能
+
+本文件的对外接口有4个:
+1、exlcd.init(args)   -- LCD初始化函数
+2、exlcd.bkl(level)   -- 设置背光亮度接口,level 亮度级别(0-100)
+3、exlcd.sleep()      -- 屏幕休眠
+4、exlcd.wakeup()     -- 屏幕唤醒
+]]
+
+local exlcd = {}
+
+-- 屏幕状态管理表
+local screen_state = {
+    is_sleeping = false,   -- 是否休眠中标识
+    last_brightness = 100,  -- 默认亮度100%
+    backlight_on = true,   -- 背光默认开启
+    lcd_config = nil       -- 存储LCD配置
+}
+
+-- LCD初始化函数
+-- @param args LCD参数配置表
+-- @return 初始化成功状态
+function exlcd.init(args)
+
+    if type(args) ~= "table" then
+        log.error("exlcd", "参数必须为表")
+        return false
+    end
+
+    -- 检查必要参数
+    if not args.LCD_MODEL then
+        log.error("exlcd", "缺少必要参数: LCD_MODEL")
+        return false
+    end
+
+    -- LCD型号映射表
+    local lcd_models = {
+        AirLCD_1000 = "st7796",
+        AirLCD_1001 = "st7796",
+        Air780EHM_LCD_1 = "st7796",
+        Air780EHM_LCD_2 = "st7796",
+        Air780EHM_LCD_3 = "st7796",
+        Air780EHM_LCD_4 = "st7796",
+        AirLCD_1020 = "nv3052c"
+    }
+
+    -- 确定LCD型号
+    local lcd_model = lcd_models[args.LCD_MODEL] or args.LCD_MODEL
+
+    -- 存储LCD配置供其他函数使用
+    screen_state.lcd_config = {
+        pin_pwr = args.pin_pwr,
+        pin_pwm = args.pin_pwm,
+        model = lcd_model
+    }
+
+    -- 设置电源引脚 (可选)
+    if args.pin_vcc then
+        gpio.setup(args.pin_vcc, 1, gpio.PULLUP)
+        gpio.set(args.pin_vcc, 1)
+    end
+
+    -- 设置背光电源引脚 (可选)
+    if args.pin_pwr then
+        gpio.setup(args.pin_pwr, 1, gpio.PULLUP)
+        gpio.set(args.pin_pwr, 1)  -- 默认开启背光
+    end
+
+    -- 设置PWM背光引脚 (可选)
+    if args.pin_pwm then
+        pwm.setup(args.pin_pwm, 1000, screen_state.last_brightness)
+        pwm.open(args.pin_pwm, 1000, screen_state.last_brightness)
+    end
+
+    -- 屏幕初始化 (spi_dev和init_in_service为可选参数)
+    local lcd_init = lcd.init(
+        lcd_model,
+        args,
+        args.spi_dev and args.spi_dev or nil,
+        args.init_in_service and args.init_in_service or nil
+    )
+    log.info("exlcd", "LCD初始化", lcd_init)
+    return lcd_init
+end
+
+-- 设置背光亮度接口
+-- 使用背光PWM模式控制亮度
+-- @param level 亮度级别(0-100)
+
+function exlcd.bkl(level)
+    -- 检查屏幕状态和PWM配置
+    if screen_state.is_sleeping then
+        log.warn("exlcd", "屏幕处于休眠状态,无法调节背光")
+        return false
+    end
+    if not screen_state.lcd_config.pin_pwm then
+        log.error("exlcd", "PWM配置不存在,无法调节背光")
+        return false
+    end
+
+    -- 确保GPIO已关闭 
+    gpio.close(screen_state.lcd_config.pin_pwr)
+
+    -- 设置并开启PWM
+    pwm.stop(screen_state.lcd_config.pin_pwm)
+    pwm.close(screen_state.lcd_config.pin_pwm)
+    pwm.setup(screen_state.lcd_config.pin_pwm, 1000, 100)
+    pwm.open(screen_state.lcd_config.pin_pwm, 1000, level)
+    screen_state.last_brightness = level
+    screen_state.backlight_on = (level > 0)
+    log.info("exlcd", "背光设置为", level, "%")
+    return true
+end
+
+
+
+-- function exlcd.bkl(level)
+--     -- 检查屏幕状态
+--     if screen_state.is_sleeping then
+--         log.warn("exlcd", "屏幕处于休眠状态,无法调节背光")
+--         return
+--     end
+
+--     -- 确保亮度在有效范围内
+--     -- level = level or screen_state.last_brightness
+--     -- level = math.max(0, math.min(100, level or 0))
+
+--     if screen_state.lcd_config.pin_pwr then
+--          gpio.close(screen_state.lcd_config.pin_pwr)
+--     end
+
+--     -- 设置PWM背光 (如果配置了PWM引脚)
+--     if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+--         --pwm.setup(screen_state.lcd_config.pin_pwm, 1000, level)
+--         pwm.open(screen_state.lcd_config.pin_pwm, 1000, level)
+--     -- 如果没有PWM但配置了电源引脚,则通过开关控制
+--     elseif screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+--         gpio.set(screen_state.lcd_config.pin_pwr, level > 0 and 1 or 0)
+--     end
+
+--     screen_state.last_brightness = level
+--     screen_state.backlight_on = (level > 0)
+--     log.info("exlcd", "背光设置为", level, "%")
+-- end
+
+-- 屏幕休眠
+function exlcd.sleep()
+    if not screen_state.is_sleeping then
+        -- 关闭PWM背光 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+            pwm.close(screen_state.lcd_config.pin_pwm)
+        end
+
+        -- 关闭背光电源 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+            gpio.setup(screen_state.lcd_config.pin_pwr, 1, gpio.PULLUP)
+            gpio.set(screen_state.lcd_config.pin_pwr, 0)
+        end
+
+        -- 执行LCD睡眠
+        lcd.sleep()
+        screen_state.is_sleeping = true
+        log.info("exlcd", "LCD进入休眠状态")
+    end
+end
+
+-- 屏幕唤醒
+function exlcd.wakeup()
+    if screen_state.is_sleeping then
+        -- 开启背光电源 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+            gpio.set(screen_state.lcd_config.pin_pwr, 1)
+        end
+
+        -- 唤醒LCD
+        lcd.wakeup()
+        sys.wait(100)  -- 等待100ms稳定
+
+        -- 恢复背光设置 (如果配置了PWM引脚)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+            pwm.setup(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
+            pwm.open(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
+        end
+
+        screen_state.is_sleeping = false
+        log.info("exlcd", "LCD唤醒")
+    end
+end
+
+
+function exlcd.get_brightness()
+    return screen_state.last_brightness
+end
+
+-- 获取当前休眠状态
+function exlcd.is_sleeping()
+    return screen_state.is_sleeping
+end
+
+return exlcd

+ 495 - 0
script/libs/exlcd/exlcdAPI接口使用说明.md

@@ -0,0 +1,495 @@
+# exlcd 显示拓展库
+
+## 一、概述
+
+exlcd 显示拓展库是 LuatOS 的 LCD 显示扩展模块,提供 LCD 显示屏初始化、背光亮度 0-100 级控制、屏幕休眠、屏幕休眠唤醒、查询休眠状态等功能。
+
+## 二、核心示例
+
+1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;
+
+2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;
+
+3、更加完整和详细的 demo,请参考 [LuatOS 仓库](https://gitee.com/openLuat/LuatOS/tree/master/module) 中各个产品目录下的 demo/exlcd
+
+```lua
+-- LCD显示功能演示
+local function lcd_demo()
+    -- 初始化SPI总线(SPI屏幕需要)
+    spi.deviceSetup(lcd.HWID_0, 20, 0, 0, 8, 2000000, spi.MSB, 1, 1)
+    
+    -- 初始化LCD显示屏
+    local result = exlcd.init({
+        LCD_MODEL = "AirLCD_1000",
+        port = lcd.HWID_0,
+        pin_vcc = 141, 
+        pin_pwm = 2, 
+        pin_pwr = 25,
+        pin_rst = 19,
+        direction = 1,
+        w = 480,
+        h = 320,
+        xoffset = 0,
+        yoffset = 0
+    })
+    
+    if result then
+        log.info("LCD初始化成功")
+        
+        -- 设置背光亮度为50%
+        exlcd.bkl(50)
+        
+        -- 在屏幕上显示内容
+        lcd.clear(0x00FF)  -- 蓝色背景
+        lcd.setFont(lcd.font_opposansm32) -- 设置32号英文字体
+        lcd.setColor(0xFFFF, 0x0000)  -- 白底黑字
+        lcd.drawStr(20,172,"hello hezhou") -- 使用默认颜色绘制32号"hello hezhou"
+        
+        lcd.setFont(lcd.font_opposansm12_chinese) -- 设置12号中文字体
+        lcd.drawStr(230, 420, "你好合宙", 0xFFFF)  -- 绘制白色文字"你好合宙"
+        
+        lcd.flush()  --更新数据到屏幕
+        
+        -- 5秒后进入休眠
+        sys.wait(5000)
+        exlcd.sleep()
+        
+        -- 10秒后唤醒
+        sys.wait(10000)
+        exlcd.wakeup()
+        exlcd.bkl(80)  -- 设置背光为80%
+    else
+        log.error("LCD初始化失败")
+    end
+end
+
+-- 创建LCD任务
+sys.taskInit(lcd_demo)
+```
+
+## 三、常量详解
+
+exlcd 显示拓展库没有常量。
+
+## 四、函数详解
+
+### exlcd.init(args)
+
+**功能**
+
+初始化 LCD 显示屏,配置相应的显示芯片和参数
+
+**参数 :args**
+
+```lua
+含义说明:args:table类型,LCD初始化参数配置表,table内容格式说明如下:
+{
+    LCD_MODEL = ,
+    -- 参数含义:LCD显示屏型号;
+    -- 数据类型:string;
+    -- 取值范围:"AirLCD_1000"、"AirLCD_1001"、"Air780EHM_LCD_1"、"Air780EHM_LCD_2"、"Air780EHM_LCD_3"、"Air780EHM_LCD_4"、"AirLCD_1020"、"st7735s"、"h050iwv"、"custom"等;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:必须使用支持的型号,否则初始化失败;
+    -- 参数示例:"AirLCD_1000"
+    
+    port = ,
+    -- 参数含义:LCD端口标识;
+    -- 数据类型:string或number;
+    -- 取值范围:lcd.HWID_0、lcd.RGB、(SPI设备号)0、1、2或"device";
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:SPI屏幕需要先初始化SPI总线;
+    -- 参数示例:lcd.HWID_0 或 "device"
+
+
+    pin_vcc = ,
+    -- 参数含义:电源控制引脚;
+    -- 数据类型:number;
+    -- 取值范围:有效的GPIO引脚编号;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:用于控制LCD模块的整体电源;
+    -- 参数示例:141
+
+    pin_pwm = ,
+    -- 参数含义:PWM背光控制引脚;
+    -- 数据类型:number;
+    -- 取值范围:有效的引脚PWM编号;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:用于PWM调光,需要支持PWM功能的引脚;
+    -- 参数示例:2
+
+    pin_pwr = ,
+    -- 参数含义:背光电源控制引脚;
+    -- 数据类型:number;
+    -- 取值范围:有效的GPIO引脚编号;
+    -- 是否必选:LCD屏幕必须传入此参数传入此参数;
+    -- 注意事项:用于背光的开关控制;
+    -- 参数示例:25
+
+    pin_rst = ,
+    -- 参数含义:复位引脚;
+    -- 数据类型:number;
+    -- 取值范围:有效的GPIO引脚编号;
+    -- 是否必选:LCD屏必须传入此参数;
+    -- 注意事项:用于LCD芯片的硬件复位;
+    -- 参数示例:19
+
+    direction = ,
+    -- 参数含义:屏幕显示方向;
+    -- 数据类型:number;
+    -- 取值范围:0-3,分别对应不同的旋转角度;
+    -- 是否必选:LCD屏必须传入此参数;
+    -- 注意事项:不同LCD芯片支持的方向可能不同;
+    -- 参数示例:1
+
+    w = ,
+    -- 参数含义:屏幕宽度;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:必须与实际屏幕分辨率一致;
+    -- 参数示例:480
+
+    h = ,
+    -- 参数含义:屏幕高度;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:必须与实际屏幕分辨率一致;
+    -- 参数示例:320
+
+    xoffset = ,
+    -- 参数含义:X轴偏移量;
+    -- 数据类型:number;
+    -- 取值范围:非负整数;
+    -- 是否必选:LCD屏必须传入此参数;
+    -- 注意事项:用于调整显示位置;
+    -- 参数示例:0
+
+    yoffset = ,
+    -- 参数含义:Y轴偏移量;
+    -- 数据类型:number;
+    -- 取值范围:非负整数;
+    -- 是否必选:LCD屏必须传入此参数;
+    -- 注意事项:用于调整显示位置;
+    -- 参数示例:0
+
+    hbp = ,
+    -- 参数含义:水平后廊;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:46
+
+    hspw = ,
+    -- 参数含义:水平同步脉冲宽度;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:2
+
+    hfp = ,
+    -- 参数含义:水平前廊;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:48
+
+    vbp = ,
+    -- 参数含义:垂直后廊;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:24
+
+    vspw = ,
+    -- 参数含义:垂直同步脉冲宽度;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:2
+
+    vfp = ,
+    -- 参数含义:垂直前廊;
+    -- 数据类型:number;
+    -- 取值范围:正整数;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:24
+
+    bus_speed = ,
+    -- 参数含义:总线速度;
+    -- 数据类型:number;
+    -- 取值范围:正整数,单位Hz;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:60000000
+
+    initcmd = ,
+    -- 参数含义:初始化命令序列;
+    -- 数据类型:table;
+    -- 取值范围:命令字节数组;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:自定义驱动时使用;
+    -- 参数示例:{0xAE, 0x20, 0x10, 0x00}
+    
+    spi_dev = ,   
+    -- 参数含义:SPI设备对象;
+    -- 数据类型:userdata;
+    -- 取值范围:有效的SPI设备对象;
+    -- 是否必选:可选传入此参数;
+    -- 注意事项:当port = "device"时有效,当port ≠ "device"时可不填或者填nil;
+    -- 参数示例:spi_lcd
+
+    init_in_service = ,
+    -- 参数含义:是否在LCD服务中初始化;
+    -- 数据类型:boolean;
+    -- 取值范围:true或false;
+    -- 是否必选:可选传入此参数,默认值为false;
+    -- 注意事项:允许初始化在lcd service里运行,在后台初始化LCD;Air8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度;
+    -- 参数示例:true
+
+}
+```
+
+**返回值**
+
+- boolean 类型,初始化是否成功
+  - true:初始化成功
+  - false:初始化失败
+
+**示例**
+
+```lua
+-- SPI屏幕初始化
+spi.deviceSetup(lcd.HWID_0, 20, 0, 0, 8, 2000000, spi.MSB, 1, 1)
+local result = exlcd.init({
+    LCD_MODEL = "AirLCD_1000",
+    port = lcd.HWID_0,
+    pin_vcc = 141, 
+    pin_pwm = 2, 
+    pin_pwr = 25,
+    pin_rst = 19,
+    direction = 1,
+    w = 480,
+    h = 320,
+    xoffset = 0,
+    yoffset = 0
+})
+log.info("LCD初始化结果", result)
+
+-- RGB屏幕初始化
+local result = exlcd.init({
+    LCD_MODEL = "AirLCD_1020",
+    port = lcd.RGB,
+    w = 800,
+    h = 480
+})
+```
+
+### exlcd.bkl(level)
+
+**功能**
+
+设置 LCD 背光亮度级别
+
+**参数**
+
+```lua
+level:number类型,背光亮度级别
+参数含义:背光亮度百分比;
+数据类型:number;
+取值范围:0-100,0表示关闭背光,100表示最大亮度;
+是否必选:必须传入此参数;
+注意事项:屏幕休眠状态下无法调节背光;
+参数示例:50
+```
+
+**返回值**
+
+- boolean 类型,操作是否成功
+  - true:操作成功
+  - false:操作失败
+
+**示例**
+
+```lua
+-- 设置背光亮度为50%
+local success = exlcd.bkl(50)
+if success then
+    log.info("背光设置成功")
+else
+    log.error("背光设置失败")
+end
+
+-- 关闭背光
+exlcd.bkl(0)
+
+-- 最大亮度
+exlcd.bkl(100)
+```
+
+### exlcd.sleep()
+
+**功能**
+
+使 LCD 显示屏进入休眠状态
+
+**参数**
+
+无
+
+**返回值**
+
+无
+
+**示例**
+
+```lua
+-- 使屏幕进入休眠
+exlcd.sleep()
+log.info("屏幕已进入休眠状态")
+```
+
+### exlcd.wakeup()
+
+**功能**
+
+从休眠状态唤醒 LCD 显示屏
+
+**参数**
+
+无
+
+**返回值**
+
+无
+
+**示例**
+
+```lua
+-- 唤醒屏幕
+exlcd.wakeup()
+log.info("屏幕已唤醒")
+
+-- 唤醒后恢复之前的背光设置
+exlcd.bkl(exlcd.get_brightness())
+```
+
+### exlcd.get_brightness()
+
+**功能**
+
+获取当前背光亮度级别
+
+**参数**
+
+无
+
+**返回值**
+
+- number 类型,当前背光亮度级别(0-100)
+
+**示例**
+
+```lua
+local brightness = exlcd.get_brightness()
+log.info("当前背光亮度", brightness, "%")
+```
+
+### exlcd.is_sleeping()
+
+**功能**
+
+获取当前 LCD 休眠状态
+
+**参数**
+
+无
+
+**返回值**
+
+- boolean 类型,当前休眠状态
+  - true:屏幕处于休眠状态
+  - false:屏幕处于唤醒状态
+
+**示例**
+
+```lua
+local sleeping = exlcd.is_sleeping()
+if sleeping then
+    log.info("屏幕处于休眠状态")
+else
+    log.info("屏幕处于唤醒状态")
+end
+```
+
+## 完整使用示例
+
+```lua
+-- 完整的LCD应用示例
+local function complete_lcd_demo()
+    -- 初始化SPI
+    spi_lcd = spi.deviceSetup(0, 20, 0, 0, 8, 2000000, spi.MSB, 1, 1)
+    
+    -- 初始化LCD
+    if not exlcd.init({
+        LCD_MODEL = "Air780EHM_LCD_4",
+        port = "device",
+        pin_dc = 17, 
+        pin_pwr = 7,
+        pin_rst = 19,
+        direction = 1,
+        w = 480,
+        h = 320,
+        xoffset = 0,
+        yoffset = 0,
+    }) then
+        log.error("LCD初始化失败")
+        return
+    end
+    
+    -- 在屏幕上显示内容
+        lcd.clear(0x00FF)  -- 蓝色背景
+        lcd.setFont(lcd.font_opposansm32) -- 设置32号英文字体
+        lcd.setColor(0xFFFF, 0x0000)  -- 白底黑字
+        lcd.drawStr(20,172,"hello hezhou") -- 使用默认颜色绘制32号"hello hezhou"
+        
+        lcd.setFont(lcd.font_opposansm12_chinese) -- 设置12号中文字体
+        lcd.drawStr(230, 420, "你好合宙", 0xFFFF)  -- 绘制白色文字"你好合宙"
+        
+        lcd.flush()  --更新数据到屏幕
+    
+    -- 背光控制演示
+    for i = 0, 100, 1 do
+        exlcd.bkl(i)
+        log.info("背光亮度", i, "%")
+        sys.wait(500)
+    end
+    
+    -- 休眠唤醒演示
+    log.info("准备进入休眠")
+    sys.wait(2000)
+    exlcd.sleep()
+    
+    log.info("休眠5秒后唤醒")
+    sys.wait(5000)
+    exlcd.wakeup()
+    exlcd.bkl(80)
+    
+    -- 显示唤醒后的内容
+    lcd.clear(0x00FF00)
+    lcd.drawStr(10, 30, "Welcome Back!", 0x000000, 0x00FF00)
+    lcd.update()
+end
+
+sys.taskInit(complete_lcd_demo)
+```
+
+## 产品支持说明
+
+支持 LuatOS 开发且能够支持 LCD 屏幕的模组。

+ 13 - 0
script/libs/exlcd/exlcd版本更新说明.md

@@ -0,0 +1,13 @@
+# extp 版本管理
+
+## 版本历史
+
+### v1.0.0 - 初始版本
+**发布日期**: 2025年9月17日
+
+**新增功能**
+1. 提供LCD显示屏初始化接口
+2. 提供背光亮度0-100级控制接口
+3. 提供屏幕休眠控制接口
+4. 提供屏幕休眠唤醒接口
+5. 提供查询休眠状态接口

+ 308 - 0
script/libs/extp/extp.lua

@@ -0,0 +1,308 @@
+-- extp.lua - 触摸系统模块
+--[[
+@module  extp
+@summary 触摸系统拓展库
+@version 1.0
+@date    2025.09.17
+@author  江访
+@usage
+核心业务逻辑为:
+1、初始化触摸设备,支持多种触摸芯片
+2、处理原始触摸数据并解析为各种手势事件
+3、通过统一消息接口发布触摸事件
+4、提供消息发布控制功能
+5、提供滑动和长按阈值配置功能
+
+支持的触摸事件类型包括:
+1、RAW_DATA - 原始触摸数据
+2、TOUCH_DOWN - 按下事件
+3、MOVE_X - 水平移动
+4、MOVE_Y - 垂直移动
+5、SWIPE_LEFT - 向左滑动
+6、SWIPE_RIGHT - 向右滑动
+7、SWIPE_UP - 向上滑动
+8、SWIPE_DOWN - 向下滑动
+9、SINGLE_TAP - 单击
+10、LONG_PRESS - 长按
+
+触摸判断逻辑:
+1、按下至抬手只能触发事件5-10中的一个事件
+2、移动像素超过滑动判定阈值,
+   如果触发的是水平移动MOVE_X,抬手只会返回SWIPE_LEFT和SWIPE_RIGHT事件,
+   如果触发的是垂直移动MOVE_Y,抬手只会返回SWIPE_UP和SWIPE_DOWN事件,
+3、按下至抬手像素移动超过滑动判定阈值,
+   如果时间小于500ms判定为单击,按下至抬手的时间大于500ms判定为长按
+
+
+本文件的对外接口有5个:
+1、extp.init(args)                      -- 触摸设备初始化函数
+2、extp.setPublishEnabled(msg_type, enabled) -- 设置消息发布状态
+3、extp.getPublishEnabled(msg_type)          -- 获取消息发布状态
+4、extp.setSlideThreshold(threshold)         -- 设置滑动判定阈值
+5、extp.setLongPressThreshold(threshold)     -- 设置长按判定阈值
+
+所有触摸事件均通过sys.publish("baseTouchEvent", event_type, ...)发布
+]]
+local extp = {}
+
+-- 触摸状态变量
+local state = "IDLE"            -- 当前状态:IDLE(空闲), DOWN(按下), MOVE(移动)
+local touch_down_x = 0          -- 按下时的X坐标
+local touch_down_y = 0          -- 按下时的Y坐标
+local touch_down_time = 0       -- 按下时的时间戳
+local slide_threshold = 45      -- 滑动判定阈值(像素)
+local long_press_threshold = 500 -- 长按判定阈值(毫秒)
+local slide_direction = nil     -- 滑动方向(用于MOVE状态)
+
+-- 消息发布控制表,默认全部打开
+local publish_control = {
+    RAW_DATA = true,            -- 原始触摸数据
+    TOUCH_DOWN = true,          -- 按下事件
+    MOVE_X = true,              -- 水平移动
+    MOVE_Y = true,              -- 垂直移动
+    SWIPE_LEFT = true,          -- 向左滑动
+    SWIPE_RIGHT = true,         -- 向右滑动
+    SWIPE_UP = true,            -- 向上滑动
+    SWIPE_DOWN = true,          -- 向下滑动
+    SINGLE_TAP = true,          -- 单击
+    LONG_PRESS = true           -- 长按
+}
+
+-- 定义支持的触摸芯片配置
+local tp_configs = {
+    cst820 = { i2c_speed = i2c.FAST, tp_model = "cst820" },
+    cst9220 = { i2c_speed = i2c.SLOW, tp_model = "cst9220" },
+    gt9157 = { i2c_speed = i2c.SLOW, tp_model = "gt9157" },
+    AirLCD_1001 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    AirLCD_1020 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    Air780EHM_LCD_3 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    Air780EHM_LCD_4 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    jd9261t = { i2c_speed = i2c.SLOW, tp_model = "jd9261t" },
+    jd9261t_inited = { i2c_speed = i2c.SLOW, tp_model = "jd9261t_inited" },
+}
+
+-- 设置消息发布状态
+-- @param msg_type 消息类型 ("RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS", 或 "all")
+-- @param enabled 是否启用 (true/false)
+-- @return boolean 操作是否成功
+function extp.setPublishEnabled(msg_type, enabled)
+    if msg_type == "all" then
+        for k, _ in pairs(publish_control) do
+            publish_control[k] = enabled
+        end
+        log.info("extp", "所有消息发布", enabled and "启用" or "禁用")
+        return true
+    elseif publish_control[msg_type] ~= nil then
+        publish_control[msg_type] = enabled
+        log.info("extp", msg_type, "消息发布", enabled and "启用" or "禁用")
+        return true
+    else
+        log.error("extp", "未知的消息类型:", msg_type)
+        return false
+    end
+end
+
+-- 获取消息发布状态
+-- @param msg_type 消息类型 ("RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS")
+-- @return boolean|table 发布状态 (true/false) 或所有状态表(当msg_type为nil时)
+function extp.getPublishEnabled(msg_type)
+    if msg_type == nil then
+        return publish_control
+    elseif publish_control[msg_type] ~= nil then
+        return publish_control[msg_type]
+    else
+        log.error("extp", "未知的消息类型:", msg_type)
+        return false
+    end
+end
+
+-- 设置滑动判定阈值
+-- @param threshold number 滑动判定阈值(像素)
+-- @return boolean 操作是否成功
+function extp.setSlideThreshold(threshold)
+    if type(threshold) == "number" and threshold > 0 then
+        slide_threshold = threshold
+        log.info("extp", "滑动判定阈值设置为:", threshold)
+        return true
+    else
+        log.error("extp", "无效的滑动阈值:", threshold)
+        return false
+    end
+end
+
+-- 设置长按判定阈值
+-- @param threshold number 长按判定阈值(毫秒)
+-- @return boolean 操作是否成功
+function extp.setLongPressThreshold(threshold)
+    if type(threshold) == "number" and threshold > 0 then
+        long_press_threshold = threshold
+        log.info("extp", "长按判定阈值设置为:", threshold)
+        return true
+    else
+        log.error("extp", "无效的长按阈值:", threshold)
+        return false
+    end
+end
+
+-- 触摸回调函数
+-- 参数: tp_device-触摸设备对象, tp_data-触摸数据
+local function tp_callback(tp_device, tp_data)
+    -- 发布原始数据(如果启用)
+    if publish_control.RAW_DATA then
+        sys.publish("TP", tp_device, tp_data)  --当前消息
+        -- sys.publish("baseTouchEvent", "RAW_DATA", tp_device, tp_data)
+    end
+    -- 兼容多种数据结构:数组[1]或直接单点表;字段名兼容 x/x_coordinate, y/y_coordinate
+    local p = nil
+    if type(tp_data) == "table" then
+        p = tp_data[1] or tp_data
+    end
+    if type(p) ~= "table" then return end
+
+    local event_type = p.event or p.type or p.evt
+    local x = p.x or p.x_coordinate or 0
+    local y = p.y or p.y_coordinate or 0
+    local ms_h,times = mcu.ticks2(1)
+    local timestamp = times or p.ts or 0
+    if not event_type then return end
+    
+
+    if event_type == 2 then  -- 抬手事件
+        if state == "DOWN" or state == "MOVE" then
+            local moveX = x - touch_down_x
+            local moveY = y - touch_down_y
+
+            if moveX < -slide_threshold then
+                if publish_control.SWIPE_LEFT then
+                    sys.publish("baseTouchEvent", "SWIPE_LEFT", moveX, 0)
+                end
+            elseif moveX > slide_threshold then
+                if publish_control.SWIPE_RIGHT then
+                    sys.publish("baseTouchEvent", "SWIPE_RIGHT", moveX, 0)
+                end
+            elseif moveY < -slide_threshold then
+                if publish_control.SWIPE_UP then
+                    sys.publish("baseTouchEvent", "SWIPE_UP", 0, moveY)
+                end
+            elseif moveY > slide_threshold then
+                if publish_control.SWIPE_DOWN then
+                    sys.publish("baseTouchEvent", "SWIPE_DOWN", 0, moveY)
+                end
+            else
+                -- 计算按下时间
+                local press_time = timestamp - touch_down_time
+
+                -- 判断是单击还是长按
+                if press_time < long_press_threshold then
+                    if publish_control.SINGLE_TAP then
+                        sys.publish("baseTouchEvent", "SINGLE_TAP", touch_down_x, touch_down_y)
+                    end
+                else
+                    if publish_control.LONG_PRESS then
+                        sys.publish("baseTouchEvent", "LONG_PRESS", touch_down_x, touch_down_y)
+                    end
+                end
+            end
+            state = "IDLE"
+        end
+    elseif event_type == 1 or event_type == 3 then  -- 按下或移动事件
+        if state == "IDLE" and event_type == 1 then
+            -- 从空闲状态接收到按下事件
+            state = "DOWN"
+            touch_down_x = x
+            touch_down_y = y
+            touch_down_time = timestamp
+            slide_direction = nil
+
+            -- 发布按下事件
+            if publish_control.TOUCH_DOWN then
+                sys.publish("baseTouchEvent", "TOUCH_DOWN", x, y)
+            end
+        elseif state == "DOWN" and event_type == 3 then
+            -- 在按下状态下接收到移动事件
+            if math.abs(x - touch_down_x) >= slide_threshold or math.abs(y - touch_down_y) >= slide_threshold then
+                state = "MOVE"
+                -- 确定滑动方向
+                if math.abs(x - touch_down_x) > math.abs(y - touch_down_y) then
+                    -- 水平滑动
+                    if x - touch_down_x < 0 then
+                        slide_direction = "LEFT"
+                    else
+                        slide_direction = "RIGHT"
+                    end
+                else
+                    -- 垂直滑动
+                    if y - touch_down_y < 0 then
+                        slide_direction = "UP"
+                    else
+                        slide_direction = "DOWN"
+                    end
+                end
+            end
+        elseif state == "MOVE" and event_type == 3 then
+            -- 在移动状态下接收到移动事件
+            -- 根据滑动方向发布相应的移动事件
+            if slide_direction == "LEFT" or slide_direction == "RIGHT" then
+                -- 水平滑动,发布MOVE_X事件
+                if publish_control.MOVE_X then
+                    sys.publish("baseTouchEvent", "MOVE_X", x - touch_down_x, 0)
+                end
+            else
+                -- 垂直滑动,发布MOVE_Y事件
+                if publish_control.MOVE_Y then
+                    sys.publish("baseTouchEvent", "MOVE_Y", 0, y - touch_down_y)
+                end
+            end
+        end
+    end
+end
+
+-- 初始化触摸功能
+-- @param args table 初始化参数表,包含以下字段:
+--   TP_MODEL: string 触摸芯片型号 ("cst820", "cst9220", "gt9157", "jd9261t", "AirLCD_1001", "Air780EHM_LCD_3", "Air780EHM_LCD_4")
+--   i2c_id: number I2C总线ID
+--   pin_rst: number 复位引脚
+--   pin_int: number 中断引脚
+-- @return boolean 初始化是否成功
+function extp.init(args)
+    if type(args) ~= "table" then
+        log.error("extp", "参数必须为表")
+        return false
+    end
+
+    -- 检查必要参数
+    if not args.TP_MODEL then
+        log.error("extp", "缺少必要参数: TP_MODEL")
+        return false
+    end
+
+    local TP_MODEL, tp_i2c_id, tp_pin_rst, tp_pin_int = args.TP_MODEL, args.i2c_id, args.pin_rst, args.pin_int
+
+    -- 检查是否支持该型号
+    local config = tp_configs[TP_MODEL]
+    if not config then
+        log.error("extp", "不支持的触摸型号:", TP_MODEL)
+        return false
+    end
+
+    -- 统一初始化流程
+    if type(tp_i2c_id) ~= "userdata" and  config.i2c_speed ~= nil then
+        i2c.setup(tp_i2c_id, config.i2c_speed)
+    end
+
+    local tp_device = tp.init(config.tp_model, {port=tp_i2c_id, pin_rst=tp_pin_rst, pin_int=tp_pin_int,w =  1000,h =  800}, tp_callback)
+    log.info("tp_device",tp_device)
+    if tp_device ~= nil then return true end
+
+    -- 若硬件触摸初始化失败,尝试PC触摸回退
+    log.warn("extp", "触摸初始化失败,尝试PC触摸回退")
+    local ok_pc, dev_pc = pcall(tp.init, "pc", { port = 0 }, tp_callback)
+    if ok_pc and dev_pc then
+        log.info("extp", "PC触摸回退成功")
+        return true
+    end
+    log.error("extp", "PC触摸回退失败")
+    return false
+end
+
+return extp

+ 394 - 0
script/libs/extp/extpAPI接口使用说明.md

@@ -0,0 +1,394 @@
+# extp 触摸拓展库
+
+## 一、概述
+
+extp 触摸扩展库是 tp 核心库功能的扩展,调用初始化接口完成初始化后,当有触摸操作时,不用再写程序去分析触摸数据,只需接收触摸时 extp 扩展库发布的手势消息和数据,只管触摸结果反馈即可。
+
+extp 触摸扩展库发布的消息有 10 种,可以通过接口查询消息开启状态,控制发布哪些消息和关闭哪些消息。也能通过接口修改默认判定阈值,以适配多种尺寸触摸屏。
+
+### **1.1 extp 触摸拓展库支持功能**
+
+1. 通过 extp.init(args)接口进行初始化触摸芯片
+2. 触摸操作会自动转换为对应的手势,然后通过全局消息发布,并携带手势类型和坐标参数。
+
+   1. 消息格式为:`sys.publish("baseTouchEvent", event_type, arg1, arg2)`
+   2. `baseTouchEvent` 为:发布的消息标志
+   3. `event_type` 为:事件类型
+   4. `arg1, arg2` 为:事件类型携带的参数
+   5. `event_type` 包含事件和参数 `arg1, arg2` 有:
+      1. RAW_DATA:原始触摸数据,`arg1` 为 tp_device, `arg2` 为 tp_data;
+      2. TOUCH_DOWN:按下瞬间事件,`arg1` 为按下 x 坐标, `arg2` 为按下 y 坐标;
+      3. MOVE_X:水平移动,`arg1` 为为水平移动距离, `arg2` 为 0;
+      4. MOVE_Y:垂直移动,`arg1` 为 0, `arg2` 为垂直移动距离;
+      5. SWIPE_LEFT:向左滑动,`arg1` 为向左滑动距离, `arg2` 为 0;
+      6. SWIPE_RIGHT:向右滑动,`arg1` 为向右滑动距离, `arg2` 为 0;
+      7. SWIPE_UP:向上滑动,`arg1` 为 0, `arg2` 为向上滑动距离;
+      8. SWIPE_DOWN:向下滑动,`arg1` 为 0, `arg2` 为向下滑动距离;
+      9. SINGLE_TAP:单击,`arg1` 为点击 x 坐标, `arg2` 为点击 y 坐标;
+      10. LONG_PRESS:长按,`arg1` 为点击 x 坐标, `arg2` 为点击 y 坐标;
+3. 提供触摸后消息发布打开/关闭的接口:extp.setPublishEnabled(msg_type, enabled),支持单个打开/关闭、全部打开/关闭。
+4. 提供获取单个/全部消息当前是开启/关闭状态查询接口:extp.getPublishEnabled(msg_type)。
+5. 提供滑动多少像素点后判定滑动方向阈值修改接口 extp.setSlideThreshold(threshold),以适配不同尺寸的屏幕,默认为 45 像素。
+6. 提供单击和长按判定阈值修改接口 extp.setSlideThreshold(threshold),默认按下到抬手时间在 500ms 内为单击,大于等于 500ms 为长按。
+
+### 1.2 **extp** 触摸判断逻辑
+
+- TOUCH_DOWN、MOVE_X、MOVE_Y 是按下至抬手的中间状态
+- 按下至抬手后只能触发事件 SWIPE_LEF、SWIPE_RIGHT、SWIPE_UP、SWIPE_DOWN、SINGLE_TAP、LONG_PRESS 中的一个事件
+- 当按下并移动,移动像素超过滑动判定阈值,
+
+  如果触发的是水平移动 MOVE_X,抬手只会返回 SWIPE_LEFT 和 SWIPE_RIGHT 事件,
+
+  如果触发的是垂直移动 MOVE_Y,抬手只会返回 SWIPE_UP 和 SWIPE_DOWN 事件,
+- 按下至抬手像素移动超过滑动判定阈值,
+
+  如果按下至抬手时间小于 500ms 判定为单击,按下至抬手时间大于 500ms 判定为长按
+
+## 二、核心示例
+
+1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;
+
+2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;
+
+3、更加完整和详细的 demo,请参考 [LuatOS 仓库](https://gitee.com/openLuat/LuatOS/tree/master/module) 中各个产品目录下的 demo/extp
+
+```lua
+-- 触摸功能演示
+local function touch_demo()
+    -- 初始化I2C总线
+    i2c.setup(1, i2c.SLOW)
+    
+    -- 初始化触摸屏
+    local result = extp.init({
+        TP_MODEL = "Air780EHM_LCD_4", 
+        port = 1, 
+        pin_rst = 20, 
+        pin_int = 22
+    })
+    
+    if result then
+        log.info("触摸屏初始化成功")
+    else
+        log.error("触摸屏初始化失败")
+        return
+    end
+    
+    -- 订阅触摸事件
+    while true do
+        local result, event_type, arg1, arg2 = sys.waitUntil("baseTouchEvent", 1000)
+        if result then
+            if event_type == "TOUCH_DOWN" then
+                log.info("按下事件", "X:", arg1, "Y:", arg2)
+            elseif event_type == "SWIPE_LEFT" then
+                log.info("向左滑动")
+            elseif event_type == "SWIPE_RIGHT" then
+                log.info("向右滑动")
+            elseif event_type == "MOVE_X" then
+                log.info("水平移动", "距离:", arg1)
+            elseif event_type == "MOVE_Y" then
+                log.info("垂直移动", "距离:", arg2)
+            elseif event_type == "SINGLE_TAP" then
+                log.info("单击", "X:", arg1, "Y:", arg2)
+            elseif event_type == "LONG_PRESS" then
+                log.info("长按", "X:", arg1, "Y:", arg2)
+            elseif event_type == "RAW_DATA" then
+                log.info("原始触摸数据", arg1, arg2)
+            end
+        end
+    end
+end
+
+-- 创建触摸任务
+sys.taskInit(touch_demo)
+```
+
+## 三、常量详解
+
+**扩展库常量,顾名思义是由合宙 LuatOS 扩展库中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;**
+
+extp 扩展库没有常量。
+
+## 四、函数详解
+
+### 4.1 extp.init(args)
+
+**功能**
+
+初始化触摸设备,配置相应的触摸芯片
+
+**参数**  :**args**
+
+```lua
+含义说明:args:table类型,初始化参数配置表,table内容格式说明如下:
+{
+    TP_MODEL = ,
+    -- 参数含义:触摸芯片型号;
+    -- 数据类型:string;
+    -- 取值范围:"cst820"、"cst9220"、"gt9157"、"jd9261t"、"AirLCD_1001"、"Air780EHM_LCD_3"、"Air780EHM_LCD_4"等;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:必须使用支持的型号,否则初始化失败;
+    -- 参数示例:"Air780EHM_LCD_4"
+
+    port = ,
+    -- 参数含义:I2C总线ID;
+    -- 数据类型:number;
+    -- 取值范围:有效的I2C总线编号,或者软件I2C,软件I2C会比硬件慢;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:需要先使用i2c.setup初始化对应的I2C总线;
+    -- 参数示例:硬件I2C:0、1  软件I2C:i2c.createSoft(20, 21)
+
+    pin_rst = ,
+    -- 参数含义:复位引脚编号;
+    -- 数据类型:number;
+    -- 取值范围:有效的GPIO引脚编号;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:引脚需要正确连接到触摸芯片的复位引脚;
+    -- 参数示例:20
+
+    pin_int = 
+    -- 参数含义:中断引脚编号;
+    -- 数据类型:number;
+    -- 取值范围:有效的GPIO引脚编号;
+    -- 是否必选:必须传入此参数;
+    -- 注意事项:引脚需要正确连接到触摸芯片的中断引脚;
+    -- 参数示例:22
+}
+```
+
+**返回值**
+
+- boolean 类型,初始化是否成功
+  - true:初始化成功
+  - false:初始化失败
+
+**示例 1 硬件 I2C 初始化**
+
+```lua
+i2c.setup(1, i2c.SLOW)
+local result = extp.init({
+    TP_MODEL = "Air780EHM_LCD_4", 
+    port = 1, 
+    pin_rst = 20, 
+    pin_int = 22
+})
+log.info("初始化结果", result)
+```
+
+**示例 2 软件 I2C 初始化**
+
+```lua
+-- 创建软件I2C对象
+local softI2C = i2c.createSoft(20, 21)  -- SCL=20, SDA=21
+
+-- 初始化
+local result = extp.init({
+    TP_MODEL = "Air780EHM_LCD_4", 
+    port = softI2C, 
+    pin_rst = 20, 
+    pin_int = 22
+})
+log.info("初始化结果", result)
+```
+
+### 4.2 extp.setPublishEnabled(msg_type, enabled)
+
+**功能**
+
+设置指定消息类型的发布状态,控制是否发布特定类型的触摸事件
+
+**参数  :msg_type**
+
+```lua
+含义说明:msg_type:string类型,消息类型
+参数含义:要设置发布状态的消息类型;
+数据类型:string;
+取值范围:"RAW_DATA"、"TOUCH_DOWN"、"MOVE_X"、"MOVE_Y"、"SWIPE_LEFT"、"SWIPE_RIGHT"、"SWIPE_UP"、"SWIPE_DOWN"、"SINGLE_TAP"、"LONG_PRESS"、"all";
+是否必选:必须传入此参数;
+注意事项:"all"表示设置所有消息类型的发布状态;
+参数示例:"MOVE_X"
+```
+
+**参数  :enabled**
+
+```lua
+enabled:boolean类型,是否启用发布
+参数含义:是否启用该消息类型的发布;
+数据类型:boolean;
+取值范围:true表示启用发布,false表示禁用发布;
+是否必选:必须传入此参数;
+注意事项:设置后会立即生效;
+参数示例:true
+```
+
+**返回值**
+
+- boolean 类型,操作是否成功
+  - true:操作成功
+  - false:操作失败
+
+**示例**
+
+```lua
+-- 启用所有消息发布
+extp.setPublishEnabled("all", true)
+
+-- 禁用原始数据发布
+extp.setPublishEnabled("RAW_DATA", false)
+
+-- 启用水平移动消息发布
+extp.setPublishEnabled("MOVE_X", true)
+```
+
+### 4.3 extp.getPublishEnabled(msg_type)
+
+**功能**
+
+获取指定消息类型的发布状态或所有消息类型的发布状态
+
+**参数**  **:msg_type**
+
+```go
+含义说明:msg_type:string或nil类型,消息类型
+参数含义:要获取发布状态的消息类型;
+数据类型:string或nil;
+取值范围:当为具体消息类型时,取值范围同setPublishEnabled;当为nil时表示获取所有消息状态;
+是否必选:可选传入此参数;
+注意事项:如果传入nil,则返回所有消息类型的发布状态表;
+参数示例:nil 或 "MOVE_X"
+```
+
+**返回值**
+
+- 当 msg_type 为具体消息类型时:boolean 类型,该消息类型的发布状态
+- 当 msg_type 为 nil 时:table 类型,所有消息类型的发布状态表
+- 当 msg_type 无效时:false,表示操作失败
+
+**示例**
+
+```lua
+-- 获取所有消息类型的发布状态
+local all_status = extp.getPublishEnabled()
+log.info("所有消息状态", json.encode(all_status))
+
+-- 获取水平移动消息的发布状态
+local movex_enabled = extp.getPublishEnabled("MOVE_X")
+log.info("MOVE_X状态", movex_enabled)
+```
+
+### 4.4 extp.setSlideThreshold(threshold)
+
+**功能**
+
+设置滑动判定阈值,用于确定何时将触摸移动识别为滑动事件
+
+**参数** ** :threshold**
+
+```lua
+含义说明:threshold:number类型,滑动判定阈值
+参数含义:滑动判定的像素阈值;
+数据类型:number;
+取值范围:必须大于0的数值;
+是否必选:必须传入此参数;
+注意事项:阈值设置过小可能导致误识别,过大可能导致滑动不灵敏;
+参数示例:30
+```
+
+**返回值**
+
+- boolean 类型,操作是否成功
+  - true:操作成功
+  - false:操作失败
+
+**示例**
+
+```lua
+-- 设置滑动判定阈值为30像素
+local success = extp.setSlideThreshold(30)
+if success then
+    log.info("滑动阈值设置成功")
+else
+    log.error("滑动阈值设置失败")
+end
+```
+
+### 4.5 extp.setLongPressThreshold(threshold)
+
+**功能**
+
+修改长按判定阈值,用于确定何时将长按识别为长按事件而非单击事件
+
+**参数  :threshold**
+
+```lua
+含义说明:threshold:number类型,长按判定阈值
+参数含义:长按判定的时间阈值;
+数据类型:number;
+取值范围:必须大于0的数值,单位毫秒;
+是否必选:必须传入此参数;
+注意事项:阈值设置过小可能导致长按被误识别为单击,过大可能导致用户体验不佳;
+参数示例:800
+```
+
+**返回值**
+
+- boolean 类型,操作是否成功
+  - true:操作成功
+  - false:操作失败
+
+**示例**
+
+```lua
+-- 设置长按判定阈值为800毫秒
+local success = extp.setLongPressThreshold(800)
+if success then
+    log.info("长按阈值设置成功")
+else
+    log.error("长按阈值设置失败")
+end
+```
+
+## 五、完整使用示例
+
+```lua
+-- 完整的触摸应用示例
+local function ui_main()
+    
+    -- 初始化触摸屏
+    if not extp.init({TP_MODEL = "Air780EHM_LCD_4", port=1, pin_rst=20, pin_int=22}) then
+        log.error("触摸屏初始化失败")
+        return
+    end
+    
+    -- 自定义配置
+    extp.setSlideThreshold(40)        -- 设置滑动阈值为40像素
+    extp.setLongPressThreshold(600)   -- 设置长按阈值为600毫秒
+    
+    -- 禁用不需要的事件类型
+    extp.setPublishEnabled("RAW_DATA", false)
+    
+    -- 事件处理循环
+    while true do
+        local result, event_type, arg1, arg2 = sys.waitUntil("baseTouchEvent")
+        if result then
+            -- 根据事件类型处理不同的触摸动作
+            if event_type == "SINGLE_TAP" then
+                -- 处理单击事件
+                handle_single_tap(arg1, arg2)
+            elseif event_type == "LONG_PRESS" then
+                -- 处理长按事件
+                handle_long_press(arg1, arg2)
+            elseif event_type == "SWIPE_LEFT" then
+                -- 处理左滑事件
+                handle_swipe_left()
+            -- 其他事件处理...
+            end
+        end
+    end
+end
+
+sys.taskInit(ui_main)
+```
+
+## 六、产品支持说明
+
+支持 TP 核心库的产品,具体可以查看[选型手册](https://docs.openluat.com/air780epm/common/product/)中对应型号是否支持 TP 核心库。

+ 58 - 0
script/libs/extp/extp版本更新说明.md

@@ -0,0 +1,58 @@
+# extp 版本管理
+
+## 版本历史
+
+### v1.0.0 - 初始版本
+**发布日期**: 2025年9月17日
+
+**新增功能**
+1. 通过 extp.init(args)接口进行初始化触摸芯片
+2. 触摸操作会自动转换为对应的手势,然后通过全局消息发布,并携带手势类型和坐标参数。
+
+   1. 消息格式为:`sys.publish("baseTouchEvent", event_type, arg1, arg2)`
+   2. `baseTouchEvent` 为:发布的消息标志
+   3. `event_type` 为:事件类型
+   4. `arg1, arg2` 为:事件类型携带的参数
+   5. `event_type` 包含事件和参数 `arg1, arg2` 有:
+      1. RAW_DATA:原始触摸数据,`arg1` 为 tp_device, `arg2` 为 tp_data;
+      2. TOUCH_DOWN:按下瞬间事件,`arg1` 为按下 x 坐标, `arg2` 为按下 y 坐标;
+      3. MOVE_X:水平移动,`arg1` 为为水平移动距离, `arg2` 为 0;
+      4. MOVE_Y:垂直移动,`arg1` 为 0, `arg2` 为垂直移动距离;
+      5. SWIPE_LEFT:向左滑动,`arg1` 为向左滑动距离, `arg2` 为 0;
+      6. SWIPE_RIGHT:向右滑动,`arg1` 为向右滑动距离, `arg2` 为 0;
+      7. SWIPE_UP:向上滑动,`arg1` 为 0, `arg2` 为向上滑动距离;
+      8. SWIPE_DOWN:向下滑动,`arg1` 为 0, `arg2` 为向下滑动距离;
+      9. SINGLE_TAP:单击,`arg1` 为点击 x 坐标, `arg2` 为点击 y 坐标;
+      10. LONG_PRESS:长按,`arg1` 为点击 x 坐标, `arg2` 为点击 y 坐标;
+3. 提供触摸后消息发布打开/关闭的接口:extp.setPublishEnabled(msg_type, enabled),支持单个打开/关闭、全部打开/关闭。
+4. 提供获取单个/全部消息当前是开启/关闭状态查询接口:extp.getPublishEnabled(msg_type)。
+5. 提供滑动多少像素点后判定滑动方向阈值修改接口 extp.setSlideThreshold(threshold),以适配不同尺寸的屏幕,默认为 45 像素。
+6. 提供单击和长按判定阈值修改接口 extp.setSlideThreshold(threshold),默认按下到抬手时间在 500ms 内为单击,大于等于 500ms 为长按。
+
+**核心特性**
+- TOUCH_DOWN、MOVE_X、MOVE_Y 是按下至抬手的中间状态
+- 按下至抬手后只能触发事件 SWIPE_LEF、SWIPE_RIGHT、SWIPE_UP、SWIPE_DOWN、SINGLE_TAP、LONG_PRESS 中的一个事件
+- 当按下并移动,移动像素超过滑动判定阈值,
+
+  如果触发的是水平移动 MOVE_X,抬手只会返回 SWIPE_LEFT 和 SWIPE_RIGHT 事件,
+
+  如果触发的是垂直移动 MOVE_Y,抬手只会返回 SWIPE_UP 和 SWIPE_DOWN 事件,
+- 按下至抬手像素移动超过滑动判定阈值,
+
+  如果按下至抬手时间小于 500ms 判定为单击,按下至抬手时间大于 500ms 判定为长按
+
+
+### v1.0.1 - 初始版本
+
+**发布日期**: 2025年9月23日
+
+**新增功能**
+新增对软件I2C触摸的支持
+
+
+### v1.0.2 - 初始版本
+
+**发布日期**: 2025年10月9日
+
+**新增功能**
+修改单击和长按时间判定依据,修改系统返回的32为时间为64位时间