Last Updated: 2026-01-01
Target Version: BlinkGTK v0.9.29-dev or later
Level: Beginner to Intermediate
Language: English |
This tutorial teaches you how to perform event-driven programming in Python with BlinkGTK.
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk
import blink_gtk
class BrowserWindow(Gtk.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_title("Signal Example")
self.set_default_size(800, 600)
self.web_view = blink_gtk.WebView.new()
self.set_child(self.web_view)
# Connect signal
self.web_view.connect('load-changed', self.on_load_changed)
def on_load_changed(self, web_view, load_event):
"""Load state changed"""
events = {
blink_gtk.LoadEvent.STARTED: "Started",
blink_gtk.LoadEvent.REDIRECTED: "Redirected",
blink_gtk.LoadEvent.COMMITTED: "Committed",
blink_gtk.LoadEvent.FINISHED: "Finished",
}
print(f"[load-changed] {events.get(load_event, 'Unknown')}")# Simple logging
web_view.connect('load-changed',
lambda w, e: print(f"Load event: {e}"))
# Title update
web_view.connect('title-changed',
lambda w, title: self.set_title(f"{title} - Browser"))def on_load_changed(self, web_view, load_event):
"""Load state changed"""
if load_event == blink_gtk.LoadEvent.STARTED:
self.progress_bar.set_fraction(0.0)
self.progress_bar.set_visible(True)
self.status_label.set_text("Loading...")
elif load_event == blink_gtk.LoadEvent.COMMITTED:
self.progress_bar.set_fraction(0.5)
elif load_event == blink_gtk.LoadEvent.FINISHED:
self.progress_bar.set_fraction(1.0)
self.progress_bar.set_visible(False)
self.status_label.set_text("Done")
# Update navigation buttons
self.back_button.set_sensitive(web_view.can_go_back())
self.forward_button.set_sensitive(web_view.can_go_forward())def on_load_failed(self, web_view, failing_url, error_code):
"""Load failed"""
error_messages = {
-2: "Failed to load",
-7: "Timeout",
-105: "Server not found",
-106: "No internet connection",
}
error_msg = error_messages.get(error_code, f"Unknown error (code: {error_code})")
# Error dialog
dialog = Gtk.MessageDialog(
transient_for=self,
modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text="Loading Error"
)
dialog.format_secondary_text(f"{error_msg}\n\nURL: {failing_url}")
dialog.connect('response', lambda d, r: d.destroy())
dialog.show()def on_title_changed(self, web_view, title):
"""Title changed"""
self.set_title(f"{title} - Python Browser")
print(f"[title-changed] {title}")def on_uri_changed(self, web_view, uri):
"""URI changed"""
self.url_entry.set_text(uri)
# Update security icon
if uri.startswith("https://"):
self.security_icon.set_from_icon_name("dialog-password")
self.security_icon.set_tooltip_text("This page is secure")
else:
self.security_icon.set_from_icon_name("dialog-warning")
self.security_icon.set_tooltip_text("This page is not secure")from functools import wraps
from typing import Callable
def signal_handler(signal_name: str) -> Callable:
"""Signal handler decorator"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error in {signal_name} handler: {e}")
return wrapper
return decorator
class BrowserWindow(Gtk.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.web_view = blink_gtk.WebView.new()
self.set_child(self.web_view)
# Connect decorated handlers
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 == blink_gtk.LoadEvent.FINISHED:
print("Loading complete")
@signal_handler('load-failed')
def on_load_failed(self, web_view, failing_url, error_code):
print(f"Error: {error_code}")from typing import Dict, List, Callable
class SignalManager:
"""Class to manage signals"""
def __init__(self, web_view: blink_gtk.WebView):
self.web_view = web_view
self.handlers: Dict[str, List[int]] = {}
def connect(self, signal_name: str, handler: Callable) -> int:
"""Connect a signal"""
handler_id = self.web_view.connect(signal_name, handler)
if signal_name not in self.handlers:
self.handlers[signal_name] = []
self.handlers[signal_name].append(handler_id)
return handler_id
def disconnect(self, signal_name: str) -> None:
"""Disconnect all handlers for a specific signal"""
if signal_name in self.handlers:
for handler_id in self.handlers[signal_name]:
self.web_view.disconnect(handler_id)
del self.handlers[signal_name]
def disconnect_all(self) -> None:
"""Disconnect all signal handlers"""
for signal_name in list(self.handlers.keys()):
self.disconnect(signal_name)
# Usage example
signal_manager = SignalManager(web_view)
signal_manager.connect('load-changed', on_load_changed)
signal_manager.connect('title-changed', on_title_changed)
# Later cleanup
signal_manager.disconnect_all()import asyncio
from gi.repository import GLib
class AsyncBrowserWindow(Gtk.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.web_view = blink_gtk.WebView.new()
self.set_child(self.web_view)
# Event
self.load_finished_event = asyncio.Event()
self.web_view.connect('load-changed', self.on_load_changed)
def on_load_changed(self, web_view, load_event):
if load_event == blink_gtk.LoadEvent.FINISHED:
# Set event
self.load_finished_event.set()
async def load_url_async(self, url: str) -> None:
"""Load URL asynchronously"""
self.load_finished_event.clear()
self.web_view.load_uri(url)
await self.load_finished_event.wait()
print(f"Loading complete: {url}")
# Usage example
async def main():
window = AsyncBrowserWindow()
# Load multiple URLs sequentially
urls = [
"https://example.com",
"https://www.chromium.org",
"https://www.python.org",
]
for url in urls:
await window.load_url_async(url)
await asyncio.sleep(2) # Wait 2 secondsimport asyncio
from typing import List
class MultiTabBrowser:
"""Browser with multiple tabs"""
def __init__(self):
self.tabs: List[blink_gtk.WebView] = []
async def load_multiple_urls(self, urls: List[str]) -> None:
"""Load multiple URLs in parallel"""
tasks = []
for url in urls:
web_view = blink_gtk.WebView.new()
self.tabs.append(web_view)
# Create async task
task = asyncio.create_task(self._load_url_async(web_view, url))
tasks.append(task)
# Wait for all to complete
await asyncio.gather(*tasks)
print(f"Loaded {len(urls)} URLs")
async def _load_url_async(self, web_view: blink_gtk.WebView, url: str) -> None:
"""Load single URL asynchronously"""
event = asyncio.Event()
def on_load_changed(w, load_event):
if load_event == blink_gtk.LoadEvent.FINISHED:
event.set()
web_view.connect('load-changed', on_load_changed)
web_view.load_uri(url)
await event.wait()import asyncio
async def load_with_timeout(
web_view: blink_gtk.WebView,
url: str,
timeout: float = 30.0
) -> bool:
"""Load URL with timeout"""
event = asyncio.Event()
def on_load_changed(w, load_event):
if load_event == blink_gtk.LoadEvent.FINISHED:
event.set()
def on_load_failed(w, failing_url, error_code):
event.set()
web_view.connect('load-changed', on_load_changed)
web_view.connect('load-failed', on_load_failed)
web_view.load_uri(url)
try:
await asyncio.wait_for(event.wait(), timeout=timeout)
return True
except asyncio.TimeoutError:
print(f"Timeout: {url}")
web_view.stop()
return False#!/usr/bin/env python3
"""Complete browser with all features"""
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk
import blink_gtk
class CompleteBrowser(Gtk.ApplicationWindow):
"""Complete browser with all features"""
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):
"""Build UI"""
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# Toolbar
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
toolbar.set_margin_start(5)
toolbar.set_margin_end(5)
toolbar.set_margin_top(5)
toolbar.set_margin_bottom(5)
self.back_button = Gtk.Button.new_from_icon_name("go-previous")
self.forward_button = Gtk.Button.new_from_icon_name("go-next")
self.reload_button = Gtk.Button.new_from_icon_name("view-refresh")
self.stop_button = Gtk.Button.new_from_icon_name("process-stop")
self.security_icon = Gtk.Image.new_from_icon_name("dialog-warning")
self.url_entry = Gtk.Entry()
self.stop_button.set_visible(False)
self.back_button.set_sensitive(False)
self.forward_button.set_sensitive(False)
self.url_entry.set_hexpand(True)
for widget in [self.back_button, self.forward_button,
self.reload_button, self.stop_button,
self.security_icon, self.url_entry]:
toolbar.append(widget)
# Settings
settings_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
settings_box.set_margin_start(5)
settings_box.set_margin_end(5)
settings_box.set_margin_bottom(5)
self.js_switch = Gtk.Switch()
self.images_switch = Gtk.Switch()
self.storage_switch = Gtk.Switch()
for switch in [self.js_switch, self.images_switch, self.storage_switch]:
switch.set_active(True)
settings_box.append(Gtk.Label(label="JavaScript:"))
settings_box.append(self.js_switch)
settings_box.append(Gtk.Label(label="Images:"))
settings_box.append(self.images_switch)
settings_box.append(Gtk.Label(label="Storage:"))
settings_box.append(self.storage_switch)
# Progress bar
self.progress_bar = Gtk.ProgressBar()
self.progress_bar.set_visible(False)
# WebView
self.web_view = blink_gtk.WebView.new()
# Status bar
self.status_label = Gtk.Label(label="Ready")
self.status_label.set_halign(Gtk.Align.START)
self.status_label.set_margin_start(5)
self.status_label.set_margin_bottom(2)
# Layout
vbox.append(toolbar)
vbox.append(settings_box)
vbox.append(self.progress_bar)
vbox.append(self.web_view)
vbox.append(self.status_label)
self.web_view.set_vexpand(True)
self.set_child(vbox)
def _connect_signals(self):
"""Connect signals"""
# WebView signals
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)
# Buttons
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
self.js_switch.connect('notify::active',
lambda s, _: self.web_view.set_enable_javascript(s.get_active()))
self.images_switch.connect('notify::active',
lambda s, _: self.web_view.set_auto_load_images(s.get_active()))
self.storage_switch.connect('notify::active',
lambda s, _: self.web_view.set_enable_local_storage(s.get_active()))
def on_load_changed(self, web_view, load_event):
"""Load state changed"""
if load_event == blink_gtk.LoadEvent.STARTED:
self.progress_bar.set_fraction(0.0)
self.progress_bar.set_visible(True)
self.stop_button.set_visible(True)
self.reload_button.set_visible(False)
self.status_label.set_text("Loading...")
elif load_event == blink_gtk.LoadEvent.COMMITTED:
self.progress_bar.set_fraction(0.5)
elif load_event == blink_gtk.LoadEvent.FINISHED:
self.progress_bar.set_fraction(1.0)
self.progress_bar.set_visible(False)
self.stop_button.set_visible(False)
self.reload_button.set_visible(True)
self.status_label.set_text("Done")
self.back_button.set_sensitive(web_view.can_go_back())
self.forward_button.set_sensitive(web_view.can_go_forward())
def on_load_failed(self, web_view, failing_url, error_code):
"""Load failed"""
error_messages = {
-2: "Failed to load",
-7: "Timeout",
-105: "Server not found",
-106: "No internet connection",
}
error_msg = error_messages.get(error_code, f"Unknown error (code: {error_code})")
self.status_label.set_text(f"{error_msg} (code: {error_code})")
# Error dialog
dialog = Gtk.MessageDialog(
transient_for=self,
modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text="Loading Error"
)
dialog.format_secondary_text(f"{error_msg}\n\nURL: {failing_url}\nError code: {error_code}")
dialog.connect('response', lambda d, r: d.destroy())
dialog.show()
self.progress_bar.set_visible(False)
self.stop_button.set_visible(False)
self.reload_button.set_visible(True)
def on_title_changed(self, web_view, title):
"""Title changed"""
self.set_title(f"{title} - Complete Browser")
def on_uri_changed(self, web_view, uri):
"""URI changed"""
self.url_entry.set_text(uri)
if uri.startswith("https://"):
self.security_icon.set_from_icon_name("dialog-password")
self.security_icon.set_tooltip_text("This page is secure")
else:
self.security_icon.set_from_icon_name("dialog-warning")
self.security_icon.set_tooltip_text("This page is not secure")
def on_url_activate(self, entry):
"""URL entry activated"""
url = entry.get_text()
self.web_view.load_uri(url)
def on_activate(app):
window = CompleteBrowser(application=app)
window.web_view.load_uri("https://www.chromium.org")
window.present()
if __name__ == '__main__':
blink_gtk.init()
app = Gtk.Application(application_id='org.example.complete')
app.connect('activate', on_activate)
app.run()
blink_gtk.shutdown()In this tutorial, you learned how to handle events in BlinkGTK with Python.
Author: BlinkGTK Development Team
License: BSD 3-Clause
Feedback: daisy19@gmail.com