BlinkGTK イベント処理チュートリアル (Python/PyGObject)

最終更新: 2026-07-29
対象バージョン: BlinkGTK v1.1.0 以降
レベル: 初級〜中級
言語: 日本語 |

English


目次

  1. はじめに
  2. シグナルの基本
  3. 4つのシグナル
  4. Pythonicなパターン
  5. async/await統合
  6. 完全なブラウザ実装

はじめに

このチュートリアルでは、Pythonでイベント駆動プログラミングを行う方法を学びます。

学習内容


シグナルの基本

シグナル接続

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)

        # シグナル接続
        self.web_view.connect('load-changed', self.on_load_changed)

    def on_load_changed(self, web_view, load_event):
        """ロード状態変化"""
        events = {
            blink_gtk.LoadEvent.STARTED: "開始",
            blink_gtk.LoadEvent.REDIRECTED: "リダイレクト",
            blink_gtk.LoadEvent.COMMITTED: "コミット",
            blink_gtk.LoadEvent.FINISHED: "完了",
        }
        print(f"[load-changed] {events.get(load_event, '不明')}")

lambdaを使用した簡潔な記法

# 簡単なログ出力
web_view.connect('load-changed',
                lambda w, e: print(f"Load event: {e}"))

# タイトル更新
web_view.connect('title-changed',
                lambda w, title: self.set_title(f"{title} - Browser"))

4つのシグナル

1. load-changed

def on_load_changed(self, web_view, load_event):
    """ロード状態変化"""
    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("読み込み中...")

    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("完了")

        # ナビゲーションボタン更新
        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):
    """ロード失敗"""
    error_messages = {
        -2: "読み込み失敗",
        -7: "タイムアウト",
        -105: "サーバーが見つかりません",
        -106: "インターネット未接続",
    }

    error_msg = error_messages.get(error_code, f"不明なエラー (コード: {error_code})")

    # エラーダイアログ
    dialog = Gtk.MessageDialog(
        transient_for=self,
        modal=True,
        message_type=Gtk.MessageType.ERROR,
        buttons=Gtk.ButtonsType.OK,
        text="読み込みエラー"
    )
    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):
    """タイトル変更"""
    self.set_title(f"{title} - Python Browser")
    print(f"[title-changed] {title}")

4. uri-changed

def on_uri_changed(self, web_view, uri):
    """URI変更"""
    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("このページは安全です")
    else:
        self.security_icon.set_from_icon_name("dialog-warning")
        self.security_icon.set_tooltip_text("このページは保護されていません")

Pythonicなパターン

1. デコレータを使用したシグナルハンドラ

from functools import wraps
from typing import Callable


def signal_handler(signal_name: str) -> Callable:
    """シグナルハンドラデコレータ"""
    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)

        # デコレータ付きハンドラを接続
        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("読み込み完了")

    @signal_handler('load-failed')
    def on_load_failed(self, web_view, failing_url, error_code):
        print(f"エラー: {error_code}")

2. シグナルマネージャークラス

from typing import Dict, List, Callable


class SignalManager:
    """シグナルを管理するクラス"""

    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:
        """シグナルを接続"""
        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:
        """特定のシグナルのハンドラをすべて切断"""
        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:
        """すべてのシグナルハンドラを切断"""
        for signal_name in list(self.handlers.keys()):
            self.disconnect(signal_name)


# 使用例
signal_manager = SignalManager(web_view)
signal_manager.connect('load-changed', on_load_changed)
signal_manager.connect('title-changed', on_title_changed)

# 後でクリーンアップ
signal_manager.disconnect_all()

async/await統合

1. 非同期ロード

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)

        # イベント
        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:
            # イベントをセット
            self.load_finished_event.set()

    async def load_url_async(self, url: str) -> None:
        """URLを非同期で読み込む"""
        self.load_finished_event.clear()
        self.web_view.load_uri(url)
        await self.load_finished_event.wait()
        print(f"読み込み完了: {url}")


# 使用例
async def main():
    window = AsyncBrowserWindow()

    # 複数URLを順次読み込み
    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)  # 2秒待機

2. 並行ロード

import asyncio
from typing import List


class MultiTabBrowser:
    """複数タブを持つブラウザ"""

    def __init__(self):
        self.tabs: List[blink_gtk.WebView] = []

    async def load_multiple_urls(self, urls: List[str]) -> None:
        """複数URLを並行して読み込む"""
        tasks = []

        for url in urls:
            web_view = blink_gtk.WebView.new()
            self.tabs.append(web_view)

            # 非同期タスクを作成
            task = asyncio.create_task(self._load_url_async(web_view, url))
            tasks.append(task)

        # すべて完了まで待機
        await asyncio.gather(*tasks)
        print(f"{len(urls)}個のURLの読み込み完了")

    async def _load_url_async(self, web_view: blink_gtk.WebView, url: str) -> None:
        """単一URLを非同期で読み込む"""
        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. タイムアウト付きロード

import asyncio


async def load_with_timeout(
    web_view: blink_gtk.WebView,
    url: str,
    timeout: float = 30.0
) -> bool:
    """タイムアウト付きでURLを読み込む"""
    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"タイムアウト: {url}")
        web_view.stop()
        return False

完全なブラウザ実装

#!/usr/bin/env python3
"""完全な機能を持つブラウザ"""

import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk
import blink_gtk


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):
        """UIを構築"""
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # ツールバー
        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="画像:"))
        settings_box.append(self.images_switch)
        settings_box.append(Gtk.Label(label="ストレージ:"))
        settings_box.append(self.storage_switch)

        # プログレスバー
        self.progress_bar = Gtk.ProgressBar()
        self.progress_bar.set_visible(False)

        # WebView
        self.web_view = blink_gtk.WebView.new()

        # ステータスバー
        self.status_label = Gtk.Label(label="準備完了")
        self.status_label.set_halign(Gtk.Align.START)
        self.status_label.set_margin_start(5)
        self.status_label.set_margin_bottom(2)

        # レイアウト
        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):
        """シグナルを接続"""
        # WebViewシグナル
        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
        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):
        """ロード状態変化"""
        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("読み込み中...")

        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("完了")

            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):
        """ロード失敗"""
        error_messages = {
            -2: "読み込み失敗",
            -7: "タイムアウト",
            -105: "サーバーが見つかりません",
            -106: "インターネット未接続",
        }

        error_msg = error_messages.get(error_code, f"不明なエラー (コード: {error_code})")
        self.status_label.set_text(f"{error_msg} (コード: {error_code})")

        # エラーダイアログ
        dialog = Gtk.MessageDialog(
            transient_for=self,
            modal=True,
            message_type=Gtk.MessageType.ERROR,
            buttons=Gtk.ButtonsType.OK,
            text="読み込みエラー"
        )
        dialog.format_secondary_text(f"{error_msg}\n\nURL: {failing_url}\nエラーコード: {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):
        """タイトル変更"""
        self.set_title(f"{title} - Complete Browser")

    def on_uri_changed(self, web_view, uri):
        """URI変更"""
        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("このページは安全です")
        else:
            self.security_icon.set_from_icon_name("dialog-warning")
            self.security_icon.set_tooltip_text("このページは保護されていません")

    def on_url_activate(self, entry):
        """URLエントリでEnter"""
        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()

まとめ

このチュートリアルでは、PythonでBlinkGTKのイベント処理を行う方法を学びました。

学んだこと

次のステップ


作成者: BlinkGTK開発チーム
ライセンス: BSD 3-Clause
フィードバック: daisy19@gmail.com