L'Isola di Niente
L'Isola di Niente » PyGObject Tips » Gtk(PyGObject) Tips

Gtk(PyGObject) Tips

GUI アプリケーションの基本、ウインドウを作ってみましょう。

GtkWindow

以下はウインドウを表示するだけの最小限コード
#!/usr/bin/env python3

from gi.repository import Gtk                 # /usr/lib64/girepository-1.0 の *.typelib を参照

window = Gtk.Window()                         # GtkWindow の作成
window.connect("delete-event", Gtk.main_quit) # [閉じる]ボタンの処理
window.show()                                 # 表示する
Gtk.main()                                    # メインループ
一行目は実行パーミッションを与えた場合に起動するアプリケーションを指定。
Python2 を使うなら python のみに書き換える。
必要なら二行目にエンコード指定、書かないと日本語等を例外判定する。

後はコメントのとおりです。

メインループとは大雑把に説明すると OS に
「ボク当てに[マウス左ボタンをクリックした]とかのシグナルは何かないですか?」
と延々問い合わせ続ける処理、gtk_main_quit() を呼ぶまで続く無限ループのことだと思えばいい。
GUI を持つアプリケーションはそうやって動いています。

それと GtkWidget は作成した時点では非表示です、子 Widget をまとめて表示させるなら show_all() を使う。
ということで最小限上記のコードが必要になります。

サブクラス、継承

サブクラスとはテンプレートを引き継いだもの、継承とは class を使ってその処理を簡素化したもの。
オブジェクト指向言語でよく使われる言葉ですが難しくありません、単なるテンプレートのコピーです。
たとえば上記最小限コードの GtkWindow を継承したクラス作成なら以下で終わり。
#!/usr/bin/env python3

from gi.repository import Gtk

class Template(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)

window = Template()
window.connect("delete-event", Gtk.main_quit)
window.show()
Gtk.main()
これは直接 GtkWindow を作成したのと何も変わりません、単に継承のみを行った例です。
他の言語を知っているなら奇妙な部分が __init__ で親クラスの __init__ を呼んでいること。
Glade で作ったコードにはこんなことは行いませんが継承を行う場合は必須です。
Python の __init__ では自分が作られただけで継承元は何も行われていません、ので必須であります。

これがたとえば C++ で MFC だと
class CHogeWnd : public CWnd{
}

で継承元クラスの CWnd は初期化が完了しているのですけど違います、勘違いしないように気をつけよう。
もっと書くと __init__ はコンストラクタではありません、この時点で self は完成しています。
この辺について詳しくは初心者本で、動的言語として当然だけど初心者はまだ意味を理解しなくていい。

では継承の継承をしてみます。
#!/usr/bin/env python3

from gi.repository import Gtk

class Template(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)

class Template2(Template):
    def __init__(self):
        Template.__init__(self)
        self.set_title("たいとるばぁ")
        self.resize(500, 5)

class Template3(Template2):
    def __init__(self):
        Template2.__init__(self)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

Template3()
Gtk.main()
これを実行するとタイトルバーが「たいとるばぁ」で超横長なウインドウが作られたはずです。
class とはテンプレートであってテンプレートに何か追加するのが継承だとこれで解ってくれるかな?

これではアレなので普通の最低限テンプレートを以下に。
#!/usr/bin/env python3

from gi.repository import Gtk

class Win(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

Win()
Gtk.main()

シグナルハンドラ

さてウインドウだけでは何もできません。
マウスクリックやキーボードをタイプ時の処理を作成する必要がある。

ユーザーが何かアクションを起こした場合に GTK+ はシグナルを飛ばします。
このシグナルを受け取った時にアプリが行う処理がハンドラです。

基本的に Widget の connect メソッドで指定していきます。
Widget 毎に引数が違うので各々 Widget のヘルプで確認してください。

親 Widget については下記のように do_ の接頭子でオーバーライドすることも可能。
self と widget 引数は当然同じ self になるので自前コネクトから widget 引数が減ります。
[ do_シグナル名 ] の名前で予約されているので上書きしないよう注意。
子 Widget は無理ですので素直に connect メソッドを利用。
#!/usr/bin/env python3

from gi.repository import Gtk, Gdk

class Win(Gtk.Window):
    """
        do_* のハンドラを消してコメントアウトを外すと
        まったく同じということが解る
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        #self.connect("button-press-event", self.on_button_press_event)
        #self.connect("delete-event", self.on_delete_event)
        self.show_all()

    def do_delete_event(self, event):
        # [閉じる] ボタンが押された
        Gtk.main_quit()

    def do_button_press_event(self, event):
        # マウスボタンが押された
        # ウインドウのドコを掴んでも移動できるようにする
        self.begin_move_drag( event.button, event.x_root, event.y_root, event.time)

    '''
    def on_delete_event(self, widget, event):
        # [閉じる] ボタンが押された
        Gtk.main_quit()

    def on_button_press_event(self, widget, event):
        # マウスボタンが押された
        # ウインドウのドコを掴んでも移動できるようにする
        self.begin_move_drag( event.button, event.x_root, event.y_root, event.time)
    '''

Win()
Gtk.main()

自前コネクトするならハンドラ名は何でもいいけどお約束はある。
GNOME 関連では _cb のサフィックスを付けるのがお約束のようです。
on_ のプリフィックスを好む人は Windows 出身に多い(筆者とか...)
大文字を使うと鼻で笑われることは秘密。

パッキング

GtkButton や GtkEntry の部品をパッキングしていきます。
部品を配置するのにまずコンテナ(レイアウタ)を利用します。

GTK3 では GtkHBox, GtkVBox は非推奨になっています(今は一応使えます)
GtkBox を作成時に引数で縦横を指定して使うようにしましょう。
この場合 PyGtk 互換の Gtk.Box() では適用されないので new() を利用。

#!/usr/bin/env python3

from gi.repository import Gtk

class Win(Gtk.Window):
    """
        縦: Gtk.Orientation.VERTICAL
        横: Gtk.Orientation.HORIZONTAL
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        self.connect("delete-event", Gtk.main_quit)
        # Container (pygtk @ vbox = Gtk.VBox() )
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
        # Widget
        button = Gtk.Button.new_with_label("ボタン")
        button.connect("clicked", self.on_button_clicked)
        self.entry = Gtk.Entry.new()
        # Pack
        vbox.pack_start(button, True, True, 0);
        vbox.pack_start(self.entry, False, False, 0);
        self.add(vbox)
        # Show
        self.show_all()

    def on_button_clicked(self, widget):
        self.entry.set_text("Hello World")

Win()
Gtk.main()

img/packing.png

ボタンを押すと Hello World と書き出されるはずです。
ウインドウをリサイズするとボタンの大きさが連動して大きくなるのが解ります。

パッキングの定義は以下になっています。
def pack_start(child, expand, fill, padding)
button の pack_start の引数を変更するとどうなるか試せば動作が理解できると思う。

この例のように Window 自体やコンテナの大きさはコンテンツの大きさにより確定されます。

枠無しウインドウ

枠無しウインドウを作るには Gtk.WindowType.POPUP を指定するだけです。

ただしこの場合ウインドウとは認識されずフォーカスを持つこともできません。
たとえば GtkEntry を置いても書き込むことができないという具合です。
当然 [閉じる] ボタンも無い状態なので終了させる手段が必須。
#!/usr/bin/env python3

from gi.repository import Gtk, Gdk

class Win(Gtk.Window):
    def __init__(self):
        """
            枠無しウインドウその1
            Gtk.WindowType.POPUP を指定するだけ
        """
        Gtk.Window.__init__(self, Gtk.WindowType.POPUP)
        # Mouse Click を検知させる処理
        self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self.connect("button-press-event", self.on_button_press_event)
        # この Entry には書き込みできない
        entry = Gtk.Entry()
        self.add(entry)
        self.resize(200, 200)
        #
        self.show_all()

    def on_button_press_event(self, widget, event):
        """
            Double Click で終了
            GTK+3.4 以前では Gdk.EventType._2BUTTON_PRESS を利用
        """
        if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
            Gtk.main_quit()

Win()
Gtk.main()

ウインドウとして認識させたい場合は decorated プロパティが使えます。
#!/usr/bin/env python3

from gi.repository import Gtk, Gdk

class Win(Gtk.Window):
    def __init__(self):
        """
            枠無しウインドウその2
            decorated プロパティ利用
        """
        Gtk.Window.__init__(self)
        self.connect("delete-event", Gtk.main_quit)
        # Mouse Click を検知させる処理
        self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self.connect("button-press-event", self.on_button_press_event)
        self.show_all()

    def on_button_press_event(self, widget, event):
        """
            ダブルクリック毎に切り替わる
            コッチならフォーカスを持てる
            ついでに枠無しでも動かせるように
        """
        if event.type == Gdk.EventType.BUTTON_PRESS:
            self.begin_move_drag( event.button, event.x_root, event.y_root, event.time)
        elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
            self.props.decorated = self.props.decorated == False

Win()
Gtk.main()

GtkApplication

Gtk+3.0 より GtkApplication が追加されました。
GtkApplication は同一 ID の Application を一つしか作成しません。
もう一つ作成しようとすると以前作成した Application に引数が転送され activate や open シグナルが発行されます。
startup シグナルは初回にしか発行されませんので注意。

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

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

import sys
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)
        self._win = Gtk.ApplicationWindow.new(self)
        self._win.show()

    def do_activate(self):
        """
            呼び出される毎に最前面に
        """
        self._win.present()
        
if __name__ == '__main__':
    app = App()
    app.run(sys.argv)

do_* ハンドラはもちろん自前でコネクトしてもいいです。
PyGObject 3.8 以降は Gio.ApplicationFlags.HANDLES_OPEN が使えるようになった。
#!/usr/bin/env python3

import sys
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 AppClass(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):
        """
            self.add_window なら GtkWindow でも可ですけど
            GtkApplicationWindow でないと ApplicationMenu が使えない
        """
        Gtk.Application.do_startup(self)
        self.window = Win(self)

    def do_activate(self):
        """
            実行時に引数指定無しだとココに来る
        """
        self.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 += "{0}\n".format(f.get_uri())
        self.window.add_uri(s)
        self.window.present()

if __name__ == "__main__":
    app = AppClass()
    app.run(sys.argv)

アプリケーションメニュー

GNOME3 にてアクティビティ横のタスクをクリックで出るメニュー。
環境によってはタイトルバーの下になる場合もあります。

複数起動した同一アプリを一気に終了させるのに便利、メニューの追加もできる。
多分タッチパネルのメニューボタン向けとして作られたものだと思われます。
GNOME 主要アプリのメニューはどんどんコレになっています、時代の流れですね。
#!/usr/bin/env python3

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

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

    def do_startup(self):
        Gtk.Application.do_startup(self)
        # AppMenu
        menu = Gio.Menu()
        menu.append("New", "app.new")
        menu.append("Quit", "app.quit")
        self.set_app_menu(menu)
        # option "new"
        new_action = Gio.SimpleAction.new("new", None)
        new_action.connect("activate", self.new_cb)
        self.add_action(new_action)
        # option "quit"
        quit_action = Gio.SimpleAction.new("quit", None)
        quit_action.connect("activate", self.quit_cb)
        self.add_action(quit_action)

    def new_cb(self, action, parameter):
        print("New")

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

if __name__ == "__main__":
    app = App()
    app.run(sys.argv)

GtkApplication で利用するウインドウは GtkApplicationWindow にする。
GtkWindow だとこのアプリケーションメニューが出せません。
更にアプリケーションメニューを作成した後にウインドウを作らないと適用されないので注意。

Glade

同じものを Glade を使って作ってみましょう。
レイアウト方法は少し弄くれば解るとしてシグナルの接続方法を。
[ハンドラーの名前]の所でクリックし o を打ち込む。

img/glade.png

ちなみに b 等の Widget 頭文字を打ち込めば button1_clicked_cb みたいな選択肢になる。
又 gtk_main_quit 等もココで直接指定できますが Python ではエラーになる。

img/delete_event.png

on_ のプリフィクスか _cb のサフィックスのどちらを使うかはお好みで。
ハンドラの名前はこのどちらかでないと他人がコードを読む時に困ります。

次にハンドラの接続、こうするのが一番簡単。
#!/usr/bin/env python3

from gi.repository import Gtk

class Win():
    def __init__(self):
        self.builder = Gtk.Builder()
        self.builder.add_from_file("test.glade")
        self.builder.connect_signals(self)
        window = self.builder.get_object("window1")
        window.show_all()

    def on_button1_clicked(self, widget, data=None):
        entry = self.builder.get_object("entry1")
        entry.set_text("Hello World")

    def on_window1_delete_event(self, widget, data=None):
        Gtk.main_quit()

if __name__ == "__main__":
    Win()
    Gtk.main()

筆者は Glade を使わないので Glade の解説はおしまい。
後はココが解り安かった。
20. Glade and Gtk.Builder — Python GTK+ 3 Tutorial 1.0 documentation
Copyright(C) sasakima-nao All rights reserved 2002 --- 2017.