Paepoi

Paepoi » PyGObject Tips » Gtk(PyGObject) Tips | コンテナ(3.10 以降)

Gtk(PyGObject) Tips | コンテナ(3.10 以降)

# 最終更新日 2019.08.17

2019 年現在の仕様に追記と書き換え。
数が非常に多いので 3.10 以前からあるもの は別ページにしています。

GtkHeaderBar
タイトルバーをコンテナ化して Widget を置くことができるようにしたもの
小さな画面でもクライアントエリアのサイズを最大限に大きくできるメリットがある
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        set_custom_title で中心
        pack_start|end   で左右にパッキング
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # 進む戻るボタン
        back = Gtk.Button.new_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU)
        forw = Gtk.Button.new_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU)
        #back.set_valign(Gtk.Align.CENTER) 不要になった
        #forw.set_valign(Gtk.Align.CENTER)
        #
        # Nautilus の進む戻るボタンの同様に合体させる
        bfbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        bfbox.get_style_context().add_class('linked')
        bfbox.pack_start(back, False, False, 0)
        bfbox.pack_start(forw, False, False, 0)
        # メニューボタンが必要になったので
        menu = Gtk.Button.new_from_icon_name('open-menu-symbolic', Gtk.IconSize.MENU)
        # GtkHeaderBar
        hbar = Gtk.HeaderBar(show_close_button=True)
        hbar.pack_start(bfbox)
        hbar.set_custom_title(Gtk.Label(label='タイトル'))
        hbar.pack_end(menu)
        # self
        self.set_titlebar(hbar)
        self.resize(300, 100)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkheaderbar.png
GtkStack
GtkStack はコンテナです、 GtkNotebook のように単独ページを切り替えできます。
add したコンテナやウイジェットを切り替えして一画面として利用します。
切り替えのエフェクトを GtkStackTransitionType で指定することも可能。

GtkStackSwitcher 又は GtkStackSidebar と合わせて使います。
サンプルコードはそちらで。

GtkStackSwitcher
GtkStackSwitcher は GtkStack の各ページを切り替えするものです。
諸々の指定は GtkStack 側にしてこちらは切り替えだけを行なう感じになります。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        スイッチャーをあまり増やせないという欠点がある
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # GtkStack
        stack = Gtk.Stack(transition_type=Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
        # GtkStackSwitcher
        sw = Gtk.StackSwitcher(stack=stack)
        # page1
        label1 = Gtk.Label(label='かっこいい')
        stack.add_titled(label1, 'suzuki', 'スズキ')
        # page2
        label2 = Gtk.Label(label='平凡')
        stack.add_titled(label2, 'yamaha', 'ヤマハ')
        # page2
        label3 = Gtk.Label(label='カワサキか...')
        stack.add_titled(label3, 'kawasaki', 'カワサキ')
        # GtkHeaderBar
        hbar = Gtk.HeaderBar(show_close_button=True)
        hbar.set_custom_title(sw)
        # self
        self.set_titlebar(hbar)
        self.add(stack)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkstackswicher.png
GtkStackSidebar
3.16 で追加された GtkStackSwitcher の派生、サイドバーバージョンです。
GNOME の設定画面で使われています。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        ちょっとサイドバーの高さが大きいような
        CROSSFADE にすると GNOME 設定と同じになる
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # GtkStack
        stack = Gtk.Stack(transition_type=Gtk.StackTransitionType.CROSSFADE)
        # GtkStackSwitcher
        ss = Gtk.StackSidebar(stack=stack)
        # page1
        label1 = Gtk.Label(label='かっこいい')
        stack.add_titled(label1, 'suzuki', 'スズキ')
        # page2
        label2 = Gtk.Label(label='平凡')
        stack.add_titled(label2, 'yamaha', 'ヤマハ')
        # page2
        label3 = Gtk.Label(label='カワサキか...')
        stack.add_titled(label3, 'kawasaki', 'カワサキ')
        # Paned
        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        paned.add1(ss)
        paned.add2(stack)
        # self
        self.add(paned)
        self.resize(300, 200)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkstacksidebar.png
GtkRevealer
GtkRevealer は Widget の表示非表示をアニメーションするコンテナです。
親ウインドウの show_all() に左右されず視覚効果も指定できて便利。
#!/usr/bin/env python3

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

TYPES = dict(
    CrossFade= Gtk.RevealerTransitionType.CROSSFADE,
    SlideRight=Gtk.RevealerTransitionType.SLIDE_RIGHT,
    SlideLeft= Gtk.RevealerTransitionType.SLIDE_LEFT,
    SlideUp=   Gtk.RevealerTransitionType.SLIDE_UP,
    SlideDown= Gtk.RevealerTransitionType.SLIDE_DOWN )

class Win(Gtk.ApplicationWindow):
    '''
        valign, halign を調節しないと上手く動かない
        デフォルトはどちらも GTK_ALIGN_FILL
        このサンプルでは一旦 CROSSFADE に変更するとたまに動かなくなる場合あり
        通常は途中でアニメーション効果を変えることはないので問題無いけど
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py', resizable=False)
        # button
        button = Gtk.Button(label='Click')
        button.connect('clicked', self.on_button_clicked)
        # GtkReveal
        self.revealer = Gtk.Revealer(
            transition_duration=2000,
            transition_type=Gtk.RevealerTransitionType.CROSSFADE,
            halign = Gtk.Align.START,
            valign = Gtk.Align.START)
        # このラベルをアニメーションさせる
        label = Gtk.Label(label='<span bgcolor="#0ff" size="x-large">Hello\nWorld</span>', use_markup=True)
        self.revealer.add(label)
        # パッキング
        hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        root = None
        for key, value in TYPES.items():
            r = Gtk.RadioButton(label=key, group=root)
            if not root:
                root = r
            r.connect('toggled', self.on_radio_toggled, value)
            hbox.pack_start(r, False, False, 0)
        hbox.pack_start(button, False, False, 0)
        hbox.pack_start(self.revealer, False, False, 0)
        self.add(hbox)
        self.show_all()

    def on_radio_toggled(self, button, user_data):
        # align の調節
        if user_data == Gtk.RevealerTransitionType.SLIDE_UP:
            self.revealer.props.valign = Gtk.Align.END
        elif user_data == Gtk.RevealerTransitionType.SLIDE_LEFT:
            self.revealer.props.halign = Gtk.Align.END
        else:
            self.revealer.props.valign = Gtk.Align.START
            self.revealer.props.halign = Gtk.Align.START
        # 反映
        self.revealer.set_transition_type(user_data)

    def on_button_clicked(self, widget):
        # アニメーションさせる
        self.revealer.set_reveal_child(not self.revealer.props.reveal_child)

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkrevealer.png
GtkListBox
GtkListBox は GtkBox をリストビューのように選択可能にするコンテナです。
また選択状態になった、クリックされた等のシグナルも受け取れます。

Windows でいうところの LISTBOX とは全然違い上記が可能なコンテナです。
Android, iOS のスマートフォン設定画面を思い浮かべると理解しやすい。
#!/usr/bin/env python3

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

DATA = ['YAMAHA', 'HONDA', 'KAWASAKI', 'SUZUKI']

class Win(Gtk.ApplicationWindow):
    '''
        どう動くかだけのサンプルコード
        以前は GtkCssProvider の例も書いていたけど繁栄されなくなったので削除
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py', resizable=False)
        # ListBox
        listbox = Gtk.ListBox()
        listbox.connect('row-activated', self.on_listbox_row_activated)
        for i, data in enumerate(DATA):
            hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
            hbox.pack_start(Gtk.Label(label=data), False, False, 0)
            hbox.pack_end(Gtk.Switch(), False, False, 0)
            listbox.insert(hbox, i)
        # Selection
        combobox = Gtk.ComboBoxText()
        combobox.append_text('Gtk.SelectionMode.NONE')
        combobox.append_text('Gtk.SelectionMode.SINGLE')
        combobox.set_active(listbox.get_selection_mode())
        combobox.connect('changed', self.on_combobox_changed, listbox)
        # CheckButton
        checkbutton = Gtk.CheckButton(label='Single Click Mode')
        checkbutton.set_active(listbox.get_activate_on_single_click())
        checkbutton.connect('clicked', self.on_checkbutton_clicked, listbox)
        # Pack
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
        vbox.pack_start(listbox, False, False, 0)
        vbox.pack_start(combobox, False, False, 0)
        vbox.pack_start(checkbutton, False, False, 0)
        self.add(vbox)
        self.set_border_width(10)
        self.show_all()

    def on_listbox_row_activated(self, listbox, row):
        s = DATA[row.get_index()]
        self.props.title = f'{s} を選択'

    def on_combobox_changed(self, combo, listbox):
        b = combo.get_active() == 1
        listbox.set_selection_mode(b)

    def on_checkbutton_clicked(self, button, listbox):
        b = button.get_active()
        listbox.set_activate_on_single_click(b)

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtklistbox.png
GtkActionBar
GtkActionBar は Widget 配置を左右及び中心に綺麗に振り分けるコンテナ。
GtkBox にダミーを入れて振り分ける必要もう無い(実際に筆者はやっていた)
最上部は GtkHeaderBar があるので最下部で振り分けたい時に便利。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        関数は GtkHeaderBar と同じ
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # クライアントエリア(ダミー)
        area = Gtk.DrawingArea()
        # 中心
        c1 = Gtk.Button.new_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU)
        c2 = Gtk.Button.new_from_icon_name('camera-photo-symbolic', Gtk.IconSize.MENU)
        c3 = Gtk.Button.new_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU)
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        hbox.pack_start(c1, False, False, 0)
        hbox.pack_start(c2, False, False, 0)
        hbox.pack_start(c3, False, False, 0)
        hbox.get_style_context().add_class('linked')
        # 左右
        left = Gtk.Button.new_from_icon_name('user-trash-symbolic', Gtk.IconSize.MENU)
        right = Gtk.Button.new_from_icon_name('open-menu-symbolic', Gtk.IconSize.MENU)
        # GtkActionBar
        bar = Gtk.ActionBar()
        bar.pack_start(left)
        bar.set_center_widget(hbox)
        bar.pack_end(right)
        # pack
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(area, True, True, 0)
        vbox.pack_start(bar, False, False, 0)
        # self
        self.add(vbox)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkactionbar.png
GtkPopover
GtkPopover はコンテンツをポップアップする Widget です。
メニューのような単一用途ではなくどんな Widget でも入れることができる。
常に表示は不要だけど即呼び出したい補助機能に利用すると便利。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        関数は GtkHeaderBar と同じ
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # クライアントエリア(ダミー)
        area = Gtk.DrawingArea()
        # このボタンでポップアップさせる
        button = Gtk.Button(label='アイテム')
        button.connect('clicked', self.on_option_button_clicked)
        # GtkActionBar
        bar = Gtk.ActionBar()
        bar.pack_end(button)
        # ポップアップコンテンツ
        c1 = Gtk.CheckButton(label='ムチを使う')
        c2 = Gtk.CheckButton(label='ロウソクを使う')
        c3 = Gtk.CheckButton(label='三角木馬を使う')
        popbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        popbox.pack_start(c1, False, False, 0)
        popbox.pack_start(c2, False, False, 0)
        popbox.pack_start(c3, False, False, 0)
        # GtkPopover
        self.pop = Gtk.Popover(relative_to=button, child=popbox)
        # パッキング
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(area, True, True, 0)
        vbox.pack_start(bar, False, False, 0)
        self.add(vbox)
        self.show_all()

    def on_option_button_clicked(self, widget):
        '''
            コンテンツ Widget は非表示になっている
            一括表示でいいなら show_all でいい
        '''
        self.pop.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkpopover.png
GtkPopoverMenu
GtkPopoverMenu は GtkPopover のサブクラス。
ポップアップコンテンツをメニューに特化しサブメニュー機能を追加したものです。

メニューアイテムは GtkMenuItem ではなく GtkModelButton を使います。
この GtkModelButton の action-name プロパティに GAction を指定しハンドラにします。
アプリケーションで利用の場合 [app] のプリフィクスが必要。
ウインドウで利用の場合 [win] のプリフィクスが必要。
下記でテキストビューアを作ってみましたのでこんな感じに。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        サブメニュー不要なら GtkPopover でよかったりする
        サブメニューについては別ページを後で作る予定
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # このボタンでポップアップさせる
        button = Gtk.Button.new_from_icon_name('open-menu-symbolic', Gtk.IconSize.MENU)
        button.connect('clicked', self.on_option_button_clicked)
        # GtkModelButton
        menu_open  = Gtk.ModelButton(active=True, action_name='app.new_file_action', text='開く(_O)', use_markup=True)
        menu_new   = Gtk.ModelButton(active=True, action_name='app.new_window_action', text='新しいウインドウ(_N)', use_markup=True)
        menu_quit  = Gtk.ModelButton(active=True, action_name='app.quit_action', text='終了(_Q)', use_markup=True)
        # ポップアップコンテンツ(メニュー)
        vbox = Gtk.Box(visible=True, margin=10, orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(menu_open, False, False, 0)
        vbox.pack_start(menu_new, False, False, 0)
        vbox.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
        vbox.pack_start(menu_quit, False, False, 0)
        # GtkPopoverMenu
        self.popovermenu = Gtk.PopoverMenu(relative_to=button, child=vbox)
        # GtkHeaderBar
        hbar = Gtk.HeaderBar(show_close_button=True)
        hbar.pack_end(button)
        # GtkTextView and GtkScrolledWindow
        self.view = Gtk.TextView()
        sw = Gtk.ScrolledWindow(child=self.view)
        # self
        self.set_titlebar(hbar)
        self.add(sw)
        self.show_all()

    def on_option_button_clicked(self, widget):
        self.popovermenu.show_all()

    def open_dialog(self):
        dlg = Gtk.FileChooserNative(
            title='Open',
            transient_for=self,
            action=Gtk.FileChooserAction.OPEN)
        if dlg.run() == Gtk.ResponseType.ACCEPT:
            #self.set_uri(dlg.get_uri())
            with open(dlg.get_filename()) as f:
                s = f.read()
                self.view.get_buffer().set_text(s)
        dlg.destroy()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

    def on_new_file_action(self, action, parameter):
        self.props.active_window.open_dialog()

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

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

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

app = App()
app.run(sys.argv)
container1/gtkpopovermenu.png
GtkFlowBox
GtkFlowBox は Widget を幅に合わせて自動的に改行するコンテナ。
CSS3 の FlexBox みたいなのを期待したけど少し違います。
どうやら最小横幅を基準にコンテンツの高さを決定するようです。

GtkIconView のセルと違いコチラはどんな Widget でも入れられる。
それと GtkListBox 同様にコンテンツを選択することもできる。
#!/usr/bin/env python3

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

# 画像が沢山あるディレクトリに書き換えしてください
PICTURES = '/home/sasakima-nao/www/tips/pygobject/container1'

class Win(Gtk.ApplicationWindow):
    '''
        サムネイルされた後にウインドウのサイズを変更すると動作が解る
        valign の指定は必須だったけど不要になったみたい
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # GtkFlowBox
        flowbox = Gtk.FlowBox()#valign=Gtk.Align.START)
        # 指定ディレクトリのファイルを探す
        d = Gio.file_new_for_path(PICTURES)
        enum = d.enumerate_children(Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, 0)
        for info in enum:
            content_type = info.get_content_type()
            if content_type == 'image/jpeg' or content_type == 'image/png':
                fullpath = f'{PICTURES}/{info.get_name()}'
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(fullpath, 100, 100, True)
                image = Gtk.Image(pixbuf=pixbuf)
                flowbox.add(image)
        scroll = Gtk.ScrolledWindow(child=flowbox)
        # self
        self.add(scroll)
        self.resize(400, 300)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

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

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

app = App()
app.run(sys.argv)
container1/gtkflowbox.png
Copyright(C) sasakima-nao All rights reserved 2002 --- 2020.