Last Updated: 2026-10-21
Target Version: BlinkGTK 1.2.5 or later
Level: Beginner to Intermediate
Language: 日本語 | English
This tutorial shows how to handle events in Python with BlinkGTK
signals.
Every complete program here was run and checked with BlinkGTK 1.2.5
(Chromium 156).
blinkgtk-run python3 <file> (why)The code previously on this page did not work, because it used forms
that differ from the real API, such as import blink_gtk,
app.run() and a title-changed handler with an
argument.
Replace it with the code on this page.
The following program prints the four load-related signals to the
console.
Run it as
blinkgtk-run python3 signals.py https://example.com/ (why blinkgtk-run
is needed).
#!/usr/bin/env python3
"""Print the four load-related signals to the console"""
import sys
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("BlinkGTK", "0.1")
from gi.repository import BlinkGTK, Gtk # noqa: E402
EVENT_NAMES = {
BlinkGTK.LoadEvent.STARTED: "started",
BlinkGTK.LoadEvent.COMMITTED: "committed",
BlinkGTK.LoadEvent.FINISHED: "finished",
}
def on_load_changed(view, event):
print(f"[load-changed] {EVENT_NAMES.get(event, event)}: {view.get_uri()}", flush=True)
def on_load_failed(view, error_code, failing_uri):
print(f"[load-failed] error code {error_code}: {failing_uri}", flush=True)
def on_title_changed(view):
print(f"[title-changed] {view.get_title()}", flush=True)
def on_uri_changed(view):
print(f"[uri-changed] {view.get_uri()}", flush=True)
def on_activate(app, url):
window = Gtk.ApplicationWindow(application=app, title="Signal Example")
window.set_default_size(800, 600)
view = BlinkGTK.WebView.new()
view.connect("load-changed", on_load_changed)
view.connect("load-failed", on_load_failed)
view.connect("title-changed", on_title_changed)
view.connect("uri-changed", on_uri_changed)
window.set_child(view)
window.present()
view.load_uri(url)
def main():
url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/"
ok, _argv = BlinkGTK.gtk_init(sys.argv[:1])
if not ok:
print("Could not initialize BlinkGTK", file=sys.stderr)
return 1
app = Gtk.Application(application_id="org.example.SignalExample")
app.connect("activate", on_activate, url)
rc = BlinkGTK.gtk_application_run(app)
BlinkGTK.gtk_shutdown()
return rc
if __name__ == "__main__":
sys.exit(main())Opening https://example.com/ produces the signals in
this order:
[uri-changed] https://example.com/
[load-changed] started: https://example.com/
[load-changed] committed: https://example.com/
[title-changed] Example Domain
[load-changed] finished: https://example.com/
For some pages, uri-changed arrives once more when the
document is committed (for example, when opening a local file).
Do not write code that depends on how many times
uri-changed arrives.
Every program uses BlinkGTK in this order:
BlinkGTK.gtk_init(sys.argv[:1]) — once, before creating
any widgetBlinkGTK.gtk_application_run(app) — do not use
app.run(). Returns when the last window is closedBlinkGTK.gtk_shutdown() — call it at the end# Simple logging (value_nick is a name such as "started")
web_view.connect("load-changed",
lambda w, e: print(f"Load event: {e.value_nick}"))
# Updating the title (title-changed has no argument, so read get_title())
web_view.connect("title-changed",
lambda w: window.set_title(f"{w.get_title()} - Browser"))The handler receives (web_view, load_event).
load_event is a BlinkGTK.LoadEvent value.
def on_load_changed(self, web_view, load_event):
"""The load state changed (used for status text)"""
if load_event == BlinkGTK.LoadEvent.COMMITTED:
self.status_label.set_text("Document committed")
elif load_event == BlinkGTK.LoadEvent.FINISHED:
self.status_label.set_text("Done")STARTED arrives only when load_uri() /
load_html() is called. It does not arrive for navigation
through links, back, forward or reloadSTARTED / FINISHEDBlinkGTK.LoadEvent.REDIRECTED is reserved for future
use and is not emitted at presentThe handler receives
(web_view, error_code, failing_uri).
def on_load_failed(self, web_view, error_code, failing_uri):
"""A load ended without committing"""
# Ignore failures that are not for the page on screen (such as iframes) before touching the UI
if failing_uri != (web_view.get_uri() or failing_uri):
return
if error_code == -3:
# Aborted. Happens normally with the stop button or when another page is opened during a load
self.status_label.set_text("Aborted")
return
self.status_label.set_text(f"Could not load (error code {error_code}): {failing_uri}")error_code is a Chromium network error code (a negative
value). It is not a BlinkGTK.LoadError value-3)COMMITTED and
FINISHED instead of load-failed.
FINISHED also arrives when an error page is shownfailing_uri with get_uri()
and skip those first (an abort, -3, can also come from an
iframe)The handler receives only (web_view). Read the new title
with get_title().
def on_title_changed(self, web_view):
"""The title changed"""
title = web_view.get_title()
self.set_title(f"{title} - Python Browser" if title else "Python Browser")The handler receives only (web_view). Read the new URI
with get_uri().
def on_uri_changed(self, web_view):
"""The URI changed"""
uri = web_view.get_uri() or ""
self.url_entry.set_text(uri)
# Show the connection type
if uri.startswith("https://"):
self.security_label.set_text("HTTPS")
elif uri.startswith("http://"):
self.security_label.set_text("HTTP")
else:
self.security_label.set_text("")Changes to the is-loading property arrive through
notify::is-loading. The handler receives
(web_view, pspec); read the value with
web_view.is_loading().
It arrives when a load starts and when it ends, for every kind of
navigation, including links, back, forward and reload.
However a load ends (finished, failed or aborted), the value returns to
False.
def on_is_loading_changed(self, web_view, _pspec):
"""Switch the loading indicator and the stop and reload buttons"""
loading = web_view.is_loading()
self.progress_bar.set_visible(loading)
if loading:
self.progress_bar.pulse()
self.stop_button.set_sensitive(loading)
self.reload_button.set_sensitive(not loading)
self.back_button.set_sensitive(web_view.can_go_back())
self.forward_button.set_sensitive(web_view.can_go_forward())
web_view.connect("notify::is-loading", self.on_is_loading_changed)It may arrive twice in a row at the start, so decide by the value, not by the number of notifications.
title and uri are also properties, so you
can receive changes through notify::title and
notify::uri.
web_view.connect("notify::title", lambda w, pspec: print(f"title = {w.props.title}"))When an exception is raised in a GObject signal handler, the program
does not stop; the exception is only printed.
To make it clear which signal the exception came from, wrap the handler
with a decorator.
import sys
from functools import wraps
from typing import Callable
def signal_handler(signal_name: str) -> Callable:
"""Print exceptions raised in the handler, with the signal name"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Exception in {signal_name} handler: {e!r}", file=sys.stderr)
return wrapper
return decorator
class BrowserWindow(Gtk.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.web_view = BlinkGTK.WebView.new()
self.set_child(self.web_view)
self.web_view.connect("load-changed", self.on_load_changed)
self.web_view.connect("load-failed", self.on_load_failed)
@signal_handler("load-changed")
def on_load_changed(self, web_view, load_event):
if load_event == BlinkGTK.LoadEvent.FINISHED:
print("Load finished")
@signal_handler("load-failed")
def on_load_failed(self, web_view, error_code, failing_uri):
print(f"Ended without committing: {failing_uri} (error code {error_code})")Keeping the IDs returned by connect() lets you remove
the handlers later with disconnect().
from typing import Callable, Dict, List
class SignalManager:
"""Remember the handlers connected to a WebView, per signal name"""
def __init__(self, web_view: BlinkGTK.WebView):
self.web_view = web_view
self.handlers: Dict[str, List[int]] = {}
def connect(self, signal_name: str, handler: Callable) -> int:
handler_id = self.web_view.connect(signal_name, handler)
self.handlers.setdefault(signal_name, []).append(handler_id)
return handler_id
def disconnect(self, signal_name: str) -> None:
for handler_id in self.handlers.pop(signal_name, []):
self.web_view.disconnect(handler_id)
def disconnect_all(self) -> None:
for signal_name in list(self.handlers):
self.disconnect(signal_name)
# Usage
signal_manager = SignalManager(web_view)
signal_manager.connect("load-changed", on_load_changed)
signal_manager.connect("title-changed", on_title_changed)
# Remove them later
signal_manager.disconnect_all()BlinkGTK.gtk_application_run() runs the GLib main
loop.
An asyncio event loop started with asyncio.run() or similar
does not run during that time, so waiting on an
asyncio.Event for a load to finish does not work as is.
To do the next step after a load finishes, start it when
notify::is-loading reports that is-loading has
returned to False.
If the load fails, load-failed arrives before the value
returns to False, so remember what it reported.
The following program loads the URLs given as arguments one after
another and stops any load that does not finish within 30 seconds.
When all are done it closes the window and the program exits.
#!/usr/bin/env python3
"""Load several URLs one after another, then close the window"""
import sys
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("BlinkGTK", "0.1")
from gi.repository import BlinkGTK, GLib, Gtk # noqa: E402
TIMEOUT_SECONDS = 30
class SequentialLoader:
def __init__(self, window, view, urls):
self.window = window
self.view = view
self.urls = list(urls)
self.current = None
self.timeout_id = 0
self.started = False
self.error_code = None
self.timed_out = False
# Detect the end of a load with notify::is-loading. However the load ends (finished, failed
# or aborted), is-loading returns to False. A failure sends load-failed before that
view.connect("notify::is-loading", self.on_is_loading_changed)
view.connect("load-failed", self.on_load_failed)
def load_next(self):
if not self.urls:
print("All URLs processed", flush=True)
self.window.close() # closing the last window makes gtk_application_run() return
return GLib.SOURCE_REMOVE
self.current = self.urls.pop(0)
self.started = False
self.error_code = None
self.timed_out = False
self.timeout_id = GLib.timeout_add_seconds(TIMEOUT_SECONDS, self.on_timeout)
self.view.load_uri(self.current)
return GLib.SOURCE_REMOVE
def done(self, message):
if self.timeout_id:
GLib.source_remove(self.timeout_id)
self.timeout_id = 0
print(f"{message}: {self.current}", flush=True)
# Do not call the next load_uri() inside the signal handler; call it from the main loop
GLib.idle_add(self.load_next)
def on_is_loading_changed(self, view, _pspec):
if view.is_loading():
self.started = True
elif self.started:
self.started = False
if self.timed_out:
self.done("Stopped after the time limit")
elif self.error_code is not None:
self.done(f"Ended without committing (error code {self.error_code})")
else:
self.done(f"Finished ({view.get_title() or 'no title'})")
def on_load_failed(self, view, error_code, failing_uri):
# Ignore failures from iframes and the like. Chromium reports a normalized URL (for example
# with a trailing /), so compare with the URI being shown, not only the string you passed
if failing_uri in (view.get_uri(), self.current) or \
failing_uri.rstrip("/") == self.current.rstrip("/"):
self.error_code = error_code
def on_timeout(self):
self.timeout_id = 0
self.timed_out = True
print(f"Not finished after {TIMEOUT_SECONDS} s, stopping: {self.current}", flush=True)
self.view.stop() # move on once is-loading returns to False
return GLib.SOURCE_REMOVE
def on_activate(app, urls):
window = Gtk.ApplicationWindow(application=app, title="Sequential Loader")
window.set_default_size(800, 600)
view = BlinkGTK.WebView.new()
window.set_child(view)
window.present()
loader = SequentialLoader(window, view, urls)
window.loader = loader # keep the loader alive as long as the window
loader.load_next()
def main():
urls = sys.argv[1:] or ["https://example.com/", "https://www.gtk.org/"]
ok, _argv = BlinkGTK.gtk_init(sys.argv[:1])
if not ok:
print("Could not initialize BlinkGTK", file=sys.stderr)
return 1
app = Gtk.Application(application_id="org.example.SequentialLoader")
app.connect("activate", on_activate, urls)
rc = BlinkGTK.gtk_application_run(app)
BlinkGTK.gtk_shutdown()
return rc
if __name__ == "__main__":
sys.exit(main())Example:
blinkgtk-run python3 sequential_loader.py https://example.com/ https://www.gtk.org/FINISHED is not used to detect the end because it does
not arrive for loads that end without committing (such as a stopped
load). If you stop() a load before it commits,
load-failed (error code -3) arrives and then
is-loading returns to False. If you stop it
after it commits, load-failed may not arrive, so the
time-out is recorded by the program itself and reported separatelyload_uri() is called after returning to the
main loop with GLib.idle_add(), not inside the signal
handlerA browser that uses the four signals and switches for JavaScript, images and local storage.
#!/usr/bin/env python3
"""A browser that uses the four signals and setting switches"""
import sys
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("BlinkGTK", "0.1")
from gi.repository import BlinkGTK, Gtk # noqa: E402
class CompleteBrowser(Gtk.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_title("Complete Browser (Python)")
self.set_default_size(1200, 800)
self._build_ui()
self._connect_signals()
def _build_ui(self):
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# Toolbar
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
for side in ("start", "end", "top", "bottom"):
getattr(toolbar, f"set_margin_{side}")(6)
self.back_button = Gtk.Button(label="←", sensitive=False)
self.forward_button = Gtk.Button(label="→", sensitive=False)
self.reload_button = Gtk.Button(label="↻")
self.stop_button = Gtk.Button(label="×", sensitive=False)
self.security_label = Gtk.Label(label="")
self.url_entry = Gtk.Entry(hexpand=True)
for widget in (self.back_button, self.forward_button, self.reload_button,
self.stop_button, self.security_label, self.url_entry):
toolbar.append(widget)
# Setting switches
settings_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
settings_box.set_margin_start(6)
settings_box.set_margin_bottom(6)
self.js_switch = Gtk.Switch(active=True)
self.images_switch = Gtk.Switch(active=True)
self.storage_switch = Gtk.Switch(active=True)
for label, switch in (("JavaScript:", self.js_switch),
("Images:", self.images_switch),
("Local storage:", self.storage_switch)):
settings_box.append(Gtk.Label(label=label))
settings_box.append(switch)
self.progress_bar = Gtk.ProgressBar(visible=False)
self.web_view = BlinkGTK.WebView.new()
self.web_view.set_vexpand(True)
self.status_label = Gtk.Label(label="Ready", xalign=0.0)
self.status_label.set_margin_start(6)
self.status_label.set_margin_bottom(3)
for widget in (toolbar, settings_box, self.progress_bar,
self.web_view, self.status_label):
vbox.append(widget)
self.set_child(vbox)
def _connect_signals(self):
self.web_view.connect("notify::is-loading", self.on_is_loading_changed)
self.web_view.connect("load-changed", self.on_load_changed)
self.web_view.connect("load-failed", self.on_load_failed)
self.web_view.connect("title-changed", self.on_title_changed)
self.web_view.connect("uri-changed", self.on_uri_changed)
self.back_button.connect("clicked", lambda _b: self.web_view.go_back())
self.forward_button.connect("clicked", lambda _b: self.web_view.go_forward())
self.reload_button.connect("clicked", lambda _b: self.web_view.reload())
self.stop_button.connect("clicked", lambda _b: self.web_view.stop())
self.url_entry.connect("activate", self.on_url_activate)
# Settings are used from the next load, so reload after a change
self.js_switch.connect("notify::active", self.on_setting_toggled,
self.web_view.set_javascript_enabled)
self.images_switch.connect("notify::active", self.on_setting_toggled,
self.web_view.set_images_enabled)
self.storage_switch.connect("notify::active", self.on_setting_toggled,
self.web_view.set_local_storage_enabled)
def on_setting_toggled(self, switch, _pspec, setter):
setter(switch.get_active())
if self.web_view.get_uri():
self.web_view.reload()
def on_is_loading_changed(self, web_view, _pspec):
"""Loading indicator and buttons (arrives for every navigation, including links, back and reload)"""
loading = web_view.is_loading()
self.progress_bar.set_visible(loading)
if loading:
self.progress_bar.pulse()
self.status_label.set_text("Loading")
self.stop_button.set_sensitive(loading)
self.reload_button.set_sensitive(not loading)
self.back_button.set_sensitive(web_view.can_go_back())
self.forward_button.set_sensitive(web_view.can_go_forward())
def on_load_changed(self, web_view, load_event):
"""Used only for the status text"""
if load_event == BlinkGTK.LoadEvent.FINISHED:
self.status_label.set_text("Done")
def on_load_failed(self, web_view, error_code, failing_uri):
# Ignore failures that are not for the page on screen (such as iframes) before touching the UI
if failing_uri != (web_view.get_uri() or failing_uri):
return
if error_code == -3:
self.status_label.set_text("Aborted")
else:
self.status_label.set_text(f"Could not load (error code {error_code}): {failing_uri}")
def on_title_changed(self, web_view):
title = web_view.get_title()
self.set_title(f"{title} - Complete Browser" if title else "Complete Browser")
def on_uri_changed(self, web_view):
uri = web_view.get_uri() or ""
self.url_entry.set_text(uri)
if uri.startswith("https://"):
self.security_label.set_text("HTTPS")
elif uri.startswith("http://"):
self.security_label.set_text("HTTP")
else:
self.security_label.set_text("")
def on_url_activate(self, entry):
url = entry.get_text().strip()
if url and "://" not in url and not url.startswith(("about:", "data:")):
url = "https://" + url
if url:
self.web_view.load_uri(url)
def on_activate(app, url):
window = CompleteBrowser(application=app)
window.present()
window.web_view.load_uri(url)
def main():
url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/"
ok, _argv = BlinkGTK.gtk_init(sys.argv[:1])
if not ok:
print("Could not initialize BlinkGTK", file=sys.stderr)
return 1
app = Gtk.Application(application_id="org.example.CompleteBrowser")
app.connect("activate", on_activate, url)
rc = BlinkGTK.gtk_application_run(app)
BlinkGTK.gtk_shutdown()
return rc
if __name__ == "__main__":
sys.exit(main())notify::is-loading; the status text from
load-changed and load-failedreload() after a switch
is toggledThis tutorial showed how to handle BlinkGTK events in Python.
notify::is-loadingAuthor: BlinkGTK Development Team
License: BSD 3-Clause
Feedback: daisy19@gmail.com