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_text、reveal_item),用于读取状态或驱动已挂载的组件。
- 声明式 — 在构建器参数里传
id的约束:id来自props.id。需要命令式控制、或需要事件与组件精确对应的组件必须有id;Markdown设置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) | 页面标题栏,操作子组件只允许 IconButton、Menu 和 Dropdown |
Scaffold(*body, id=None, app_bar=None) | 带可选标题栏的页面骨架,为正文提供有界主轴空间 |
AppBar 与 Scaffold
组件树使用 Scaffold 组织完整页面。app_bar= 会把 AppBar 放在组件树的第一个子节点;IDE 只把第一个且类型为 AppBar 的子节点识别为标题栏,其余子节点组成正文。正文在主轴上获得有限尺寸,因此 VirtualList、DataTable 和 Flex 等组件可以直接填满剩余空间。
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 为渲染器视图贡献的标题菜单。需要动态菜单时,将 Menu 或 Dropdown 作为 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_tap 时 id 必填 |
CodeBlock(code, *, id=None, language=None, show_line_numbers=None) | 代码块 |
Badge(label, *, id=None, tone=None) | 徽章 |
Icon 的 name 必须是 pyrite_sdk.api.icons.Icons(MaterialIcon);Image/Video 的 src 必须通过 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_id、item_type、checked、selected、target_id、target_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 | 类型化行基类,属性 id、metadata |
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) | 范围请求,属性 start、count |
命令式控制器
通过 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) | 取消焦点 |
各组件控制器
| 组件 | 额外方法 |
|---|---|
TextField | get_text、set_text(text)、clear、select_all、get_selection、set_selection(start, end=None)、replace_selection(text) |
NumberField | 继承 TextField,另有 get_value、set_value(value)、increment、decrement |
VirtualList | select(id)、clear_selection、reveal_item(id, animated=True)、jump_to_index(index)、animate_to_index(index)、scroll_by(delta, animated=True)、get_visible_range |
TreeView | select(id)、clear_selection、reveal(id, animated=True)、expand(id)、collapse(id)、toggle(id)、expand_all、collapse_all、is_expanded(id)、get_visible_nodes |
PropertyGrid | 继承 TreeView,另有 activate(id) |
DataTable | select_row(id)、clear_selection、reveal_row(id, animated=True)、reveal_cell(row_id, column_id, animated=True)、jump_to_row(index)、animate_to_row(index)、get_visible_range |
Tabs | select(id)、next、previous、get_selected |
Section | expand、collapse、toggle、is_expanded |
SplitView | get_ratios、set_ratio(index, ratio)、set_ratios(ratios)、reset |
Video | play、pause、seek_to(position_ms)、set_volume(volume)、set_speed(speed)、set_looping(looping)、get_state、enter_fullscreen、exit_fullscreen |
Image | reload、evict_cache、get_intrinsic_size |
Markdown | scroll_to_anchor(anchor)、get_anchor_offset(anchor)、select_all、copy_selection |
Menu | open、close、toggle、is_open |
MenuBar | open_menu(id)、close、is_open |
Dropdown | open、close、select(id)、get_selected、is_open |
ContextMenu | show(x=None, y=None)、close、is_open |
Dialog | show、close(result=None)、is_open |
Canvas | push_ops(ops, layer=None)、set_ops(ops, layer=None)、clear、clear_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.ops。snapshot() 会将组件绑定到 ViewModel,因此事件处理器执行时可以使用 self.surface.controller。
绘制操作
pyrite_sdk.api.canvas 提供与线缆 schema 一致的构建函数:
| 分类 | 构建函数 |
|---|---|
| 基本图形 | line、polyline、rect、rrect、circle、oval、arc、points、polygon |
| 路径 | path,以及 move_to、line_to、quad_to、cubic_to、close |
| 内容 | text、image |
| 裁剪 | clip_rect、clip_rrect、clip_path |
| 图层 | save、restore、save_layer、group |
| 变换 | translate、scale、rotate、matrix |
| 画笔 | paint、linear_gradient、radial_gradient、Color |
画笔支持填充或描边、线宽、端点、连接、透明度、抗锯齿、渐变和 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_ops 或 set_ops 的序列化参数上限为 2 MiB,越界时 SDK 在发送前抛出 ViewProtocolError。
尺寸、视口与事件
省略宽度或高度时,Canvas 填满父布局在该轴提供的有限空间。父布局给出无界约束时,IDE 使用稳定的 300 × 200 逻辑像素兜底值,避免布局随内容变化。viewport 支持 scale、offsetX、offsetY、minScale、maxScale、panEnabled 和 zoomEnabled;缩放值会限制在有效范围内。
宿主在本地维护命中索引、平移缩放、悬停高亮、拖动预览和框选视觉。只有已绑定处理器的语义事件会经过 Bridge:tap、drag、hover 和 pointer。hover、pointer 及拖动更新会被节流;拖动的 start 和 end 立即发送,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")))