Paepoi

Paepoi » PyGObject Tips » Gtk(PyGObject) Tips | ウインドウを作る

Gtk(PyGObject) Tips | ウインドウを作る

# 最終更新日 2019.08.12

2019 年現在の仕様に追記と書き換え、プロパティ関連の追記。
サポートの終わった Python2 関連、及び継承等のウルトラ初心者向け解説の削除。
GtkApplication 関連をページ分離。
文字列をシングルクォートに統一等を行いました。

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

import gi
gi.require_version('Gtk', '3.0')              # 使用する Gir バージョンの指定
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()                                    # メインループ
一行目はシバン、shebang の g は発音しないことは中学で習ったよね。
3.18 以降は使用する Gir のバージョンが複数存在する場合に指定が必要になった。
指定をしなくても動きますが stderr に警告を吐くので書いたほうがいい。

ウインドウに付いている「閉じる」ボタンをクリックすると delete-event シグナルが発生します。
このシグナルを受け取った時にメインループを終了させるという流れです。

メインループとは大雑把に説明すると OS に
「ボク当てにマウスのボタンをクリックしたとかのシグナルは何かないですか?」
と延々問い合わせ続ける処理、gtk_main_quit() 関数を呼ぶまで続く無限ループのこと。
この無限ループが無いとシグナルが発生しても受け取ることができませんので必須です。
GUI を持つアプリケーションはすべての OS でそうやって動いています。

それと GtkWindow を含む GtkWidget サブクラスは作成した時点では非表示です。
子 Widget をまとめて表示させるなら show_all() を使う。

上記から拡張すると全部グローバル変数になってしまい色々困ります。
以下に筆者がよく利用する普通の最低限テンプレートを。
#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
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 に送ります。
このシグナルを受け取った時にアプリが行う処理がハンドラです。

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

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

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

class Win(Gtk.Window):
    '''
        do_button_press_event を消してコメントアウトを外すと
        まったく同じ動作ということが解るサンプル
    '''
    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.show_all()

    def do_delete_event(self, event):
        # delete-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_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 出身に多い(筆者とか...)
大文字を使うと鼻で笑われることは秘密。

プロパティ
devhelp で Widget の項目を見ると Property というものがあります。
これは各 Widget のインスタンスから props メソッドで辿ることができます。
又はインスタンス作成時の引数にデフォルト引数の形で指定することも可能。
個別に指定するより短いコードになるので大いに活用しましょう。

#!/usr/bin/env python3

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

class Win(Gtk.Window):
    '''
        プロパティの例
    '''
    def __init__(self):
        Gtk.Window.__init__(self, title='タイトルです')
        # Widget
        button = Gtk.Button(label='Alt+_b で終了', use_underline=True)
        # or
        #button = Gtk.Button.new_with_label('Alt+_b で終了')
        #button.props.use_underline = True
        button.connect('clicked', self.on_button_clicked)
        self.add(button)
        # Show
        self.show_all()

    def do_delete_event(self, event):
        # True を戻すと無効になる
        self.props.title = 'Alt+_b で終了してください'
        return True

    def on_button_clicked(self, widget):
        Gtk.main_quit()

Win()
Gtk.main()

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

PyGObject では HBox, VBox が残っていますが非推奨なので使わないように。
GtkBox を作成時に orientation プロパティで縦横を指定。

#!/usr/bin/env python3

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

class Win(Gtk.Window):
    '''
        縦: Gtk.Orientation.VERTICAL
        横: Gtk.Orientation.HORIZONTAL
    '''
    def __init__(self):
        Gtk.Window.__init__(self)
        # 縦型コンテナの作成
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        # gtk_box_new を使う場合は引数どおりに指定する
        #vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
        # ボタン
        button = Gtk.Button(label='ボタン')
        button.connect('clicked', self.on_button_clicked)
        # 一行エディットは後で参照するので self にくっつける
        self.entry = Gtk.Entry(editable=False, text='ボタンを押せ')
        # パッキング
        vbox.pack_start(button, True, True, 0);
        vbox.pack_start(self.entry, False, False, 0);
        self.add(vbox)
        # Show
        self.show_all()

    def do_delete_event(self, event):
        Gtk.main_quit()

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

Win()
Gtk.main()

ボタンを押すと Hello World と書き出されるはずです。
ウインドウをリサイズするとボタンの大きさが連動して大きくなるのが解ります。
又コンテンツの必要最小限の大きさ以下にはならないこともここで確認できます。

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

枠無しウインドウ
枠無しウインドウは decorated プロパティを使います。
デスクトップマスコット等の背景透過画像を表示させるには以下のようにします。
#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf

# 背景透過な PNG 画像を用意して指定してください
PNGFILE = 'test.png'

class Win(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, app_paintable=True, decorated=False)
        # 背景透過可能か確認、現行 GNOME が動く環境なら問題ないはずですが
        screen = self.get_screen()
        visual = screen.get_rgba_visual()
        if visual != None and screen.is_composited():
            self.set_visual(visual)
        else:
            print('この環境では背景透過できません')
            Gtk.main_quit()
        # 画像のロード
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(PNGFILE)
        # シグナルの処理
        self.connect('button-press-event', self.on_button_press_event)
        self.connect('draw', self.on_draw)
        # 画像サイズにリサイズ
        self.resize(self.pixbuf.get_width(), self.pixbuf.get_height())
        self.show_all()

    def on_draw(self, widget, cr):
        Gdk.cairo_set_source_pixbuf(cr, self.pixbuf, 0, 0)
        cr.paint()

    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:
            Gtk.main_quit()

Win()
Gtk.main()

Glade
筆者は Glade を使っていないので他人の解説を見てください。
21. Glade and Gtk.Builder — Python GTK+ 3 Tutorial 3.4 documentation
Copyright(C) sasakima-nao All rights reserved 2002 --- 2020.