Monday, September 04, 2006

PyGTK を使ったカスタム・ウィジットの書き方

Writing a Custom Widget Using PyGTK by Mark Mruss


#この文書はクリエイティブ・コモンズの帰属 - 非営利 - 同一条件許諾 2.5でライセンスされている。


僕のシンプルなアプケーション PyWine に加えたかった機能のひとつは、ワインを簡単に評価できる方法だ。これにはたくさんのやりかたがあるけど、チュートリアルを書くということもあって、iTunes で曲を評価するときと同じやりかたで評価できるようにすることに決めた。iTunes を使ったことがないなら、ゼロから5つまでの星を使って評価できるものだと理解すればいい。これは slider や Horizontal Scale のように動作する。違いは、描画が線の代わりに星だということだ。



このチュートリアルのすべてのソースは以下のリンクからダウンロードできる:

http://www.learningpython.com/sources/starhscale.tar.gz

僕が見つけた、このトピックに関して最も役に立つリンクを3つ挙げておく: A song for the lovers、PyGTK のサイトの writing a widget turotial、PyGTK cvs の widget.py という例。

以下のコードの基本部分はほとんどが widget.py を基にしている。この例では、もう少しやることがあるからコードの追加はあるけど。このチュートリアルをよく理解するために widget.py を何度か読むことをすすめる。

starhscale.py という名前のファイルから始めようか。これはまあ Python のいつものやつだ:

#!/usr/bin/env python

try:
import gtk
import gobject
from gtk import gdk
except:
raise SystemExit

import pygtk
if gtk.pygtk_version < (2, 0):
print "PyGtk 2.0 or later required for this widget"
raise SystemExit


これは驚くにはあたらない。これからクラスを作って初期化しよう。このクラスは StarHScale と呼ぶことにしようか:

class StarHScale(gtk.Widget):
"""A horizontal Scale Widget that attempts to mimic the star
rating scheme used in iTunes"""

def __init__(self, max_stars=5, stars=0):
"""Initialization, numstars is the total number
of stars that may be visible, and stars is the current
number of stars to draw"""

#Initialize the Widget
gtk.Widget.__init__(self)

self.max_stars = max_stars
self.stars = stars

# Init the list to blank
self.sizes = []
for count in range(0,self.max_stars):
self.sizes.append((count * PIXMAP_SIZE) + BORDER_WIDTH)


これは何をやっているのだろうか? はじめにあるのは、gtk.Widget のサブクラスの StarHScale ウィジットの定義で、これはPyGTK のすべてのウィジットのベースクラスだ。次にわりとシンプルな __init__ ルーチンがある。ここで、いくつかのパラメータ(表示する星の最大数と現在表示している星の数)をセットして親クラスを初期化する。

この関数の最後でリストが作られているのが分かるだろう。これはそれぞれの星の X 軸上(横軸上)の位置を示したものだ。今はよく分からないかもしれないけど、これからの使われかたをみれば明らかになると思う。PIXMAP_SIZE と BORDER_WIDTH は StarHScale の外で以下のように定義されるグローバル変数だ:

BORDER_WIDTH = 5
PIXMAP_SIZE = 22


次に書く関数は do_realize() だ。この do_realize() 関数は gtk.widget.realize() 関数と関連していて、GDK ウィンドウ表示リソースをウィジットが割り当てるときに呼ばれる。

これは少し複雑に聞こえるかもしれないけど、要するに、do_realize() 関数はウィジットがその GDK ウィンドウ・リソースを作るところだ(多くの場合それはウィジットが最終的に描画される gtk.gdk.Window だ。)。これをしっかり理解するには gtk.gdk.Window が何なのかを理解するのが助けになる。以下は PyGTK ドキュメンテーションの説明:

gtk.gdk.Window はスクリーンの長方形の領域です。それは低レベルオブジェクトで、 gtk.Widget や gtk.Window などの高レベルオブジェクトを実装するのに用いられます。gtk.Window はトップレベルのウィンドウです。ここでの「ウィンドウ」というのは、ユーザが思い浮かべるタイトルバーその他がついたオブジェクトのことです。gtk.Window は、ほとんどのウィジットが gtk.gdk.Window を使うという理由から、いくつかの gtk.gdk.Window を含むかもしれません。

gtk.gdk.Window オブジェクトは入力とイベントについてネイティブのウィンドウ・システムとやりとりします。いくつかの gtk.Widget オブジェクトは関連した gtk.gdk.Window を持たず、イベントを受け取ることができません。このようなウィンドウをもたないウィジットに代わってイベントを受け取るためには、gtk.EventBox が使われなければなりません。


ということで、gtk.gdk.Window っていうのは、僕らが普通に考えるような「ウィンドウ」じゃなくて、なんらかの「描画」がなされるようなスクリーンの長方形の領域のことだ。僕らの StarHScale ウィジットでは、gtk.gdk.Window というのは星が描画される場所ということになる。もし他のツールキットや言語でプログラミングをやったことがあるなら、これをウィジットが描き込む "surface" と考えると分かるかもしれない。do_realize() のコードのほとんどは widget.py の例から取ってきた:

def do_realize(self):
"""Called when the widget should create all of its
windowing resources. We will create our gtk.gdk.Window
and load our star pixmap."""

# First set an internal flag telling that we're realized
self.set_flags(self.flags() | gtk.REALIZED)

# Create a new gdk.Window which we can draw on.
# Also say that we want to receive exposure events
# and button click and button press events

self.window = gdk.Window(
self.get_parent_window(),
width=self.allocation.width,
height=self.allocation.height,
window_type=gdk.WINDOW_CHILD,
wclass=gdk.INPUT_OUTPUT,
event_mask=self.get_events() | gdk.EXPOSURE_MASK
| gdk.BUTTON1_MOTION_MASK | gdk.BUTTON_PRESS_MASK
| gtk.gdk.POINTER_MOTION_MASK
| gtk.gdk.POINTER_MOTION_HINT_MASK)

# Associate the gdk.Window with ourselves, Gtk+ needs a reference
# between the widget and the gdk window
self.window.set_user_data(self)

# Attach the style to the gdk.Window, a style contains colors and
# GC contextes used for drawing
self.style.attach(self.window)

# The default color of the background should be what
# the style (theme engine) tells us.
self.style.set_background(self.window, gtk.STATE_NORMAL)
self.window.move_resize(*self.allocation)
# load the star xpm
self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(
self.window, self.style.bg[gtk.STATE_NORMAL], STAR_PIXMAP)

# self.style is a gtk.Style object, self.style.fg_gc is
# anarray or graphic contexts used for drawing the forground
# colours
self.gc = self.style.fg_gc[gtk.STATE_NORMAL]

self.connect("motion_notify_event", self.motion_notify_event)


ここのコードはちょっと量があるから、少し手間をかけて説明することにしよう。はじめのステップは、自分たち [訳注:ウィジットのこと、でいいのかな] が認識されている --- つまり、自分たちに関連した gtk.gdk.Window を自分たちが持っている --- というフラグを立てることだ。

次のステップは、StarHScale ウィジットに関連される gtk.gdk.Window を実際に作ることだ。これを作成するとき、そのたくさんの属性もセットしなきゃいけない。すべての利用可能な属性については PyGTK ドキュメンテーション を見てもらうことにして、ここでは僕らが設定する属性についてだけ見てみよう:

parent: gtk.gdk.Window
width: ピクセルで表示したそのウィンドウの幅
height: ピクセルで表示したそのウィンドウの高さ
window_type: ウィンドウのタイプ
event_mask: ウィンドウが受け取ったイベントの bitmask
wclass: ウィンドウのクラス - gtk.gdk.INPUT_OUTPUT あるいは gtk.gdk.INPUT_ONLY のどちらか


gtk.gdk.Window のイベント・マスクには、いくつかのイベントを加える。どうしてかというと、このウィジットはマウスとやりとりをするからだ。つぎに、ウィジットの gtk.gdk.Window とウィジット・スタイル間の必要な接続をする。最後にバックグラウンドの色をセットしてウィンドウを割り当てられた場所に動かす(self.allocation)。

次のステップは do_realize() のコードが widget.py の例から分岐するところだ。ここで、pixmap_create_from_xpm_d 関数を使って星の pixmap を作る:

# load the star xpm
self.pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(
self.window
, self.style.bg[gtk.STATE_NORMAL]
, STAR_PIXMAP)


以下が gtk.gdk.Pixmap の説明だ:

gtk.gdk.Pixmap は、画面には現れない gtk.gdk.Drawable のひとつです。これはスタンダードな gtk.gdk.Drawable の描画プリミティブ(drawing primiteves)によって描画され、ほかの gtk.gdk.Drawable(たとえば gtk.gdk.Window)に draw_drawable() メソッドを使用してコピーされることができます。pixmap の深度はピクセルごとのビット数です。A bitmaps は単に深度1の gtk.gdk.Pixmap です(つまり、モノクロの pixmap です -- それぞれのピクセルは on か off のどちらかです)。


pixmap を使う目的は星を描画することだ。僕らはいま、このウィジットを xpm ファイルを持つことなくポータブルにしたいから、そのデータをただロードする。これをするには、StarHScale の外に STAR_PIXMAP を「グローバル」に定義する必要がある:

STAR_PIXMAP = ["22 22 77 1",
" c None",
". c #626260",
"+ c #5E5F5C",
"@ c #636461",
"# c #949492",
"$ c #62625F",
"% c #6E6E6B",
"& c #AEAEAC",
"* c #757673",
"= c #61625F",
"- c #9C9C9B",
"; c #ACACAB",
"> c #9F9F9E",
", c #61635F",
"' c #656663",
") c #A5A5A4",
"! c #ADADAB",
"~ c #646562",
"{ c #61615F",
"] c #6C6D6A",
"^ c #797977",
"/ c #868684",
"( c #A0A19E",
"_ c #AAAAA8",
": c #A3A3A2",
"< c #AAAAA7",
"[ c #9F9F9F",
"} c #888887",
"| c #7E7E7C",
"1 c #6C6C69",
"2 c #626360",
"3 c #A5A5A3",
"4 c #ABABAA",
"5 c #A9A9A7",
"6 c #A2A2A1",
"7 c #A3A3A1",
"8 c #A7A7A6",
"9 c #A8A8A6",
"0 c #686866",
"a c #A4A4A2",
"b c #A4A4A3",
"c c #A1A19F",
"d c #9D9D9C",
"e c #9D9D9B",
"f c #A7A7A5",
"g c #666664",
"h c #A1A1A0",
"i c #9E9E9D",
"j c #646461",
"k c #A6A6A4",
"l c #A0A09F",
"m c #9F9F9D",
"n c #A9A9A8",
"o c #A0A09E",
"p c #9B9B9A",
"q c #ACACAA",
"r c #60615E",
"s c #ADADAC",
"t c #A2A2A0",
"u c #A8A8A7",
"v c #6E6F6C",
"w c #787976",
"x c #969695",
"y c #8B8B8A",
"z c #91918F",
"A c #71716E",
"B c #636360",
"C c #686966",
"D c #999997",
"E c #71716F",
"F c #61615E",
"G c #6C6C6A",
"H c #616260",
"I c #5F605E",
"J c #5D5E5B",
"K c #565654",
"L c #5F5F5D",
" ",
" ",
" . ",
" + ",
" @#$ ",
" %&* ",
" =-;>, ",
" ';)!' ",
" ~{{]^/(_:< [}|*1@, ",
" 23&4_5367895&80 ",
" 2a4b:7c>def)g ",
" 2c4:h>id56j ",
" {k8lmeln2 ",
" j8bmoppqr ",
" {stusnd4v ",
" ws;x@yq;/ ",
" zfAB {CmD{ ",
" rE{ FGH ",
" IJ KL ",
" ",
" ",
" "]


この星は、すばらしい Tango Desktop Project Art Libre Set のものを基にしている。星の色を少しだけ暗めに変更した。

次に、通常状態のフォアグラウンドgtk.gdk.GC(グラフィック・コンテキスト)について触れておこう。gtk.gdkGC というのは、PyGTK のドキュメントによると、フォアグラウンドの色やラインの幅みたいな描画方法についての情報をカプセル化するオブジェクトのことだ。だから基本的には、これはたくさんの描画設定がひとつのオブジェクトにカプセル化されたものだと思えばいい。

最後に、do_realize() 関数の終わりでは、 ウィジットの上のユーザのマウスの操作を追跡するのに使う "motion_notify_event" と自分たちをコネクトする。

ウィジット作成の次のステップは do_unrealize() 関数だ。これはウィジットがすべてのリソースを開放するときに呼ばれる。widget.py の例は次のものを呼んでいる:

self.window.set_user_data(None)


でも僕がこれを試すと型エラーが出た。だから代わりにウィンドウを破壊(destroy)することにした。どういう風にするのが正しいのかあまり確信はないし、リソースのクリアを心配する必要があるのかもあやしいんだけど、とにかく僕が使ったのは以下のようなコードだ:

def do_unrealize(self):
# The do_unrealized method is responsible for freeing the GDK resources
# De-associate the window we created in do_realize with ourselves
self.window.destroy()


次の2つの関数はウィジットのサイズを扱う。はじめの do_size_request() は、PyGTK が、ウィジットがどれだけの大きさなになれるのかを知るために PyGTK によって呼ばれる。2つめの do_size_allocate() 関数は、ウィジットに実際どのぐらいの大きさになるのかを伝えるために PyGTK によって呼ばれる:

def do_size_request(self, requisition):
"""From Widget.py: The do_size_request method Gtk+ is calling
on a widget to ask it the widget how large it wishes to be.
It's not guaranteed that gtk+ will actually give this size
to the widget. So we will send gtk+ the size needed for
the maximum amount of stars"""

requisition.height = PIXMAP_SIZE
requisition.width = (PIXMAP_SIZE * self.max_stars) + (BORDER_WIDTH * 2)


def do_size_allocate(self, allocation):
"""The do_size_allocate is called by when the actual
size is known and the widget is told how much space
could actually be allocated Save the allocated space
self.allocation = allocation. The following code is
identical to the widget.py example"""

if self.flags() & gtk.REALIZED:
self.window.move_resize(*allocation)


次の関数は do_expose_event() だ。これはウィジットが自分自身を実際に描画するときに呼ばれる。StarHScale ではこの関数はとてもシンプルになる:

def do_expose_event(self, event):
"""This is where the widget must draw itself."""

#Draw the correct number of stars. Each time you draw another star
#move over by 22 pixels. which is the size of the star.
for count in range(0,self.stars):
self.window.draw_drawable(self.gc, self.pixmap, 0, 0
, self.sizes[count]
, 0,-1, -1)


これは基本的には単に現在の星の数(self.stars)をループする。それで、draw_drawable 関数を使って星の pixmap をウィンドウに描画する。X軸のどこに星を描き込むかを決定するためには self.sizes リスト(これは __init__ 関数で計算した)を使うことになる。

ここではじめてユーザがウィジットとやりとりして星を見せたり隠したりできるようにする。これをするには、"motion_notify_event" と "button_press_event" の2つに注目する。do_realize() 関数のときにもしかしたら気づいたかもしれないけど、ここでは gtk.POINTER_MOTION_MASK と gtk.POINTER_MOTION_HINT_MASK の2つに注目した。この理由は PyGTK ドキュメンテーション を見ることにしよう:

けれども、POINTER_MOTION_MASK の仕様を定めるだけでは問題が生じることになります。これではユーザがマウスを動かすたびにサーバがイベント・キューに新しいモーション・イベントを加えることになります。仮にひとつのモーション・イベントを処理するのに 0.1 秒かかって、Xサーバが 0.05 秒ごとに新しいモーションをキューしたと考えてみてください。これではすぐにユーザの描画に追いつけなくなります。もしユーザが 5 秒間描画すれば、ユーザマウスを離した後、描画が追いつくのにあと 5 秒かかるのです。よって処理するイベントごとにひとつだけモーションを取得することにします。これをするために POINTER_MOTION_HINT_MASK の仕様を定めるのです。

POINTER_MOTION_HINT_MASK を決定するとき、はじめてポインタがウィンドウに入ったか、ボタンが押されたか、あるいはボタンが離されたと同時にサーバはモーション・イベントを送ります。その後につづくモーション・イベントは、以下のように gtk.gdk.Window メソッドを使ってポインタの位置を明示的に要求するまで送られることはありません。

x, y, mask = window.get_pointer()


motion_notify_event ハンドラは以下のようなものだ:

def motion_notify_event(self, widget, event):
# if this is a hint, then let's get all the necessary
# information, if not it's all we need.
if event.is_hint:
x, y, state = event.window.get_pointer()
else:
x = event.x
y = event.y
state = event.state

new_stars = 0
if (state & gtk.gdk.BUTTON1_MASK):
# loop through the sizes and see if the
# number of stars should change
self.check_for_new_stars(event.x)


この関数はとてもシンプルだ。はじめに gtk.gdk.BUTTON_PRESS_EVENT を起動させるのが左クリックだったこと、そしてevent.x(イベント時のマウスの位置)を check_for_new_stars() に渡すことを確認する。

def check_for_new_stars(self, xPos):
"""This function will determine how many stars
will be show based on an x coordinate. If the
number of stars changes the widget will be invalidated
and the new number drawn"""

# loop through the sizes and see if the
# number of stars should change
new_stars = 0
for size in self.sizes:
if (xPos < size):
# we've reached the star number
break
new_stars = new_stars + 1

#set the new value
self.set_value(new_stars)


この check_for_new_stars() はわりとそのままの関数だ。x座標をパラメータとしてとってきて、それからその情報をもとにいくつの星が見えるかを決定する。いくつの星が見えるかは、self.sizes リストをループして、事前に計算されているそれぞれの星のスタート地点と、わたされたx座標とを比較して決める。現在の星のスタート位置よりも、(マウスのポインタの)x座標が大きくならないギリギリのところまで星を加えつづける。次に、星の数をセットする self.set_value() を呼んだときに新しい星が加えられることを確認しよう。

def set_value(self, value):
"""Sets the current number of stars that will be
drawn. If the number is different then the current
number the widget will be redrawn"""

if (value >= 0):
if (self.stars != value):
self.stars = value
#check for the maximum
if (self.stars > self.max_stars):
self.stars = self.max_stars
#redraw the widget
self.window.invalidate_rect(self.allocation,True)


set_value() もまたシンプルな関数だ。これは、いつくかのチェックをして、それから現在の星の数をセットするものだ。もし星の数が変更されたなら、ウィジットは再描画される。

この時点で3つの関数が残っているけど、これらは全部ウィジットをもっと使い易くするものだ。コードを見れば何をしているかの見当はつくと思う:

def get_value(self):
"""Get the current number of stars displayed"""

return self.stars

def set_max_value(self, max_value):
"""set the maximum number of stars"""

if (self.max_stars != max_value):
"""Save the old max in case it is less then the
current number of stars, in which case we will
have to redraw"""

if (max_value > 0):
self.max_stars = max_value
#reinit the sizes list (should really be a separate function
self.sizes = []
for count in range(0,self.max_stars):
self.sizes.append((count * PIXMAP_SIZE) + BORDER_WIDTH)
"""do we have to change the current number of
stars?"""
if (self.stars > self.max_stars):
self.set_value(self.max_stars)

def get_max_value(self):
"""Get the maximum number of stars that can be shown"""

return self.max_stars


ここで、ウィンドウを作成して、starhscale.py を誰かがダイレクトに実行したときに StarHScale ウィジットをそのウィンドウに加えるっていうコードを書いて starhscale.py を終わりにする:

if __name__ == "__main__":
# register the class as a Gtk widget
gobject.type_register(StarHScale)

win = gtk.Window()
win.resize(200,50)
win.connect('delete-event', gtk.main_quit)

starScale = StarHScale(10,5)

win.add(starScale)
win.show_all()
gtk.main()


これでこのファイルを実行すれば以下のような結果が得られるはずだ:


[Flash]


はぁ〜。これでおしまい。このチュートリアルが役に立つことを願う。次のチュートリアルはこれを gtk.Treeview に加える作業についてだ。

なお、このチュートリアルのソースはすべて以下のリンクでダウンロードできる:

http://www.learningpython.com/sources/starhscale.tar.gz

0 comments: