Paepoi

Paepoi » PyGObject Tips » Gtk(PyGObject) Tips | Draw(画像,PDF,SVG,図形)

Gtk(PyGObject) Tips | Draw(画像,PDF,SVG,図形)

# 最終更新日 2019.08.18

2019 年現在の仕様に追記と書き換え。
PyGtk 関連の削除、PDF の描写を追加、拡縮を Matrix 計算に変更、他書き直しと追記を沢山。
タイトルを [線や図形の描写] から [Draw(画像,PDF,SVG,図形)] に変更。

draw シグナル
GTK3 では描写処理はすべて cairo を使うようになりました。
cairo 自体は GTK2 からも使えましたが GTK3 は以前の方法は使えません。

draw シグナルは GtkWidget が提供しています。
GtkDrawingArea を利用が鉄板ですが GtkWidget のサブクラスならどの Widget にでも描写できます。
GtkApplicationWindow に直接描写だとタイトルバー部分まで描写される困ったことになるので注意。

シグナルの引数に cairo_t が含まれていてそれを利用して描写を行う手順になっています。
文字列の描写に pango が必要ない等で大幅に解りやすくなりました。
描写の原点は左上となっています。

move_to, line_to のようにお馴染みなピクセル単位の絶対位置移動は他の OS と同様。
rel_line_to のように rel 付きの関数は相対位置での指定が可能です。

GTK+ はダブルバッファリングがデフォルトで行われておりチラツキはありません。
それだと困る場合( GStreamer を使う等)にだけ無効にさせる。
area.set_double_buffered(False)

描写色を決める set_source_rgb, set_source_rgba は 0.0〜1.0 です。
HTML ではありませんので 255 とか決め打ちできません。
又 PyGObject では float 指定の所に整数を書いても処理してくれます。
以上をふまえて。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    '''
        draw のサンプル
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        area = Gtk.DrawingArea()
        area.connect('draw', self.on_draw)
        self.add(area)
        self.resize(300, 100)
        self.show_all()

    def on_draw(self, widget, cr):
        # サイズ取得
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # 黒で全体を塗りつぶす、RGB を 0.0 から 1.0 の範囲で指定
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, aw, ah)
        cr.fill()
        # グレーで線を引く
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.set_line_width(5.0)
        cr.move_to(aw, 0)
        cr.line_to(0, ah)
        cr.stroke()
        # 白で文字列描写、基準位置は左下、改行未対応
        cr.set_source_rgb(1, 1, 1)
        cr.select_font_face('Monospace', 0, 0)
        cr.set_font_size(48)
        cr.move_to(5, ah - 5)
        cr.show_text('AIAIAIAIAI')
        # 等幅になっているのを確認

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)
draw/draw.png
画像の描写
以下はファイルをドロップすると画像を DrawingArea に書き出すだけの例。
Matrix 計算でリサイズしても中心に表示されるサンプルコードです。
#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        '''
            DnD 画像ビューアー
        '''
        Gtk.ApplicationWindow.__init__(self, application=app, title='DnD Picture Viewer')
        # Pixbuf
        self.pixbuf = None
        # GtkDrawingArea
        self.area = Gtk.DrawingArea()
        self.area.connect('draw', self.on_draw)
        self.add(self.area)
        # DnD 処理、これだけになりました
        self.drag_dest_add_uri_targets()
        # self
        self.resize(200, 200)
        self.show_all()

    def do_drag_data_received(self, drag_context, x, y, data, info, time):
        '''
            ファイルがドロップされたので GtkPixbuf の作成と再描写要求
        '''
        uri = data.get_uris()[0]
        try:
            # macOS 上 URI (smb://***) 等も対応
            f = Gio.File.new_for_uri(uri)
            path = f.get_path()
            # Pixbuf 作り直し
            self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
            # 再描写要求
            self.area.queue_draw()
        except Exception as e:
            # 画像では無かった場合
            print(e)

    def on_draw(self, widget, cr):
        '''
            背景を黒にして画像を表示
            Matrix による拡縮計算は PDF, SVG でも応用できる
        '''
        # DrawingArea のサイズ
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # 黒で塗りつぶす
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, aw, ah)
        cr.fill()
        # Pixbuf が set されているなら描写する
        if self.pixbuf:
            # Pixbuf のサイズ
            w = self.pixbuf.get_width()
            h = self.pixbuf.get_height()
            # 小さい方に合わせる Matorix 計算
            if aw * h > ah * w:
                n = ah/h
                matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n/2, 0)
                cr.transform(matrix)
            else:
                n = aw/w
                matrix = cairo.Matrix(n, 0, 0, n, n, ah/2-h*n/2)
                cr.transform(matrix)
            Gdk.cairo_set_source_pixbuf(cr, self.pixbuf, 0, 0)
            cr.paint()

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)
draw/picture.png
PDF の描写
PDF の読み込みは Poppler というライブラリを使います、GNOME なら最初からあります。
以下はファイルをドロップすると PDF を DrawingArea に書き出すだけの例。
PDF にはページがあるのでページめくりできるようにしました。
Matrix 計算でリサイズしても中心に表示されるサンプルコードです。
#!/usr/bin/env python3

import sys, gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Poppler', '0.18')
from gi.repository import Gtk, Gdk, Gio, Poppler

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        '''
            DnD PDF ビューアー
            Space で次のページ、BackSpace で戻る
        '''
        Gtk.ApplicationWindow.__init__(self, application=app, title='DnD PDF Viewer')
        # PDF
        self.pdf = None
        self.pdfpage = None
        self.page_num = 0
        self.page_max = 0
        # GtkDrawingArea
        self.area = Gtk.DrawingArea()
        self.area.connect('draw', self.on_draw)
        self.add(self.area)
        # DnD 処理、これだけになりました
        self.drag_dest_add_uri_targets()
        # self
        self.resize(200, 200)
        self.show_all()

    def do_drag_data_received(self, drag_context, x, y, data, info, time):
        '''
            ファイルがドロップされたので PDF の読み込みと再描写要求
        '''
        uri = data.get_uris()[0]
        f = Gio.file_new_for_uri(uri)
        try:
            self.pdf = Poppler.Document.new_from_gfile(f)
            self.page_num = 0
            self.page_max = self.pdf.get_n_pages()
            self.pdfpage = self.pdf.get_page(0)
            # 再描写要求
            self.area.queue_draw()
            # タイトルバーに現在のページナンバーを表示
            self.props.title = f'{self.page_num+1}/{self.page_max}'
        except Exception as e:
            # PDFでは無かった場合
            print(e)

    def do_key_press_event(self, event):
        if self.pdf:
            if event.keyval == Gdk.KEY_space:
                if self.page_num < self.page_max - 1:
                    self.page_num += 1
                    self.pdfpage = self.pdf.get_page(self.page_num)
                    self.area.queue_draw()
                    self.props.title = f'{self.page_num+1}/{self.page_max}'
            elif event.keyval == Gdk.KEY_BackSpace:
                if self.page_num > 0:
                    self.page_num -= 1
                    self.pdfpage = self.pdf.get_page(self.page_num)
                    self.area.queue_draw()
                    self.props.title = f'{self.page_num+1}/{self.page_max}'

    def on_draw(self, widget, cr):
        '''
            背景を黒にして PDF をページ毎表示
        '''
        # DrawingArea のサイズ
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # 黒で塗りつぶす
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, aw, ah)
        cr.fill()
        # self.pdf が set されているなら描写する
        if self.pdf:
            # PDF のページサイズ
            w, h = self.pdfpage.get_size()
            # 小さい方に合わせる Matrix 計算、pixbuf と同じ
            if aw * h > ah * w:
                n = ah/h
                matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n/2, 0)
                cr.transform(matrix)
            else:
                n = aw/w
                matrix = cairo.Matrix(n, 0, 0, n, n, ah/2-h*n/2)
                cr.transform(matrix)
            # PDF は背景が透過なので PDF の部分だけ白で塗り潰す
            cr.set_source_rgb(1, 1, 1)
            cr.rectangle(0, 0, w, h)
            cr.fill()
            self.pdfpage.render(cr)

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)
draw/poppler.png
SVG 画像の描写
SVG は解説するまでもなく拡大しても崩れないベクターデータの画像です。
GtkPixbuf は SVG を表示できますがラスタライズされるため拡大に弱い。
拡大等を行う場合は Rsvg というライブラリを利用する、GNOME なら最初からあります。
#!/usr/bin/env python3

import sys, gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Rsvg', '2.0')
from gi.repository import Gtk, Gdk, Gio, Rsvg

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        '''
            DnD SVG ビューアー
        '''
        Gtk.ApplicationWindow.__init__(self, application=app, title='DnD SVG Viewer')
        # svg
        self.svg = None
        # GtkDrawingArea
        self.area = Gtk.DrawingArea()
        self.area.connect('draw', self.on_draw)
        self.add(self.area)
        # DnD 処理、これだけになりました
        self.drag_dest_add_uri_targets()
        # self
        self.resize(200, 200)
        self.show_all()

    def do_drag_data_received(self, drag_context, x, y, data, info, time):
        '''
            ファイルがドロップされたので SVG の読み込みと再描写要求
        '''
        uri = data.get_uris()[0]
        try:
            f = Gio.File.new_for_uri(uri)
            path = f.get_path()
            self.svg = Rsvg.Handle.new_from_file(path)
            # 再描写要求
            self.area.queue_draw()
        except Exception as e:
            # SVG 画像では無かった場合
            print(e)

    def on_draw(self, widget, cr):
        '''
            背景を白にして SVG を表示
            タグによりにズレる場合があるみたい
        '''
        # DrawingArea のサイズ
        aw = widget.get_allocated_width()
        ah = widget.get_allocated_height()
        # 白で塗りつぶす
        cr.set_source_rgb(1, 1, 1)
        cr.rectangle(0, 0, aw, ah)
        cr.fill()
        # self.pdf が set されているなら描写する
        if self.svg:
            # PDF のページサイズ
            w = self.svg.get_dimensions().width
            h = self.svg.get_dimensions().height
            # 小さい方に合わせる Matrix 計算、pixbuf と同じ
            if aw * h > ah * w:
                n = ah/h
                matrix = cairo.Matrix(n, 0, 0, n, aw/2-w*n/2, 0)
                cr.transform(matrix)
            else:
                n = aw/w
                matrix = cairo.Matrix(n, 0, 0, n, n, ah/2-h*n/2)
                cr.transform(matrix)
            self.svg.render_cairo(cr)

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)
draw/rsvg.png
図形の描写
2D 図形描写は実際のアプリケーションでは現在ほぼ使われないのでパスしたいのが本音。
3D の Clutter(OpenGL ES) を GNOME は提供していますしゲームなら Unity 等を勉強したほうがいい。
とはいえ基本中の基本だし 3D をやるにも必須な知識も得られると思う。
cairo はチュートリアルサイトも多いのでそちらを活用するということで。

cairo 公式チュートリアル(C 言語)
Cairo Tutorial
cairo 公式のサンプルコード(C 言語)
Cairo samples

for Python
Cairo Tutorial for Python Programmers

Python チュートリアルでの cairo は gir ではなくモジュール側の cairo です。
お試しをする場合の import に注意してください。
from gi.repository import Gtk #,cairo
import cairo
又チュートリアルサイトの数値は異常に小さくそのままコピペでは 1px 分にしか描写されません。
cr.scale(width, height) にて cairo_t の大きさを指定すれば引き延ばしされて描写できます。
又はピクセル単位で大きな数値に変更することもできます。

以下はコピペ用テンプレート。
#!/usr/bin/env python3

import sys, gi, cairo
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')
        area = Gtk.DrawingArea()
        area.connect('draw', self.on_draw)
        self.add(area)
        self.show_all()

    def on_draw(self, widget, cr):
        # DrawingArea の大きさを得る
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()
        # cairo_t を引き伸ばす
        cr.scale(width, height)
        #
        # 以下にチュートリアルのコードをコピペ
        #

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)

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