Skip to content

mashu3/tkwry

Repository files navigation

tkwry

License: MIT PyPI - Python Version GitHub Release PyPI Version Downloads Status: Alpha CI

Keep Tkinter — give it the WebView it never had.

Embed a real system WebView (wry) inside your Frame: modern HTML, JS, and IPC in the same layout as your buttons and tabs — one mainloop, no floating overlay.

Alpha — Early preview (see PyPI badge for the current version). APIs and behavior may change without notice. Not recommended for production use yet.


📖 Overview

Tkinter is still a solid GUI shell — it just had no first-class way to host modern web content inside a widget. Overlay-style WebViews drift out of sync when you move, resize, or switch tabs.

tkwry fills that missing piece:

  • True child embeddingbuild_as_child via HWND, NSView, or X11 window ID
  • One event loop — Tk mainloop only; no separate app runtime
  • IPC bridge — JavaScript → Python callbacks without freezing the UI
  • Layout-aware — tracks pack / grid / place, tabs, and PanedWindow

Pre-built abi3 wheels ship for Windows (x86_64, arm64) and macOS (Apple Silicon + Intel) — these are the primary release targets. Linux is best-effort: build from source (sdist / git); timing and headless behavior are not guaranteed in v0.0.x.


🔧 Requirements

  • Python 3.10+
  • Tkinter (included with most Python builds)
  • Building from source (git clone, pip install git+…, or Linux) — Rust toolchain (stable); pip uses maturin as the build backend
  • Windows (x86_64, arm64)
    • WebView2 Runtime must be installed (pre-installed on many Windows 10/11 systems).
    • Without WebView2, tkwry is not supported on Windows — there is no fallback engine.
  • macOS — 11 (Big Sur) or later; Apple Silicon (arm64) or Intel (x86_64); system WKWebView (no extra runtime)
  • Linux — WebKitGTK 4.1 + GTK 3 dev packages; X11 or XWayland ($DISPLAY); build the extension from source (see below)

📦 Installation

PyPI (recommended — Windows / macOS wheels)

pip install tkwry

From a git clone (source build)

Cloning the repo and installing locally compiles the Rust extension on your machine. You need a Rust toolchain (rustup) and platform runtimes from Requirements above (WebView2 on Windows, etc.). pip pulls in maturin automatically as the build backend.

git clone https://github.com/mashu3/tkwry.git
cd tkwry
pip install -e .

Use this for development and for running the examples from the tree.

Install a git revision with pip (source build)

pip install git+https://github.com/mashu3/tkwry.git

This also builds from source (sdist via git), not a pre-built wheel. It requires Rust and will fail on machines without a working toolchain — same as pip install . / pip install -e .. Prefer the PyPI wheel on Windows and macOS when you do not need bleeding-edge changes.

Linux (source install, best-effort)

Linux builds from source and runs for many apps, but v0.0.x does not treat Linux stability as a release requirement — focus is on Windows and macOS wheels. Install system dependencies, then:

# Debian / Ubuntu
sudo apt install \
  libwebkit2gtk-4.1-dev \
  libgtk-3-dev \
  libglib2.0-dev

# Runtime (for end users of your app)
# sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0

pip install maturin
git clone https://github.com/mashu3/tkwry.git
cd tkwry
pip install .

GTK events are pumped automatically on a Tk timer while your app runs.


🚀 Usage

Basic WebView

import tkinter as tk
from tkwry import WebView

root = tk.Tk()
root.geometry("900x600")

frame = tk.Frame(root, bg="#222")
frame.pack(fill="both", expand=True, padx=8, pady=8)

web = WebView(frame, url="https://github.com")

root.mainloop()

IPC (JavaScript → Python)

def on_message(msg: str) -> None:
    print("from JS:", msg)

web = WebView(
    frame,
    html='<button onclick="window.ipc.postMessage(\'hi\')">send</button>',
    ipc_handler=on_message,
)

Load HTML / evaluate JavaScript

web.load_html("<h1>Hello</h1>")
web.eval_js("document.title = 'Hi'")  # fire-and-forget (Tk idle, no return value)
web.eval_js("bad()", on_error=lambda exc: print("eval failed:", exc))
web.eval_js_with_callback("document.title", print)  # async; callback on Tk main thread
web.load_url("https://example.com")
web.reload()
print(web.url)
web.focus()
web.open_devtools()

Rapid load_url / load_html calls are coalesced (last-wins)load(A); load(B); load(C) loads C only.

eval_js does not return a result (not synchronous). Use eval_js_with_callback when you need the JavaScript return value as a str. Pass on_error= to handle evaluation failures on the Tk main thread; otherwise the traceback is printed to stderr (EvalErrorHandler).

Layout / resize

Bounds sync runs automatically on <Configure>, <Map>, and <Unmap>. Call sync_bounds() manually after custom layout changes so the WebView reflows (e.g. centered images):

web.sync_bounds()

Navigation / lifecycle callbacks

from tkwry import NewWindowResponse, PageLoadEvent

web = WebView(
    frame,
    url="https://example.com",
    on_page_load=lambda evt, url: print(evt, url),
    on_title_changed=lambda title: root.title(title),
    on_navigation=lambda url: url.startswith("https://"),
    on_new_window=lambda url: NewWindowResponse.Deny,
)

on_page_load fires PageLoadEvent.Started and PageLoadEvent.Finished for every navigation. Events that occurred before a handler was registered are discarded when you call set_on_page_load (or pass on_page_load in the constructor from the start).

Callback threads: on_page_load and on_title_changed run on the Tk main thread (queued). on_navigation and on_new_window run synchronously on the WebKit thread — wry needs an immediate return value, so they cannot be queued. Keep those handlers fast and avoid Tk widget calls; defer work with root.after if needed.

Callback exceptions are printed to stderr and do not stop event delivery.

Drag & drop (native OS path)

File drops from Finder / Explorer are handled by the OS WebView. Your handler runs on the Tk main thread (tkwry queues events from WebKit automatically). The handler is notify-only (-> None); drops are always accepted and cannot be denied from Python.

from tkwry import DragDropEvent

def on_drop(event, paths, position):
    if event == DragDropEvent.Drop:
        print("files:", paths)

web = WebView(frame, html="...", drag_drop_handler=on_drop)

See examples/dnd_demo.py.

Cleanup

web.destroy()   # release native webview; host Frame is kept
# or destroy the host Frame — both tear down the webview

📚 API summary

Category Members
Content load_url, load_html, reload, url
JavaScript eval_js (on_error), eval_js_with_callback
IPC set_ipc_handler
Callbacks set_on_navigation, set_on_page_load, set_on_title_changed, set_on_new_window, set_drag_drop_handler
Appearance set_background_color, focus, focus_parent, open_devtools, close_devtools, is_devtools_open
Layout pack, grid, place, sync_bounds (delegate to host Frame except sync_bounds)
Lifecycle destroy, destroyed, native

Constructor options: url, html, ipc_handler, devtools, background_color, user_agent, initialization_script, focused, plus the callback hooks above.

Enums: PageLoadEvent, NewWindowResponse, DragDropEvent.

Type aliases: IpcHandler, NavigationHandler, PageLoadHandler, TitleChangedHandler, NewWindowHandler, DragDropHandler, EvalCallback, EvalErrorHandler.


⚠️ Known limitations

  • Alpha — APIs may change; not recommended for production yet
  • Windows — WebView2 Runtime required; systems without it are unsupported
  • Linux — source install only (no PyPI wheel); best-effort in v0.0.x — headless CI and event timing are not release blockers
  • DevTools — macOS uses private APIs; avoid in Mac App Store release builds
  • macOS input — Tk text widgets and the WebView share one window; tkwry routes focus automatically (see macOS embedding). IME and other advanced input may still differ from a standalone browser
  • Drag & drop — drop target is the WebView area only (not arbitrary Tk widgets; use tkinterdnd2 for those)

See CHANGELOG.md for release history.


🌐 Platform notes

OS Arch Parent handle Engine Notes
Windows x86_64, arm64 Frame.winfo_id() → HWND WebView2 WebView2 Runtime required
macOS arm64, x86_64 Toplevel content NSView WKWebView See macOS embedding below
Linux winfo_id() → X11 window ID WebKitGTK Source install; best-effort (not a wheel release target)

macOS embedding

On macOS, Tk child Frames usually do not get their own NSView — that is a property of the Tk Aqua backend, not something tkwry can turn into per-frame native views without upstream Tk changes.

tkwry works around this by:

  1. Attaching the WebView to the toplevel content view
  2. Positioning it with set_bounds to match your Frame (<Configure>)
  3. Hiding it with set_visible(False) when the frame is unmapped (<Unmap>) — e.g. another ttk.Notebook tab is selected

Keyboard focus (macOS): tkwry routes input between Tk widgets (Entry, Text, …) and the WebView automatically. Rust hit-tests clicks at the NSEvent layer and switches first responder; Python drains focus signals on the Tk main thread so keystrokes reach the correct target. Use web.focus() and web.focus_parent() when you need explicit control — see examples/url_demo.py. IME and other advanced input may still behave differently than in a standalone browser.

You do not need extra code for tabs or panes — see examples/multi_demo.py. IPC, page-load, title, eval, and drag-and-drop handlers are dispatched on the Tk main thread via an internal queue (avoids WebKit deadlocks). on_navigation and on_new_window are synchronous WebKit-thread hooks — see Navigation / lifecycle callbacks.


💡 Why child-window embedding?

Tkinter apps already have a window and a layout. The web belongs inside a Frame — same mainloop, same tabs and panes — not in a separate top-level webview that floats beside your UI. tkwry wraps wry's build_as_child against the native surface Tk gives your widgets.


🧩 Features

  • Child-window embedding — WebView is a native child of your Tk window surface, not a floating overlay
  • Bounds & visibility sync — follows <Configure>, <Map>, and <Unmap> (tabs / Notebook work out of the box on macOS)
  • Deferred callbacks — IPC, page load, title, eval results, and DnD queue to Tk (avoids macOS deadlocks)
  • URL safety — normalizes and validates URLs before navigation
  • DevToolsopen_devtools() / devtools=True for debugging
  • Native drag & drop — OS-level file drops into the WebView (no tkinterdnd2)
  • Navigation hookson_page_load, on_title_changed (Tk thread); on_navigation, on_new_window (WebKit thread, synchronous)
  • Multiple layouts — works with pack, grid, place, Notebook, and PanedWindow (see examples)
  • Plotly-ready — load HTML + eval_js for interactive charts
  • Folium-ready — embed Leaflet maps from Folium HTML (right-click to pin)
  • Markdown-ready — Monaco editor + live preview in a PanedWindow (see examples/markdown_demo.py; CDN required)
  • Alpha, but tested — CI runs pytest tests/ on Windows (x86_64 + arm64), macOS, and Linux (Xvfb + WebKitGTK); many timing-sensitive integration tests are skipped on Linux CI (best-effort, not a release blocker)

📁 Examples

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e .
Script Description
examples/url_demo.py URL bar + embedded page
examples/ipc_demo.py JavaScript ↔ Tkinter IPC
examples/multi_demo.py Multiple WebViews, tabs, panes
examples/plotly_demo.py Plotly charts (pip install plotly)
examples/folium_demo.py Folium maps (pip install folium)
examples/markdown_demo.py Monaco markdown editor + live preview (CDN)
examples/dnd_demo.py Native file drag & drop into WebView
python examples/url_demo.py
python examples/ipc_demo.py
python examples/multi_demo.py
python examples/plotly_demo.py
python examples/folium_demo.py
python examples/markdown_demo.py
python examples/dnd_demo.py

📝 License

This project is licensed under the MIT License. See LICENSE.

This project links against wry, which is dual-licensed (Apache-2.0 or MIT). tkwry uses wry under MIT; see NOTICE for attribution.


👨‍💻 Author

mashu3

Contributors

About

Real WebView inside Tkinter Frames — not a floating window. Powered by wry.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors