Programming」カテゴリーアーカイブ

Python with GTK3 @ GtkTreeView

gjots2 が ORBit2 を必要とするので GNOME3 に入れたくない。
というわけで代わりになるものを自作することにした。
まてよ、私は GtkTreeView を ListView としてしか使ったことがない。
GtkListStore を GtkTreeStore にすればいい、だと思うけど実験だ。

ところで PyGI のウイジェットには new というメソッドがあるわけだが。

store = Gtk.TreeStore.new([str, str])
# or Compatibility PyGtk
store = Gtk.TreeStore(str, str)

GtkTreeStore を作るには new でシーケンスを渡すか PyGtk と同様に作成。
というか TreeStore(str, str) のほうは多分 PyGtk 互換用だろう。
C では数と内容をズラズラだから PyGI 的にはシーケンスで書くほうが正しいかと思う。

けど new_with_model とかのパラメータ指定差を吸収する互換も便利だから捨てがたい。
パラメータが void しか無いウイジェットは互換のままでいいと思うけど。

そういえば、最近気がついたのだが Property も props メソッドから辿れる。
どちらでも結果は同じなのでお好みで、しかしなんというか Gtk# 臭くなったものだ。
ハイフンをアンダースコアに変換するのを忘れないでね。

# Convert '-' to '_'
tree.props.headers_visible = False
# or Compatibility PyGtk
tree.set_property("headers-visible", False)

それはともかく、GtkTreeView の利用方法。
CellRenderer, TreeViewColumn, TreeStore(ListStore) が必要。

ツリーに表示するレンダラを決める、今回は文字列なので GtkCellRendererText を。
そのレンダラを表示するカラムが必要、複数作れば ListView になると解るね。
シーケンスの何番目データをツリー表示するか、今回は最初のデータなので 0 を指定。

型を指定してシーケンスにして TreeStore を作成。
ソレを TreeModel として TreeView 作成のパラメータにする。
先ほど作成したカラムをアペンド、コレでツリー部分作成は終わり。

ツリー選択を変更したシグナルを捕まえるコードを忘れずに。
ハンドラの widget は GtkTreeSelection になるようだ。

後は GtkTreeIter を介してデータを入れていく。
GtkTreeIter に None を指定すればツリーの先頭に入る。

人により微妙に言い回しが違う専門用語だらけになったけどなんとなく解って。
GTK+ には簡単な ListBox が無いので嫌でも使うハメになる時があるから。

ということでサンプルコード。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio, GtkSource

class TreeWin(Gtk.Window):
    """
        GtkTreeView Sample
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        # CellRenderer
        cell = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn("Tree Title", cell)
        column.add_attribute(cell, "text", 0)
        # TreeView and TreeStore
        store = Gtk.TreeStore.new([str, str])
        tree = Gtk.TreeView.new_with_model(store)
        tree.append_column(column)
        # Hide Header
        tree.props.headers_visible = False
        # Signal
        selection = tree.get_selection()
        selection.connect("changed", self.selection_changeed)
        #
        # Data
        it0 = store.append(None, ["1.0", "Kaname Madoka"])
        store.append(it0, ["1.1", "Akemi Homura"])
        store.append(it0, ["1.2", "Tomoe Mami"])
        store.append(None, ["2.0", "Miki Sayaka"])
        it1 = store.append(None, ["3.0", "Sakura Kyoko"])
        store.append(it1, ["3.1", "QB"])
        #
        # GtkSourceView
        self.view = GtkSource.View()
        self.view.set_show_line_numbers(True)
        # Paned and ScrolledWindow
        swa = Gtk.ScrolledWindow()
        swa.add(tree)
        swb = Gtk.ScrolledWindow()
        swb.add(self.view)
        hp = Gtk.HPaned()
        hp.add1(swa)
        hp.add2(swb)
        # self
        self.set_title("TreeView Test")
        self.add(hp)
        self.show_all()

    def selection_changeed(self, widget, data=None):
        """
            Params @ widget: GtkTreeSelection
        """
        model, it = widget.get_selected()
        if it:
            buf = self.view.get_buffer()
            buf.set_text(model.get_value(it, 1))

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.treeview",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)
        
    def on_activate(self, data=None):
        w = TreeWin()
        w.set_application(self)
    
if __name__ == "__main__":
    app = App()
    app.run(None)

ツリー構築だけならそんなに難しくはないね。
後はファイルを読み込んでどうメモリ展開するかだ。

せっかくなので不満だった部分
・GtkSourceView を利用した色分け表示
・タイトルは一行目ではなく独自データに
なんかも実装しつつ Gjots2 完全互換も可能にできればいいな。

# おまけ

窓の杜 – 【NEWS】「AzPainter」「AzDrawing」の作者がWindows向けの全ソフトの開発終了を宣言

書いていることが Linux を初めたばかり丸解りなんだが…
AU○RA とかみたく寒いことにならなきゃいいけど。

Linux では Windows なんて比較にならないほど有名どころ以外は見向きもされないのだから外国人向けに英語でサンプルコードでも書いて自分が使う範囲だけ作ってたほうがマシだって。

Gio get_description

最近は溜まりまくっている Python 平書きコードの整理をしている。
といっても大半が PyGtk の覚書だったものを PyGI に作り直しであるが。
こんな状態だっったりする。

ソレより GNOME3 で gnomevfs が使えなくなったのをなんとかしないと。
Gio で「ファイルの種類」を得る方法が中々見つからず苦労した。
一旦 Content Type を得て変換するようだと解った。

とりあえず GLocalFile を作成。
query_info の引数に得たい情報文字列をコンマ区切りで突っ込む。
Gio.FILE_ATTRIBUTE_STANDARD_SIZE 等は只の #define された文字列。
GFileInfo
後はメソッドで取り出し、こんな感じかな。

ということで解ったところまで。
ファイルを何かドロップすると詳細が書き出されるウインドウ。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
import gi
try:
    gi.require_version("Gtk", "3.0")
except:
    print "This Program is GTK+ 3.0 or later."
    sys.exit()

from gi.repository import Gtk, Gdk, Gio

res = """----------
Name: {0}
Size: {1} byte
Content Type: {2}
Mime Type: {3}
Description: {4}
----------
"""

attr = (
    Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
    Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
    Gio.FILE_ATTRIBUTE_STANDARD_SIZE )

class Win(Gtk.Window):
    """
        Information at Dropped File
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        # DnD
        dnd_list = Gtk.TargetEntry.new("text/uri-list", 0, 0)
        self.drag_dest_set(
                Gtk.DestDefaults.MOTION |
                Gtk.DestDefaults.HIGHLIGHT |
                Gtk.DestDefaults.DROP,
                [dnd_list],
                Gdk.DragAction.MOVE )
        # GtkLabel
        self.label = Gtk.Label("Please drop your files")
        self.add(self.label)
        # Gtk.Window
        self.drag_dest_add_uri_targets()
        self.connect("drag-data-received", self.on_drag_data_received)
        self.set_title("dnd_type")
        self.show_all()

    def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
        uris = data.get_uris()
        s = ""
        for uri in uris:
            # Create GLocalFile
            f = Gio.file_new_for_uri(uri)
            # Create GFileInfo
            info = f.query_info(
                    ",".join(attr),
                    Gio.FileQueryInfoFlags.NONE,
                    None )
            # Anyway Get Content Type
            ct = info.get_content_type()
            # splintf
            s += res.format(
                    info.get_display_name(),
                    info.get_size(),
                    ct,
                    Gio.content_type_get_mime_type(ct),
                    Gio.content_type_get_description(ct) )
        self.label.set_text(s)

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.akemi.homura",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)
        
    def on_activate(self, application, user_data=None):
        w = Win()
        w.set_application(self)
    
if __name__ == "__main__":
    app = App()
    app.run(None)

うんコレで拡張子無しでもキチンと description が得られる。
Mime Type って Content Type と何が違うのかイマイチ解らない。

後は Last Write Time 等なんだがまだ解っていない。
os.stat から得られるのは知っているが Gio でやりたいので。

てゆーか昔のコードはほとんど役に立たなくなってしまったような。
もう少し纏めたら又 Tips ページを作ります。

ll command and GtkSourceView3

Fedora 16 の update 通知があったので適用。
100 個もあるのか、まぁ chrome や flash も含まれていることだし。

PyGI が上書きされちゃったみたいだけど GBoxed 関連は修正されていた。
修正してくれたようで、放置されると疑ってごめんなさい。

ところで Fedora は ~/.bash_profile, ~/.bashrc どちらにもエイリアス指定が無い。
けど ll が通る、’ls -alF’ ではなく ‘ls -l’ のエイリアスのようだが。
Ubuntu から乗り換えて半年以上たつのに今頃違いに気がついた私って…

Ubuntu は ~/.bashrc で指定だけど Fedora はチト違うんだね。
てか Fedora は which コマンドのみで alias も探せるようにしているのか。
alias コマンドで充分だと思うけど。

【88】シェル(bash)を自分用にカスタマイズ【alias の覚書】 – 分室の分室

ついでに見つけたページ、lls は早速真似させていただくことにする。
Content Type でソートできれば拡張子もいらないけどさすがに無理か。

**********

ところで上画像のように Update に GtkSourceView があった。
何って Gedit や Anjuta の GTK+ 製エディタ部品そのものです。
テキストの DnD 編集で Gedit が落ちるのは、直っていなかった…

思いついた、コレを使って Gjot2 の代わりでも試しに作ってみようかなと。
Gedit のハイライト設定をパクれば表示は Gedit と同じになって便利かも。

API ドキュメントは以下にある。
GtkSourceView 3 Reference Manual

PyGI からは GtkSource の import で使える。
とりあえず基本読み書きと行番号表示のみの最小限コード。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio, Gdk, GtkSource

class TextEditor(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        # GtkUIManager and GtkAccelGroup
        self.uimanager = Gtk.UIManager()
        accelgroup = self.uimanager.get_accel_group()
        self.add_accel_group(accelgroup)
        # GtkActionGroup
        self.actiongroup = Gtk.ActionGroup("editor_menu")
        # GtkActionEntry
        ae = [  ("open", Gtk.STOCK_OPEN, "_Open", "<Control>O", "open", self.on_open),
                ("save", Gtk.STOCK_SAVE, "_Save", "<Control>S", "save", self.on_save),
                ("save_as", Gtk.STOCK_SAVE_AS, "Save as...", "<Shift><Control>S", "save as", self.on_save_as),
                ("quit", Gtk.STOCK_QUIT, "Quit", "<Control>Q", "quit", self.on_quit),
                ("file", None, "_File") ]
        self.actiongroup.add_actions(ae)
        self.uimanager.insert_action_group(self.actiongroup, 0)
        self.uimanager.add_ui_from_string(menu_xml)
        # MenuBar
        menubar = self.uimanager.get_widget('/MenuBar')
        # Toolbar and Style
        toolbar = self.uimanager.get_widget('/ToolBar')
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        style = toolbar.get_style_context()
        Gtk.StyleContext.add_class(style, Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
        # StatusBar
        statusbar = Gtk.Statusbar()
        #
        #
        # Create GtkSourceView
        self.view = GtkSource.View()
        # Show Line Number
        self.view.set_show_line_numbers(True)
        #
        #
        # Add
        sw = Gtk.ScrolledWindow()
        sw.add(self.view)
        vbox = Gtk.VBox()
        vbox.pack_start(menubar, False, True, 0)
        vbox.pack_start(toolbar, False, True, 0)
        vbox.pack_start(sw, True, True, 0)
        vbox.pack_start(statusbar, False, True, 0)
        self.add(vbox)
        # self
        self.open_filename = ""
        self.resize(300, 300)
        self.set_title("Text Editor")
        self.show_all()
        self.set_focus(self.view)

    def on_open(self, widget, data=None):
        dlg = Gtk.FileChooserDialog(
                "Open File",
                self,
                Gtk.FileChooserAction.OPEN,
                (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 
                Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT))
        r = dlg.run()
        if r == Gtk.ResponseType.ACCEPT:
            self.open_filename = dlg.get_filename()
            f = open(self.open_filename, "r")
            t = f.read()
            f.close()
            buf = self.view.get_buffer()
            buf.set_text(t)
        dlg.destroy()

    def on_save(self, widget, data=None):
        if self.open_filename == "":
            self.on_save_as(widget)
        else:
            buf = self.view.get_buffer()
            t = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False)
            f = open(self.open_filename, "w")
            f.write(t)
            f.close()

    def on_save_as(self, widget, data=None):
        dlg = Gtk.FileChooserDialog(
                "Save File",
                self,
                Gtk.FileChooserAction.SAVE,
                (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 
                Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT))
        r = dlg.run()
        if r == Gtk.ResponseType.ACCEPT:
            buf = self.view.get_buffer()
            t = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False)
            self.open_filename = dlg.get_filename()
            f = open(self.open_filename, "w")
            f.write(t)
            f.close()
        dlg.destroy()

    def on_quit(self, widget, data=None):
        self.emit("destroy")

menu_xml = """<ui>
    <menubar name="MenuBar">
        <menu action="file">
            <menuitem action="open"/>
            <menuitem action="save"/>
            <menuitem action="save_as"/>
            <separator/>
            <menuitem action="quit"/>
        </menu>
    </menubar>
    <toolbar name="ToolBar">
        <toolitem action="open"/>
        <toolitem action="save"/>
    </toolbar>
</ui>"""

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.editor",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)
        
    def on_activate(self, data=None):
        w = TextEditor()
        w.set_application(self)
    
if __name__ == "__main__":
    app = App()
    app.run(None)

FileChooserDialog の save で WARNNING が出て困っていたけど…
Gedit でも出るヤン、FileChooserDialog 自体のバグだろう。

読み書きに関しては Gio を使うべきなんだろうけど今はワカンネ!
これで問題なく読み書きできるから充分かと。

ついでに今頃解ったけど destroy を emit すれば閉じることができる。
GtkApplication が管理するので delete-event は使えない。

しかし GtkSourceView のみなら DnD 編集しても落っこちないのね。
Gedit 側のバグだったみたい、とにかく気長に修正待ちするけど。

ということで Gedit と同じ行番号表示ができるエディタでっきあっがりぃ。

コレを上手く使って Gjot2 の代わりっぽいを作りたいな。
平書きな Python コードも既に 1000 個を越えているしもっとまとめたい。
っっってその大半が PyGtk のコード、PyGI に書き直しメンドクセ!
まあその財産があるからこんなコードが数時間で書けるのであるが。

ClutterActor on GtkContainer

Clutter は魅力的ではあるが描写の全部を自前で用意する必要がある。
つまり GtkButton, GtkListView のような Widget は無い。

それなら ClutterActor を GtkContainer 上に pack できればよくね?
やってみると普通に拒否、全くの別物だからそりゃ当然の話だ。

しかし GNOME3 自体はしっかり連携している。
知らないだけで何か方法があるはずだ。

/usr/lib64/girepository-1.0
をよく見ると GtkClutter というものがある。

Clutter Gtk integration on Vimeo

WPF のような新規ウイジェットではなく GTK+ をエフェクトするみたい。
そりゃ GTK+3 自体が新規だし元から連携していてもおかしくない。

Clutter-Gtk 1.0.2 Reference Manual

どうやら GtkClutterWindow は GtkWindow のサブクラス。
ClutterActor が表面に張り付いている状態って認識でいいのかな?
Python からは get_stage() で ClutterStage が取得できそうである。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
from gi.repository import Gtk, Clutter, GtkClutter, ClutterX11

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

#ClutterX11.set_use_argb_visual(True)
#ClutterX11.set_display(ClutterX11.get_default_display())
#ClutterX11.disable_event_retrieval()
Clutter.init(sys.argv)

w = GtkClutter.Window()
w.connect("destroy", quit)
w.set_title("GtkClutter.Window")
w.resize(300, 200)

stage = w.get_stage()
stage.set_size(300.0, 200.0)
w.show_all()
Gtk.main()

WARNING 出まくり、初期化とか色々やってみたけど駄目だった。
とりあえず get_stage() だけはできている、使い方が悪いのだろう。
ついでにこの場合 delete-event ではなく destroy 指定でないと終了しない。

直感でやっても上手くいかない、もっと良い方法は無いだろうかと検索。

Stage Widget

gtk_clutter_embed_new ()
で作成した Object なら Gtk+ コンテナに入れられるようだ。
試すと上手くいったので ClutterGst でやってみる。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
from gi.repository import Gtk, Clutter, GtkClutter, ClutterGst

class ClutterEmbedTest(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        # Video Texture
        tx = ClutterGst.VideoTexture()
        tx.set_uri("file:///home/sasakima-nao/movie/bike/ninjya1000_1.mp4")
        # Create the Clutter Widget
        embed = GtkClutter.Embed.new()
        embed.connect("size-allocate", self.on_size_allocate, tx)
        # ClutterStage
        stage = embed.get_stage()
        stage.add_actor(tx)
        # Buttons
        play = Gtk.Button(stock=Gtk.STOCK_MEDIA_PLAY)
        play.connect("clicked", self.on_play, tx)
        # packing
        vbox = Gtk.VBox()
        vbox.pack_start(embed, True, True, 0)
        vbox.pack_start(play, False, False, 0)
        self.add(vbox)
        self.set_title("ClutterEmbedTest")
        self.connect("delete-event", self.on_quit)
        self.show_all()

    def on_size_allocate(self, widget, allocation, tx):
        w = widget.get_allocation().width
        h = widget.get_allocation().height
        tx.set_size(w, h)

    def on_play(self, widget, tx):
        tx.set_playing(True)

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

if __name__ == '__main__':
    # initialized
    Clutter.init(sys.argv)
    ClutterGst.init(0, "")
    ClutterEmbedTest()
    Gtk.main()

よしコレなら何も警告は出ない。
警告が出ないようガンバった WPF 3.0 アプリが 3.5 で無意味にされた過去が…
初心者はワケワカだろうけど、こんな苦労も数年後には無意味になるのでしょう。

やはり親ウインドウサイズに連動しないので size-allocate シグナルを利用。
ClutterActor のサイズは float だけど int のままサイズ変更できた。
コレで GTK+ Widget と ClutterActor の共存アプリケーションができる。

アスペクト比保持とかはどうやるんだろう?
というか最初のビデオでは GTK+ widget にエフェクトが掛かっていたような…
まだ先は長い、リファレンスマニュアル無しで作るのは狂気だがコレが楽しい。

ClutterGst PyGI

どうも GTK3 は xid を得る方法も全然違うようで。
python – DrawingArea Cannot Get XID – Stack Overflow

expose-event が draw に変わっていたりでもうよく解らない。
PyGI で Gst を扱う情報が見つからず全然アプリ開発が進まない。

ClutterGst というのがあるけどよく解らないので検索。

PyClutter video tutorial ? Hindsight Labs

こんなのを見つけたけど古いバインディングだ。
PyGI で書き直して実験してみることにした。

いくら探しても ClutterGst の Reference Manual が見つからない。
しかたがないので dir() で漁って例外で確認しているけどよくワカラン。
例外で playbin2 がどうとか出た、描写エンジンは playbin2 なんだね。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
from gi.repository import Clutter, ClutterGst

class TestStage(Clutter.Stage):
    def __init__(self):
        Clutter.Stage.__init__(self)
        # ClutterGst
        self.tx = ClutterGst.VideoTexture()
        self.tx.set_uri("file:///home/sasakima-nao/movie/bike/ninjya1000.flv")
        self.tx.set_playing(True)
        self.add_actor(self.tx)
        # self
        self.connect("destroy", self.quit)
        self.set_title("ClutterGst Test")
        # Not linked to the window size
        self.set_size(400, 400)
        self.show_all()

    def quit(self, widget):
        Clutter.main_quit()

if __name__ == '__main__':
    init = Clutter.init(sys.argv)
    if init[0] == Clutter.InitError.SUCCESS:
        # Param (argc, argv) ???
        ClutterGst.init(0, "")
        TestStage()
        Clutter.main()

ClutterGst も専用の初期化が必要であるようです。
パラメータが int と str だったけどもしかして C 言語方式?
sys.argv シーケンスでは展開してくれない。

pygst みたく Window 枠に合わせて描写されない。
当然 DirectShow みたくウインドウより大きな映像ははみ出す。

絶対位置描写だと又ウインドウサイズ計算が面倒くさくなる…
設定とかあるのかな、もう少し弄くってみるけど。