BlinkGTK Event Handling Tutorial (Python/PyGObject)

Last Updated: 2026-01-01
Target Version: BlinkGTK v0.9.29-dev or later
Level: Beginner to Intermediate
Language: English |

日本語


Table of Contents

  1. Introduction
  2. Signal Basics
  3. Four Signals
  4. Pythonic Patterns
  5. async/await Integration
  6. Complete Browser Implementation

Introduction

This tutorial teaches you how to perform event-driven programming in Python with BlinkGTK.

What You'll Learn


Signal Basics

Connecting Signals

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')}")

Concise Syntax with Lambda

# 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"))

Four Signals

1. load-changed

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())

2. load-failed

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()

3. title-changed

def on_title_changed(self, web_view, title):
    """Title changed"""
    self.set_title(f"{title} - Python Browser")
    print(f"[title-changed] {title}")

4. uri-changed

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")

Pythonic Patterns

1. Decorator for Signal Handlers

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}")

2. Signal Manager Class

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()

async/await Integration

1. Asynchronous Loading

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 seconds

2. Parallel Loading

import 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()

3. Loading with Timeout

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

Complete Browser Implementation

#!/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()

Summary

In this tutorial, you learned how to handle events in BlinkGTK with Python.

What You Learned

Next Steps


Author: BlinkGTK Development Team
License: BSD 3-Clause
Feedback: daisy19@gmail.com