モンキーPython (Python3対応): 第2回 お絵かきプログラムを作ってみる 後編

今回の目次:

  1. 関数を定義する
    • イベント駆動
    • 字下げで固まりを作る
  2. 名前空間とスコープ
  3. Tkinter
  4. クラスを定義する
    • メソッドを定義する
    • インスタンス変数
  5. サンプル:お絵かきプログラム
    • 概要
    • 色を変更する(オプションメニュー)
    • 線の太さを変更する(スケール)
  6. 今回のまとめ

@ 関数を定義する

前回掲載したサンプルプログラム (01-06.py) では、ウィンドウとボタンを表示しただけで、押しても何も起こりませんでした。今回は、ボタンを押すと文字列を出力するようにしてみます。

次のようにします。ボタンを押すと、コンソールに「ボタンが押された!」と表示されます。

02-05.py on_clicked関数を定義した
[POPUP]
  1. # -*- coding:utf-8 -*-
  2. import tkinter
  3. def on_clicked():
  4. print("ボタンが押された!")
  5. window = tkinter.Tk()
  6. label = tkinter.Label(window, text = "サンプル")
  7. label.pack()
  8. button = tkinter.Button(window, text = "ボタンです。押してください。",
  9. command = on_clicked )
  10. button.pack()
  11. window.mainloop()

自分で関数を定義するには、次のように書きます;

def 定義する関数名 ( 引数名, ... ) :
    文
    文
    ...

defの行の最後に「:」(コロン)を書き、次の行から行を字下げ(左側に空白を入れる)して、実行する文を書いていきます。字下げするのを止めたところがその関数の終わりになります。

tkinter.Button()のキーワード引数 commandon_clicked 関数を登録します。ボタンが押された時には、Tk#mainloop()の中から登録された関数が呼び出されます。

■イベント駆動

ウィンドウを表示するプログラムや、あるいはWebサーバで動くプログラムなどでは、ユーザからの操作を待ち受けます。

あらかじめ何をどのような順序で操作されるか、プログラムの側では分かりません。ですので、プログラミングでは, 自分の関数を登録しておいて、実際にユーザがそのような操作 (上の例ではボタンを押す。) があったときに、呼び出してもらうようにします。

これをイベント駆動プログラミング (event-driven programming) といいます。

■字下げで固まりを作る

Python の特徴の一つが、字下げでソースコード上の固まりを表すことです。上の例でも、字下げで関数の範囲を示しました。

次の例を見てください。for は繰り返しを行う命令です。

02-06.py

[POPUP]
  1. mylist = [1, "10", False, (1, 2, 3), "日本語テキスト",
  2. "あああ"] # 継続行はブロックではない
  3. for v in mylist:
  4. print("value: ", v)
  5. # 空行があってもブロックは継続
  6. print("x")
  7. print("finish.")
$ python3 02-06.py
value:  1
x
value:  10
x
value:  False
x
value:  (1, 2, 3)
x
value:  日本語テキスト
x
value:  あああ
x
finish.

forの次の行から字下げしています。print("x")までが固まりです。ここまでが繰り返されます。

@ 名前空間とスコープ

前回, 変数がオブジェクトを指すラベル(付箋)だということを解説しました。変数は、ソースコード上の文脈によっては、同じ名前であっても別のオブジェクトを指すことがあります。ある変数が有効なソースコード上の範囲をスコープ (scope) といいます。

実行時, それぞれの変数は, いずれかの名前空間の中にあります。ある関数が呼び出されてその中の文が実行される時、新しい名前空間が導入されます。関数の引数、それから関数の中で代入した変数は、関数の外側の変数とは別の名前空間に属します。関数の中の文の実行が終わるときに、その関数の名前空間は消滅します。

次のスクリプトは、関数fooの外側と内側で, 同じ名前の変数yを使っています。

02-07.py

[POPUP]
  1. y = 100
  2. def foo(x):
  3. y = x * 2
  4. return y
  5. print( foo(1) )
  6. print( foo(2) )
  7. print( y )
$ python3 02-07.py    
2
4
100

return文は、関数の戻り値を伴って、関数から抜けます。

foo() の中で変数yに代入していますが、外側の変数yには影響しません (外側のyが指すオブジェクトは変わりません。) (図3)。

関数内でのみ有効な変数をローカル変数といいます。言い換えると、ローカル変数のスコープは関数です。なお、すべての関数のなかから参照できる変数をグローバル変数といいます。

グローバル変数は、ソースコードが大きくなると、どこで値が書き換えられるかが分かりにくくなるため、あまり使うべきではありません。global宣言でグローバル変数になります。

図3 名前空間は入れ子

ただし注意したいのは、変数が新しく取られても、オブジェクトがコピーされるわけではないことです。

関数呼び出しの引数は、名前 (変数) が新しく取られるだけで、呼び出し元のオブジェクトが共有されます。関数のなかで, 引数の変数を介してオブジェクトを操作すると、その影響は呼び出した側にまで及びます。

変数から変数への代入と同じです。

次のソースコードは、リストを関数の中で変更します。関数の外側で生成したオブジェクトが変更されます。

02-08.py

[POPUP]
  1. def hoge(ary):
  2. ary.append(10)
  3. ary = [1, 2, 3]
  4. hoge(ary)
  5. print( ary ) #=> [1, 2, 3, 10]

少し高度になりますが、Python では、変数が見つからないときは、ソースコードの字面上で外側に変数を探しに行きます。

02-09.py

[POPUP]
  1. def g(x):
  2. def f(y):
  3. return x * y # xはg()の引数
  4. return f # 関数fを返す
  5. gg = g(2)
  6. print( gg(3) ) #=> 6
  7. hh = g(10)
  8. print( hh(4) ) #=> 40

Python は関数を入れ子にできます。関数fの中には変数yしかありません。変数xは、外側の、g()の引数のxを使います。

g(2)とすると、def f(y)の中では、2 * yとなります。戻り値である関数fに3を与えると, (2 * 3) で 6が得られます。

@ Tkinter

これまでのサンプルでは、ラベル、ボタンをウィンドウに貼り付けてきました。Tkinterには、それ以外にも、いろいろなものがあります。

■ウィジェット

ウィンドウに貼れるものをウィジェット (widget) といいます。

Note.

ウィジェットは、「コントロール」あるいは「コンポーネント」と呼ぶこともあります。

いくつかのウィジェットをウィンドウに並べてみます (02-10.py)。実行すると、画面1のように表示されます。

02-10.py いろいろなウィジェット
[POPUP]
  1. # -*- coding:utf-8 -*-
  2. import tkinter
  3. window = tkinter.Tk()
  4. tkinter.Button(window, text = "ボタン").pack()
  5. tkinter.Checkbutton(window, text = "チェックボタン").pack()
  6. entry = tkinter.Entry(window)
  7. entry.insert(tkinter.END, "エントリ")
  8. entry.pack()
  9. frame = tkinter.LabelFrame(window, text = "ラベル付きフレーム")
  10. frame.pack()
  11. tkinter.Label(frame, text = "ラベル").pack()
  12. listbox = tkinter.Listbox(window, height = 3)
  13. listbox.insert(tkinter.END, "リストボックス")
  14. listbox.insert(tkinter.END, "項目2")
  15. listbox.pack()
  16. tkinter.Scale(window, orient = tkinter.HORIZONTAL).pack()
  17. tkinter.Spinbox(window).pack()
  18. window.mainloop()
画面1 主なウィジェット

ウィジェットのクラスとその内容は表1のとおりです。それぞれのクラスの使い方は、紙面もないので、リファレンスマニュアルを参照してください。

表1 Tkinterのクラス
Tkinterクラス名内容
Widget
 Button 押しボタン
 Canvas キャンバス
 Checkbutton チェックボタン
 Entry エントリ(1行テキスト入力)
 Frame フレーム
 Label ラベル
 LabelFrame ラベル付きフレーム
 Listbox リストボックス
 Menu メニューバー
 Menubutton (推奨されない。MenuかOptionMenuを使う。)
  OptionMenu オプションメニュー
 Message (推奨されない。Labelを使う。)
 PanedWindow ペイン区切り
 Radiobutton ラジオボタン
 Scale スケール(スライダ)
 Scrollbar スクロールバー
 Spinbox スピンボックス
 Text 複数行テキスト入力

ウィジェットの機能のうち、共通の部分は Widgetクラスで定義されています。リファレンスマニュアルなどでは、調べたいウィジェットのクラスだけではなく、Widgetクラスも調べる必要があります。オプションメニューについても、OptionMenuクラス、Menubuttonクラス、Widgetクラスを遡って調べる必要があります。

■イベントの束縛

Tkinterでは、マウスのボタンが押された、ドラッグされた、キーボードのキーが打たれた、など、ユーザーがウィンドウ上で何らかのことをしたりすると、イベントとしてプログラムに伝えられます。

イベントは、マウスのボタンを押したときの座標、どのボタンを押したのか、どのキーを打ったのか、などの情報をまとめたものです。イベントを受け取るためには、あらかじめ関数を登録しておく必要があります。関数を登録しておくと、これらユーザーの操作などがあったときに、Tk#mainloop()の中からその関数が呼び出されます。

これが「イベント駆動プログラミング」でしたね。

すでに見たように、押しボタン (Buttonクラス) では、commandキーワード引数で関数を登録しておけば、クリックしたときにその関数が呼び出されました。ウィジェット特有のイベントはこのように特別な指定方法がありますが、一般的なイベントは、bindメソッドでイベントと関数を結び付け(束縛)ます。

bindメソッドは次のように書きます。

    widgetオブジェクト . bind ( イベントを表す文字列, 関数・メソッド名 )

例えば、リスト7のようにします。「<Button-1>」は、マウスの左ボタンを押す、というイベントです。実際にイベントが発生したときには、イベントの情報を格納したオブジェクトを引数として、登録しておいた関数が呼び出されます。

02-11.py bind()メソッドで左ボタンを登録
[POPUP]
  1. # -*- coding:utf-8 -*
  2. import tkinter
  3. import sys
  4. def on_pressed(event):
  5. sys.exit() # プログラムを終了
  6. window = tkinter.Tk()
  7. label = tkinter.Label(window, text = "マウスボタンを押すと終了")
  8. label.bind("<Button-1>", on_pressed)
  9. label.pack()
  10. window.mainloop()

表2に、イベントの一部を掲げます。

表2 イベント(一部)
イベント内容
<Button-x>マウスのボタンが押された。xはボタン番号で、1=左ボタン、2=中ボタン、3=右ボタン
<Bx-Motion>マウスがドラッグされた。xはボタン番号。
<ButtonRelease-x>マウスのボタンが離された。xはボタン番号
<Double-Button-x>マウスがダブルクリックされた。xはボタン番号
<Enter>マウスポインタがウィジェットに重なった(入ってきた)。キーボードのEnterキーが押されたにあらず。
<Leave>マウスポインタがウィジェットから離れた

@ クラスを定義する

関数は、引数として与えられたオブジェクトと、関数内で生成したオブジェクト、グローバル変数しか参照できません。大きな仕事をするには、多くの関数が協力・分担して、いろいろなことができなければなりません。

図4 オブジェクトとメソッド

すでに見てきたように、それぞれのオブジェクトは多くのメソッドを持ち、これらがオブジェクトの内部情報を共有しています。外側から見ると、メソッドに対して指図することで(メソッド呼び出し)、オブジェクトを操作します(図4)。

我々もこのようなオブジェクトを作っていい頃合いです。オブジェクトの振る舞い(メソッド)は、クラスで定義します。

クラスを定義する文法は、次のようになります。

class 定義するクラス名 :
    メソッド定義など...

■メソッドを定義する

メソッドを定義する文法は、関数とほとんど同じで、クラス定義のなかで次のようにします。

def 定義するメソッド名 ( 自オブジェクト変数名, 引数, ... ) :
    文...

関数定義と違うのは、最初の引数として, 操作対象となるオブジェクトを指す変数を用意することだけです。この変数の名前は何でもいいのですが、慣例的に selfとします。

字下げして文を書いたり、呼び出されるたびに新しい名前空間が導入されるのも同じです(ローカル変数のスコープも同じ)。

■インスタンス変数

オブジェクトの内部状態を保持する変数をインスタンス変数といいます。インスタンス変数は、次のように書きます。

    オブジェクト . 変数名        # 文法

実際の書き方を 02-12.py に示します。__init__メソッドは、特別なメソッドで、オブジェクトが生成されるときに呼び出されます。

02-12.py クラスとメソッドを作る
[POPUP]
  1. # -*- coding:utf-8 -*-
  2. class my_class:
  3. def __init__(self, v):
  4. self.value = v
  5. # 加算
  6. def add(self, v):
  7. self.value += v
  8. # 減算
  9. def sub(self, v):
  10. self.value -= v
  11. def get_value(self):
  12. return self.value
  13. obj1 = my_class( 100 )
  14. obj2 = my_class( 1000 )
  15. obj1.add(10)
  16. obj2.sub(30)
  17. print( obj1.get_value(), obj2.get_value() ) #=> 110 970

上記の 02-12.py の self.value がインスタンス変数です。インスタンス変数は、オブジェクトが生成されるたびに作られ、オブジェクトのメソッドで共有できます。オブジェクトが消滅するまで生き続けます。

@ サンプル:お絵かきプログラム

それでは、これまでに見てきたことを踏まえて、ごく短いお絵かきプログラムを書いてみます(リスト9)。実行結果は、画面2のようになります。

02-13.py をダウンロード ダウンロード後、拡張子を .py に変更してください。

画面2 お絵かきプログラムの実行結果

■概要

どのようなことをしているか、順に見ていきましょう。

(1) Scribble#__init__() (リスト9囲み3)

Scribble#__init__() は、単にcreate_window() を呼び出すだけです。

[POPUP]
  1. # ウィンドウを作る
  2. def create_window(self):
  3. window = tkinter.Tk()
  4. self.canvas = tkinter.Canvas(window, bg = "white",
  5. width = 300, height = 300)
  6. self.canvas.pack()
  7. quit_button = tkinter.Button(window, text = "終了",
  8. command = window.quit)
  9. quit_button.pack(side = tkinter.RIGHT)
  10. self.canvas.bind("<ButtonPress-1>", self.on_pressed)
  11. self.canvas.bind("<B1-Motion>", self.on_dragged)

お絵かきのためのキャンバスオブジェクトと、プログラムを終了するためのボタンオブジェクトを作ります。キャンバスオブジェクトは、bg引数で背景を白に、widthheightで大きさを指定します。

ボタンオブジェクトには、tkinter.Tk#quitメソッドを登録しておきます。クリックされたら、このメソッドが呼び出され、プログラムが終了します。

キャンバス はbind() でイベントを結び付けます。ユーザーがマウスの左ボタンを押したとき、マウスをドラッグしたときに、それぞれon_pressedメソッド、on_draggedメソッドが呼び出されるようにします。

(2) Scribble#on_pressed() (リスト9囲み1)

[POPUP]
  1. # ボタンが押された
  2. def on_pressed(self, event):
  3. self.sx = event.x
  4. self.sy = event.y
  5. self.canvas.create_oval(self.sx, self.sy, event.x, event.y,
  6. outline = self.color.get(),
  7. width = self.width.get())

ボタンが押されたら点を描きます。tkinter.Canvas#create_oval()は楕円を描くメソッドです。この上下左右の座標を1点にすることで、点を描きます。

インスタンス変数sxsyに現在位置を入れておきます。on_dragged()でこれを使います。

self.color, self.widthについては後述。

(3) Scribble#on_dragged() (リスト9囲み2)

[POPUP]
  1. # ドラッグ
  2. def on_dragged(self, event):
  3. self.canvas.create_line(self.sx, self.sy, event.x, event.y,
  4. fill = self.color.get(),
  5. width = self.width.get())
  6. self.sx = event.x
  7. self.sy = event.y

マウスがドラッグされたときは、直前の座標との間に線を描けばうまくいきます。on_pressed()で保存していた座標との間に、tkinter.Canvas#create_line()で線を描きます。

その後、最後の座標(sx, sy)を更新しておきます。こうすることで、さらにマウスを動かしたときに、今度はここから次の座標まで線を引けます。

@ ■色を変更する(オプションメニュー)

オプションメニューとスケールを使って、線の色と太さを変更できるようにしてみましょう。

Scribble#create_window() の中頃です。

色を変更できるようにする
[POPUP]
  1. # 色を選ぶ
  2. COLORS = ["red", "green", "blue", "#FF00FF", "black"]
  3. self.color = tkinter.StringVar()
  4. self.color.set(COLORS[1])
  5. b = tkinter.OptionMenu(window, self.color, *COLORS)
  6. b.pack(side = tkinter.LEFT)

オプションメニュー (OptionMenuクラス) オブジェクトを生成し、それをウィンドウに配置します。

動作を順に説明すると、

  1. オプションメニューに表示する色の選択肢のリストを生成します。色は、"red", "green" など色の名前を指定することも、"#FF00FF" のように赤、緑、青それぞれ00〜255(16進数で00〜FF)の範囲で指定することもできます。
  2. インスタンス変数 colorStringVar オブジェクトを代入します。現在の色の意図です。

    StringVarオブジェクトは、setメソッドで値を設定でき、getメソッドで値を読み取ることができます。代入だとオブジェクトが差し替わってしまう対策です。

  3. オプションメニューで最初に表示する値を StringVar#set() で設定します。COLORS[n]で (n + 1) 番目の値が得られるので、ここでは2番目の"green"を設定することになります。
  4. OptionMenuオブジェクトを生成します。最初の引数として, 貼り付けるウィンドウのオブジェクトを、2番目の引数としてオプションメニューで選択された項目の値を格納するオブジェクトを指定します。3番目以降の引数で選択肢を指定します。

    COLORSの前の「*」は、リストの要素をあたかもソースコード上に展開する記号で、次のように書いたかのように動きます。

    tkinter.OptionMenu(window, self.color, "red", "green", "blue", "#FF00FF", "black")
    
  5. OptionMenuオブジェクトの pack メソッドで、左詰めにします。

点を打つところ、線を引くところで、self.color.get() を呼び出して、そのときそのときの選択色を使うようにします。

画面3 色の変更

@ ■線の太さを変更する(スケール)

線の太さを, スケールを使って変えられるようにしましょう。create_window() の後半です。

線の太さを変更できるようにする
[POPUP]
  1. # 線の太さを選ぶ
  2. self.width = tkinter.Scale(window, from_ = 1, to = 15,
  3. orient = tkinter.HORIZONTAL)
  4. self.width.set(5)
  5. self.width.pack(side = tkinter.LEFT)
  6. return window;

スケールオブジェクトを生成し、ウィンドウに貼り付けます。

  1. tkinter.Scale()でスケールオブジェクトを生成します。最初の引数としてウィンドウのオブジェクトを指定します。キーワード引数from_toで、スケールの範囲を指定します。また、orientで方向を指定します。tkinter.HORIZONTALだと水平方向のスケールになります。
  2. 最初の値(初期値)をScaleオブジェクトのsetメソッドで指定します。
  3. Scaleオブジェクトのpackメソッドで、左詰めで詰めます。

on_pressed(), on_dragged() にて, self.width.get() で現在のスケールの値を得ます。これでペンの太さと色を変えられるようになりました(画面4)。

画面4 線の太さの変更を追加

@ 今回のまとめ