Paepoi

Paepoi » PyGObject Tips » Gtk(PyGObject) Tips | GtkApplication

Gtk(PyGObject) Tips | GtkApplication

# 最終更新日 2019.08.12

2019 年現在の仕様に追記と書き換え。
記述内容が増えたので GtkWindow のページと分離。
文字列をシングルクォートに統一、及び fstring 化(python 3.6 以降)を行いました。

GtkApplication
Gtk+3.0 より GtkApplication が追加されました。
GtkApplication は同一 ID の Application を一つしか作成しません。
二つ目のアプリケーションを起動しようとすると以前作成した Application に引数が転送されます。
転送先 Application 側で activate や open シグナルが発行されます。
startup シグナルは初回にしか発行されませんので注意。

ウインドウは GtkApplicationWindow という GtkWindow のサブクラスを使います。
GtkApplicationWindow でないと ApplicationMenu が使えなかったけど廃止になってしまったし。。。
application プロパティは GtkWindow にあるので実際にはどちらでもいいような。。。

メインループを GtkApplication が受け持つので Gtk.main() でループを作る必要は無い。
ウインドウ側で delete-event 処理を行う必要も無い、閉じるボタンで自動的に破棄されます。
GtkApplication 自体は管理するオブジェクトが無くなった時点で終了する参照カウンタ方式。
これにより複数ウインドウの管理や多重起動制御を GtkApplication に任せられるようになった。

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

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

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

    def do_startup(self):
        '''
            初回起動のみ
            Gtk.Application.do_startup(self) が必須
        '''
        Gtk.Application.do_startup(self)
        win = Gtk.ApplicationWindow.new(self)
        win.props.title = ''
        win.show()

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

app = App()
app.run(sys.argv)

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', '3.0')
from gi.repository import Gtk, Gio

class Win(Gtk.ApplicationWindow):
    '''
        HANDLES_OPEN の実験用
        何か引数にファイルを指定してもう一つ起動すると URI が追記される
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, title='Title', application=app)
        swin = Gtk.ScrolledWindow()
        self.view = Gtk.TextView()
        swin.add(self.view)
        self.add(swin)
        self.resize(320, 240)
        self.show_all()

    def add_uri(self, uris):
        buf = self.view.get_buffer()
        it = buf.get_end_iter()
        buf.insert(it, uris)

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

    def do_startup(self):
        '''
            最初のウインドウはココで作るのが鉄板のようです
        '''
        Gtk.Application.do_startup(self)
        Win(self)

    def do_activate(self):
        '''
            実行時に引数指定無しだとココに来る
            以前はこの関数で最前面に移動していたけど今はダメ
        '''
        self.props.active_window.present()

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

app = App()
app.run(sys.argv)

handle-local-options
handle-local-options は -v 等のオプションを処理するためのシグナルです。
このシグナルは二つ目のアプリケーションを起動しようとした場合は転送前に発生します。
-v でバージョンの表示等に使います。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Oppai')
        self.show_all()

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 Oppai Version', None)

    def do_handle_local_options(self, options):
        # get option
        if options.lookup_value('version', GLib.VariantType.new('b')):
            self.show_version()
            return 0
        return -1

    def do_activate(self):
        self.window = Win(self)
        self.window.present()

    def show_version(self):
        print('Oppai 0.0.1')

app = App()
app.run(sys.argv)

command-line
command-line は --new-window 等のオプションを処理するためのシグナルです。
G_APPLICATION_HANDLES_COMMAND_LINE を GApplication 作成時の flags に指定すると使えます。
このシグナルは二つ目のアプリケーションを起動しようとした場合は転送先 GApplication で発生します。
注意点はこの指定を行うと open シグナルが発生しないようなのでファイル引数は自前で行う必要がある。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Chikubi')
        self.resize(150, 60)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id='bura.oppai.chikubi',
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
            # devhelp では併用できると書いているんだけど。。。。。
            #flags=Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        # --version, -v option
        self.add_main_option('version', b'v', GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 'Show Chikubi Version', None)
        # --new-window, -n option
        self.add_main_option('new-window', b'n', GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 'New Chikubi 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')):
            w = Win(self)
        # ファイル引数を自前で処理
        arg = command_line.get_arguments()[1:]
        for s in arg:
            # Option ignored
            if not s.startswith('-'):
                # URI? (file:///...)
                if '//' in s:
                    f = Gio.file_new_for_uri(s)
                else:
                    f = Gio.file_new_for_path(s)
                if f.query_exists():
                    self.props.active_window.set_title(s)
                    break
        return 0

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

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)

''' 使えるようになるかもしれないので
    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 = App()
app.run(sys.argv)

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

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

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id='suzuki.bike.kakkoii',
            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 を作る
        Win(self)

    def on_new_window_action(self, action, parameter):
        Win(self)

    def on_quit_action(self, action, parameter):
        self.quit()

app = App()
app.run(sys.argv)

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