widget 状态和交互
widget.state、按钮 ±、习惯清单、开关与链接;AppIntent 桌面交互。
桌面小组件不执行任意 Python 回调。按钮、开关、列表勾选只能触发受控 AppIntent:改写 widget.state、刷新时间线,或打开链接。
边界:本篇讲桌面点击与 widget.state。预览面板里的配色、标题默认值用 widget.param,不要混用。
#本篇目标
| 项 | 说明 |
|---|---|
| 何时用 | 计数器、完成开关、习惯勾选、打开网页/深链 |
| 核心 API | widget.state.* → .increment() / .toggle() / .toggle_item() |
| 节点 | w.button(..., action=)、w.toggle(..., state=)、w.link(...) |
| 限制 | action= 必须来自 state 工厂;不能传 Python 函数 |
| 发布后 | 改 state key 需重新发布;桌面可能需删组件重装 |
#快速开始
单按钮累加 + 数字过渡:比 概览 · 饮水打卡 的 ± 环图更简单,专讲 action 与 numericText。
预期效果:小组件预览面板显示与脚本一致的布局与交互。
已复制
import widget
streak = widget.state.int("streak", 0)
accent = widget.param.color("主色", "#2563EB")
w = widget.Widget(background=("#FFFFFF", "#0F172A"), padding=14)
w.text("连续签到", size=16, weight="semibold").line_limit(1)
(
w.value(streak, unit="天")
.id("streak")
.content_transition("numericText")
.monospaced_digit()
.line_limit(1)
.min_scale(0.72)
)
(
w.button("签到", action=streak.increment(), background=accent, color="#FFFFFF")
.line_limit(1)
.min_scale(0.72)
.pressed(scale=0.94)
)
w.render()
要点:
streak.increment()返回 AppIntent,赋给action=;不能写字符串或 Python 函数。.content_transition("numericText")+.id(...):数字变化时系统过渡动画。.pressed(scale=0.94)等价于button(..., press={"scale": 0.94})。widget.param.color只调配色,不改桌面计数值。
#交互示例
#习惯清单(state.list + toggle_item)
多项勾选用 widget.state.list;每项 done.toggle_item(i) 切换是否在列表里。
预期效果:小组件预览面板显示与脚本一致的布局与交互。
已复制
import widget
habits = ["晨跑", "喝水 8 杯", "阅读 30 分", "整理桌面"]
done = widget.state.list("habits_done", [])
accent = widget.param.color("完成色", "#22C55E")
w = widget.Widget(background=("#FAFAFA", "#1C1C1E"), padding=14)
w.text("今日习惯", size=16, weight="semibold").line_limit(1)
for i, label in enumerate(habits):
checked = done.contains(i)
mark = "✓ " if checked else "○ "
(
w.button(
mark + label,
action=done.toggle_item(i),
style="plain",
color=accent if checked else "secondary",
size=13,
)
.line_limit(1)
.min_scale(0.72)
)
w.render()
done.contains(i):预览/桌面判断第i项是否已勾选;toggle_item(i)的i类型须与contains一致。- 多项也可用多个
w.toggle(..., state=);长清单用state.list更省节点。 - 九宫格、Bingo 等表格布局见 布局与尺寸 的
table()示例。
#开关(state.bool + w.toggle)
布尔状态用 state= 绑定,不要手写静态 True/False 配假 action。
预期效果:小组件预览面板显示与脚本一致的布局与交互。
已复制
import widget
alarm_on = widget.state.bool("alarm_on", False)
accent = widget.param.color("开启色", "#F59E0B")
w = widget.Widget(background=("#FFFBEB", "#292524"), padding=14)
w.text("早起闹钟", size=16, weight="semibold").line_limit(1)
w.toggle("明早 7:00", state=alarm_on, color=accent, style="switch")
# 脚本每次重建时读取 state 当前值(非 AppUI 式实时绑定)
status = "已开启,记得早睡" if alarm_on else "已关闭"
w.text(status, size=13, color=accent if alarm_on else "secondary").line_limit(1).min_scale(0.72)
w.render()
w.toggle(..., color=accent):着色用color=,没有tint=参数。style="switch"/"checkbox"控制外观;与state=搭配最省事。
#链接(w.link)
链接打开 URL;与按钮/开关不要叠在同一块可点区域。
预期效果:小组件预览面板显示与脚本一致的布局与交互。
已复制
import widget
label = widget.param.text("链接标题", "Apple 官网")
accent = widget.param.color("链接色", "#2563EB")
w = widget.Widget(background=("#FFFFFF", "#111827"), padding=14)
w.text("快捷入口", size=16, weight="semibold").line_limit(1)
w.link(label, "https://www.apple.com", icon="safari.fill", color=accent)
w.text("点击在 Safari 打开", size=12, color="secondary").line_limit(1).min_scale(0.72)
w.render()
- 第一参数是显示标题,第二参数是 URL 字符串。
- 可选
icon=在标题旁显示 SF Symbol。 icon=时返回行容器,不能链.line_limit();无icon时返回 text 句柄才可链修饰符。- 整块链接与局部
button分清层级;同一容器不要既当链接又塞按钮。
#心智模型
已复制
widget.state.int/bool/list/... ← 桌面持久化
↓
.increment() / .toggle() / .toggle_item(i) ← AppIntent 工厂
↓
w.button(..., action=...) 或 w.toggle(..., state=...)
↓
用户点击 → 扩展执行 Intent → 状态改写 → WidgetKit 刷新时间线
- 先声明 state:在
Widget()构建前,与widget.param同级。 - action 只接工厂方法:
count.increment()、done.toggle()、items.toggle_item(2)。 - 开关优先
state=:w.toggle("标题", state=done),让运行时自动绑done.toggle()。 - 链接不走 state:
w.link(title, url)直接打开;深链同样写 URL 字符串。 - 改 key 要重发:
"score"改成"points"后,旧桌面实例可能仍读写"score"。
#与 widget.param 怎么分
widget.param | widget.state | |
|---|---|---|
| 谁改 | 预览面板 / Studio | 桌面点击 |
| 典型 | 主题色、默认标题 | 计数、开关、清单勾选 |
| 传给 UI | color=、background= | value / state= + action |
#API 参考
#状态类型
| API | 适合 | 常用动作 |
|---|---|---|
widget.state.int(key, default) | 次数、步数、分数 | .increment(by=1)、.decrement(by=1)、.set(n) |
widget.state.float(key, default) | 进度、比例 | 同上 |
widget.state.bool(key, default) | 完成、开启 | .toggle();配合 w.toggle(..., state=) |
widget.state.str(key, default) | 当前模式、选中项 | .set("模式A") |
widget.state.list(key, default) | 多选清单、Bingo | .toggle_item(item)、.contains(item)、.set([]) |
key 是持久化 ID;改名等同新状态,需重新发布。
#交互节点
已复制
count = widget.state.int("count", 0)
done = widget.state.bool("done", False)
items = widget.state.list("picked", [])
w.button("加 1", action=count.increment())
w.button("减 2", action=count.decrement(by=2))
w.button("归零", action=count.set(0), style="plain")
w.toggle("完成", state=done, color="#22C55E")
w.button("选 A", action=items.toggle_item("A"), style="plain")
w.link("文档", "https://example.com", icon="book.fill")
#动效修饰(可选)
已复制
w.button("+", action=count.increment()).pressed(scale=0.94)
w.value(count).content_transition("numericText").monospaced_digit()
#常见错误
| 错误写法 | 后果 | 修正 |
|---|---|---|
action="increment" 或 Python 函数 | 点击无反应 | 用 count.increment() 等工厂 |
w.toggle(..., tint=accent) | TypeError | 用 color=accent |
静态 True + 手写 toggle action | 开关不保存 | w.toggle(..., state=done) |
用 widget.param.bool 当桌面开关 | 只在预览里变 | 交互用 widget.state.bool |
链接容器里再塞 button | 点击区域冲突 | 链接、按钮分块布局 |
| 改 state key 不重发 | 计数错乱或归零 | 重新发布;删桌面组件重装 |
#失败路径
| 现象 | 处理 |
|---|---|
| 按钮点击没反应 | 确认 action 来自 widget.state 工厂方法 |
| Toggle 显示但不记忆 | 改用 state=,勿拼静态布尔 |
| 清单勾选无效 | toggle_item 的参数类型与 contains 一致(都用 int 或都用 str) |
| 链接打不开 | 检查 URL 含 https://;勿与按钮叠层 |
| 点击后桌面仍旧 | AppIntent 已执行但 WidgetKit 缓存 → 重新运行发布;删组件重装 |
| 预览能点、桌面不能 | 确认已走 从脚本到桌面 发布流程 |
#相关文档
| 文档 | 用途 |
|---|---|
| widget 概览 | 总览与入门示例 |
| 参数面板 | widget.param 与 state 区分 |
| 从脚本到桌面 | 发布后在桌面验证交互 |
| 布局与尺寸 | table() 九宫格、row() 横排 |
| 时间线和动画 | content_transition、数字过渡 |
| 排错 | 预览/桌面不一致 |
| API 参考 | widget.state、button、toggle、link 签名 |
相关 API:widget-api-reference.md