components — 通用组件 API

原生通用组件层的构建器、事件与命令式控制器。

原生通用组件是声明式数据,而非可执行的组件代码。每个构建器返回一个 schema 节点({"type", "props", "children", "events"}),IDE 会对照其组件注册表进行校验,事件通过 ide.view.event 帧投递。

COMPONENT_SCHEMA_VERSION = 1,必须与宿主的 componentSchemaVersion 一致。

工作原理

组件的处理管线是"声明式数据 + 本地回调查找":

构建器 → schema 节点 (Component/dict)
   → 加入视图模型树(snapshot / patch 发送)
   → _wire_nodes 把事件处理函数压成 {event: True} 标记
   → IDE 按组件注册表校验并渲染
   → 用户操作 → IDE 回发 ide.view.event 帧(含 component_id + event + payload)
   → SDK 在本地查表调用对应 handler

要点:

  • 事件处理函数不通过线缆传输。发送时每个事件被序列化为 {event: True} 标记;宿主把事件名原样回传,SDK 在自己的 handler 表里查找并调用。因此 handler 可以是任意 Python 可调用对象(包括闭包)。
  • handler 表随快照 / 补丁重建。每次 snapshot/patch 后,SDK 都会从最新的组件树重新收集 (component_id, event) → handler;某个节点从树里移除时,它的事件处理函数也一并失效。
  • 两种编程模型
    • 声明式 — 在构建器参数里传 on_press= / on_change= 等,或事后用 component.on(event, handler)(链式、可返回自身)。
    • 命令式 — 通过 component.controller 调用宿主方法(如 get_textreveal_item),用于读取状态或驱动已挂载的组件。
  • id 的约束id 来自 props.id。需要命令式控制、或需要事件与组件精确对应的组件必须有 idMarkdown 设置 on_link_tap 时同样要求 id

Component

Component(type, props=None, children=None, events=None) 继承 dict,可直接序列化进视图快照/补丁。

属性/方法说明
.id组件 ID(来自 props.id
.on(event, handler)附加事件处理函数,返回自身,支持链式调用
.controller命令式控制器,用于操作已挂载组件
.to_json()序列化为线缆节点,事件处理函数替换为 {event: True} 标记
from pyrite_sdk.api.components import Button

btn = Button(id="save", label="保存", on_press=lambda e: print("保存"))
print(btn.to_json())

布局

构建器参数
Row(*children, id=None, gap=None, align=None, justify=None)水平排列
Column(*children, id=None, gap=None, align=None, justify=None)垂直排列
Flex(*children, id=None, direction=None, flex=None, gap=None)弹性布局
Grid(*children, columns, id=None, gap=None)网格布局
Wrap(*children, id=None, gap=None, run_gap=None)自动换行布局
SplitView(*children, id=None, direction=None, initial_ratio=None)分栏布局
Tabs(*children, id=None, selected=None)标签页容器,selected 为选中标签 ID
Tab(child=None, *, id, label, icon=None)单个标签
Section(*children, id=None, title=None, collapsible=None, collapsed=None)可折叠区块
Toolbar(*children, id=None, dense=None)工具栏
AppBar(*actions, id=None, title=None)页面标题栏,操作子组件只允许 IconButtonMenuDropdown
Scaffold(*body, id=None, app_bar=None)带可选标题栏的页面骨架,为正文提供有界主轴空间

AppBar 与 Scaffold

组件树使用 Scaffold 组织完整页面。app_bar= 会把 AppBar 放在组件树的第一个子节点;IDE 只把第一个且类型为 AppBar 的子节点识别为标题栏,其余子节点组成正文。正文在主轴上获得有限尺寸,因此 VirtualListDataTableFlex 等组件可以直接填满剩余空间。

from pyrite_sdk.api import components as c
from pyrite_sdk.api.icons import Icons

page = c.Scaffold(
    c.VirtualList(id="results", items=[]),
    app_bar=c.AppBar(
        c.IconButton(
            id="refresh",
            icon=Icons.refresh,
            tooltip="刷新",
            on_press=lambda event: refresh(),
        ),
        title="分析结果",
    ),
)

组件树中的 AppBar 只显示自身声明的操作,不会合并 Manifest 为渲染器视图贡献的标题菜单。需要动态菜单时,将 MenuDropdown 作为 AppBar 子组件。

内容

构建器参数
Text(value, *, id=None, style=None, muted=None, max_lines=None)文本
Icon(name, *, id=None, size=None)图标,name 必须是 MaterialIcon
Image(src, *, id=None, width=None, height=None, fit=None)图片,src 必须是 PluginResource
Video(src, *, id=None, width=None, height=None, fit=None, autoplay=None, looping=None, muted=None, show_controls=None)视频
Canvas(*, id, width=None, height=None, ops=None, interactive=None, viewport=None, on_tap=None, on_drag=None, on_hover=None, on_pointer=None)高性能自定义绘制表面
Markdown(value, *, id=None, on_link_tap=None)Markdown,设置 on_link_tapid 必填
CodeBlock(code, *, id=None, language=None, show_line_numbers=None)代码块
Badge(label, *, id=None, tone=None)徽章

Iconname 必须是 pyrite_sdk.api.icons.IconsMaterialIcon);Image/Videosrc 必须通过 self.resources.asset(...) 获取,否则抛出 TypeError

输入

构建器参数
TextField(*, id, value=None, placeholder=None, label=None, enabled=None, multiline=None, on_change=None, on_submit=None)文本输入
NumberField(*, id, value=None, min=None, max=None, step=None, label=None, enabled=None, on_change=None, on_submit=None)数字输入
Select(*, id, options, value=None, label=None, enabled=None, on_change=None)下拉选择
Checkbox(*, id, value=None, label=None, enabled=None, on_change=None)复选
Switch(*, id, value=None, label=None, enabled=None, on_change=None)开关
Slider(*, id, value=None, min=None, max=None, step=None, enabled=None, on_change=None)滑块

菜单与对话框

构建器参数
Menu(*, id, items, label=None, icon=None, tooltip=None, enabled=None, icon_only=None, alignment=None, offset_x=None, offset_y=None, use_root_overlay=None, child=None, on_select=None)弹出菜单
MenuBar(*, id, items, on_select=None)菜单栏
ContextMenu(child=None, *, id, items, enabled=None, on_select=None)右键菜单
Dropdown(*, id, items, label=None, on_select=None)下拉
Dialog(*children, id, title=None, open=None, on_close=None)对话框
Tooltip(child=None, *, id=None, message)提示气泡

操作

构建器参数
Button(*, id, label, icon=None, variant=None, enabled=None, on_press=None)按钮
IconButton(*, id, icon, tooltip=None, enabled=None, on_press=None)图标按钮

数据

构建器参数
VirtualList(*, id, items=None, item_count=None, item_height=None, selected_id=None, empty_label=None, on_select=None, on_activate=None, on_request_range=None, on_context_menu=None)虚拟列表
TreeView(*, id, nodes=None, expanded_ids=None, selected_id=None, indent=None, searchable=None, empty_label=None, on_select=None, on_activate=None, on_expand=None, on_collapse=None, on_request_children=None, on_context_menu=None)树形视图
DataTable(*, id, columns, rows=None, row_count=None, row_height=None, show_header=None, selected_id=None, sort_column=None, sort_ascending=None, empty_label=None, on_select=None, on_activate=None, on_sort=None, on_request_range=None, on_context_menu=None)数据表格
PropertyGrid(*, id, entries, on_select=None, on_expand=None)属性网格

菜单辅助

说明
MenuItem(*, id, label, icon=None, trailing_icon=None, shortcut=None, enabled=None, visible=None, tone=None, close_on_select=None)菜单项
MenuDivider(label=None)分隔线
MenuCheckboxItem(*, checked=False, **kwargs)复选菜单项
MenuRadioItem(*, selected=False, **kwargs)单选菜单项
Submenu(*, label, children, id=None, icon=None, enabled=None, visible=None)子菜单
MenuShortcut(key, *, primary=False, control=False, shift=False, alt=False, meta=False)快捷键,跨平台自动适配
MenuEvent菜单选中事件,属性:item_iditem_typecheckedselectedtarget_idtarget_type
items = [
    MenuItem(id="copy", label="复制", shortcut=MenuShortcut("c", primary=True)),
    MenuCheckboxItem(id="bold", label="加粗", checked=True),
    MenuDivider(),
    Submenu(label="插入", children=[
        MenuItem(id="image", label="图片"),
    ]),
]

数据项

说明
DataItem类型化行基类,属性 idmetadata
ListItem(id, label, icon=None, **data)列表项
TreeNode(id, label, parent_id=None, icon=None, has_children=False, **data)树节点
TableRow(id, cells, **data)表格行
PropertyEntry(id, name, value="", type_name="", parent_id=None, has_children=False, **metadata)属性条目
SelectOption(value, label, disabled=False)选择项
RangeRequest(payload)范围请求,属性 startcount

命令式控制器

通过 component.controller 访问。控制器方法异步执行,可传 callback。所有方法需要组件带有 id 并且已经绑定到 ViewModel(即被包含在快照中)。

基类 ComponentController

方法说明
is_mounted(callback=None)检查是否已挂载
ensure_visible(alignment=0.5, animated=True, callback=None)确保可见
get_bounds(callback=None)获取边界
request_focus(callback=None)请求焦点
unfocus(callback=None)取消焦点

各组件控制器

组件额外方法
TextFieldget_textset_text(text)clearselect_allget_selectionset_selection(start, end=None)replace_selection(text)
NumberField继承 TextField,另有 get_valueset_value(value)incrementdecrement
VirtualListselect(id)clear_selectionreveal_item(id, animated=True)jump_to_index(index)animate_to_index(index)scroll_by(delta, animated=True)get_visible_range
TreeViewselect(id)clear_selectionreveal(id, animated=True)expand(id)collapse(id)toggle(id)expand_allcollapse_allis_expanded(id)get_visible_nodes
PropertyGrid继承 TreeView,另有 activate(id)
DataTableselect_row(id)clear_selectionreveal_row(id, animated=True)reveal_cell(row_id, column_id, animated=True)jump_to_row(index)animate_to_row(index)get_visible_range
Tabsselect(id)nextpreviousget_selected
Sectionexpandcollapsetoggleis_expanded
SplitViewget_ratiosset_ratio(index, ratio)set_ratios(ratios)reset
Videoplaypauseseek_to(position_ms)set_volume(volume)set_speed(speed)set_looping(looping)get_stateenter_fullscreenexit_fullscreen
Imagereloadevict_cacheget_intrinsic_size
Markdownscroll_to_anchor(anchor)get_anchor_offset(anchor)select_allcopy_selection
Menuopenclosetoggleis_open
MenuBaropen_menu(id)closeis_open
Dropdownopencloseselect(id)get_selectedis_open
ContextMenushow(x=None, y=None)closeis_open
Dialogshowclose(result=None)is_open
Canvaspush_ops(ops, layer=None)set_ops(ops, layer=None)clearclear_layer(layer)hit_test(x, y)

控制器方法通过 sdk.view.component.invoke 调用宿主,需要组件已挂载。组件未绑定到 ViewModel 或缺少 id 时会抛出异常。

Canvas

Canvas 将大量绘制操作留在 Flutter 侧执行,适合波形、曲线、节点图、标注层和设备可视化。props.ops 是随视图快照和补丁同步的持久基础层;CanvasController 操作的是宿主内存中的临时覆盖层。视图重新挂载、可见性恢复或协议 resync 后,覆盖层会清空,必须保留的图形应写入 ops

完整示例

from pyrite_sdk.api import canvas
from pyrite_sdk.api.components import Canvas, Scaffold
from pyrite_sdk.core.plugin import UiPlugin


class ScopePlugin(UiPlugin):
    def __init__(self):
        super().__init__()
        self.view = self.views.create("scope-plugin.main")
        self.surface = Canvas(
            id="scope",
            width=320,
            height=180,
            interactive=True,
            viewport={
                "scale": 1.0,
                "minScale": 0.25,
                "maxScale": 8.0,
                "panEnabled": True,
                "zoomEnabled": True,
            },
            ops=[
                canvas.rect(
                    0,
                    0,
                    320,
                    180,
                    paint=canvas.paint(color=canvas.Color.surface),
                ),
                canvas.line(
                    12,
                    90,
                    308,
                    90,
                    id="zero-line",
                    paint=canvas.paint(
                        color=canvas.Color.primary,
                        stroke_width=2,
                    ),
                ),
            ],
            on_tap=self._on_tap,
        )
        self.view.snapshot([Scaffold(self.surface)])

    def on_start(self):
        self.view.open()

    def _on_tap(self, event):
        x = event.get("x", 0)
        y = event.get("y", 0)
        self.surface.controller.set_ops(
            [
                canvas.circle(
                    x,
                    y,
                    5,
                    paint=canvas.paint(color=canvas.Color.primary),
                )
            ],
            layer="cursor",
        )


plugin = ScopePlugin()
plugin.start()

示例中的矩形和基准线属于持久基础层。点击 Canvas 后,set_ops()cursor 覆盖层绘制标记;下一次点击会替换该层,而不会修改 props.opssnapshot() 会将组件绑定到 ViewModel,因此事件处理器执行时可以使用 self.surface.controller

绘制操作

pyrite_sdk.api.canvas 提供与线缆 schema 一致的构建函数:

分类构建函数
基本图形linepolylinerectrrectcircleovalarcpointspolygon
路径path,以及 move_toline_toquad_tocubic_toclose
内容textimage
裁剪clip_rectclip_rrectclip_path
图层saverestoresave_layergroup
变换translatescalerotatematrix
画笔paintlinear_gradientradial_gradientColor

画笔支持填充或描边、线宽、端点、连接、透明度、抗锯齿、渐变和 Flutter blend mode。颜色可用 #RRGGBB#AARRGGBB,也可使用 canvas.Color.primary 等主题色。坐标单位为逻辑像素,角度使用弧度,正方向为顺时针。

图片必须通过插件资源引用,不能在操作中内联位图字节:

logo = self.resources.asset("assets/logo.png")
op = canvas.image(logo, 16, 16, width=64, height=64, id="logo")

覆盖层与调用限制

surface.controller.set_ops(preview_ops, layer="preview")
surface.controller.push_ops(marker_ops, layer="markers")
surface.controller.clear_layer("preview")
surface.controller.hit_test(120, 80, callback=lambda **result: print(result))

push_ops 向指定层追加操作,set_ops 替换指定层;省略 layer 时,set_ops 替换全部覆盖层。clear() 只清除覆盖层,不修改持久基础层。单次 push_opsset_ops 的序列化参数上限为 2 MiB,越界时 SDK 在发送前抛出 ViewProtocolError

尺寸、视口与事件

省略宽度或高度时,Canvas 填满父布局在该轴提供的有限空间。父布局给出无界约束时,IDE 使用稳定的 300 × 200 逻辑像素兜底值,避免布局随内容变化。viewport 支持 scaleoffsetXoffsetYminScalemaxScalepanEnabledzoomEnabled;缩放值会限制在有效范围内。

宿主在本地维护命中索引、平移缩放、悬停高亮、拖动预览和框选视觉。只有已绑定处理器的语义事件会经过 Bridge:tapdraghoverpointerhoverpointer 及拖动更新会被节流;拖动的 startend 立即发送,end 前会先刷新最后一个待处理的 update。事件坐标使用内容坐标,带 id 且有可计算边界的绘制操作可在 payload 中返回 elementId

每个基础层和覆盖层都拥有独立的 Flutter Canvas 保存栈。多余的 restore 不会越过组件绘制边界,裁剪和变换后的命中区域与显示区域保持一致。

通用组件示例

from pyrite_sdk.api.components import (
    Column, TextField, Button, Text, COMPONENT_SCHEMA_VERSION,
)

assert COMPONENT_SCHEMA_VERSION == 1

def build_form():
    return Column(
        Text("配置", style="headline"),
        TextField(
            id="name",
            label="名称",
            on_change=lambda payload: print(payload),
        ),
        Button(
            id="submit",
            label="提交",
            on_press=lambda e: print("已提交"),
        ),
        gap=8,
    )

事件也可以事后通过 .on() 附加,返回组件自身便于链式调用:

field = TextField(id="name", label="名称")
field.on("change", lambda payload: print(payload.get("value")))

On this page