試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

PyGTKでGUI部品を自前で描画する(後半)

PyGTKでGUI部品を自前で描画する(前半)」の内容を踏まえた上でのコード例を

  • GDKのみ使用
  • Cairoを主に使用

の2つそれぞれについて貼り付ける。
ウィンドウサイズを変更すると、それぞれ、大きさの情報を利用した部分については伸びたり縮んだりする。

GDKのみ使用したもの

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

import sys
import os
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import pango
  import gtk
except:
  sys.exit(1)


class MyDrawingArea(gtk.DrawingArea):
  """
  自分で描画するGUI部品
  """
  def __init__(self):
    gtk.DrawingArea.__init__(self)
    self.__gc = None
    self.__width = self.__height = 0
    self.__fontdesc = pango.FontDescription('Monospace 10')
    self.__layout = self.create_pango_layout('This is a TEST.')
    self.connect('size-allocate', self.__on_self_size_allocate)
    self.connect('realize', self.__on_self_realize)
    self.connect('expose-event', self.__on_self_expose_event)
  def __on_self_realize(self, widget):
    """
    一部の初期化
    """
    print '__on_self_realize()'
    # GCオブジェクトはこの段階でないと作れない
    self.__gc = widget.window.new_gc()  # 描画に必要な情報を持つ
    # 線の属性
    self.__gc.set_line_attributes(1, gtk.gdk.LINE_SOLID,
                                  gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
    # 色
    self.__gc.set_rgb_fg_color(gtk.gdk.Color('#cc3366'))
    # 同様に色の数だけGCオブジェクトを用意
    self.__gc2 = widget.window.new_gc()  # 描画に必要な情報を持つ
    self.__gc2.set_rgb_fg_color(gtk.gdk.Color('#336600'))
    self.__gc3 = widget.window.new_gc()
    self.__gc3.set_rgb_fg_color(gtk.gdk.Color('#003399'))
    self.__gc4 = widget.window.new_gc()
    self.__gc4.set_rgb_fg_color(gtk.gdk.Color('#996633'))
  def __on_self_size_allocate(self, widget, allocation):
    """
    GUI部品のサイズの変更を受け取る
    """
    print '__on_self_size_allocate()'
    self.__width = allocation.width
    self.__height = allocation.height
  def __on_self_expose_event(self, widget, event):
    """
    描画を行う
    同じ領域に重なる場合、後で描いたほうが上になる
    """
    print '__on_self_expose_event()'
    self.__fontdesc.set_size(self.__width * 40)
    self.__layout.set_font_description(self.__fontdesc)
    # 線を描画
    # 2-3番目の引数が始点,4-5番目の引数が終点のX,Y座標
    widget.window.draw_line(self.__gc,
                            self.__width / 2, 0,
                            self.__width / 2, self.__height)
    widget.window.draw_line(self.__gc,
                            0, self.__height / 2,
                            self.__width, self.__height / 2)

    # 四角を描画
    # 3-4番目の引数が左上の(X,Y),5-6番目の引数が(幅,高さ)
    # 多角形はdraw_polygon()
    widget.window.draw_rectangle(self.__gc2,
                                 True,
                                 self.__width / 2 - 10, self.__height / 2 - 10,
                                 20, 20)

    # 円(の一部)
    # 最後の引数は回転角度(64倍すると度単位になる)
    # 0度が3時の方向で反時計回り
    widget.window.draw_arc(self.__gc3,
                           True,
                           self.__width / 4, self.__height / 4,
                           self.__width / 2, self.__height / 2,
                           0,
                           60 * 64)  # 60度

    # Pixbuf
    # 3-4番目がPixbuf内の切り取りの左上(X,Y)
    # 5-6番目がDrawingArea内の貼り付け位置の左上(X,Y)
    # 7-8番目がPixbuf内の切り取りの(幅,高さ)
    # 9-10番目がDrawingArea内の貼り付けの(幅,高さ) -1で元の長さ
    pixbuf_red = gtk.gdk.pixbuf_new_from_file('/usr/share/pixmaps/apple-green.png')
    pixbuf_green = gtk.gdk.pixbuf_new_from_file('/usr/share/pixmaps/apple-red.png')
    widget.window.draw_pixbuf(self.__gc,
                              pixbuf_red,
                              0, 0,
                              10, 40,
                              pixbuf_red.props.width / 2, pixbuf_red.props.height,
                              -1, pixbuf_red.props.height / 2)
    widget.window.draw_pixbuf(self.__gc, pixbuf_green,
                              pixbuf_green.props.width / 2, 0,
                              10 + pixbuf_red.props.width / 2, 40,
                              pixbuf_green.props.width / 2, pixbuf_green.props.height,
                              -1, pixbuf_red.props.height / 2)

    # GUI部品の左上を基準にして文字列を描画
    # 2-3番目の引数が左上のX,Y座標
    widget.window.draw_layout(self.__gc4,
                              0, 0,
                              self.__layout)
    # GUI部品の真ん中を基準にして文字列を描画
    widget.window.draw_layout(self.__gc4,
                              self.__width / 2, self.__height / 2,
                              self.__layout)

class MainWindow(gtk.Window):
  """
  メインウィンドウ
  """
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    self.props.title = 'PyGTK DrawingArea test (GDK)'
    self.set_size_request(320, 200)
    self.__accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.__accelgroup)
    self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup)
    self.__menu_file = gtk.Menu()
    self.__menu_file.add(self.__item_quit)
    self.__item_file = gtk.MenuItem('_File')
    self.__item_file.props.submenu = self.__menu_file
    self.__menubar = gtk.MenuBar()
    self.__menubar.append(self.__item_file)
    self.__drawingarea = MyDrawingArea()
    self.__vbox = gtk.VBox()
    self.__vbox.pack_start(self.__menubar, expand=False, fill=False)
    self.__vbox.pack_start(self.__drawingarea)
    self.add(self.__vbox)
    self.__item_quit.connect('activate', gtk.main_quit)
    self.connect('delete_event', gtk.main_quit)

class PyGTKDrawingAreaGDKTest:
  """
  DrawingAreaを用いた描画のテスト(GDKのみ使用)
  """
  def main(self):
    """
    ウィンドウを作成してGTK+のメインループを呼ぶ
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


if __name__ == '__main__':
  app = PyGTKDrawingAreaGDKTest()
  app.main()


Cairoを主に使用したもの

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

import math
import sys
import os
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import pango
  import cairo
  import gtk
except:
  sys.exit(1)


class MyDrawingArea(gtk.DrawingArea):
  """
  自分で描画するGUI部品
  """
  def __init__(self):
    gtk.DrawingArea.__init__(self)
    self.__width = self.__height = 0
    self.connect('size-allocate', self.__on_self_size_allocate)
    self.connect('expose-event', self.__on_self_expose_event)
  def __on_self_size_allocate(self, widget, allocation):
    """
    GUI部品のサイズの変更を受け取る
    """
    print '__on_self_size_allocate()'
    self.__width = allocation.width
    self.__height = allocation.height
  def __on_self_expose_event(self, widget, event):
    """
    描画を行う
    同じ領域に重なる場合、後で描いたほうが上になる
    pycairoのContextクラスのリファレンス:
    http://cairographics.org/documentation/pycairo/reference/context.html#class-context
    """
    print '__on_self_expose_event()'
    ctx = widget.window.cairo_create()  # pycairoのContextクラスを継承したもの
    ctx.set_source_color(gtk.gdk.Color('#ffffff'))
    ctx.new_path()                         # パスの開始
    ctx.move_to(0, 0)                      # 左上に移動
    ctx.rel_line_to(self.__width, 0)       # 左上から右上へのパス
    ctx.rel_line_to(0, self.__height)      # 右上から右下へのパス
    ctx.rel_line_to(-1 * self.__width, 0)  # 右下から左下へのパス
    ctx.close_path()                       # パスを閉じる
    # 四角を描画する場合はrectangle()を用いるほうが楽
    # (new_path()からclose_path()までの流れを置き換える)
#    ctx.rectangle(0, 0, self.__width, self.__height)
    # fill()はパス内の塗り潰し・stroke()はパスの輪郭線を描画
    ctx.fill()

    # gtk.gdk.color_parse()から得たgtk.gdk.Colorオブジェクトを用いた色の指定
    col = gtk.gdk.color_parse('#ff0000')
    ctx2 = widget.window.cairo_create()
    ctx2.set_source_rgb(col.red_float, col.green_float, col.blue_float)
    # 円(の一部)は3時の方向から時計回り・180度=math.pi
    # arc_negative()では描画される部分とされない部分が逆だが
    # 時計回りに変わりはない(反時計回りではない)
    ctx2.arc(self.__width / 2, self.__height / 2, min(self.__width, self.__height) / 4, 0, 2 * math.pi)
    ctx2.fill()

    ctx3 = widget.window.cairo_create()
    ctx3.set_source_color(gtk.gdk.Color('#ff9999'))
    ctx3.arc(self.__width / 2, self.__height / 2, min(self.__width, self.__height) / 4, 0, 2 * math.pi)
    ctx3.stroke()  # 輪郭線のみ描画

    ctx4 = widget.window.cairo_create()
    # 直接RGB各成分の値(0.0-1.0)を入れる形の色指定
    # set_source_rgba()ではアルファ成分も指定できる
    ctx4.set_source_rgb(0.3, 0.3, 0.3)
    # フォントの指定
    ctx4.select_font_face('Serif', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
    ctx4.set_font_size(32)
    # 文字のレイアウトでは大きくしたときに崩れない(ズレない)ようにするのは難しい
    # text_extents()を使うと便利だが、ここでは使わないで描画することにする
    # 環境によっては真ん中からズレるかもしれない
    ctx4.move_to(self.__width / 2 - 32 - 64 * self.__width / 100000, self.__height * 15 / 16)
    # 文字列の描画
    ctx4.show_text('日本')

    # 真ん中上から真ん中下への線
    ctx5 = widget.window.cairo_create()
    ctx5.set_source_rgba(0.1, 0.1, 0.1, 0.25)
    ctx5.new_path()
    ctx5.move_to(self.__width / 2, 0)
    ctx5.rel_line_to(0, self.__height)
    ctx5.close_path()
    ctx5.stroke()

    # 真ん中左から真ん中右への線
    ctx6 = widget.window.cairo_create()
    ctx6.set_source_rgba(0.1, 0.1, 0.1, 0.25)
    ctx6.new_path()
    ctx6.move_to(0, self.__height / 2)
    ctx6.rel_line_to(self.__width, 0)
    ctx6.close_path()
    ctx6.stroke()

    text = '見本'
    size = min(self.__width, self.__height) / 1.75
    ctx7 = widget.window.cairo_create()
    ctx7.set_source_rgba(0.0, 0.3, 0.9, 0.3)
    ctx7.select_font_face('Serif', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
    ctx7.set_font_size(size)
    # text_extents()はフォントサイズなどを指定した後で呼ぶ
    (x_bearing, y_bearing, width, height, x_advance, y_advance) = ctx7.text_extents(text)
    print 'size:%d, extents:[%d,%d/%d,%d/%d,%d]' % (size, x_bearing, y_bearing, width, height, x_advance, y_advance)
    # 得られる幅/高さとbearingとを用いることで中央寄せすることができる
    ctx7.move_to(self.__width / 2 - width / 2 - x_bearing, self.__height / 2 - height / 2 - y_bearing)
    ctx7.show_text(text)

class MainWindow(gtk.Window):
  """
  メインウィンドウ
  """
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    self.props.title = 'PyGTK DrawingArea test (Cairo)'
    self.set_size_request(320, 200)
    self.__accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.__accelgroup)
    self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup)
    self.__menu_file = gtk.Menu()
    self.__menu_file.add(self.__item_quit)
    self.__item_file = gtk.MenuItem('_File')
    self.__item_file.props.submenu = self.__menu_file
    self.__menubar = gtk.MenuBar()
    self.__menubar.append(self.__item_file)
    self.__drawingarea = MyDrawingArea()
    self.__vbox = gtk.VBox()
    self.__vbox.pack_start(self.__menubar, expand=False, fill=False)
    self.__vbox.pack_start(self.__drawingarea)
    self.add(self.__vbox)
    self.__item_quit.connect('activate', gtk.main_quit)
    self.connect('delete_event', gtk.main_quit)

class PyGTKDrawingAreaCairoTest:
  """
  DrawingAreaを用いた描画のテスト(Cairo使用)
  """
  def main(self):
    """
    ウィンドウを作成してGTK+のメインループを呼ぶ
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


if __name__ == '__main__':
  app = PyGTKDrawingAreaCairoTest()
  app.main()

関連記事:

使用したバージョン:

  • Python 2.6.4
  • PyGTK 2.16.0
  • pycairo 1.8.6