WebViewUI 是一个基于 PyWebView 的桌面窗口 UI 层,用来快速创建带自绘标题栏、窗口控制 API、前端桥接和基础配置持久化的本地 WebView 应用。
它最初从 PicScanner 项目中拆出,当前重点支持 Windows + WebView2。非 Windows 平台会保留基础 PyWebView 能力,Windows 原生标题栏、拖动、缩放、Aero snap 和导航遮罩能力由 wintitle.py 提供。
- 自绘标题栏,由 Python 注入到页面,不需要业务 HTML 手写标题栏。
- 原生窗口行为,保留 Windows 拖动、缩放、最大化、最小化和 Aero snap。
WindowApi基类,统一暴露最小化、最大化、关闭、拖动、缩放等窗口动作。- Python 后端对象通过
window.pywebview.api暴露给前端。 - 支持主窗口和多子窗口,每个窗口可使用独立
api_prefix。 - 支持 bootstrap 启动页、WebView 深色背景、原生导航遮罩,减少首帧白闪。
- 内置简单配置持久化,默认写入应用根目录下的
data/config.json。 - 支持 WebView2 DevTools 调试开关。
WebViewUI/
WebViewUI/
__init__.py 对外导出 WebViewApp、WindowApi、config 等入口
app.py WebViewApp 主窗口装配流程
window_api.py 暴露给 JS 的窗口控制 API 基类
titlebar.py 标题栏 JS、启动页 HTML、页面补丁 JS 构建器
wintitle.py Windows 原生窗口行为层
config.py WebViewUI 框架配置读写
examples/
minimal/
app.py 最小运行示例
index.html 示例前端页面
README.md
requirements.txt
pyproject.toml
CHANGELOG.md
LICENSE
- Python 3.9 或更高版本。
- Windows 推荐安装 Microsoft Edge WebView2 Runtime。
- Python 依赖见
requirements.txt,当前核心依赖为pywebview>=5.0。
安装依赖:
python -m pip install -r requirements.txt开发模式安装:
python -m pip install -e .从仓库根目录运行:
python examples/minimal/app.py示例会创建一个本地 WebView 窗口,并演示前端通过 window.pywebview.api 调用 Python 方法。
from pathlib import Path
from WebViewUI import WebViewApp, WindowApi
class MyApi(WindowApi):
def __init__(self):
super().__init__()
def get_startup_state(self):
return {"success": True, "message": "ready"}
def main():
entry = Path("ui/index.html").resolve()
app = WebViewApp(
entry_url=str(entry),
js_api=MyApi(),
title="My App",
width=1280,
height=820,
min_size=(1040, 680),
brand="My App",
titlebar_height=36,
use_bootstrap=False,
use_native_nav_cover=False,
)
app.run()
if __name__ == "__main__":
main()关键点:
entry_url可以是本地 HTML 文件路径,也可以是http://、https://、file://、data:URL。js_api推荐传入WindowApi的子类实例。brand会显示在 WebViewUI 注入的自绘标题栏左侧。titlebar_height控制标题栏高度,默认是36。app.run()会创建窗口并进入 PyWebView 事件循环,是阻塞调用。
业务 API 继承 WindowApi:
from WebViewUI import WindowApi
class MyApi(WindowApi):
def __init__(self):
super().__init__()
def hello(self, name="WebViewUI"):
return {"success": True, "message": f"Hello, {name}"}继承 WindowApi 的原因:
WindowApi已提供最小化、最大化、关闭、拖动、边缘缩放等窗口控制方法。WebViewApp会把同一个js_api绑定到主窗口。- 自绘标题栏通过
webview_window_action(action, api_prefix, payload)调用窗口控制。 - 业务方法会和窗口控制方法一起暴露给
window.pywebview.api。
Python 方法返回值应优先使用可 JSON 序列化的数据,例如 dict、list、str、int、bool、None。
前端需要等待 pywebviewready 后再调用 Python API:
function api() {
return window.pywebview && window.pywebview.api ? window.pywebview.api : null;
}
function call(name, ...args) {
const bridge = api();
if (!bridge || !bridge[name]) {
return Promise.reject(new Error("pywebview bridge not ready: " + name));
}
return bridge[name](...args);
}
window.addEventListener("pywebviewready", async () => {
const state = await call("get_startup_state");
console.log(state);
});不要在业务页面里手写窗口按钮逻辑;标题栏按钮由 WebViewUI 注入并自动连接到 WindowApi。
WebViewApp.run() 的主流程:
- 读取调试配置,必要时设置 WebView2 启动参数。
- 调用
_resolve_entry()把本地 HTML 路径转换为file://URL。 - 调用
webview.create_window(...)创建 PyWebView 窗口。 - 调用
self.js_api.bind(win, api_prefix="")绑定主窗口。 - 注册
win.events.shown += self._on_shown。 - 注册
win.events.loaded += self._on_loaded。 - 调用
webview.start(...)启动事件循环。
_on_shown() 负责窗口出现后的原生层初始化:
- 调用
wintitle.install(win, emulate_snap=False)安装 Windows 原生行为。 - 设置 WebView 深色背景,减少首帧白屏。
- 按配置安装原生导航遮罩。
- 注入标题栏脚本和页面布局补丁作为 startup script。
- 启动标题栏 keepalive 线程。
- bootstrap 模式下延迟跳转到真实页面。
_on_loaded() 负责页面加载后的补充注入:
- 读取当前
location.href便于日志定位。 - 页面加载完成后释放原生导航遮罩。
- 再次注入标题栏脚本和页面布局补丁。
- 刷新原生边框并同步最大化状态。
- 对首帧做尺寸 nudge,稳定布局。
WebViewUI 的标题栏不是写在业务 HTML 里的,而是由 Python 注入 JS 创建:
build_titlebar_js({
"titlebar_height": self.titlebar_height,
"api_prefix": "",
"brand": brand,
})标题栏包含:
- 品牌文字
- 最小化按钮
- 最大化或还原按钮
- 关闭按钮
- 拖动区域
- 边缘 resize 热区
窗口按钮统一调用:
window.pywebview.api.webview_window_action(action, API_PREFIX, payload || {})页面布局补丁由 build_page_patch_js() 注入,会给真实页面补充标题栏高度相关样式。业务 CSS 可以使用:
:root {
--titlebar-h: var(--nc-titlebar-h, 36px);
}如果修改 titlebar_height,需要同步检查业务 CSS 中与标题栏高度相关的变量和 calc(...)。
WebViewApp 支持 bootstrap 启动页:
WebViewApp(
entry_url="ui/index.html",
js_api=MyApi(),
use_bootstrap=True,
)bootstrap 启动页由 build_bootstrap_html() 生成,它只做通用窗口壳:
- 深色背景
- 标题栏
- 加载文案
- 最小化、最大化、关闭按钮
本地 HTML 页面如果加载很快,可以设置 use_bootstrap=False,让真实页面直接负责启动体验。
use_native_nav_cover 控制是否启用 Windows 原生导航遮罩:
WebViewApp(
entry_url="ui/index.html",
js_api=MyApi(),
use_native_nav_cover=True,
)遮罩由 wintitle.install_navigation_cover() 安装,用于减少 WebView 页面跳转过程中的白闪。如果应用要加载远程页面、频繁导航或首屏资源较重,可以开启它,并观察 _on_shown()、_on_loaded() 中的日志。
WebViewApp 支持通过参数或配置启用 DevTools:
WebViewApp(
entry_url="ui/index.html",
js_api=MyApi(),
devtools=True,
)启用后会设置 WebView2 参数:
--remote-debugging-port=<devtools_port>
--auto-open-devtools-for-tabs
对应配置在 data/config.json,默认值来自 WebViewUI/config.py:
{
"devtools_enabled": false,
"devtools_auto_open": false,
"devtools_port": 9222
}运行时日志会输出:
[WebViewUI] devtools enabled, inspect: http://127.0.0.1:9222
WebViewUI/config.py 会在应用根目录下创建:
data/config.json
默认配置包括:
window_width
window_height
window_min_width
window_min_height
native_navigation_cover
use_bootstrap_shell
devtools_enabled
devtools_auto_open
devtools_port
message.bootstrap
app_id
使用方式:
from WebViewUI import config
width = config.get("window_width", 960)
config.set("devtools_enabled", True)
snapshot = config.snapshot()注意:
Config只管理 WebViewUI 框架配置。- 业务配置、用户数据、文件权限、数据库等应放在业务模块中维护。
app_id首次运行自动生成并持久化。
WindowApi.create_child_window(opts) 可以创建带自绘标题栏的子窗口。所有窗口共享同一个 js_api 实例,每个窗口用 api_prefix 区分。
示例:
from pathlib import Path
from WebViewUI import WindowApi
class MyApi(WindowApi):
def open_help_window(self):
return self.create_child_window({
"url": Path("ui/help.html").resolve().as_uri(),
"title": "Help",
"width": 720,
"height": 520,
"min_size": (520, 360),
"brand": "Help",
"titlebar_height": 36,
"api_prefix": "help_",
"use_bootstrap": True,
"bootstrap_msg": "加载中",
})opts 支持字段:
url 或 html 子窗口入口,二选一
title 窗口标题
width, height 初始尺寸
min_size 最小尺寸
brand 标题栏品牌
titlebar_height 标题栏高度
use_bootstrap 是否使用启动页
bootstrap_msg 启动页文案
use_native_nav_cover 是否启用原生导航遮罩
api_prefix 子窗口 API 前缀
返回值:
{"success": True, "api_prefix": "help_", "reused": False}如果相同 api_prefix 的窗口已经存在,会激活已有窗口并返回:
{"success": True, "api_prefix": "help_", "reused": True}WindowApi.webview_window_action(action, api_prefix="", payload=None) 是标题栏统一入口。
支持动作:
minimize_window
maximize_window
close_window
start_window_drag
start_window_resize
is_window_maximized
sync_window_state
titlebar_double_click
主窗口也暴露了同名便捷方法:
minimize_window()
maximize_window()
close_window()
start_window_drag()
start_window_resize(edge="right")
is_window_maximized()
sync_window_state()
titlebar_double_click()前端业务代码通常不需要直接调用这些方法,因为标题栏注入脚本已经处理了按钮、拖动、双击和边缘缩放。
wintitle.py 是 Windows 专用层,非 Windows 平台相关函数为空操作。
它负责:
- 安装 custom chrome 行为。
- 保留 Windows 原生拖动、缩放、最大化动画和 Aero snap。
- 同步最大化状态到标题栏按钮。
- 设置 WebView2 深色背景。
- 安装和释放导航遮罩。
- 刷新窗口边框,稳定首帧布局。
业务代码通常不直接调用 wintitle.py。需要扩展窗口行为时,优先在 WindowApi 或 WebViewApp 层加清晰的公开方法。
修改或发布新版本时建议确认:
- 更新
CHANGELOG.md中的版本号和日期。 - 运行
python -m compileall -q WebViewUI examples。 - 运行
python examples/minimal/app.py确认可视窗口和前端桥接正常。 - 如需发布到 PyPI,先执行
python -m build检查打包产物。
核心函数:
WebViewApp.__init__():保存窗口参数、构建标题栏脚本、准备配置。WebViewApp._resolve_entry():把本地文件路径转换为file://URL。WebViewApp.run():创建主窗口、绑定 API、注册事件并启动 PyWebView。WebViewApp._on_shown():安装原生窗口行为、注入 startup script、启动 keepalive。WebViewApp._on_loaded():真实页面加载后再次注入标题栏和页面补丁。WebViewApp.create_child():委托WindowApi.create_child_window()创建子窗口。WindowApi.bind():把窗口实例绑定到指定api_prefix。WindowApi.webview_window_action():标题栏 JS 调用窗口动作的统一入口。WindowApi.create_child_window():创建带自绘标题栏的子窗口。build_titlebar_js():生成自绘标题栏注入脚本。build_page_patch_js():生成页面布局补丁脚本。build_bootstrap_html():生成 bootstrap 启动页 HTML。
主流程:
python app.py
-> 创建 WindowApi 子类实例
-> 创建 WebViewApp(entry_url=..., js_api=...)
-> WebViewApp.run()
-> webview.create_window(js_api=...)
-> WindowApi.bind(win, api_prefix="")
-> webview.start()
-> 窗口 shown: 安装原生行为、注入标题栏、启动 keepalive
-> 页面 loaded: 再次注入标题栏和页面补丁、刷新窗口状态
-> 前端 pywebviewready
-> 前端通过 window.pywebview.api 调用 Python API
WebViewUI 使用 GNU General Public License v3.0 许可证。详见 LICENSE。