跳转至

02 exeasyui扩展库应用

作者:沈园园 | 最后修改:2026-04-20

一、概述

AirLCD_1000 是合宙推出的一款SPI接口,3.5寸屏,480*320 的LCD配件板,其中:

1、显示屏的驱动IC是:ST7796;

2、8个插针管脚:LCD_CLK LCD_SDA LCD_RST LCD_RS LCD_CS BKL VCC GND;

  • BKL:背光控制管脚,默认高电平,背光常亮;低电平时背光熄灭;
  • VCC:2.8V-3.3V;

3、适用于Air780系列/Air8000系列;

airui 是基于 LVGL 9.4 版本进行图形层封装的 LuatOS 核心库,把常用组件、事件管理、输入和基础视觉主题封装为更易上手的 Lua 接口,便于在支持 LuatOS 的设备和 PC 上统一开发。

建议使用airui来开发显示界面,airui demo参考:https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EHM_Air780EHV_Air780EGH/demo/ui/airui/single

二、演示功能概述

1、核心主程序模块

  1. main.lua - 主程序入口,负责系统初始化和任务调度
  2. ui_main.lua - exeasyui 主程序,负责页面管理和按键事件分发和执行exeasyui的任务调度

2、显示页面模块

  1. home_page.lua - 主页模块,提供应用入口和导航功能
  2. component_page.lua - UI 组件演示模块
  3. default_font_page.lua - 默认字体演示模块
  4. hzfont_page.lua - HZFont 矢量字体演示模块

3、驱动模块

  1. hw_default_font_drv.lua - LCD显示驱动配置和默认字体驱动模块,使用内置 12 号点阵字体
  2. hw_hzfont_drv.lua - LCD显示驱动配置和HZFont 矢量字体驱动模块
  3. hw_customer_font_drv.lua - LCD显示驱动配置和自定义外部字体驱动模块(开发中)
  4. key_drv.lua - 按键硬件驱动模块,负责GPIO初始化和按键事件发布

当前演示的exeasyui V1.7.0版本还不支持同时启用多种字体,仅支持选择一种字体初始化,同时启用多种字体功能正在开发中

使用 HZfont 需要使用 V2020 版本及以上的 14 号或者114号固件,且 14 号或114号固件仅支持 HZfont,不支持内置12号中文字体

4 、按键处理模块

key_handler.lua - 按键逻辑处理模块,负责光标管理和界面导航

  • BOOT键 在可点击区域间循环切换
  • 开机键 模拟点击当前中心区域

三、准备硬件环境

1、Air780EGH核心板 × 1

2、AirLCD_1000 配件板 × 1

3、母对母杜邦线 × 14,杜邦线太长的话,会出现 spi 通信不稳定的现象

4、TYPE-C 数据线 × 1

6、Air780EGH核心板和 AirLCD_1000配件板的硬件接线方式为:

  • Air780EGH核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VDD_EXT 引脚为 3.3V,可以直接给 AirLCD_1000配件板供电;
  • 为了演示方便,所以 Air780EGH核心板上电后直接通过 VBAT 引脚给 AirLCD_1000配件板供电;
  • 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;

LCD 显示屏接线

Air780EHM/Air780EHV/Air780EGH 核心板 AirLCD_1000配件板
53/LCD_CLK SCLK/CLK
52/LCD_CS CS
49/LCD_RST RES/RST
50/LCD_SDA SDA/MOS
51/LCD_RS DC/RS
22/GPIO1 BLK
VBAT VCC
GND GND

四、准备软件环境

4.1 软件环境

在开始实践本示例之前,先筹备一下软件环境:

1、烧录工具:Luatools 下载调试工具

2、本demo开发测试时使用的固件为LuatOS-SoC_V2018_Air780EGH 1号固件,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

注意:使用 HZfont 需要使用 V2020 版本及以上的 14 号固件或114号固件,且 14 号固件或114号固件仅支持 HZfont 使用其他字体

3、脚本文件:https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui

4、lib脚本文件:使用Luatools烧录时,勾选 添加默认lib 选项,使用默认lib脚本文件

准备好软件环境之后,接下来查看如何烧录项目文件到Air780EGH核心板,将本篇文章中演示使用的项目文件烧录到Air780EGH核心板中。

4.2 API 介绍

exeasyui UI扩展库:https://docs.openluat.com/osapi/ext/exeasyui/

五、程序结构

AirLCD_1000/exeasyui/
│── main.lua
│── hw_drv
    │── hw_customer_font_drv.lua
    │── hw_default_font_drv.lua   
    │── hw_hzfont_drv.lua       
│── images                    
    │── xxx.jpg       
│── tp_key_drv
    │── key_drv.lua 
│── ui
    │──  component_page.lua
    │──  default_font_page.lua           
    │──  home_page.lua
    │──  hzfont_page.lua    
    │──  key_handler.lua
    │──  ui_main.lua            
│── readme.md

5.1 文件说明

1、hw_drv

  • hw_customer_font_drv.lua:LCD显示驱动配置和自定义外部字体驱动模块(开发中)
  • hw_default_font_drv.lua:LCD显示驱动配置和默认字体驱动模块,使用内置 12 号点阵字体
  • hw_hzfont_drv.lua:LCD显示驱动配置和HZFont 矢量字体驱动模块

2、images

  • xxx.jpg:演示图片文件(和lua脚本文件一起烧录,会自动放置在/luadb/目录下)

3、tp_key_drv

  • key_drv.lua:按键硬件驱动模块,负责GPIO初始化和按键事件发布

5、ui

  • ui_main.lua:exeasyui 主程序,负责页面管理和按键事件分发和执行exeasyui的任务调度
  • home_page.lua :主页模块,提供应用入口和导航功能
  • key_handler.lua :按键处理功能模块
  • component_page.lua:UI 组件演示模块
  • default_font_page.lua:默认字体演示模块
  • hzfont_page.lua:HZFont 矢量字体演示模块

六、代码详解

6.1 main.lua

主程序文件 main.lua 是整个项目的入口点。它负责初始化系统环境。

6.2 hw_customer_font_drv.lua

用户外部自定义外部点阵字体、lcd显示驱动配置和tp触摸驱动配置的驱动配置文件

-- hw_customer_font_drv是用户外部自定义外部点阵字体、lcd显示驱动配置和tp触摸驱动配置的驱动配置文件,目前exeasyui自定义字体功能正在开发中。

6.3 hw_default_font_drv.lua

本文件为默认字体、lcd和tp驱动模块,核心业务逻辑为:

1、使用lcd内核固件中自带的12号中文字体;

2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;

3、提供无需外部硬件的字体显示能力;

-- 使用默认12号中文字体初始化exEasyUI硬件
ui.hw_init({
    -- lcd_config参数填写可以参考合宙exlcd显示扩展库exlcd.init(param)接口说明:https://docs.openluat.com/osapi/ext/exlcd/#31-exlcdinitparam
    lcd_config = {
        lcd_model = "AirLCD_1000", -- LCD型号
        -- pin_vcc = 24,           -- 供电引脚,使用GPIO控制屏幕供电可配置
        pin_rst = 36,              -- 复位引脚
        pin_pwr = 1,               -- 背光控制引脚GPIO ID号
        pin_pwm = 0,               -- 背光控制引脚PWM ID号
        port = lcd.HWID_0,         -- 驱动端口
        -- pin_dc = nil,          -- lcd数据/命令选择引脚GPIO ID号,使用lcd 专用 SPI 接口 lcd.HWID_0不需要填此参数,使用通用SPI接口需要赋值
        direction = 0,             -- lcd屏幕方向 0:0° 1:90° 2:180° 3:270°,屏幕方向和分辨率保存一致
        w = 320,                   -- lcd 水平分辨率
        h = 480,                   -- lcd 竖直分辨率
        xoffset = 0,               -- x偏移(不同屏幕ic 不同屏幕方向会有差异)
        yoffset = 0,               -- y偏移(不同屏幕ic 不同屏幕方向会有差异)
        sleepcmd = 0X10,           -- 睡眠命令,默认0X10
        wakecmd = 0X11,            -- 唤醒命令,默认0X11
        -- bus_speed = 50*1000*1000,                            -- SPI总线速度,不填默认50M,若速率要求更高需要进行设置
        -- interface_mode = lcd.WIRE_4_BIT_8_INTERFACE_I,       -- lcd模式,默认lcd.WIRE_4_BIT_8_INTERFACE_I
        -- direction0 = {0x36,0x00},                            -- 0°方向的命令,(不同屏幕ic会有差异)
        -- direction90 = {0x36,0x60},                           -- 90°方向的命令,(不同屏幕ic会有差异)
        -- direction180 ={0x36,0xc0} ,                          -- 180°方向的命令,(不同屏幕ic会有差异)
        -- direction270 = {0x36,0xA0},                          -- 270°方向的命令,(不同屏幕ic会有差异)
        -- hbp = nil,                                           -- 水平后廊
        -- hspw = nil,                                          -- 水平同步脉冲宽度
        -- hfp = 0,                                             -- 水平前廊
        -- vbp = 0,                                             -- 垂直后廊
        -- vspw = 0,                                            -- 垂直同步脉冲宽度
        -- vfp = 0,                                             -- 垂直前廊
        -- initcmd = nil,                                       -- 自定义屏幕初始化命令表
        -- flush_rate = nil,                                    -- 刷新率
        -- spi_dev = nil,                                       -- spi设备,当port = "device"时有效,当port ≠ "device"时可不填或者填nil
        -- init_in_service = false,                             -- 允许初始化在lcd service里运行,在后台初始化LCD,默认是false,Air8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度,默认SPI总线速度80M
    }
})

6.4 hw_hzfont_drv.lua

本文件为HZFont矢量字体、lcd和tp驱动模块,核心业务逻辑为:

1、使用V2020版本及以上14号固件或114号固件内置的hzfont合宙矢量字库;

2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;

3、提供动态字体大小调整和高质量字体显示能力;

-- 使用Air780EHM/EHV/EGH V2020版本及以上14号固件内置的hzfont合宙矢量字库初始化exEasyUI硬件
ui.hw_init({
    font_config = { type = "hzfont", size = 24, antialias = -1 }, -- 默认-1,表示自动抗锯齿

    -- lcd_config参数填写可以参考合宙exlcd显示扩展库exlcd.init(param)接口说明:https://docs.openluat.com/osapi/ext/exlcd/#31-exlcdinitparam
    lcd_config = {
        lcd_model = "AirLCD_1000", -- LCD型号
        -- pin_vcc = 24,           -- 供电引脚,使用GPIO控制屏幕供电可配置
        pin_rst = 36,              -- 复位引脚
        pin_pwr = 1,               -- 背光控制引脚GPIO ID号
        pin_pwm = 0,               -- 背光控制引脚PWM ID号
        port = lcd.HWID_0,         -- 驱动端口
        -- pin_dc = 0xFF,          -- lcd数据/命令选择引脚GPIO ID号,使用lcd 专用 SPI 接口 lcd.HWID_0不需要填此参数,使用通用SPI接口需要赋值
        direction = 0,             -- lcd屏幕方向 0:0° 1:90° 2:180° 3:270°,屏幕方向和分辨率保存一致
        w = 320,                   -- lcd 水平分辨率
        h = 480,                   -- lcd 竖直分辨率
        xoffset = 0,               -- x偏移(不同屏幕ic 不同屏幕方向会有差异)
        yoffset = 0,               -- y偏移(不同屏幕ic 不同屏幕方向会有差异)
        sleepcmd = 0X10,           -- 睡眠命令,默认0X10
        wakecmd = 0X11,            -- 唤醒命令,默认0X11
        -- bus_speed = 50*1000*1000,                            -- SPI总线速度,不填默认50M,若速率要求更高需要进行设置
        -- interface_mode = lcd.WIRE_4_BIT_8_INTERFACE_I,       -- lcd模式,默认lcd.WIRE_4_BIT_8_INTERFACE_I
        -- direction0 = {0x36,0x00},                            -- 0°方向的命令,(不同屏幕ic会有差异)
        -- direction90 = {0x36,0x60},                           -- 90°方向的命令,(不同屏幕ic会有差异)
        -- direction180 ={0x36,0xc0} ,                          -- 180°方向的命令,(不同屏幕ic会有差异)
        -- direction270 = {0x36,0xA0},                          -- 270°方向的命令,(不同屏幕ic会有差异)
        -- hbp = nil,                                           -- 水平后廊
        -- hspw = nil,                                          -- 水平同步脉冲宽度
        -- hfp = 0,                                             -- 水平前廊
        -- vbp = 0,                                             -- 垂直后廊
        -- vspw = 0,                                            -- 垂直同步脉冲宽度
        -- vfp = 0,                                             -- 垂直前廊
        -- initcmd = nil,                                       -- 自定义屏幕初始化命令表
        -- flush_rate = nil,                                    -- 刷新率
        -- spi_dev = nil,                                       -- spi设备,当port = "device"时有效,当port ≠ "device"时可不填或者填nil
        -- init_in_service = false,                             -- 允许初始化在lcd service里运行,在后台初始化LCD,默认是false,Air8000/G/W/T/A、Air780EHM/EGH/EHV 支持填true,可加快初始化速度,默认SPI总线速度80M
    }
})

6.5 key_drv.lua

本文件为按键驱动功能模块,核心业务逻辑为:

1、初始化BOOT键和PWR键的GPIO;

2、配置按键事件的中断处理函数;

3、实现按键防抖功能,防止误触发;

4、对外发布按键消息;

local key_drv = {}

-- 按键定义
local key_boot = 0           -- GPIO0按键(BOOT键)
local key_pwr = gpio.PWR_KEY -- 电源按键


-- 按键事件处理函数
local function handle_boot_key(val)

    if val == 1 then
        sys.publish("KEY_EVENT", "boot_down")
    else
        sys.publish("KEY_EVENT", "boot_up")
    end
end

local function handle_pwr_key(val)

    if val == 1 then
        sys.publish("KEY_EVENT", "pwr_up")
    else
        sys.publish("KEY_EVENT", "pwr_down")
    end
end

--[[
初始化按键GPIO
配置BOOT键和PWR键的GPIO中断

@api key_drv.init()
@summary 配置BOOT键和PWR键的GPIO中断
@return bool 初始化只会返回true

@usage
local result = key_drv.init()
if result then
    log.info("按键驱动初始化成功")
end
]]
function key_drv.init()
    gpio.setup(key_boot, handle_boot_key, gpio.PULLDOWN, gpio.BOTH)
    gpio.debounce(key_boot, 50, 0)     -- 防抖,防止频繁触发

    gpio.setup(key_pwr, handle_pwr_key, gpio.PULLUP, gpio.BOTH)
    gpio.debounce(key_pwr, 50, 0)     -- 防抖,防止频繁触发

    log.info("key_drv", "按键初始化完成")
    return true
end

key_drv.init()
return key_drv

6.6 ui_main.lua

本文件为exEasyUI主程序模块,核心业务逻辑为:

1、初始化UI主题和窗口系统;

2、注册所有页面的可点击区域回调函数;

3、包装窗口方法以捕获页面切换事件;

4、订阅按键事件并分发到按键处理器;

5、启动UI渲染主循环,维持界面刷新;

local home_page = require("home_page")
local key_handler = require("key_handler")

-- 页面名称常量
local PAGE_NAMES = {
    HOME = "home",
    COMPONENT = "component",
    DEFAULT_FONT = "default_font",
    HZFONT = "hzfont"
}

-- 当前页面
local current_page = nil

-- 处理按键事件
local function handle_key_event(event_type)
    if event_type == "boot_down" then
        key_handler.next_area()
    elseif event_type == "pwr_down" then
        key_handler.simulate_click()
    end
end

-- 主页区域回调函数
local function home_page_callback()
    key_handler.clear_areas()
    key_handler.add_area(20, 100, 280, 80) -- 组件按钮
    key_handler.add_area(20, 200, 280, 80) -- 默认字体按钮
    key_handler.add_area(20, 300, 280, 80) -- HZFont按钮
    log.info("ui_main", "已注册主页区域,共3个区域")
end

-- 组件页面区域回调函数
local function component_page_callback()
    key_handler.clear_areas()
    key_handler.add_area(20, 20, 60, 30)   -- 返回按钮
    key_handler.add_area(210, 100, 70, 26) -- +10%按钮
    key_handler.add_area(20, 170, 60, 30)  -- 复选框A
    key_handler.add_area(120, 170, 60, 30) -- 复选框B
    key_handler.add_area(20, 430, 80, 30)  -- 普通按钮
    key_handler.add_area(110, 430, 80, 30) -- 蓝色按钮
    key_handler.add_area(200, 430, 64, 30) -- 图片按钮
    log.info("ui_main", "已注册组件页面区域,共7个区域")
end

-- 默认字体页面区域回调函数
local function default_font_page_callback()
    key_handler.clear_areas()
    key_handler.add_area(20, 20, 60, 30) -- 返回
    log.info("ui_main", "已注册默认字体页面区域,共1个区域")
end

-- HZFont页面区域回调函数
local function hzfont_page_callback()
    key_handler.clear_areas()
    key_handler.add_area(20, 20, 60, 30)   -- 返回
    key_handler.add_area(20, 140, 120, 30) -- 切换字体按钮
    log.info("ui_main", "已注册HZFont页面区域,共2个区域")
end

-- 注册所有页面的回调函数
local function register_all_pages()
    key_handler.register_page(PAGE_NAMES.HOME, home_page_callback)
    key_handler.register_page(PAGE_NAMES.COMPONENT, component_page_callback)
    key_handler.register_page(PAGE_NAMES.DEFAULT_FONT, default_font_page_callback)
    key_handler.register_page(PAGE_NAMES.HZFONT, hzfont_page_callback)
end

-- 页面切换处理
local function handle_page_change(page_name)
    if not page_name then
        page_name = PAGE_NAMES.HOME
    end

    if page_name ~= current_page then
        current_page = page_name
        key_handler.switch_to_page(page_name)
    end
end

-- 页面切换任务函数
local function show_subpage_task(page_name)
    handle_page_change(page_name)
end

local function back_task()
    handle_page_change(PAGE_NAMES.HOME)
end

-- 包装的show_subpage方法
local function wrapped_show_subpage(self, page_name)
    -- 调用原始方法
    local original_show_subpage = getmetatable(self).original_show_subpage
    original_show_subpage(self, page_name)

    if page_name then
        sys.taskInit(show_subpage_task, page_name)
    end
end

-- 包装的back方法
local function wrapped_back(self)
    -- 调用原始方法
    local original_back = getmetatable(self).original_back
    original_back(self)

    sys.taskInit(back_task)
end

-- 包装窗口方法
local function wrap_window_methods()
    local test_win = ui.window({})
    local window_meta = getmetatable(test_win)

    -- 保存原始方法
    window_meta.original_show_subpage = window_meta.show_subpage
    window_meta.original_back = window_meta.back

    -- 替换为包装的方法
    window_meta.show_subpage = wrapped_show_subpage
    window_meta.back = wrapped_back
end

-- 启动UI主任务
local function ui_main()
    -- 初始化UI主题
    ui.sw_init({ theme = "light" })

    -- 包装窗口方法
    wrap_window_methods()
    register_all_pages()

    -- 创建主页
    home_page.create()

    -- 确保光标显示
    key_handler.switch_to_page(PAGE_NAMES.HOME)

    -- 订阅按键事件
    sys.subscribe("KEY_EVENT", handle_key_event)

end

sys.taskInit(ui_main)

6.7 home_page.lua

本文件为主页功能模块,核心业务逻辑为:

1、创建应用主窗口并配置背景颜色;

2、配置子页面工厂函数,管理各演示页面的创建;

3、创建标题和功能按钮,提供页面导航功能;

4、处理按钮点击事件,实现页面切换;

function home_page.create()
    -- 创建主页
    local home = ui.window({ background_color = ui.COLOR_WHITE })
    home.visible = true

    -- 配置子页面工厂
    home:configure_subpages({
        component = function() return require("component_page").create(ui) end,
        default_font = function() return require("default_font_page").create(ui) end,
        hzfont = function() return require("hzfont_page").create(ui) end
    })

    -- 标题
    local title = ui.label({
        x = 80,
        y = 30,
        text = "exEasyUI v1.7.0演示系统",
        color = ui.COLOR_BLACK,
        size = 16
    })

    local subtitle = ui.label({
        x = 80,
        y = 60,
        text = "boot键:选择 pwr键:确认",
        color = ui.COLOR_GRAY,
        size = 12
    })

    -- 组件演示按钮
    local btn_component = ui.button({
        x = 20,
        y = 100,
        w = 280,
        h = 80,
        text = "组件演示",
        bg_color = ui.COLOR_BLUE,
        text_color = ui.COLOR_WHITE,
        on_click = function()
            home:show_subpage("component")
        end
    })

    -- 默认字体演示按钮
    local btn_default_font = ui.button({
        x = 20,
        y = 200,
        w = 280,
        h = 80,
        text = "默认字体演示",
        bg_color = ui.COLOR_RED,
        text_color = ui.COLOR_WHITE,
        on_click = function()
            home:show_subpage("default_font")
        end
    })

    -- HZFont演示按钮
    local btn_hzfont = ui.button({
        x = 20,
        y = 300,
        w = 280,
        h = 80,
        text = "HZFont演示",
        bg_color = ui.COLOR_ORANGE,
        text_color = ui.COLOR_WHITE,
        on_click = function()
            home:show_subpage("hzfont")
        end
    })

    -- 添加所有组件到窗口
    home:add(title)
    home:add(subtitle)
    home:add(btn_component)
    home:add(btn_default_font)
    home:add(btn_hzfont)

    ui.add(home)
end

return home_page

6.8 key_handler.lua

本文件为按键处理功能模块,核心业务逻辑为:

1、管理页面可点击区域列表,支持动态区域注册;

2、处理按键事件,实现光标在可点击区域间的切换;

3、提供光标绘制和清除功能,视觉上标记当前焦点;

4、支持模拟点击操作,触发界面元素响应;

5、实现页面切换时的区域列表更新;

local key_handler = {}

-- 模块内部状态
local clickable_areas = {}
local current_area_index = 0
local page_callbacks = {}
local config = {
    cursor_thickness = 2,
    cursor_color = 0xFF0000,
    show_cursor = true,
    last_switch_time = 0
}
local current_drawn_rect = nil


--[[
注册页面区域回调函数;

@api key_handler.register_page(page_name, callback)

@summary 注册页面的可点击区域回调函数

@string
page_name
页面名称标识符,用于后续页面切换时引用;

@function
callback
区域注册回调函数,函数内应调用key_handler.clear_areas()key_handler.add_area()来定义页面的可点击区域;

@return nil

@usage
key_handler.register_page("home", function()
    key_handler.clear_areas()
    key_handler.add_area(20, 100, 280, 50)  -- 主页按钮1
    key_handler.add_area(20, 170, 280, 50)  -- 主页按钮2
end)
]]
function key_handler.register_page(page_name, callback)
    page_callbacks[page_name] = callback
end

--[[
切换到指定页面并更新区域列表;

@api key_handler.switch_to_page(page_name)

@summary 切换到指定页面

@string
page_name
目标页面名称,必须已通过key_handler.register_page()注册;

@return boolean
切换成功返回true,失败返回false

@usage
local result = key_handler.switch_to_page("home")
if result then
    log.info("成功切换到主页")
end
]]
function key_handler.switch_to_page(page_name)
    key_handler.clear_cursor()

    if page_callbacks[page_name] then
        page_callbacks[page_name]()
    else
        clickable_areas = {}
    end

    if #clickable_areas > 0 then
        current_area_index = 1
        config.last_switch_time = mcu.ticks()
        key_handler.draw_cursor()
        return true
    else
        current_area_index = 0
        return false
    end
end

-- 清空区域
function key_handler.clear_areas()
    key_handler.clear_cursor()
    clickable_areas = {}
    current_area_index = 0
end

-- 添加区域
function key_handler.add_area(x, y, w, h)
    table.insert(clickable_areas, { x = x, y = y, w = w, h = h })

    if #clickable_areas == 1 and current_area_index == 0 then
        current_area_index = 1
        config.last_switch_time = mcu.ticks()
    end
end

-- 获取当前区域矩形
local function get_current_area_rect()
    if current_area_index < 1 or current_area_index > #clickable_areas then
        return nil
    end

    local area = clickable_areas[current_area_index]
    return {
        x1 = area.x - 3,
        y1 = area.y - 3,
        x2 = area.x + area.w + 3,
        y2 = area.y + area.h + 3
    }
end

-- 获取当前区域中心
local function get_current_area_center()
    if current_area_index < 1 or current_area_index > #clickable_areas then
        return 160, 240
    end

    local area = clickable_areas[current_area_index]
    local center_x = area.x + math.floor(area.w / 2)
    local center_y = area.y + math.floor(area.h / 2)

    return center_x, center_y
end

-- 清除光标
function key_handler.clear_cursor()
    if current_drawn_rect then
        lcd.setColor(0xFFFFFF, 0xFFFFFF)
        local r = current_drawn_rect
        for i = 1, config.cursor_thickness do
            local offset = i - 1
            lcd.drawRectangle(
                r.x1 + offset,
                r.y1 + offset,
                r.x2 - offset,
                r.y2 - offset
            )
        end
        current_drawn_rect = nil
    end
end

-- 绘制光标
function key_handler.draw_cursor()
    if not config.show_cursor then
        return
    end
    if current_area_index < 1 or current_area_index > #clickable_areas then
        return
    end

    key_handler.clear_cursor()

    local rect = get_current_area_rect()
    if not rect then return end

    current_drawn_rect = rect
    lcd.setColor(0xFFFFFF, config.cursor_color)

    local thickness = config.cursor_thickness
    for i = 1, thickness do
        local offset = i - 1
        lcd.drawRectangle(
            rect.x1 + offset,
            rect.y1 + offset,
            rect.x2 - offset,
            rect.y2 - offset
        )
    end

    config.last_switch_time = mcu.ticks()
end

--[[
切换到下一个可点击区域;

@api key_handler.next_area()

@summary 在可点击区域间循环切换焦点

@return boolean
切换成功返回true,失败返回false

@usage
local result = key_handler.next_area()
if result then
    log.info("已切换到下一个区域")
end
]]
function key_handler.next_area()
    if #clickable_areas == 0 then
        return false
    end

    local old_index = current_area_index
    current_area_index = current_area_index + 1
    if current_area_index > #clickable_areas then
        current_area_index = 1
    end

    key_handler.draw_cursor()
    return true
end

--[[
模拟点击当前选中区域;

@api key_handler.simulate_click()

@summary 模拟点击当前焦点区域

@return number, number
返回点击的坐标X, Y;如果当前没有选中区域,返回0, 0

@usage
local x, y = key_handler.simulate_click()
log.info("点击坐标:", x, ",", y)
]]
function key_handler.simulate_click()
    if #clickable_areas == 0 or current_area_index == 0 then
        return 0, 0
    end

    local center_x, center_y = get_current_area_center()
    -- exeasyui必须先按下再单击
    sys.publish("BASE_TOUCH_EVENT", "TOUCH_DOWN", center_x, center_y)
    sys.publish("BASE_TOUCH_EVENT", "SINGLE_TAP", center_x, center_y)

    return center_x, center_y
end

--[[
显示或隐藏光标;

@api key_handler.set_visible(visible)

@summary 设置光标可见性

@boolean
visible
true表示显示光标false表示隐藏光标

@return nil

@usage
key_handler.set_visible(true)   -- 显示光标
key_handler.set_visible(false)  -- 隐藏光标
]]
function key_handler.set_visible(visible)
    config.show_cursor = visible

    if not visible then
        key_handler.clear_cursor()
    else
        key_handler.draw_cursor()
    end
end

--[[
获取当前选中区域信息;

@api key_handler.get_current_area()

@summary 获取当前焦点区域的详细信息

@return table or nil
区域信息表,包含以下字段:
- index: number 当前区域索引
- total: number 总区域数
- x: number 区域X坐标
- y: number 区域Y坐标
- w: number 区域宽度
- h: number 区域高度
- center_x: number 中心点X坐标
- center_y: number 中心点Y坐标
如果没有选中区域,返回nil

@usage
local area = key_handler.get_current_area()
if area then
    log.info("当前区域:", area.index, "/", area.total)
end
]]
function key_handler.get_current_area()
    if current_area_index < 1 or current_area_index > #clickable_areas then
        return nil
    end

    local area = clickable_areas[current_area_index]
    local center_x, center_y = get_current_area_center()

    return {
        index = current_area_index,
        total = #clickable_areas,
        x = area.x,
        y = area.y,
        w = area.w,
        h = area.h,
        center_x = center_x,
        center_y = center_y
    }
end

return key_handler

6.9 component_page.lua

本文件为组件演示页面功能模块,核心业务逻辑为:

1、创建带上下滚动功能的演示窗口;

2、展示进度条、按钮、复选框、图片轮播等UI组件;

3、演示组件的交互功能和事件处理;

4、提供返回主页的导航功能;

local component_page = {}

--[[
创建组件演示页面;

@api component_page.create(ui)
@summary 创建组件演示页面界面
@table ui UI库对象
@return table 组件演示窗口对象

@usage
-- 在子页面工厂中调用创建组件演示页面
local component_page = require("component_page").create(ui)
]]

function component_page.create(ui)
    local win = ui.window({
        background_color = ui.COLOR_WHITE,
        x = 0,
        y = 0,
        w = 320,
        h = 480
    })


    -- 标题
    local title = ui.label({
        x = 120,
        y = 25,
        text = "组件演示",
        color = ui.COLOR_BLACK,
        size = 16
    })

    -- 返回按钮
    local btn_back = ui.button({
        x = 20,
        y = 20,
        w = 60,
        h = 30,
        text = "返回",
        on_click = function()
            win:back()
        end
    })

    -- ==================== 1. 进度条组件演示 ====================
    local progress_label = ui.label({
        x = 20,
        y = 70,
        text = "1. 进度条组件:",
        color = ui.COLOR_BLACK,
        size = 14
    })

    local progress_value = 0
    local progress_bar = ui.progress_bar({
        x = 20,
        y = 100,
        w = 180,
        h = 26,
        progress = progress_value
    })

    local btn_progress = ui.button({
        x = 210,
        y = 100,
        w = 70,
        h = 26,
        text = "+10%",
        on_click = function()
            progress_value = progress_value + 10
            if progress_value > 100 then
                progress_value = 0
            end
            progress_bar:set_progress(progress_value)
            progress_bar:set_text("进度: " .. progress_value .. "%")
        end
    })

    -- ==================== 2. 复选框组件演示 ====================
    local checkbox_label = ui.label({
        x = 20,
        y = 140,
        text = "2. 复选框组件:",
        color = ui.COLOR_BLACK,
        size = 14
    })

    local checkbox1 = ui.check_box({
        x = 20,
        y = 170,
        text = "选项A",
        checked = false,
        on_change = function(checked)
            log.info("component_page", "选项A:", checked)
        end
    })

    local checkbox2 = ui.check_box({
        x = 120,
        y = 170,
        text = "选项B",
        checked = true,
        on_change = function(checked)
            log.info("component_page", "选项B:", checked)
        end
    })

    -- -- ==================== 3. 图片轮播组件演示 ====================
    local picture_label = ui.label({
        x = 20,
        y = 220,
        text = "3. 图片轮播组件:",
        color = ui.COLOR_BLACK,
        size = 14
    })

    local picture = ui.picture({
        x = 20,
        y = 250,
        w = 128,
        h = 128,
        sources = { "/luadb/1.jpg", "/luadb/2.jpg", "/luadb/3.jpg" },
        autoplay = true,
        interval = 2000
    })

    -- ==================== 4. 按钮组件演示 ====================
    local button_label = ui.label({
        x = 20,
        y = 400,
        text = "4. 按钮组件:",
        color = ui.COLOR_BLACK,
        size = 14
    })

    -- 普通按钮
    local normal_btn = ui.button({
        x = 20,
        y = 430,
        w = 80,
        h = 30,
        text = "普通按钮",
        on_click = function()
            log.info("component_page", "普通按钮被点击")
        end
    })

    -- 带颜色的按钮
    local colored_btn = ui.button({
        x = 110,
        y = 430,
        w = 80,
        h = 30,
        text = "蓝色按钮",
        bg_color = ui.COLOR_BLUE,
        text_color = ui.COLOR_WHITE,
        on_click = function()
            log.info("component_page", "蓝色按钮被点击")
        end
    })

    -- 图片按钮
    local image_btn = ui.button({
        x = 200,
        y = 413,
        w = 64,
        h = 64,
        src = "/luadb/4.jpg",
        src_toggled = "/luadb/5.jpg",
        toggle = true,
        on_click = function()
            log.info("component_page", "图片按钮被点击")
        end
    })

    -- 添加所有组件到窗口
    win:add(title)
    win:add(btn_back)
    win:add(progress_label)
    win:add(progress_bar)
    win:add(btn_progress)
    win:add(checkbox_label)
    win:add(checkbox1)
    win:add(checkbox2)
    win:add(picture_label)
    win:add(picture)
    win:add(button_label)
    win:add(normal_btn)
    win:add(colored_btn)
    win:add(image_btn)

    return win
end

return component_page

6.10 default_font_page.lua

本文件为默认字体演示页面功能模块,核心业务逻辑为:

1、创建演示窗口,展示内置12号点阵字体的固定大小特性;

2、演示数字、符号、中英文混排的显示效果;

3、展示默认字体的特性和限制说明;

4、提供返回主页的导航功能;

local default_font_page = {}

--[[
创建默认字体演示页面;

@api default_font_page.create(ui)
@summary 创建默认字体演示页面界面
@table ui UI库对象
@return table 默认字体演示窗口对象

@usage
-- 在子页面工厂中调用创建默认字体演示页面
local default_font_page = require("default_font_page").create(ui)
]]

function default_font_page.create(ui)
    local win = ui.window({
        background_color = ui.COLOR_WHITE,
        x = 0,
        y = 0,
        w = 320,
        h = 480
    })

    -- 标题
    local title = ui.label({
        x = 120,
        y = 25,
        text = "默认字体演示",
        color = ui.COLOR_BLACK,
        size = 12
    })

    -- 返回按钮
    local btn_back = ui.button({
        x = 20,
        y = 20,
        w = 60,
        h = 30,
        text = "返回",
        on_click = function()
            win:back()
        end
    })

    -- 字体演示标题
    local demo_title = ui.label({
        x = 20,
        y = 70,
        text = "字体演示 (固定12号):",
        color = ui.COLOR_BLACK,
        size = 12
    })

    -- 数字演示
    local number_demo = ui.label({
        x = 20,
        y = 100,
        text = "1、数字: 0123456789",
        color = ui.COLOR_BLUE,
        size = 12
    })

    -- 符号演示
    local symbol_demo = ui.label({
        x = 20,
        y = 130,
        text = "2、符号: !@#$%^&*()_+-=[]",
        color = ui.COLOR_ORANGE,
        size = 12
    })

    -- 中英文演示
    local text_demo = ui.label({
        x = 20,
        y = 160,
        text = "3、中英文: Hello 世界 ABC",
        color = ui.COLOR_RED,
        size = 12
    })

    -- 默认字体特性说明
    local feature_title = ui.label({
        x = 20,
        y = 200,
        text = "默认字体特性:",
        color = ui.COLOR_BLACK,
        size = 12
    })

    local feature1 = ui.label({
        x = 20,
        y = 230,
        text = "- 内置12号点阵字体",
        color = ui.COLOR_GRAY,
        size = 12
    })

    local feature2 = ui.label({
        x = 20,
        y = 260,
        text = "- 无需外部硬件支持",
        color = ui.COLOR_GRAY,
        size = 12
    })

    local feature3 = ui.label({
        x = 20,
        y = 290,
        text = "- 启动快速,资源占用小",
        color = ui.COLOR_GRAY,
        size = 12
    })

    local feature4 = ui.label({
        x = 20,
        y = 320,
        text = "- 字体大小固定为12号",
        color = ui.COLOR_GRAY,
        size = 12
    })

    -- 添加所有组件到窗口
    win:add(title)
    win:add(btn_back)
    win:add(demo_title)
    win:add(number_demo)
    win:add(symbol_demo)
    win:add(text_demo)
    win:add(feature_title)
    win:add(feature1)
    win:add(feature2)
    win:add(feature3)
    win:add(feature4)

    return win
end

return default_font_page

6.11 hzfont_page.lua

本文件为HZFont矢量字体演示页面模块,核心业务逻辑为:

1、创建HZFont矢量字体演示页面,展示矢量字体的动态调整能力;

2、演示多种字体大小和颜色的文本显示效果;

3、提供字体大小动态切换功能,展示矢量字体的缩放优势;

4、展示HZFont矢量字体的特性和应用场景;

local hzfont_page = {}

--[[
页面演示状态记录表

@table demo_state
@field current_size number 当前演示字体大小,初始值为16
]]
local demo_state = {
    current_size = 16  -- 当前字体大小,从16号开始
}

--[[
创建HZFont矢量字体演示页面窗口

@api hzfont_page.create(ui)

@summary 创建HZFont矢量字体演示页面

@param ui table exEasyUI库对象,用于创建UI组件

@return table 返回创建的窗口对象

@usage
-- 在页面切换逻辑中调用
local hzfont_win = hzfont_page.create(ui)
return hzfont_win
]]
function hzfont_page.create(ui)
    --[[
    创建主窗口
    @param background_color number 窗口背景颜色,白色
    @param x number 窗口左上角X坐标0
    @param y number 窗口左上角Y坐标0
    @param w number 窗口宽度,320像素
    @param h number 窗口高度,480像素
    ]]
    local win = ui.window({
        background_color = ui.COLOR_WHITE,
        x = 0,
        y = 0,
        w = 320,
        h = 480
    })

    --[[
    页面标题标签
    @param x number 标签左上角X坐标85像素
    @param y number 标签左上角Y坐标25像素
    @param text string 标签文本内容
    @param color number 文本颜色,黑色
    @param size number 字体大小,16
    ]]
    local title = ui.label({
        x = 85,
        y = 25,
        text = "HZFont矢量字体演示",
        color = ui.COLOR_BLACK,
        size = 16
    })

    --[[
    返回按钮
    @param x number 按钮左上角X坐标20像素
    @param y number 按钮左上角Y坐标20像素
    @param w number 按钮宽度,60像素
    @param h number 按钮高度,30像素
    @param text string 按钮文本
    @param on_click function 按钮点击回调函数,调用窗口的back方法返回上一页
    ]]
    local btn_back = ui.button({
        x = 20,
        y = 20,
        w = 60,
        h = 30,
        text = "返回",
        on_click = function()
            win:back()
        end
    })

    --[[
    动态字体大小调整标题
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标70像素
    @param text string 标签文本内容
    @param color number 文本颜色,黑色
    @param size number 字体大小,16
    ]]
    local dynamic_title = ui.label({
        x = 20,
        y = 70,
        text = "动态字体大小调整:",
        color = ui.COLOR_BLACK,
        size = 16
    })

    --[[
    字体大小信息显示标签
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标100像素
    @param text string 标签文本内容,显示当前字体大小
    @param color number 文本颜色,蓝色
    @param size number 字体大小,16
    ]]
    local size_info = ui.label({
        x = 20,
        y = 100,
        text = "当前大小: 16号 (可调整)",
        color = ui.COLOR_BLUE,
        size = 16
    })

    --[[
    字体大小切换按钮
    @param x number 按钮左上角X坐标20像素
    @param y number 按钮左上角Y坐标140像素
    @param w number 按钮宽度,120像素
    @param h number 按钮高度,30像素
    @param text string 按钮文本
    @param on_click function 按钮点击回调函数,切换字体大小
    ]]
    local btn_change_size = ui.button({
        x = 20,
        y = 140,
        w = 120,
        h = 30,
        text = "切换字体大小",
        on_click = function()
            -- 每次点击增加4号字体大小
            demo_state.current_size = demo_state.current_size + 4

            -- 当字体大小超过32号时,重置为12号
            if demo_state.current_size > 32 then
                demo_state.current_size = 12
            end

            -- 更新标签文本和字体大小
            size_info:set_text("当前字体大小: " .. demo_state.current_size .. "号")
            size_info:set_size(demo_state.current_size)
        end
    })

    --[[
    字体演示区域标题
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标180像素
    @param text string 标签文本内容
    @param color number 文本颜色,黑色
    @param size number 字体大小,16
    ]]
    local demo_title = ui.label({
        x = 20,
        y = 180,
        text = "字体演示:",
        color = ui.COLOR_BLACK,
        size = 16
    })

    --[[
    数字演示标签
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标210像素
    @param text string 标签文本内容,显示数字
    @param color number 文本颜色,蓝色
    @param size number 字体大小,14
    ]]
    local number_demo = ui.label({
        x = 20,
        y = 210,
        text = "数字: 0123456789",
        color = ui.COLOR_BLUE,
        size = 14
    })

    --[[
    符号演示标签
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标250像素
    @param text string 标签文本内容,显示特殊符号
    @param color number 文本颜色,橙色
    @param size number 字体大小,20
    ]]
    local symbol_demo = ui.label({
        x = 20,
        y = 250,
        text = "符号: !@#$%^&*()_+-=[]",
        color = ui.COLOR_ORANGE,
        size = 20
    })

    --[[
    中英文演示标签
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标300像素
    @param text string 标签文本内容,显示中英文混合文本
    @param color number 文本颜色,红色
    @param size number 字体大小,28
    ]]
    local text_demo = ui.label({
        x = 20,
        y = 300,
        text = "中英文: LuatOS",
        color = ui.COLOR_RED,
        size = 28
    })

    --[[
    HZFont特性说明标题
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标350像素
    @param text string 标签文本内容
    @param color number 文本颜色,黑色
    @param size number 字体大小,14
    ]]
    local feature_title = ui.label({
        x = 20,
        y = 350,
        text = "HZFont特性:",
        color = ui.COLOR_BLACK,
        size = 14
    })

    --[[
    HZFont特性说明第一条
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标380像素
    @param text string 标签文本内容,说明HZFont的内置特性
    @param color number 文本颜色,灰色
    @param size number 字体大小,12
    ]]
    local feature1 = ui.label({
        x = 20,
        y = 380,
        text = "- 内置矢量字体,无需外部硬件",
        color = ui.COLOR_GRAY,
        size = 12
    })

    --[[
    HZFont特性说明第二条
    @param x number 标签左上角X坐标20像素
    @param y number 标签左上角Y坐标410像素
    @param text string 标签文本内容,说明HZFont的动态调整能力
    @param color number 文本颜色,灰色
    @param size number 字体大小,12
    ]]
    local feature2 = ui.label({
        x = 20,
        y = 410,
        text = "- 支持10-100号字体动态调整",
        color = ui.COLOR_GRAY,
        size = 12
    })

    --[[
    将创建的UI组件添加到窗口
    按照从背景到前景的顺序添加,确保显示层次正确
    ]]
    win:add(title)
    win:add(btn_back)
    win:add(dynamic_title)
    win:add(size_info)
    win:add(btn_change_size)
    win:add(demo_title)
    win:add(number_demo)
    win:add(symbol_demo)
    win:add(text_demo)
    win:add(feature_title)
    win:add(feature1)
    win:add(feature2)

    return win
end

return hzfont_page

七、显示效果展示

7.1 硬件准备

  1. 按照硬件接线表连接所有设备
  2. 通过 TYPE-C USB 口供电
  3. 检查所有接线无误

7.2 软件配置

main.lua 中配置系统参数:

-- 必须加载才能启用exeasyui的功能
ui = require("exeasyui")


-- 加载lcd、tp和字库驱动管理功能模块,有以下三种:
-- 1、使用lcd内核固件中自带的12号中文字体的hw_default_font_drv,并按lcd显示驱动配置初始化
-- 2、使用hzfont核心库驱动内核固件中支持的软件矢量字库的hw_hzfont_drv.lua,并按lcd显示驱动配置初始化
-- 3、使用自定义字体的hw_customer_font_drv(目前开发中)
-- 最新情况可查看模组选型手册中对应型号的固件列表内,支持的核心库是否包含lcd、tp、12号中文、hzfont,链接https://docs.openluat.com/air780epm/common/product/
-- 目前exeasyui V1.7.0版本支持使用已经实现的三种功能中的一种进行初始化,同时支持多种字体初始化功能正在开发中
require("hw_default_font_drv")
-- require("hw_hzfont_drv")
-- require("hw_customer_font_drv")开发中

-- 加载按键驱动模块
require("key_drv")

-- 加载exeassyui扩展库实现的用户界面功能模块
-- 实现多页面切换、触摸事件分发和界面渲染功能
-- 包含主页、组件演示页、默认字体演示页、HZfont演示页和自定义字体演示页
require("ui_main")

7.3 软件烧录

  1. 使用Luatools烧录最新内核固件
  2. 下载并烧录本项目所有脚本文件
  3. 将字体文件和图片文件随脚本文件一起烧录到脚本分区
  4. 烧录成功后设备自动重启后开始运行

7.4 功能测试

7.4.1 主页面操作

  1. 设备启动后显示主页面,光标自动定位在第一个按钮
  2. 按BOOT键在不同按钮间切换焦点
  3. 按PWR键进入对应演示页面
  4. 使用返回按钮返回主页面

7.4.2 组件演示页面

  1. 测试进度条组件的动态更新
  2. 体验复选框的状态变化
  3. 查看图片轮播效果(如有图片文件)
  4. 使用按键在不同组件间切换焦点

7.4.3 字体演示页面

  1. 默认字体页:查看固定 12 号字体的颜色和中英文显示
  2. HZFont 页:测试动态字体大小调整功能
  3. 在各页面使用按键操作界面按钮

7.5 预期效果

  • 系统启动:正常初始化,显示主页面
  • 字体显示:各字体页面正常显示,动态调整功能正常
  • 按键操作:在各页面使用按键操作界面按钮

八、常见问题

  1. 显示异常:检查 LCD 接线,确认对应驱动文件中的硬件参数正确
  2. 触摸无响应:检查 I2C 接线,确认触摸芯片型号配置正确
  3. 字体显示异常:确认选择的字体驱动
  4. 图片无法显示:确认图片文件已正确烧录到指定路径
  5. 系统卡顿:调整 ui_main.lua 中的刷新率参数