ultralight-py DOCSshabbywu/ultralight-py
DOCUMENTATION / GUIDE在 GitHub 查看源码 ↗

使用指南#

页面加载、双向桥接、资源、像素与输入的使用约定。接口签名见 公开 API, 对象代理语义见 对象桥接。

AppCore 原生窗口、系统字体、IME 和独立 API 注册表见 窗口指南。

最小离屏闭环#

import asyncio
from ultralight_py import Runtime
from ultralight_py.scripting import ScriptDriver


async def capture(runtime):
    with runtime.create_view(320, 180) as view:
        async with ScriptDriver.auto(runtime) as driver:
            await driver.page(view).load_html(
                "<style>body{background:#ff0000}</style><body>Hello",
                wait_until="settled",
                timeout=5,
            )
        print(view.evaluate("document.body.textContent"))
        frame = view.copy_pixels()
        print(frame.width, frame.height, frame.row_bytes, frame.format)
        view.clear_dirty_bounds()


with Runtime() as runtime:
    asyncio.run(capture(runtime))

已有宿主循环可直接使用 await view.wait_ready() / wait_settled();它们不隐式推进引擎。 需要脚本接口时使用 ScriptDriver(runtime, step=host.request_step),由宿主决定推进时机。 订阅与导航失效规则见 页面等待与脚本驱动 API。 点击或提交触发页面替换时,用 async with page.expect_navigation(...) 在动作前登记等待; 上下文退出后再等待新页面的选择器,动作与导航共用超时预算。

完整示例包含加载错误处理:

python examples/offscreen.py
python examples/bridge.py
python examples/resources.py
python examples/objects.py
python -m pytest tests -q

运行时、线程与释放#

  • Runtime() 返回每进程唯一实例,重复调用共享 Renderer。首次调用冻结资源配置; 可通过 Runtime(cache_path="./cache") 指定磁盘缓存及浏览器持久化存储目录。 显式目录解析为绝对路径;默认 None 保留 SDK 默认配置,后续省略会复用首次配置。 后续传入不同的目录或 loader 会报错。Runtime.close() 关闭全部 View,关闭后不能重建 Renderer;日常复用 Runtime,反复创建和关闭 View。上下文管理器退出会执行关闭。
  • Runtime、View、JSValue 的操作和显式关闭只能在 Python 主线程进行。离屏 Runtime 不启动宿主循环; 宿主自行调用 update()、refresh_display() 和 render()。update() 也推进句柄及回调清理。 AppCore 窗口由 Application 持续驱动,不能再手动更新其 Runtime。
  • View 的垃圾回收只将关闭任务入队,在后续主线程调用中处理。使用 close() 或 with 获得及时释放;atexit 会在解释器退出前关闭 Runtime。回调捕获 View/Runtime 时可能形成 Python 与原生对象之间的引用环,显式关闭会解除回调引用。
  • JSValue 保留当前文档中的值;主框架导航或关闭 View 后旧句柄报 PageGone, 不能传入其他 View/文档。JSValue.closed 表示显式关闭、文档失效或 View 已关闭;操作时仍会检查文档有效性。
  • 更新或渲染回调中允许关闭 View、导航和替换回调,不能嵌套驱动 Renderer。 JS 执行中的导航/关闭可能使当前求值报 PageGone。

页面事件与异常#

view.set_callbacks(...) 整组替换回调,省略的回调清空。支持:

回调 参数
begin_loading、window_ready、dom_ready、finish_loading FrameEvent(frame_id, is_main_frame, url) 的只读属性
fail_loading LoadFailure:frame、description、domain、code
console 字典:message、source、type、level、source_url、line、column
page_settled 页面网络与布局静默后的 URL 字符串

view.on(event, callback) 增加独立的可关闭订阅。set_callbacks() 不影响这些订阅或内部等待。

事件数据拥有独立寿命,不引用 SDK 回调期间的借用对象。事件可能包含子框架,应检查 is_main_frame。加载完成不保证加载成功,需要单独处理 fail_loading。 无脚本页面可能不触发 window_ready;依赖 DOM 时使用 dom_ready。

同步引擎错误抛 UltralightError,脚本错误抛其子类 JavaScriptError。 原生诊断提供 code、stack、source_url、line 和 column。 Python 参数类型错误使用 TypeError/ValueError,整数超范围使用 OverflowError。

Python 函数、实例方法及属性访问的异常转为页面中的 JS Error。页面事件和资源 loader 的异常被隔离, 交给 runtime.set_error_handler(callback);资源错误在后续 update() 或关闭时派发。 默认通过 sys.unraisablehook 报告,传入 None 可恢复默认处理。 错误处理器自身的异常也通过该 hook 报告,不会跨 SDK 边界抛出。

Python 与 JavaScript#

def window_ready(event):
    if event.is_main_frame:
        view.bind_function("python_add", lambda a, b: a + b)


view.set_callbacks(window_ready=window_ready)
view.load_html("<script>window.answer=python_add(20,22)</script>")
# 驱动 update(),等待主框架 DOM 就绪后:
assert view.evaluate("answer") == 42

with view.evaluate_handle("({offset:10, add(n){return this.offset+n}})") as obj:
    obj.set("offset", 20)
    with obj.get("add") as fn:
        with fn.call(2, this=obj) as result:
            assert result.to_python() == 22

上述 View.bind_function() 只绑定当前文档,不自动跨导航重放。在每次主框架 window_ready 中显式重绑; 需要加载前附着和跨导航恢复时,使用 独立 API 注册表。

evaluate() 返回 Python 值;evaluate_handle() 返回 JSValue。句柄支持 get(str_or_int)、set(key, value)、keys()、call(*args, this=None) 和 to_python()。 get() 和 call() 返回句柄,显式指定 this 可保留方法的接收对象。 set()/call() 的单个值以及 Python 可调用对象的返回值可以是同文档 JSValue。 实时 Python 容器中的 JSValue 也会在访问时检查所属文档;显式按值复制只接受基础数据和容器,不复制函数、Promise 等特殊对象。

Python JavaScript
None null
ultralight_py.undefined undefined
bool Boolean
int、float Number;JS 安全整数返回 int,其余返回 float
str String,保留 Unicode 和内嵌 NUL
list JS Array 代理;JS 原生 Array 在 Python 中为共享 JSArray 视图
tuple Python 只读代理,使用高级 Python 索引接口
dict JS Object 代理,属性访问字符串键;JS 原生对象在 Python 中为共享 JSObject 视图
普通实例、callable 属性/方法代理或可调用代理;回传时恢复原对象

Python int 必须位于 ±(2⁵³−1),不进行隐式字符串/数字转换。可变容器默认共享引用, 支持嵌套、循环引用和别名。函数、Promise、Date 等 JS 特殊对象通过 JSValue 使用。 需要独立基础数据时使用 evaluate(..., copy=True) 或 value.to_python(copy=True); 显式深复制拒绝循环引用或超过 64 层的数据,不保留对象别名。 JSObject 读取自有可枚举字符串属性,可能执行 getter;keys() 不读取属性值。

Python 对象与实时容器#

class Counter:
    def __init__(self, value):
        self.value = value
        self.history = []

    def increment(self):
        self.value += 1
        self.history.append(self.value)
        return self.value


left, right = Counter(0), Counter(100)
left_binding = view.bind_object("left", left)
right_binding = view.bind_object("right", right)
view.evaluate("left.increment(); right.increment()")
assert (left.value, right.value) == (1, 101)
assert view.evaluate("left.history") is left.history
left_binding.close()

自动暴露公开属性、property 和方法;可通过 members=[...] 收窄根对象成员。 同类实例的方法与状态分别管理,每个绑定内部保持对象身份,解除绑定使已导出的嵌套代理和方法失效。 文档级绑定需在主框架导航后重新创建;独立 API 注册表自动恢复。 View 保留 PythonBinding 会话,丢弃句柄不会解除绑定。

Python list/dict 与 JS Array/普通对象默认双向共享,包括函数参数、返回值和属性。 JS 使用索引、length、push、map、Object.keys 和 JSON.stringify;无需先转换为快照。 JS 创建的容器在 Python 中使用 JSArray/JSObject 视图,显式 copy=True 才生成独立 list/dict。 完整 API、稠密数组规则、非字符串字典键及释放规则见 对象桥接。

资源加载#

from ultralight_py import Resource, Runtime

files = {
    "page.html": Resource(b'<script src="app.js"></script>', "text/html"),
    "app.js": b"window.answer=42",
}
runtime = Runtime(assets_root="./assets", resource_loader=files.get)
view = runtime.create_view(320, 180)
view.load_url("file:///page.html")

加载顺序为内嵌 ICU/证书、Python loader、本地文件系统。SDK 资源不交给 Python loader。 assets_root=None 使用首次初始化时的工作目录,显式目录解析为绝对路径。 load_html(html, url="") 的 URL 可为相对资源提供页面基址;load_url() 也支持 HTTP/HTTPS URL。 离屏 Runtime 默认使用 ulbind17 内嵌字体;Application 使用 AppCore 系统字体。

resource_loader(path) 返回 bytes、Resource(data, mime_type=None, charset="utf-8") 或 None。只有 None 触发本地目录回退;异常或错误返回类型导致该资源读取失败。 未指定 MIME 时按扩展名推断,编码默认 UTF-8;SDK 缓冲区拥有内容副本,包括空字节串。

SDK 可从主线程和后台加载线程重复调用 loader。结果不缓存,回调应在同一次加载中返回一致内容, 共享状态需要自行同步。loader 内禁止操作 Runtime/View/JS,也不要等待主线程驱动页面。 关闭会停止新 loader 调用,释放 GIL 等待正在执行的回调结束,再清除 Python 引用。

像素#

copy_pixels() 返回拥有独立 bytes 的不可变 PixelBuffer,View 关闭后仍可使用。 包含 width、height、row_bytes、format、dirty_bounds 和 data。 格式固定为 BGRA8、预乘 alpha,行可能有填充;像素 (x,y) 的偏移为 y * row_bytes + x * 4。dirty_bounds 为 (left, top, right, bottom),右/下边界不包含在内。 读取不会清除脏区域,成功消费后调用 clear_dirty_bounds()。

frame.to_rgba() 返回拥有独立 bytes 的不可变 RGBAImage:width、height、data, 以及只读 format == "RGBA8"、row_bytes == width * 4。输出从上到下排列,去除行填充, 采用非预乘透明度;尺寸不变。images.to_rgba(frame) 是同一转换的函数入口, 也接受 RGBAImage 并直接返回它。图像在 View/Runtime 关闭后仍可使用,转换可以放入宿主线程池, 原生像素循环释放 GIL。

from ultralight_py.images import to_numpy, to_pillow

rgba = frame.to_rgba()  # 不要求 Pillow/NumPy
image = to_pillow(rgba)  # pip install 'ultralight-py[pillow]'
image.save("capture.png")  # Pillow 编码 PNG
array = to_numpy(rgba)  # pip install 'ultralight-py[numpy]'
readonly = to_numpy(rgba, copy=False)

Pillow 输出为独立、可修改的 RGBA 图像。NumPy 输出为 C 顺序的连续 (height, width, 4) uint8 数组, 默认可写且拥有独立数据;copy=False 返回 RGBA bytes 的只读视图,自动保持底层数据存活。 它只省去数组复制:BGRA 去预乘转换仍需分配 RGBA bytes。先转换一次,再将同一 RGBAImage 交给多个适配器,可以复用转换结果。基础导入和 RGBA 转换不会加载 Pillow/NumPy。

去预乘采用 min(255, channel * 255 // alpha),保留 alpha;alpha 为 0 时输出全零, 为 255 时只交换通道。此转换沿用引擎的颜色空间;8 位预乘已经损失的信息无法精确恢复。 转换忽略行尾填充和尾部额外数据,输入长度至少为 (height - 1) * row_bytes + width * 4, 允许末行没有尾部填充。尺寸要求正 uint32,行跨度必须为正整数且至少为 width * 4; 输入 data 必须为 bytes,格式必须为 BGRA8Premultiplied。 无效类型抛 TypeError,无效值或短缓冲区抛 ValueError,大小溢出抛 OverflowError,分配失败抛 MemoryError。 RGBAImage 构造器校验 data 长度恰好为 width * height * 4。

缩放、背景合成、JPEG 编码、截图队列与传输由宿主应用负责。可运行 图像示例 保存透明 PNG;性能测量见 scripts/benchmark_images.py。

输入事件#

页面就绪后,在主线程向 View 发送输入:

from ultralight_py import KeyCode, Modifiers, MouseButton

view.focus()
view.click(40, 80, button=MouseButton.LEFT)
view.input_text("你好,世界 😀")
view.key_down(KeyCode.GK_BACK)
view.key_up(KeyCode.GK_BACK)
view.scroll(delta_y=-120, x=200, y=200)

mouse_move()、mouse_down()、mouse_up() 支持拖动;click() 依次发送移动、 按下和释放,不隐式设置焦点。focus()、unfocus() 控制焦点, has_focus 和 has_input_focus 分别查询 View 焦点和可编辑元素焦点。 坐标使用 View 的逻辑像素。滚动默认使用像素,正值向左/上、负值向右/下; 传入 type=ScrollEventType.PAGE 可按页滚动,默认坐标 (-1, -1) 使用当前指针位置。

需要完整事件字段时,使用不可变的 MouseEvent、ScrollEvent、KeyEvent,分别交给 fire_mouse_event()、fire_scroll_event()、fire_key_event()。 枚举值必须使用对应枚举类型;整数参数按有符号 32 位校验,不接受 bool 或浮点数。 KeyCode 保留 SDK 的 GK_* 命名,Modifiers 支持 ALT、CTRL、META、SHIFT 组合。 鼠标事件的修饰键状态来自最近的键盘事件,宿主需要转发修饰键的按下和释放。

key_down() 发送 RAW_KEY_DOWN,不自动输入文本;key_up() 发送 KEY_UP。 input_text() 按 Unicode 码点发送 CHAR,适合已经提交的文本;它不模拟物理按键, 也不提供输入法预编辑或候选框定位。原生窗口的 IME 由 AppCore 处理。完整 KeyEvent 可指定原生键码、文本、未修饰文本、 自动重复、小键盘和系统键标志。

输入同样受主线程、对象关闭和运行时占用检查保护。在 Renderer、资源 loader、 JavaScript 桥接或输入回调中再次发送输入会报错;需要追加输入时,将操作排队到宿主下一轮。 输入回调可以关闭 View 或 Runtime。发送事件后仍需持续执行 update()、refresh_display()、render(),尤其是滚动动画。

Tkinter 适配器与交互示例#

Tkinter 适配完全位于可选的 Python 模块中,原生绑定只提供通用输入事件。 普通 import ultralight_py 不加载 tkinter,也不要求安装 Pillow。

from ultralight_py.integrations.tkinter import TkInputAdapter

adapter = TkInputAdapter(canvas, view)
# 宿主用 root.after() 驱动 Runtime,并将 copy_pixels() 转成 Tk 图像。
# 关闭窗口时先解除输入绑定,再释放 View 和 Runtime:
adapter.close()
view.close()
runtime.close()

适配器自动转发指针、按钮、滚轮、焦点及键盘事件,使用独立 bindtag 保留宿主已有绑定。 窗口失焦时释放按键和按钮,控件销毁时自动解除绑定;close() 可重复调用, 只释放自身回调、定时任务和 View 引用,不关闭 View、Runtime 或控件。 如果页面显示区域有缩放或偏移,可传入 coordinate_transform=lambda x, y: (x, y) 将控件坐标转换为 View 坐标。

wheel_pixels=40 设置每个标准滚轮单位的像素距离;适配器归一化 Aqua、Win32 和 X11 的滚轮差异,Shift+滚轮转为水平滚动,并处理 X11 自动重复的释放事件。 可打印字符与物理按键分开发送,快捷键不会重复插入文本。 macOS Aqua 已完成实际窗口验收;Win32/X11 的映射通过模拟事件测试,尚未完成对应平台实测。

python examples/tkinter_view.py
python examples/tkinter_view.py --smoke

示例包含两个 Python Counter 模型分别控制的按钮、文本框、滚动区域和事件记录,使用标准库将不透明 BGRA 帧转换为 PPM, 通过 Tk Canvas 显示。渲染、窗口尺寸更新和关闭顺序由示例宿主管理。 --smoke 创建真实窗口并检查帧尺寸、颜色、Canvas 图像及 Python 模型隔离后自动退出,需要可用的图形环境。

搜索文档

输入关键词搜索 API 与使用指南

    ↑ ↓ 选择 · Enter 打开 · Esc 关闭