PythonIDE Docs
中文
简体中文

列表与表单

List、ForEach、Form、Section、搜索、刷新和滑动操作。

ListForm 是展示结构化数据的首选:List 适合可选中、滑动操作的行;Form 适合设置页分组。ForEach 把 Python 序列映射为多行视图;Section 提供分组标题与脚注。修饰符 .list_style.swipe_actions.refreshable.searchable 直接链在列表(或承载搜索的外层容器)上;若要显式包裹整行,也可使用 SwipeActions(content=..., leading=..., trailing=...)。当前框架不提供 List / ForEach 原生 on_delete 契约,删除动作请放在行级滑动按钮中。


#预期效果

示例会展示原生 List 的搜索、刷新、空状态、行操作和长列表承载方式。

#概念速览

能力API
列表List(content)
数据驱动行ForEach(data, row_builder, key=...)
表单Form(content)Section("标题", content)Section(content, header=...)
样式 / 交互.list_style(style).swipe_actions.refreshable(action).searchable(text, on_change)
行外观.list_row_background(color).list_row_separator(visibility)

#与 AppUI 的对应关系

  • List / Form 都是 纵向 容器;Form 默认呈现分组设置样式,但仍可用 Section 组织子控件。
  • ForEach 不是布局,它把 Python 可迭代对象 展开 为多个子视图,因此必须保证 row_builder(item) 每次返回一棵合法的 View 子树。
  • .searchable修饰符:搜索框属于页面的一部分,过滤逻辑仍由你在 body() 里用 State 完成(见进阶示例)。
  • State 对嵌套 list / dict 有深度观察;为减少多余刷新,批量修改请用 batch_update

#list_style 常用取值

automaticinsetinset_groupedgroupedplainsidebar。设置页多用 inset_grouped,侧边栏列表可用 sidebar


#基础示例

静态 Form + Section,以及一个简单的 List

python
import appui

state = appui.State(notify=True, note="")


def set_note(value):
    state.note = value


def set_notify(value):
    state.notify = value is True or value == "True" or value == "true"


def body():
    return appui.NavigationStack(
        appui.Form(
            [
                appui.Section(
                    "账户",
                    [
                        appui.TextField(
                            "备注",
                            text=state.note,
                            on_change=set_note,
                        ).text_field_style("rounded_border"),
                    ],
                ),
                appui.Section(
                    "通知",
                    [
                        appui.Toggle(
                            label="接收提醒",
                            is_on=state.notify,
                            on_change=set_notify,
                        ),
                    ],
                    footer="关闭后仍保留本地数据。",
                ),
            ]
        )
        .navigation_title("表单示例")
    )


appui.run(body, state=state)

#进阶示例

ForEach 动态行、.swipe_actions 删除、.searchable 过滤、.refreshable 下拉刷新、.list_style('inset_grouped'),以及 .list_row_background / .list_row_separator

下面示例刻意把 过滤逻辑 放在 body() 顶部函数 filtered_items() 中,而不是藏在内联回调深处,便于单元测试与复用;refreshable 仅更新提示字符串,避免阻塞 UI 线程。

python
import appui

state = appui.State(
    items=["买牛奶", "写 appui 教程", "跑步三公里"],
    query="",
    refreshing_hint="",
)


def delete_item(name):
    remaining = [x for x in state.items if x != name]
    state.items = remaining


def set_query(value):
    state.query = value


def refresh_list():
    state.refreshing_hint = "已触发刷新回调(可在此拉取网络数据)"


def item_key(item):
    return item


def row_view(item):
    def delete_current():
        delete_item(item)

    return (
        appui.Text(item)
        .list_row_background("secondarySystemBackground")
        .list_row_separator("hidden")
        .swipe_actions(
            edge="trailing",
            content=[
                appui.Button(
                    "删除",
                    role="destructive",
                    action=delete_current,
                )
            ],
        )
    )


def filtered_items():
    q = state.query.strip().lower()
    if not q:
        return list(state.items)
    return [x for x in state.items if q in x.lower()]


def body():
    rows = filtered_items()
    return appui.NavigationStack(
        appui.VStack(
            [
                appui.Text("在搜索框输入以过滤").font("caption").foreground_color("secondaryLabel"),
                appui.List(
                    [
                        appui.ForEach(
                            rows,
                            row_builder=row_view,
                            key=item_key,
                        )
                    ]
                )
                .searchable(
                    text=state.query,
                    on_change=set_query,
                )
                .refreshable(action=refresh_list)
                .list_style("inset_grouped"),
                appui.Text(state.refreshing_hint or "下拉列表以触发 refreshable")
                    .font("footnote")
                    .foreground_color("systemOrange"),
            ],
            spacing=8,
        )
        .padding()
        .navigation_title("列表与搜索")
    )


appui.run(body, state=state)

#常见误区

  1. ForEach 必须提供稳定 key:字符串列表可用命名函数返回字符串本身;对象列表应使用唯一 id,否则删除 / 重排时行状态会错乱。
  2. .searchable 修饰的是「承载列表的视图」:通常与 List 链在一起;搜索文本要存入 State 并在 body() 里用同一字段过滤数据。
  3. swipe_actions 挂在「行视图」上:如示例中对 Text(item) 链式调用;挂在 List 整表上不会给每行单独滑动按钮。
  4. 原地修改 state.items 列表state.items.remove(x) 若列表是普通 list 可能不会触发深度观察;推荐 state.items = new_listbatch_update(与 State 刷新规则一致)。
  5. Section 的参数顺序:推荐 Section("标题", [views], footer="...");需要关键字时再用 Section(content=[views], header="标题")。嵌套在 Form 中时务必传入 视图列表
  6. refreshable 的回调尽量短:下拉刷新结束后系统才会收起转圈;若在回调里做长时间阻塞,界面会看起来「卡住」。可在回调里只更新标志位,把重活放到线程(注意线程安全与 State 更新方式)。
  7. list_row_background 颜色与深色模式:硬编码 #F2F2F7 在深色模式下可能对比不佳;可改用语义色如 secondarySystemBackground
  8. 同一行不要同时依赖横向 on_drag 与滑删:系统滑动按钮与自定义水平拖拽共用手势通道;建议拆成不同交互区域或移除该行的滑动按钮。

#练习题

  1. List 改为 List + NavigationLink,每行进入只读详情页。
  2. swipe_actionsedge='leading' 侧增加「完成」按钮,并用 .tint('green') 区分颜色。
  3. appui.LabeledContentForm 最后一节展示当前过滤结果条数。

#附录:仅构建 List 树(不调用 run

python
import appui

data = ["a", "b"]


def text_row(value):
    return appui.Text(value)


def text_key(value):
    return value


tree = appui.List(
    [
        appui.ForEach(
            data,
            row_builder=text_row,
            key=text_key,
        )
    ]
)
print(tree)

#延伸阅读(仓库内)

完整页面可以组合 ForEachswipe_actionssearchablelist_style


#可选:LazyVStack 承载超长列表

当行数可能上千时,可将 ForEach 放进 ScrollView + LazyVStack,以减轻一次性构建整表的开销。入口写法见 appui 概览

python
import appui

state = appui.State(rows=list(range(60)))


def lazy_row(value):
    return appui.Text(f"第 {value} 行").padding(edges="horizontal")


def lazy_key(value):
    return value


def body():
    return appui.NavigationStack(
        appui.ScrollView(
            appui.LazyVStack(
                [
                    appui.ForEach(
                        state.rows,
                        row_builder=lazy_row,
                        key=lazy_key,
                    )
                ],
                spacing=6,
            )
        )
        .navigation_title("懒加载列表示例")
    )


appui.run(body, state=state)

#排错清单(自查)

现象可能原因处理方向
搜索框无反应on_change 未写回 State确保 setattr(state, 'query', v)
滑动无删除按钮swipe_actions 挂在 List移到行视图
误以为支持原生删除直接寻找 on_delete / .onDelete改用行级 .swipe_actions(...)SwipeActions(...)
同一行横向拖拽失效该行同时配置了 on_dragswipe_actions保留其一,或把拖拽手势移到子区域
下拉不触发refreshable 未链到可滚动父级List / ScrollView 链式连接
过滤结果不刷新filtered 未依赖 state把过滤函数放在 body() 内读取最新 state