L'Isola di Niente
L'Isola di Niente » PyGObject Tips » 線や図形の描写

線や図形の描写

GTK2 ではグラフィックコンテキストを得て描写という手順でした。
GTK3 では描写処理は cairo を使うようになりました。
cairo 自体は GTK2 からも使えましたが GTK3 からは以前の方法は使えません。

GtkDrawingArea

GtkDrawingArea を利用するのは GTK2 時と同様。
しかし expose-event シグナルが draw シグナルに代わって引数も違っています。

引数に cairo_t が含まれるようになり cairo_t を利用して描写を行う手順になっています。
文字列の描写に pango が必要なくなる等で大幅に解りやすくなりました。
というよりグラフィックコンテキスト(デバイスコンテキスト)が解り辛過ぎ。

PyGtk (PyGtk は Python3 からは使えません)
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import gtk
import pango

class ExposeWin(gtk.Window):
    """
        PyGtk(GTK2) Version
    """
    def __init__(self):
        gtk.Window.__init__(self)
        self.connect("delete-event", gtk.main_quit)
        drawingarea = gtk.DrawingArea()
        drawingarea.connect("expose-event", self.on_expose_event)
        self.add(drawingarea)
        self.show_all()

    def on_expose_event(self, widget, event):
        # サイズ取得
        width = widget.allocation.width
        height = widget.allocation.height
        # グラフィック・コンテキストを得る
        gc = widget.style.fg_gc[gtk.STATE_NORMAL]
        # 赤で塗りつぶす
        gc.set_rgb_fg_color(gtk.gdk.Color(1.0, 0.0, 0.0))
        widget.window.draw_rectangle(gc, True, 0, 0, width, height)
        # 青色で文字列
        gc.set_rgb_fg_color(gtk.gdk.Color(0.0, 0.0, 1.0))
        font_desc = pango.FontDescription('Monospace 24')
        layout = widget.create_pango_layout ("MamiMami")
        layout.set_font_description(font_desc)
        widget.window.draw_layout(gc, 0, 20, layout)
        # 緑で線
        gc.set_rgb_fg_color(gtk.gdk.Color(0.0, 1.0, 0.0))
        widget.window.draw_line(gc, width, 0, 0, height)

ExposeWin()
gtk.main()

PyGObject
#!/usr/bin/env python3

from gi.repository import Gtk

class DrawWin(Gtk.Window):
    """
        PyGI(GTK3) Version
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        self.connect("delete-event", Gtk.main_quit)
        drawingarea = Gtk.DrawingArea()
        drawingarea.connect("draw", self.on_draw)
        self.add(drawingarea)
        self.show_all()

    def on_draw(self, widget, cr):
        # サイズ取得
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()
        # 赤で塗りつぶす
        cr.set_source_rgb(1.0, 0.0, 0.0)
        cr.paint()
        # 青で文字列
        cr.set_source_rgb(0.0, 0.0, 1.0)
        cr.select_font_face("Monospace", 0, 0)
        cr.set_font_size(24.0)
        cr.move_to(0, 20+24)
        cr.show_text("MamiMami")
        # 緑で線
        cr.set_source_rgb(0.0, 1.0, 0.0)
        cr.set_line_width(1.0)
        cr.move_to(width, 0)
        cr.line_to(0, height)
        cr.stroke()

DrawWin()
Gtk.main()
文字列で描写サイズが少し違うのと基準位置が左下になっていることに注意。

画像の描写

GdkPixbuf を作成し画像を読み込んでおいて
Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
cr.paint()
と draw シグナルで GdkPixbuf を cairo_t に指定するだけです。
以下はファイルをドロップすると画像を DrawingArea に書き出すだけの例。
iPhone にも対応。
#!/usr/bin/env python3
 
from gi.repository import Gtk, Gdk, Gio, GLib, GdkPixbuf
import sys
 
class DrawWin(Gtk.ApplicationWindow):
    """
        DnD 画像ビューアー
    """
    def __init__(self, app):
        """
            Window 全体をターゲットにする場合のコード
            必要に応じて Widget 単体を指定でもいい。
        """
        Gtk.ApplicationWindow.__init__(self, application=app)
        # Pixbuf
        self.pixbuf = None
        # DrawingArea
        self.da = Gtk.DrawingArea()
        self.da.connect("draw", self.on_draw)
        self.add(self.da)
        # DnD 処理、ドロップターゲットを self 全体に割り当てる
        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 )
        self.drag_dest_add_uri_targets()
        # self
        self.set_title("DnD Picture Viewer")
        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:
            # GLib.filename_from_uri(uri)[0] は駄目
            # iPhone 上 URI (afc://***) 等を変換できない
            f = Gio.File.new_for_uri(uri)
            path = f.get_path()
            # Pixbuf 作り直し
            self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
            # 再描写要求
            self.da.queue_draw()
        except Exception as e:
            # 画像では無かった場合
            print(e.message)
 
    def on_draw(self, widget, cr):
        """
            背景を黒にして画像を表示
        """
        # DrawingArea のサイズ
        d_width = widget.get_allocated_width()
        d_height = widget.get_allocated_height()
        # 黒で塗りつぶす
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, d_width, d_height)
        cr.fill()
        # Pixbuf が set されているなら描写する
        if self.pixbuf:
            # Pixbuf のサイズ
            p_width = self.pixbuf.get_width()
            p_height = self.pixbuf.get_height()
            # 小さい方に合わせる計算
            width = 0
            height = 0
            if (d_width * p_height) > (d_height * p_width):
                width = p_width * d_height / p_height
                height = d_height
            else:
                width = d_width
                height = p_height * d_width / p_width
            # リサイズされた Pixbuf 作成
            pixbuf = GdkPixbuf.Pixbuf.scale_simple(self.pixbuf, width, height, GdkPixbuf.InterpType.BILINEAR)
            # セットして表示
            Gdk.cairo_set_source_pixbuf(cr, pixbuf, (d_width-width)/2, (d_height-height)/2)
            cr.paint()

class DrawApp(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self,
            application_id="apps.test.dnd",
            flags=Gio.ApplicationFlags.FLAGS_NONE)

    def do_activate(self):
        DrawWin(self)

app = DrawApp()
app.run(sys.argv)

img2/drawingarea.png

SVG 画像の描写

SVG は解説するまでもなく拡大しても崩れないベクターデータの画像です。
GtkPixbuf は SVG を表示できますがラスタライズされるため拡大に弱い。
拡大等の加工を SVG で行う場合は Rsvg というライブラリを利用する。
#!/usr/bin/env python3
 
from gi.repository import Gtk, Gdk, Gio, GLib, Rsvg
import cairo, sys
 
class SvgWin(Gtk.ApplicationWindow):
    def __init__(self, app):
        """
            DnD SVG Viewer
        """
        Gtk.Window.__init__(self, application=app)
        # svg
        self.svg = None
        # DrawingArea
        self.drawingarea = Gtk.DrawingArea()
        self.drawingarea.connect("draw", self.on_draw)
        self.add(self.drawingarea)
        # 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 )
        self.drag_dest_add_uri_targets()
        # self
        self.set_title("DnD SVG Viewer")
        self.resize(200, 200)
        self.show_all()
 
    def do_drag_data_received(self, drag_context, x, y, data, info, time):
        """
            On Drop
        """
        uri = data.get_uris()[0]
        try:
            f = Gio.File.new_for_uri(uri)
            path = f.get_path()
            # Recreate svg
            self.svg = Rsvg.Handle.new_from_file(path)
            # invalidate
            self.drawingarea.queue_draw()
        except Exception as e:
            # SVG 画像では無かった場合
            print(e)
 
    def on_draw(self, widget, cr):
        """
            matrix は 1,4 番目引数で x,y 倍率を指定
            cr.paint() ではなく svg.render_cairo(cr)
        """
        # DrawingArea のサイズ
        d_width = widget.get_allocated_width()
        d_height = widget.get_allocated_height()
        # 黒で塗りつぶす
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, d_width, d_height)
        cr.fill()
        # SVG が set されているなら描写する
        if self.svg:
            # 10 倍に拡大
            matrix = cairo.Matrix(10, 0, 0, 10, 0, 0)
            cr.transform (matrix)
            # レンダリング
            self.svg.render_cairo(cr)
 
class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self,
                application_id="apps.test.svg",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
         
    def do_activate(self):
        SvgWin(self)

app = App()
app.run(sys.argv)

img2/rsvg.png

チュートリアル

公式(C 言語)
Cairo Tutorial

for Python
Cairo Tutorial for Python Programmers

又こちらも C 言語ですが公式のサンプルコードも充実しています。
Cairo samples

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

以下はサンプルコードのコピペ。
#!/usr/bin/env python3

from gi.repository import Gtk
import cairo

class DrawTest(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        da = Gtk.DrawingArea()
        da.connect("draw", self.on_draw)
        self.add(da)
        self.connect("delete-event", Gtk.main_quit)
        self.resize(300, 300)
        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)
        # これでチュートリアルをコピペできる
        cr.set_source_rgb(0, 0, 0)
        cr.move_to(0, 0)
        cr.line_to(1, 1)
        cr.move_to(1, 0)
        cr.line_to(0, 1)
        cr.set_line_width(0.2)
        cr.stroke()

        cr.rectangle(0, 0, 0.5, 0.5)
        cr.set_source_rgba(1, 0, 0, 0.80)
        cr.fill()

        cr.rectangle(0, 0.5, 0.5, 0.5)
        cr.set_source_rgba(0, 1, 0, 0.60)
        cr.fill()

        cr.rectangle(0.5, 0, 0.5, 0.5)
        cr.set_source_rgba(0, 0, 1, 0.40)
        cr.fill()

DrawTest()
Gtk.main()

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

描写色を決める set_source_rgb, set_source_rgba は 0.0〜1.0 です。
HTML ではありませんので 255 とか決め打ちできません。

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

色々な描写

ドロップシャドウ、マスク、グラデーションのテキトーな例。
#!/usr/bin/env python3

from gi.repository import Gtk
import cairo

class DrawTest(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        da = Gtk.DrawingArea()
        da.connect("draw", self.on_draw)
        # 以下を有効にするとダブルバッファリングが無効になる
        #da.set_double_buffered(False)
        self.add(da)
        self.connect("delete-event", Gtk.main_quit)
        self.resize(300, 300)
        self.show_all()

    def on_draw(self, widget, cr):
        # サイズを得る
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()
        # 水白で塗りつぶす
        cr.set_source_rgb(0, 1, 1)
        cr.rectangle(0, 0, width, height)
        cr.fill()
        # 青で四角形を書く
        cr.set_source_rgb(0, 0, 1)
        cr.rectangle(20, 20, width-40, height-40)
        cr.stroke()
        # 黄色で斜めにぶった切る
        cr.set_source_rgb(1, 1, 0)
        cr.move_to(50, 50)
        cr.line_to(width-50, height-50)
        cr.stroke()
        # 文字列の偽ドロップシャドウ
        cr.set_font_size(100)
        cr.set_source_rgba(0.5, 0.5, 0.5, 0.5)
        cr.move_to(5, height/2+5)
        cr.show_text("文字列")
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.move_to(0, height/2)
        cr.show_text("文字列")
        # マスクのテキトーな例
        w = width / 2
        h = height / 2
        linear = cairo.LinearGradient(0, 0, w, h)
        linear.add_color_stop_rgb(0.5, 0.8, 0.0, 0.0)
        linear.add_color_stop_rgb(1.0, 0.0, 0.8, 0.0)
        radial = cairo.RadialGradient(w, h, (w+h)/4, w, h, w+h)
        radial.add_color_stop_rgba(0.0, 0.0, 0.0, 0.0, 0.7)
        radial.add_color_stop_rgba(0.5, 0.0, 0.0, 0.0, 0.0)
        cr.set_source(linear)
        cr.mask(radial)

DrawTest()
Gtk.main()

img/draw.png
Copyright(C) sasakima-nao All rights reserved 2002 --- 2017.