PythonIDE Docs
中文
简体中文

交互与回调

按钮、输入、on_change、sheet 和行级回调的可靠写法。

触摸、长按、拖拽与捏合等交互通过视图修饰符绑定到 Python 回调。键盘与焦点用 .focused.keyboard_dismiss 控制;上下文菜单用 .context_menu;需要与系统手势共存时用 .simultaneous_gesture.high_priority_gesture

#预期效果

示例会展示点击、长按、焦点、键盘、拖拽和行级动作的反馈路径。

#概念速览

能力API
点击 / 长按.on_tap(action).on_long_press(...)
拖移 / 捏合 / 旋转.on_drag.on_magnification.on_rotation
组合手势.simultaneous_gesture(...).high_priority_gesture(...)
菜单.context_menu(content=[...])
焦点与键盘.focused(...).keyboard_dismiss(mode)
禁用.disabled(value)
气泡菜单.popover(is_presented, content, on_dismiss)
搜索和刷新.searchable(...).refreshable(...)
列表动作.swipe_actions(...)

#手势优先级

  • 普通子视图手势:默认由子控件(如 Button)消费点击。
  • simultaneous_gesture:与视图自身手势并行,适合在可滚动区域附加轻量检测。
  • high_priority_gesture:优先于子视图;仅在确有需要时使用,否则容易导致按钮点不动。

context_menucontentButton 视图列表,不要用任意字符串代替 Button

#基础示例

点击计数、长按提示、context_menu 弹出操作。示例使用命名回调,预览里每个动作都有状态反馈。

python
import appui

state = appui.State(count=0, hint="")


def add_count():
    state.count += 1


def reset_count():
    state.batch_update(count=0, hint="已归零")


def double_count():
    state.batch_update(count=state.count * 2, hint="已加倍")


def show_long_press_hint():
    state.hint = "长按已触发"


def body():
    block = (
        appui.RoundedRectangle(corner_radius=16)
        .fill("systemBlue")
        .frame(width=140, height=90)
        .on_tap(add_count)
        .on_long_press(action=show_long_press_hint, min_duration=0.4)
        .context_menu(content=[
            appui.Button("归零", action=reset_count),
            appui.Button("加倍", action=double_count),
        ])
    )

    return appui.NavigationStack(
        appui.ScrollView(
            appui.VStack([
                appui.Text(f"计数: {state.count}").font("title"),
                appui.Text(state.hint or "点击或长按蓝色块")
                    .font("footnote")
                    .foreground_color("secondaryLabel"),
                block,
            ], spacing=20).padding()
        )
        .keyboard_dismiss("interactive")
        .navigation_title("交互示例")
    )


appui.run(body, state=state)

#焦点、键盘和 Popover

focused(key=..., equals=...) 适合多个输入字段之间切换焦点。keyboard_dismiss("interactive") 通常加在 ScrollView 或列表外层。

python
import appui

state = appui.State(
    active_field="name",
    name="Ada",
    city="上海",
    show_pop=False,
    lock_inputs=False,
    gesture_log="",
)


def set_name(value):
    state.name = value


def set_city(value):
    state.city = value


def focus_name():
    state.active_field = "name"


def focus_city():
    state.active_field = "city"


def set_lock_inputs(value):
    state.lock_inputs = bool(value)


def open_popover():
    state.show_pop = True


def close_popover():
    state.show_pop = False


def log_long_press():
    state.gesture_log = "叠加长按"


def popover_content():
    return appui.VStack([
        appui.Text("Popover / Sheet 内容").font("headline"),
        appui.Button("关闭", action=close_popover),
    ], spacing=12).padding()


def body():
    popover_trigger = (
        appui.Text("点我打开 Popover")
        .padding()
        .background("systemGray5", corner_radius=8)
        .popover(is_presented=state.show_pop, on_dismiss=close_popover, content=popover_content)
        .on_tap(open_popover)
        .simultaneous_gesture(
            gesture="long_press",
            callback=log_long_press,
            min_duration=0.35,
        )
        .disabled(state.lock_inputs)
    )

    return appui.NavigationStack(
        appui.ScrollView(
            appui.VStack([
                appui.TextField("姓名", text=state.name, on_change=set_name)
                    .text_field_style("rounded_border")
                    .focused(key="name", equals=state.active_field)
                    .on_submit(focus_city),
                appui.TextField("城市", text=state.city, on_change=set_city)
                    .text_field_style("rounded_border")
                    .focused(key="city", equals=state.active_field),
                appui.HStack([
                    appui.Button("编辑姓名", action=focus_name).button_style("bordered"),
                    appui.Button("编辑城市", action=focus_city).button_style("bordered"),
                ], spacing=12),
                appui.Toggle("锁定输入", is_on=state.lock_inputs, on_change=set_lock_inputs),
                appui.Text(state.gesture_log or "在灰色区域长按可触发 simultaneous_gesture")
                    .font("caption")
                    .foreground_color("secondaryLabel"),
                popover_trigger,
            ], spacing=16).padding()
        )
        .keyboard_dismiss("interactive")
        .navigation_title("焦点与手势")
    )


appui.run(body, state=state)

#拖拽、缩放和旋转

on_dragon_magnificationon_rotation 的回调由运行时传入测量值或字典。实际项目里先把返回值显示出来,再按需要解析字段。

python
def drag_changed(value):
    state.gesture_log = str(value)


def drag_ended(value):
    state.gesture_log = f"ended: {value}"


view.on_drag(on_changed=drag_changed, on_ended=drag_ended)

如果子控件本身需要点击,谨慎使用 high_priority_gesture,它会优先于子视图手势。

#行级动作和刷新

列表行的交互要绑定稳定 id,不要依赖过滤后的下标。下拉刷新使用 .refreshable(action=...)

python
import appui

state = appui.State(
    query="",
    selected="None",
    refresh_count=0,
    rows=[
        {"id": "api", "title": "API reference", "done": False},
        {"id": "ui", "title": "Preview UI", "done": True},
        {"id": "docs", "title": "Docs guide", "done": False},
    ],
)


def row_key(row):
    return row["id"]


def set_query(value):
    state.query = value


def filtered_rows():
    keyword = state.query.strip().lower()
    if not keyword:
        return state.rows
    return [row for row in state.rows if keyword in row["title"].lower()]


def refresh_rows():
    state.refresh_count += 1


def select_row(row_id):
    for row in state.rows:
        if row["id"] == row_id:
            state.selected = row["title"]
            break


def toggle_row(row_id):
    state.rows = [
        {**row, "done": not row["done"]} if row["id"] == row_id else row
        for row in state.rows
    ]


def row_view(row):
    def select_current():
        select_row(row["id"])

    def toggle_current():
        toggle_row(row["id"])

    icon = "checkmark.circle.fill" if row["done"] else "circle"
    return (
        appui.Button(
            action=select_current,
            content=appui.HStack([
                appui.Label(row["title"], system_image=icon),
                appui.Spacer(),
                appui.Text("Open").foreground_color("secondaryLabel"),
            ]),
        )
        .button_style("plain")
        .swipe_actions(actions=[
            appui.Button("Toggle", action=toggle_current),
        ])
    )


def body():
    return appui.NavigationStack(
        appui.List([
            appui.Section("Rows", [
                appui.ForEach(filtered_rows(), row_builder=row_view, key=row_key)
            ]),
            appui.Section("State", [
                appui.LabeledContent("Selected", value=state.selected),
                appui.LabeledContent("Refresh count", value=str(state.refresh_count)),
            ]),
        ])
        .searchable(text=state.query, on_change=set_query)
        .refreshable(action=refresh_rows)
        .navigation_title("Callbacks")
    )


appui.run(body, state=state)

#常见误区

  1. on_long_presscontext_menu 都使用长按语义;同时启用时要实测是否冲突。
  2. .focused(key=..., equals=...)equals 应绑定到 State 中的当前字段名。
  3. simultaneous_gesturegesture 常用 'tap''long_press''magnification'
  4. high_priority_gesture 过度使用会导致子按钮点击失效。
  5. keyboard_dismiss 通常加在 ScrollView 或列表外层。
  6. 行级按钮要操作稳定 id,不要操作过滤后的下标。

#延伸阅读

  • 修饰符 API:查看手势、焦点和键盘相关修饰符签名。
  • 呈现 API:查看 sheet、alert、refresh、swipe action。