2026-8-23 23:11:09 [显示全部楼层]
5浏览
查看: 5|回复: 0

[M10项目] 用行空板做多界面交互面板——用状态机切换页面

[复制链接]
本帖最后由 gada888 于 2026-8-23 23:29 编辑
用行空板做多界面交互面板——用状态机切换页面

很多人第一次做硬件项目,会把所有控件堆在一个页面里:一个按钮控制灯,一个滑块调亮度,再加几个状态文字。功能少时还能运行,功能一多就很难维护。本课不从“画一个按钮”开始,而是做一个真正可扩展的多界面控制面板:主页显示灯光状态,设置页调整亮度,点击按钮切换页面,页面状态和硬件状态彼此独立。

用行空板做多界面交互面板——用状态机切换页面图1
用行空板做多界面交互面板——用状态机切换页面图13
用行空板做多界面交互面板——用状态机切换页面图14

## 先看最终效果

最终界面有两个页面:主页显示“灯:开/关”、当前亮度、灯带引脚和两个操作按钮;设置页提供亮度滑块、颜色按钮以及返回主页按钮。灯带使用 Digital RGB LED 模块,数据线接行空板 P21。点击“切换灯光”后,灯带状态改变;点击“进入设置页面”后,屏幕切换到设置页。

用行空板做多界面交互面板——用状态机切换页面图2

硬件清单很克制:行空板 M10、Digital RGB LED 模块、杜邦线和 USB 供电。这个选择适合课堂演示,因为接线少,学生能把注意力放在页面逻辑和状态管理上。

## 先定义程序状态

```python
current_page = "home"
light_on = False
brightness = 80
current_color = (255, 0, 0)
```

这几个变量就是程序的“当前状态”。`current_page` 只负责页面,`light_on`、`brightness` 和 `current_color` 只负责灯光。这样做的关键好处是:切换页面不会意外关灯,调亮度也不会改变当前页面。

## 页面不要写成一团

把界面拆成三个函数:`build_home_page()` 创建主页控件,`build_settings_page()` 创建设置页控件,`render_current_page()` 根据 `current_page` 决定显示哪一页。页面按钮只修改状态并调用渲染函数。

```python
def go_settings():
    global current_page
    current_page = "settings"
    render_current_page()

def go_home():
    global current_page
    current_page = "home"
    render_current_page()
```

【配图 3:用行空板做多界面交互面板——用状态机切换页面图3
用行空板做多界面交互面板——用状态机切换页面图4
用行空板做多界面交互面板——用状态机切换页面图5
用行空板做多界面交互面板——用状态机切换页面图6

## 状态切换和硬件控制分开

“切换灯光”按钮只负责改变 `light_on`,真正的灯带输出交给 `apply_light_state()`。如果灯光打开,就按当前颜色和亮度点亮;如果关闭,就把亮度设为零或发送关闭指令。

```python
def toggle_light():
    global light_on
    light_on = not light_on
    apply_light_state()
    render_current_page()
```

【配图 4:用行空板做多界面交互面板——用状态机切换页面图7
用行空板做多界面交互面板——用状态机切换页面图8
用行空板做多界面交互面板——用状态机切换页面图9

## 亮度滑块为什么要单独处理

滑块产生的是连续数值,按钮产生的是离散事件。滑块回调只更新 `brightness`,然后调用灯光函数;它不应该重新创建整个页面。这样拖动滑块时,页面不会闪烁,灯光也能即时变化。

```python
def on_brightness_change(value):
    global brightness
    brightness = int(value)
    apply_light_state()
```

## 运行验证清单

1. 上电后默认进入主页;
2. 点击切换按钮,灯光状态和页面文字同步变化;
3. 进入设置页,拖动亮度滑块,灯光立即变化;
4. 返回主页后,之前设置的亮度和灯光状态仍然保留;
5. 反复切换页面,不出现重复控件或界面错位。

如果界面切换后出现重复控件,检查是否每次切页都重新创建了控件;如果灯光状态错乱,检查 `light_on` 是否误写成局部变量;如果滑块能动但灯不变,检查回调是否调用了硬件输出函数。

## 这节课真正要学的是什么

这个项目训练的是三个可迁移的习惯:先定义状态,再设计页面;把界面事件和硬件动作分开;用一个渲染入口保证页面显示一致。后面做温度仪表盘、植物提醒器或灯效控制器时,都可以复用同样的结构。

【配图 5:用行空板做多界面交互面板——用状态机切换页面图10
用行空板做多界面交互面板——用状态机切换页面图11
用行空板做多界面交互面板——用状态机切换页面图12

## 完整带解释代码
# -*- coding: utf-8 -*-
"""
UNIHIKER 触屏 + WS2812 单颗灯 控制面板示例

模块化结构说明:
- 硬件初始化与常量配置区
- 全局状态变量区(当前页面、灯状态、颜色、亮度等)
- 业务逻辑函数(控制灯光、计算背景色、页面路由)
- UI 绘制函数(主页 / 设置页)
- 程序入口与主循环
"""

# ====== 导入依赖 ======
from unihiker import GUI                # 引入 UNIHIKER 的图形界面库 [web:56]
from pinpong.board import Board, Pin, NeoPixel  # 引入主控板、引脚、WS2812 控制类 [web:52]
import time                             # 引入时间模块,用于主循环延时

# ============================================================
# 一、硬件初始化与常量配置
# ============================================================

# 初始化 UNIHIKER 主控板(必须在使用 pinpong 相关类前调用)[web:56]
Board("UNIHIKER").begin()

# 定义 WS2812 所在引脚(使用 UNIHIKER 上的 P23 引脚)
NEOPIXEL_PIN = Pin(Pin.P23)     # 创建一个 Pin 对象,表示 P23 引脚 [web:52]
PIXELS_NUM = 1                  # 使用的 WS2812 灯珠数量,这里只接了 1 颗

# 创建 NeoPixel 对象,用来控制 WS2812 灯条 / 灯珠
np = NeoPixel(NEOPIXEL_PIN, PIXELS_NUM)  # 传入引脚和数量,即可控制对应灯珠 [web:52]

# 创建 GUI 对象,用于在 UNIHIKER 屏幕上绘图、显示文本、按钮等 [web:47]
gui = GUI()

# ============================================================
# 二、全局状态变量(业务状态)
# ============================================================

current_page = "home"           # 当前界面:可以是 "home" 或 "settings"
light_on = False                # 灯当前是否点亮:False=关,True=开
bg_color = (0, 0, 0)            # 屏幕背景基础颜色(R,G,B),不含亮度系数
brightness = 100                # 整体亮度百分比,范围 0~100
rgb_color = (255, 255, 0)       # WS2812 灯的基础颜色,默认黄色(255,255,0)

# ============================================================
# 三、业务逻辑函数:灯光控制 / 颜色计算 / 页面跳转
# ============================================================

def apply_led():
    """
    根据当前全局状态:
    - light_on   是否开灯
    - rgb_color  灯的基础颜色
    - brightness 亮度百分比
    计算最终颜色并写入 WS2812 灯珠。
    """
    if light_on:                             # 如果灯处于“开”的状态
        factor = brightness / 100.0          # 将 0~100 的亮度转换为 0~1 的系数
        r = int(rgb_color[0] * factor)       # 计算加亮度后的红色分量
        g = int(rgb_color[1] * factor)       # 计算加亮度后的绿色分量
        b = int(rgb_color[2] * factor)       # 计算加亮度后的蓝色分量
        np[0] = (r, g, b)                    # 把计算好的 RGB 写入第 0 号灯珠
    else:
        np[0] = (0, 0, 0)                    # 如果关灯,则写入全黑(熄灭)

def calc_bg_color():
    """
    根据当前背景基础颜色 bg_color 和 brightness,
    计算实际用于绘制背景的颜色(做一个简单的“亮度”效果)。
    """
    factor = brightness / 100.0              # 亮度转为 0~1 的系数
    r = int(bg_color[0] * factor)            # 背景红色通道乘以系数
    g = int(bg_color[1] * factor)            # 背景绿色通道乘以系数
    b = int(bg_color[2] * factor)            # 背景蓝色通道乘以系数
    return (r, g, b)                         # 返回新的 (R,G,B) 元组

def clear_screen():
    """
    清空当前屏幕内容,并用当前背景色重新填充整个屏幕区域。
    """
    gui.clear()                              # 清除所有已绘制内容
    # 使用计算后的背景色填充整块屏幕(UNIHIKER 默认分辨率 240x320)[web:47]
    gui.fill_rect(x=0, y=0, w=240, h=320, color=calc_bg_color())

def go_home():
    """
    跳转到主页。
    """
    global current_page                      # 声明使用全局变量 current_page
    current_page = "home"                    # 修改当前页面状态为“home”
    draw_home_page()                         # 重绘主页 UI

def go_settings():
    """
    跳转到设置页面。
    """
    global current_page                      # 声明使用全局变量 current_page
    current_page = "settings"                # 修改当前页面状态为“settings”
    draw_settings_page()                     # 重绘设置页 UI

def toggle_light():
    """
    切换灯光开关状态:如果原来是关,则打开;原来是开,则关闭。
    """
    global light_on                          # 使用全局的灯状态
    light_on = not light_on                  # 取反:True -> False,False -> True
    apply_led()                              # 根据新状态更新实际灯光
    draw_home_page()                         # 回到主页并刷新显示灯状态

# ------- 背景颜色设置逻辑 -------

def set_bg_red():
    """
    设置屏幕背景为红色基调(受亮度影响)。
    """
    global bg_color                          # 使用全局背景颜色
    bg_color = (255, 0, 0)                   # 纯红色
    redraw_current()                         # 重新绘制当前页面

def set_bg_green():
    """
    设置屏幕背景为绿色基调。
    """
    global bg_color
    bg_color = (0, 160, 0)                   # 偏亮的绿色
    redraw_current()

def set_bg_blue():
    """
    设置屏幕背景为蓝色基调。
    """
    global bg_color
    bg_color = (0, 80, 200)                  # 偏亮的蓝色
    redraw_current()

# ------- 灯光颜色设置逻辑 -------

def set_led_red():
    """
    将 WS2812 灯设置为红色,并确保灯是开启状态。
    """
    global rgb_color, light_on
    rgb_color = (255, 0, 0)                  # 灯颜色设为红色
    light_on = True                          # 确保灯处于“开”的状态
    apply_led()                              # 写入灯珠
    redraw_current()                         # 刷新当前页面(亮度文字等)

def set_led_green():
    """
    将 WS2812 灯设置为绿色,并确保灯是开启状态。
    """
    global rgb_color, light_on
    rgb_color = (0, 255, 0)                  # 绿色
    light_on = True
    apply_led()
    redraw_current()

def set_led_blue():
    """
    将 WS2812 灯设置为蓝色,并确保灯是开启状态。
    """
    global rgb_color, light_on
    rgb_color = (0, 0, 255)                  # 蓝色
    light_on = True
    apply_led()
    redraw_current()

# ------- 亮度调节逻辑 -------

def brightness_down():
    """
    将亮度降低 10%,但不低于 10%。
    """
    global brightness
    brightness = max(10, brightness - 10)    # 限制最小值为 10
    apply_led()                              # 调整后更新灯光亮度
    redraw_current()                         # 更新界面显示的亮度文字和背景

def brightness_up():
    """
    将亮度提高 10%,但不超过 100%。
    """
    global brightness
    brightness = min(100, brightness + 10)   # 限制最大值为 100
    apply_led()
    redraw_current()

def redraw_current():
    """
    根据 current_page 的值,决定重绘哪个页面。
    这样改变背景色 / 灯色 / 亮度后,都可以统一调用此函数刷新界面。
    """
    if current_page == "settings":           # 如果当前是设置页
        draw_settings_page()                 # 重绘设置页
    else:
        draw_home_page()                     # 否则视为主页

# ============================================================
# 四、UI 绘制函数:主页 / 设置页
# ============================================================

def draw_home_page():
    """
    绘制“主页”界面:
    - 显示标题
    - 显示一个圆形“灯泡”图标
    - 显示当前灯状态文字
    - 提示 WS2812 接线信息
    - 提供“切换灯光”和“进入设置页面”的按钮
    """
    clear_screen()                           # 先清空并涂背景

    # 绘制标题文字(左上角)
    gui.draw_text(
        x=60, y=10,
        text="控制面板 - 主页",
        origin="nw",                         # 以左上角为对齐原点
        color="white"
    )

    # 根据 light_on 状态,决定灯泡图标颜色
    lamp_color = "yellow" if light_on else "gray"
    # 在屏幕中央附近画一个圆,作为灯泡图标
    gui.fill_circle(
        x=120, y=120,                        # 圆心位置
        r=40,                                # 半径
        color=lamp_color
    )

    # 在圆下面写文字:“灯:开” 或 “灯:关”
    gui.draw_text(
        x=100, y=180,
        text="灯:开" if light_on else "灯:关",
        origin="nw",
        color="white"
    )

    # 显示说明:WS2812 接在哪个引脚
    gui.draw_text(
        x=70, y=200,
        text="WS2812 接在 P21",             # 这里你可以改成真实接脚说明
        origin="nw",
        color="white"
    )

    # ---- “切换灯光”按钮 ----
    gui.fill_round_rect(
        x=40, y=220,                         # 按钮左上角位置
        w=160, h=40,                         # 宽度、高度
        r=8,                                 # 圆角半径
        color="orange",                      # 填充颜色
        onclick=toggle_light                 # 点击回调函数 [web:50]
    )
    gui.draw_text(
        x=80, y=235,                         # 按钮内的文字位置
        text="切换灯光",
        origin="nw",
        color="black"
    )

    # ---- “进入设置页面”按钮 ----
    gui.fill_round_rect(
        x=40, y=270,
        w=160, h=40,
        r=8,
        color="skyblue",
        onclick=go_settings                  # 点击后跳转到设置页
    )
    gui.draw_text(
        x=70, y=285,
        text="进入设置页面",
        origin="nw",
        color="black"
    )

def draw_settings_page():
    """
    绘制“设置页面”界面:
    - 顶部返回按钮
    - 屏幕背景颜色选项(红 / 绿 / 蓝)
    - WS2812 灯颜色选项(红灯 / 绿灯 / 蓝灯)
    - 亮度显示与增减按钮
    """
    clear_screen()                           # 清空并涂背景

    # 页面标题
    gui.draw_text(
        x=80, y=10,
        text="设置页面",
        origin="nw",
        color="white"
    )

    # ---- 左上角“返回”按钮 ----
    gui.fill_round_rect(
        x=10, y=10,
        w=60, h=30,
        r=6,
        color="skyblue",
        onclick=go_home                      # 点击回主页
    )
    gui.draw_text(
        x=20, y=18,
        text="返回",
        origin="nw",
        color="black"
    )

    # ========= 屏幕背景颜色区域 =========
    gui.draw_text(
        x=20, y=60,
        text="屏幕背景颜色:",
        origin="nw",
        color="white"
    )

    # -- 背景:红色按钮 --
    gui.fill_round_rect(
        x=20, y=90,
        w=60, h=40,
        r=6,
        color="red",
        onclick=set_bg_red
    )
    gui.draw_text(
        x=40, y=100,
        text="红",
        origin="nw",
        color="black"
    )

    # -- 背景:绿色按钮 --
    gui.fill_round_rect(
        x=90, y=90,
        w=60, h=40,
        r=6,
        color=(0, 160, 0),
        onclick=set_bg_green
    )
    gui.draw_text(
        x=110, y=100,
        text="绿",
        origin="nw",
        color="black"
    )

    # -- 背景:蓝色按钮 --
    gui.fill_round_rect(
        x=160, y=90,
        w=60, h=40,
        r=6,
        color=(0, 80, 200),
        onclick=set_bg_blue
    )
    gui.draw_text(
        x=180, y=100,
        text="蓝",
        origin="nw",
        color="black"
    )

    # ========= WS2812 灯颜色区域 =========
    gui.draw_text(
        x=20, y=150,
        text="WS2812 颜色:",
        origin="nw",
        color="white"
    )

    # -- 红灯按钮 --
    gui.fill_round_rect(
        x=20, y=180,
        w=60, h=40,
        r=6,
        color="red",
        onclick=set_led_red
    )
    gui.draw_text(
        x=30, y=190,
        text="红灯",
        origin="nw",
        color="black"
    )

    # -- 绿灯按钮 --
    gui.fill_round_rect(
        x=90, y=180,
        w=60, h=40,
        r=6,
        color="green",
        onclick=set_led_green
    )
    gui.draw_text(
        x=100, y=190,
        text="绿灯",
        origin="nw",
        color="black"
    )

    # -- 蓝灯按钮 --
    gui.fill_round_rect(
        x=160, y=180,
        w=60, h=40,
        r=6,
        color="blue",
        onclick=set_led_blue
    )
    gui.draw_text(
        x=170, y=190,
        text="蓝灯",
        origin="nw",
        color="black"
    )

    # ========= 亮度显示与调节 =========
    gui.draw_text(
        x=20, y=240,
        text="亮度:{}%".format(brightness),  # 显示当前亮度数值
        origin="nw",
        color="white"
    )

    # -- 亮度 -10 按钮 --
    gui.fill_round_rect(
        x=20, y=270,
        w=80, h=40,
        r=6,
        color="gray",
        onclick=brightness_down
    )
    gui.draw_text(
        x=50, y=280,
        text="-10",
        origin="nw",
        color="black"
    )

    # -- 亮度 +10 按钮 --
    gui.fill_round_rect(
        x=140, y=270,
        w=80, h=40,
        r=6,
        color="gray",
        onclick=brightness_up
    )
    gui.draw_text(
        x=170, y=280,
        text="+10",
        origin="nw",
        color="black"
    )

# ============================================================
# 五、程序入口与主循环
# ============================================================

# 启动时先应用一次灯光状态(这里默认 light_on=False,相当于关灯)
apply_led()

# 默认先进入主页界面
draw_home_page()

# 主循环:在 UNIHIKER 上通常用一个死循环维持程序运行
while True:
    # 这里没有额外逻辑,只是简单 sleep 以避免 CPU 占用过高
    time.sleep(0.05)


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail