Paepoi

Paepoi » GTK4(Python) Tips » GTK4(Python) Tips | GtkApplication

GTK4(Python) Tips | GtkApplication

# 最終更新日 2023.05.14

GtkApplication
GTK4 になりメインループを開始する gtk_main 関数が廃止されました。
GMainLoop でも一応動くようですがどこにも解説されておらず非推奨のようです。
GUI アプリを作る場合は必ず GtkApplication を使う必要があります。

GtkApplication はアプリケーションのウインドウ等を一括で管理します。
UIKit や AppKit または WPF 等を知っているならお馴染の方法。
管理するオブジェクトが無くなった時点で終了する参照カウンタ方式を採用しています。
基本的に GLib::GApplication が元ですから GTK3 の時とほとんど変わっていません。

PyGObject にて GTK4 でウインドウを作る最小限のコードを下記に。
actibate シグナルを利用して present でウインドウを表示させます。
#!/usr/bin/env python3
 
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk

def activate_cb(app):
    Gtk.ApplicationWindow(application=app, title='Hello').present()

app = Gtk.Application()
app.connect('activate', activate_cb)
app.run() # 引数の sys.argv は使わないなら省略可能

application-id
GtkApplication は指定された ID の Application を一つしか作成しません。
二つ目のアプリケーションを起動しようとすると以前作成した Application に引数が転送されます。
そして転送先 Application 側で activate や open シグナルが発行されます。
これにより複数ウインドウの管理や多重起動の制御を GtkApplication に任せられるようになっています。
startup シグナルは初回にしか発行されませんので注意。

たとえば下記のようにすれば多重起動しないアプリケーションに。
startup シグナルが一番最初に発行されます。
#!/usr/bin/env python3

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

class App(Gtk.Application):
    '''
        絶対に多重起動できないアプリケーション
        もう一つ起動しようとすると初回起動分に転送される
    '''
    def __init__(self):
        '''
            ココで指定した ID の Application は一つしか作れない
            他人が絶対に使っていないと思う ID にしよう
        '''
        Gtk.Application.__init__(
            self,
            application_id='org.sasakima.myid',
            flags=Gio.ApplicationFlags.FLAGS_NONE )

    def do_startup(self):
        '''
            初回起動のみ、ウインドウはココで作っておく
            Gtk.Application.do_startup(self) 呼び出しが必須
        '''
        Gtk.Application.do_startup(self)
        Gtk.ApplicationWindow(application=self, title='')

    def do_activate(self):
        '''
            アクティブなウインドウはプロパティから得られる
            呼び出される毎にタイトルバーの文字列が増える
        '''
        self.props.active_window.props.title += '1'
        self.props.active_window.present()

App().run()

open
open はファイル引数を処理するためのシグナルです。
G_APPLICATION_HANDLES_OPEN を GApplication 作成時の flags に指定すると使えます。
ハンドラの引数に GFile のリストで渡ってくるのでそれを処理します。
GFile なので HDD 内だけでなく gvfs を使った URI 経由のファイルも扱うことができます。

sys.argv を使えばいいじゃん、と思うかもしれませんが。
二つ目からの転送をされても転送先の sys.argv は最初のままなのでこういう処理が必要になります。
#!/usr/bin/env python3
 
import gi, sys
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gio

class Win(Gtk.ApplicationWindow):
    '''
        HANDLES_OPEN の実験用
        何か引数にファイルを指定して追加起動すると URI が追記される
        引数無しで追加起動するとクリアされる
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.uri_label = Gtk.Label()
        self.set_child(self.uri_label)

class App(Gtk.Application):
    def __init__(self):
        '''
            HANDLES_OPEN の場合 open シグナルを発行
        '''
        Gtk.Application.__init__(self,
            application_id='org.bird.kawasemi',
            flags=Gio.ApplicationFlags.HANDLES_OPEN )

    def do_startup(self):
        '''
            同様にウインドウはココで作る
        '''
        Gtk.Application.do_startup(self)
        Win(self)

    def do_activate(self):
        '''
            実行時に引数指定無しだとココに来る
            このシグナルを受け取った時に背面にいた場合 GNOME の通知が入る
        '''
        self.props.active_window.uri_label.props.label = 'クリアしました'
        self.props.active_window.present()

    def do_open(self, files, n_file, hint):
        '''
            実行時に引数を指定するとココに来る
            下記のようにすると転送され open シグナルが発行
            $ ./test.py a.txt &
            $ ./test.py b.txt c.txt &
            $ # クリアする
            $ ./test.py &
        '''
        s = ''
        for f in files:
            s = f'\n{f.get_uri()}'
            self.props.active_window.uri_label.props.label += s
        self.props.active_window.present()

App().run(sys.argv)

handle-local-options
handle-local-options は -v 等のオプションを処理するためのシグナルです。
flags の指定は不要、add_main_option で指定しハンドラを書けば動作します。
このシグナルは二つ目のアプリケーションを起動しようとした場合は転送前に発生します。
-v でバージョンの表示等に使います、-h でヘルプも自動追加される。
#!/usr/bin/env python3

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

APP_VERSION = '0.0.1'

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)
        # add --version, -v option
        self.add_main_option(
            'version',
            b'v',
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            'Show OM Version',
            None)

    def do_handle_local_options(self, options):
        '''
            ゼロを戻すとそのまま終了、-1 なら起動します
        '''
        if options.lookup_value('version', GLib.VariantType.new('b')):
            print(f'{__file__} {APP_VERSION}')
            return 0
        return -1

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Gtk.ApplicationWindow(application=self)

    def do_activate(self):
        self.props.active_window.present()

App().run(sys.argv)

command-line
command-line は --new-window 等のオプションを処理するためのシグナルです。
G_APPLICATION_HANDLES_COMMAND_LINE を GApplication 作成時の flags に指定すると使えます。
このシグナルは二つ目のアプリケーションを起動しようとした場合は転送先 GApplication で発生します。

注意点はこの指定を行うと activate や open シグナルが発生しない。
Gio.ApplicationFlags
なのでファイル引数処理は自前で、GTK3 から変わっていません。
#!/usr/bin/env python3

import gi, sys
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib, Gio

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self,
            application_id='org.olympus.pen',
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        # --version, -v option
        self.add_main_option(
            'version',
            b'v',
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            'Show Pen Version',
            None)
        # --new-window, -n option
        self.add_main_option(
            'new-window',
            b'n',
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            'New Pen Window',
            None)

    def do_command_line(self, command_line):
        # --new-window を転送先で処理
        options = command_line.get_options_dict()
        if options.lookup_value('new-window', GLib.VariantType.new('b')):
            Gtk.ApplicationWindow(application=self, title='Pen')
        # ファイル引数を自前で処理
        arg = command_line.get_arguments()[1:]
        for s in arg:
            # Option ignored
            if not s.startswith('-'):
                # PATH or URI (file:///...)
                f = Gio.file_new_for_uri(s) if '//' in s else Gio.file_new_for_path(s)
                if f.query_exists():
                    self.props.active_window.set_title(s)
                    break
        self.props.active_window.present()
        return 0

    def do_handle_local_options(self, options):
        # --version は転送前に処理
        if options.lookup_value('version', GLib.VariantType.new('b')):
            print('Oppai 0.0.1')
            return 0
        return -1

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Gtk.ApplicationWindow(application=self, title='Pen')

''' 使えるようになるかもしれないので
    def do_activate(self):
        self.props.active_window.present()

    def do_open(self, files, n_file, hint):
        for f in files:
            self.props.active_window.set_title(f.get_uri())
            break
        self.props.active_window.present()
'''

App().run(sys.argv)

GAction
GNOME 3.32 でアプリケーションメニューは廃止されました。
しかしアプリケーションメニューで使っていた GAction はまだ使えます。
Ctrl+Q で全体終了、Ctrl+N で新規ウインドウ等は Application 側に実装が自然かと。
#!/usr/bin/env python3
 
import gi, sys
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gio

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id='org.oppai.chikubi',
            flags=Gio.ApplicationFlags.FLAGS_NONE )

    def do_activate(self):
        self.props.active_window.present()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        # GAction 作成
        new_window_action = Gio.SimpleAction(name='new_window_action')
        quit_action       = Gio.SimpleAction(name='quit_action')
        # 追加
        self.add_action(new_window_action)
        self.add_action(quit_action)
        # アクセラレーターの指定
        self.set_accels_for_action('app.new_window_action', ['<Control>N'])
        self.set_accels_for_action('app.quit_action', ['<Control>Q'])
        # シグナル
        new_window_action.connect('activate', self.on_new_window_action)
        quit_action.connect('activate', self.on_quit_action)
        # Window を作る
        Gtk.ApplicationWindow(application=self, title='Chikubi')

    def on_new_window_action(self, action, parameter):
        '''
            activate が発生しないので自前で present する
        '''
        w = Gtk.ApplicationWindow(application=self, title='Chikubi')
        w.present()

    def on_quit_action(self, action, parameter):
        '''
            複数ウインドウがあってもすべて閉じて終了します
        '''
        self.quit()

App().run(sys.argv)

Copyright(C) sasakima-nao All rights reserved 2002 --- 2024.