PythonIDE Docs
中文
简体中文

widget 布局与尺寸

row/column/grid/table/canvas、family 预算、when/unless 与锁屏 accessory。

小组件布局以 WidgetKit family 为边界:先定目标尺寸与信息密度,再选容器;不要把 large 的内容硬塞进 small。

边界:本篇讲 row/column/grid/table/canvaswidget.context 与防裁切。桌面点击与 widget.state状态和交互;参数面板见 参数面板

#本篇目标

说明
核心原则按 family 设计,不是把同一套 UI 整体缩小
容器row() / column() / grid() / table() / canvas() / layer()
尺寸 APIwidget.contextwidget.family_value()w.when() / w.unless()
主屏small / medium / large(158²、338×158、338×354 内容区)
锁屏circular / rectangular / inline — 极简,节点数严格受限
收尾可变文案加 .line_limit(1).min_scale(0.65+)

#快速开始

row() 横排三列指标,column() 在每列里纵向叠标题与数值——适合仪表盘,比单列更省高度。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

steps = widget.param.number("步数", 8420, min=0, max=50000)
calories = widget.param.number("千卡", 420, min=0, max=5000)
sleep_h = widget.param.number("睡眠(h)", 7, min=0, max=12)
accent = widget.param.color("主色", "#10B981")

w = widget.Widget(background=("#ECFDF5", "#064E3B"), padding=14)
w.text("今日健康", size=16, weight="semibold").line_limit(1)
with w.row(spacing=12):
    with w.column(spacing=2):
        w.text("步数", size=11, color="secondary").line_limit(1)
        w.value(steps).monospaced_digit().line_limit(1).min_scale(0.72)
    with w.column(spacing=2):
        w.text("千卡", size=11, color="secondary").line_limit(1)
        w.value(calories).monospaced_digit().line_limit(1).min_scale(0.72)
    with w.column(spacing=2):
        w.text("睡眠", size=11, color="secondary").line_limit(1)
        (
            w.value(sleep_h, unit="h")
            .monospaced_digit()
            .line_limit(1)
            .min_scale(0.72)
        )
w.rect(color=accent, height=3, corner_radius=2)
w.render()

要点:

  • 容器必须用 with w.row(): / with w.column(): 上下文管理器,不要把子节点当位置参数传入。
  • spacing= 控制子项间距;align= 可选 leading / center / trailing
  • 预览右上角切换 family,确认三列在 small 仍可读。

#心智模型

text
选定 family(small / medium / large / 锁屏 accessory)
        ↓
读 widget.context → content_width × content_height(扣掉 padding 后的可画区域)
        ↓
决定信息层级:标题 → 主值 → 次要说明 → 图表/表格
        ↓
选容器:语义流式用 row/column/grid;精确格子用 table;点位绘制用 canvas
        ↓
防裁切:line_limit + min_scale;small 隐藏次要行

#内容区预算(点)

以下为默认 padding 下的内容区宽高,脚本里用 widget.contextw.context 读取(属性,不可调用;不要写 widget.context()):

Family约 content 宽 × 高设计建议
small158 × 158标题 + 1 主值 + 1 视觉元素
medium338 × 158默认主力版;可横排 2–3 列
large338 × 354可加第二段:图表、表格、说明行
circular64 × 64SF Symbol + 极短数字/缩写
rectangular160 × 56一行或图标+一行字
inline220 × 22单行文字,无图表/表格

常量:widget.SMALLwidget.MEDIUMwidget.LARGEwidget.CIRCULARwidget.RECTANGULARwidget.INLINE

#容器怎么选

API底层适合
row(spacing=, align=)横向栈多指标横排、按钮组
column(spacing=, align=)纵向栈表单式上下结构
layer(align=)叠放背景块上叠文字/图标
grid(columns=, spacing=, equal=)等列网格周历、四宫格摘要
table(rows, columns)单路径表格线Bingo、日历格、精确分割线
canvas(fill=, coordinate_space=)点位坐标表盘、键盘、自定义图形

日常卡片优先 row / column;需要不重叠的细表格线table(),不要用多个 rect 拼边框(线宽会叠粗、交点发圆)。


#布局示例

#三尺寸信息密度(is_family 分支)

同一脚本为 small 只保留单号+状态,medium 加图标行,large 再补预计送达。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

tracking = widget.param.text("单号", "SF1234567890")
status = widget.param.text("状态", "运输中")
eta = widget.param.text("预计", "明天 18:00")
accent = widget.param.color("主色", "#F59E0B")
ctx = widget.context
pad = widget.family_value(14, small=10, large=16)

w = widget.Widget(background=("#FFFBEB", "#292524"), padding=pad)

if ctx.is_family("small"):
    w.text("快递", size=14, weight="semibold", color=accent).line_limit(1)
    w.text(tracking, size=12).line_limit(1).min_scale(0.6)
    w.text(status, size=11, color="secondary").line_limit(1).min_scale(0.72)
else:
    w.text("快递追踪", size=16, weight="semibold").line_limit(1)
    w.text(tracking, size=12, color="secondary").line_limit(1).min_scale(0.65)
    with w.row(spacing=8):
        w.symbol("shippingbox.fill").color(accent).font_size(16)
        w.text(status, size=14, weight="semibold").line_limit(1).min_scale(0.72)
    if ctx.is_family("large"):
        w.text(f"预计送达 {eta}", size=12, color="secondary").line_limit(1)
    else:
        w.text(f"预计 {eta}", size=12, color="secondary").line_limit(1).min_scale(0.72)

w.render()
  • ctx.is_family("small") 等价于 ctx.is_family(widget.SMALL)
  • 长单号在 small 必须 .min_scale(0.6) 或缩短显示字段。

#family_valuew.when("large")

只改字号/边距用 family_value整段 UI 仅 large 出现时用 w.when

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

show = widget.param.text("节目", "心灵马杀鸡")
episode = widget.param.text("集数", "第 128 期")
progress = widget.param.slider("进度", 0.42, min=0, max=1, step=0.01)
accent = widget.param.color("主色", "#8B5CF6")

title_size = widget.family_value(16, small=14, large=18)
bar_h = widget.family_value(6, small=5, large=8)
pad = widget.family_value(14, small=10, large=16)

w = widget.Widget(background=("#FAF5FF", "#1E1B4B"), padding=pad)
w.text(show, size=title_size, weight="semibold").line_limit(1).min_scale(0.72)
w.text(episode, size=12, color="secondary").line_limit(1).min_scale(0.72)
w.progress(float(progress), total=1, color=accent, height=bar_h)

with w.when("large", layout="column"):
    w.text("继续收听 →", size=12, color=accent).line_limit(1)

w.render()
  • w.when("large", layout="column"):仅 large 渲染块内子节点;layout 还可为 "row" / "layer"
  • w.unless("small"):除 small 外都显示,适合「medium/large 才显示的脚注」。

#grid 周历打卡

grid(columns=7) 按子节点顺序从左到右、从上到下填格;equal=True 等宽列。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

days = ["一", "二", "三", "四", "五", "六", "日"]
plan = widget.param.choice("打卡计划", ["一三五", "天天练", "周末"], default="一三五")
accent = widget.param.color("打卡色", "#EF4444")

active_sets = {
    "一三五": {0, 2, 4},
    "天天练": set(range(7)),
    "周末": {5, 6},
}
active = active_sets[str(plan)]

w = widget.Widget(background=("#FFFFFF", "#1C1C1E"), padding=12)
w.text("本周训练", size=15, weight="semibold").line_limit(1)
with w.grid(columns=7, spacing=4, equal=True):
    for i, day in enumerate(days):
        mark = "●" if i in active else "○"
        (
            w.text(day + mark, size=10, color=accent if i in active else "secondary")
            .line_limit(1)
            .min_scale(0.55)
            .align("center")
        )
w.render()

#table 九宫格(Bingo)

习惯打卡的纵向列表状态和交互;需要可见格线时用 table() + table.cell(row, col)

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

items = ["喝水", "散步", "整理", "阅读", "计划", "备份", "写作", "放松", "复盘"]
done = widget.state.list("bingo_done", [])
accent = widget.param.color("完成色", "#4F9A48")

w = widget.Widget(background=("#FBFAF5", "#141414"), padding=10)
w.text("今日九宫格", size=17, weight="bold").line_limit(1).min_scale(0.72).align("center")

with w.table(rows=3, columns=3, line_color=accent, line_width="hairline", fill=True) as table:
    for i, label in enumerate(items):
        checked = done.contains(i)
        with table.cell(i // 3, i % 3):
            (
                w.button(
                    ("✓ " if checked else "") + label,
                    action=done.toggle_item(i),
                    style="plain",
                    color=accent if checked else "#242326",
                    size=12,
                    press={"scale": 0.94},
                    normal={"scale": 1},
                )
                .line_limit(1)
                .min_scale(0.55)
                .align("center")
            )

w.render()

表格线建议:

  • line_width="hairline":最细像素线。
  • line_cap="butt"line_join="miter":端点不圆、交点直角。
  • 格内文字 .min_scale(0.55),避免 small 格子裁切。

#canvas 读取可画区域

需要按点坐标摆放(表盘刻度、自定义键盘)时,先读 content_width / content_height,再 .place()

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

w = widget.Widget(background="#FFFFFF", padding=12)
ctx = widget.context
cw = ctx.content_width
ch = ctx.content_height

with w.canvas(fill=True, coordinate_space="points") as c:
    c.rect(color="#EEF2F7", width=cw, height=ch).place(cw / 2, ch / 2, unit="points")
    (
        c.text(f"{int(cw)} × {int(ch)} pt", size=14, weight="semibold")
        .line_limit(1)
        .min_scale(0.72)
        .place(cw / 2, ch / 2, unit="points")
    )
    (
        c.text(ctx.family, size=11, color="secondary")
        .line_limit(1)
        .place(cw / 2, ch / 2 + 16, unit="points")
    )

w.render()
  • ctx.width / ctx.height 含 padding 前预算;摆放内容用 content_*
  • coordinate_space="points".place(x, y, unit="points") 以画布左上为原点。

#锁屏 accessory 精简版

锁屏圆形/矩形/行内节点上限低(见上表);按 family 分别写布局,不要复用 medium 整块 UI。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

steps = widget.state.int("steps", 6234)
accent = widget.param.color("主色", "#34D399")
ctx = widget.context

w = widget.Widget(background=("#F0FDF4", "#14532D"), padding=widget.family_value(8, circular=0, rectangular=4, inline=0))

if ctx.is_family("circular"):
    w.symbol("figure.walk").color(accent).font_size(14)
    w.text(str(int(steps) // 1000) + "k", size=11, weight="bold").line_limit(1).min_scale(0.6)
elif ctx.is_family("rectangular"):
    with w.row(spacing=6):
        w.symbol("figure.walk").color(accent).font_size(14)
        (
            w.value(steps, unit="步")
            .monospaced_digit()
            .line_limit(1)
            .min_scale(0.65)
        )
elif ctx.is_family("inline"):
    w.text(f"今日 {int(steps)} 步", size=12).line_limit(1).min_scale(0.72)
else:
    w.text("请切换到锁屏 accessory 预览", size=13, color="secondary").line_limit(2).min_scale(0.7)

w.render()

预览里选 circular / rectangular / inline 查看效果;发布后在锁屏编辑界面单独添加(见 从脚本到桌面)。


#防裁切清单

手段用法
单行 + 缩放.line_limit(1).min_scale(0.65)(small 可降到 0.55
等宽数字.monospaced_digit() 避免数字跳动挤版
按 family 藏行if not ctx.is_family("small"): w.text(...)
按 family 改字号widget.family_value(16, small=14, large=18)
按 family 改边距padding=widget.family_value(14, small=10, large=18)
large 填空白w.when("large") 增加图表、说明、第二表格
格内文字表格/Bingo .min_scale(0.55) + .align("center")

#API 参考

#widget.context

python
ctx = widget.context          # 或 w.context(绑定 Widget padding)
ctx.family                    # "small" | "medium" | "large" | ...
ctx.width / ctx.height        # family 预算(点)
ctx.content_width             # 扣 padding 后可画宽
ctx.content_height            # 扣 padding 后可画高
ctx.is_family("small", "medium")
ctx.value(default, small=..., large=...)  # 同 family_value,读当前 family

#widget.family_value

python
size = widget.family_value(16, small=14, medium=16, large=20)
pad = widget.family_value(14, small=10, rectangular=6, circular=4, inline=0)

#容器上下文管理器

python
with w.row(spacing=10, align="center"):
    w.symbol("star.fill").color("#F59E0B")
    w.text("标题")

with w.column(spacing=4, align="leading"):
    w.text("副标题", size=12, color="secondary")

with w.layer(align="center", background="#EEF2F7", corner_radius=8):
    w.text("叠放")

with w.grid(columns=2, spacing=8, equal=True):
    w.text("A")
    w.text("B")

with w.when("large", layout="column"):
    w.line_chart([1, 3, 2, 5])

完整签名见 API 参考rowcolumngridtablecanvaswhenunlessfamily_value.place().align()


#常见错误

错误写法后果修正
w.row(w.text(...), spacing=8)子节点不进容器with w.row(): 块内添加
large 布局原样给 small文字裁切、按钮重叠is_family / family_value 分支
多个 rect 拼表格线线粗、交点圆w.table(...)
锁屏用 medium 整块 UI构建警告或裁切circular/rectangular/inline 单独写
canvas 用 width 当边界内容溢出 paddingcontent_width / content_height
忘记 line_limit多行撑破高度可变文案 .line_limit(1).min_scale(...)
grid 子节点过多挤到下一行难控控制列数与单元内容极简
widget.context() / w.context()TypeError: not callablectx = widget.contextw.context

#按钮按压样式

press={"scale": 0.94} 写在 w.button(...) 参数里;.pressed(scale=0.94) 是链式修饰符,二者等价。交互示例见 状态和交互


#失败路径

现象处理
small 文字被裁切减行数;min_scale 降到 0.55–0.65;隐藏次要 text
medium 正常、large 空w.when("large") 加图表/说明;勿只放大字号
表格线发粗/交点圆line_width="hairline"line_cap="butt"line_join="miter"
canvas 元素跑出边界改用 content_* 算坐标;fill=True 铺满可画区
锁屏 accessory 空白预览切换到 circular/rectangular/inline;检查是否走了 else 占位分支
row 里列挤成一团增大 spacing;减少列数;small 改单列 column
预览与桌面布局不一致两端的 family 要一致;查 排错

#相关文档

文档用途
widget 概览总览与入门
参数面板widget.param 与布局配色
状态和交互表格格内 toggle_item、按钮
从脚本到桌面锁屏 accessory 添加步骤
时间线和动画数字过渡、flip 动画
资源与外观背景、渐变、图片
排错裁切与预览不一致
API 参考布局节点完整签名

相关 API:widget-api-reference.md