モンキーPython: 第2回 お絵かきプログラムを作ってみる
この文書は、Linux magazine 2005年1月号〜3月号に掲載された連載の草稿を、(株)アスキーLinux magazine編集部の許可を得て公開するものです。校正前の原稿なので読みづらいところもあるかと思います。不明な点などありましたらコメントをお送りください。
今回の目次
- 前回のおさらい
- 基本的なオブジェクト
- 関数を定義する
- 名前空間とスコープ
- Tkinter
- クラスを定義する
- サンプル:お絵かきプログラム
- 色を変更する(オプションメニュー)
- 線の太さを変更する(スケール)
- 今回のまとめ
@ 前回のおさらい
簡単に前回のおさらいをしておきましょう。
- プログラムは、コンピュータに何かをさせるための命令を書き連ねたテキストファイルである。
- プログラムの命令は、文を単位として実行される。
- 命令には、関数呼び出し、オブジェクトのメソッド呼び出し、print文、変数への代入などがある。
- オブジェクトは、内部に構造(内部状態)を持ち、その操作のためのメソッドを持つ。
- オブジェクトのメソッドはクラスで定義される。オブジェクトを生成するには、クラス名を関数のようにして呼び出すか、スクリプトに値を直接書く。
特に、どのような関数があってどう呼び出すのか、どのようなクラスがあり、そのクラスでどのようなメソッドが定義されているのか、を覚えていくことが重要です。今回は、まずは、いろいろなオブジェクトの使い方を見ていきましょう。
@ 基本的なオブジェクト
最も基本的で、多用するオブジェクトは、数値と文字列です。また、データを格納するためのオブジェクトでよく使うものに、タプル、リストがあります。
■数値
Pythonでは、数値は(短い)整数、長整数、実数、複素数に分けられます。それぞれクラス名は次の表のとおりです。
| 区分 | クラス名 |
|---|---|
| (短い)整数 | int |
| 長整数 | long |
| 実数 | float |
| 複素数 | complect |
-(231-1)〜(231-1) の整数はint、それより大きい整数はlong、小数点以下のある数値はfloatとなります。intとlongはコンピュータ内部での都合で分けられていて、通常は使い分けを意識する必要はありません。floatは(これもコンピュータ内部の都合ですが)計算誤差が発生する場合があるので、小数点以下が不要なときは整数を使います。
次のスクリプトは、数値の四則演算を行います。
print 2 + 3 * 4 #=> 14 print (2 + 3) * 4 #=> 20 print 7 / 3 #=> 2 print 7.0 / 3 #=> 2.33333333333 print 2 ** 3 #=> 8
普通の計算順序どおりに、掛け算・割り算は足し算・引き算より先に行われます。括弧で囲めば括弧の内側のほうが先に計算されます。「x ** y」は、xのy乗を表します。
割り算は、実数の場合は小数点以下まで計算されますが、整数同士だと切り捨てられます。(正確には答がマイナス無限大の方向へ丸められます。)
■文字列
文字の連なりを文字列といいます。Pythonでは、文字列は「"」(ダブルクォーテーション)または「'」(シングルクォーテーション)で囲んで表します。文字列のクラスはstrです。
文字列同士は「+」で連結することができます(書き方は x + y)。数値であってもstr() 関数で文字列に変換すれば連結できます。
例えば、リスト1のようにして文字列を操作できます。
Note.
|
リスト1のそれぞれのメソッド(いずれもstrクラスのメソッド)の働きは次のとおりです。
- count(文字列)
- 対象となる文字列オブジェクトの中に、引数の文字列がいくつあるか数えます。
- lower()
- 対象文字列中の英大文字を小文字にした新しい文字列オブジェクトを生成します(対象オブジェクトは変更しない)。
- find(文字列)
- 対象文字列から引数の文字列を検索し、最初に発見した位置(Note参照)を返します。
文字列オブジェクト[s:t]と書くと、先頭から数えて位置sから位置 (t - 1) までの文字で新しい文字列オブジェクトを作ります。
文字列には、通常モード(ASCIIモード)とUnicodeモードがあり、文字単位で扱いたいときは、文字列をUnicodeモードにします。
ASCIIモードでの文字位置と文字コード
|
文字列をUnicodeモードにするには、スクリプト中で文字列を書くときに「"」あるいは「'」の前に「u」を付けます。Unicode文字列のクラスは、unicodeです。メソッドは、strクラスの文字列オブジェクトと同じものが使えます。
リスト2で、通常モード(ASCIIモード)とUnicodeモードで文字列の扱いがどう変わるか見てみます。
Unicodeモードでは、find() の結果(戻り値)が文字単位になっていることが分かります。lower()で全角文字も小文字になるようになります。
■タプルとリスト
数値や文字列だけでは複雑なデータを表すことはできません。そこで、複数のデータを束ねるための、データを格納できるオブジェクトを導入しましょう。
Pythonでは、タプル (tuple)、リスト (list)などを用います。
タプルとリストはいずれも要素を一列にして格納します。タプルはいったん生成すると、要素の追加、削除はできません。リストは後から追加も削除もできます。この違い以外は、タプルとリストは同じように使えます。
リスト3は、タプルとリストを生成し、list#append() メソッドでリストに後ろ(右)から要素を追加します。文字列と同様に、[n]あるいは[lower:upper]で、要素を参照したり、一部分からなる新しいタプル、リストを作ることができます。
格納すると言っても、オブジェクトの内部に要素が埋め込まれる訳ではありません。外から見ると格納されているように見えますが、内部ではオブジェクトへのリンクが張られています。リスト3で、変数sが指すリストに要素を追加すると、変数listのリストが変更されたように見えます。
オブジェクトは、図2のようにオブジェクト空間に浮かんでいます。listの2番目の要素と変数sは同じオブジェクトを指しています。
@ 関数を定義する
前回掲載したサンプルプログラムでは、ウィンドウとボタンを表示しただけで、押しても何も起こりませんでした。今回は、ボタンを押すと文字列を出力するようにしてみます。自分で関数を定義します。
自分で関数を定義するには、次のようにします。
def 定義する関数名 ( 引数名, ... ) :
文
文
...
defの行の最後に「:」(コロン)を書き、次の行から行を字下げ(左側に空白を入れる)して、実行する文を書いていきます。字下げするのを止めたところがその関数の終わりになります。
例で見たほうが感じがつかめます。リスト4は、on_clicked()関数を定義して、ボタンが押されたときにターミナル画面に「ボタンが押された!」と表示します。
Tkinter.Button()のキーワード引数commandで指定し、on_clicked()を登録します。ボタンが押された時には、Tk#mainloop()の中から登録された関数が呼び出されます。
@ 名前空間とスコープ
変数がオブジェクトを指すラベル(付箋)だということは、前回も解説しました。変数は、文脈によっては、同じ名前であっても別のオブジェクトを指すことがあります。ある変数が有効な範囲をスコープ(scope)といいます。
変数は名前空間の中にありますが、ある関数が呼び出されてその中の文が実行される時、新しい名前空間が導入されます。関数の引数、それから関数の中で代入した変数は、関数の外側の変数とは別の名前空間に属します。関数の中の文の実行が終わるときに、その関数の名前空間は消滅します。
次のプログラムは、関数fooの外側と内側で同じ名前の変数yを使っています。
y = 100
def foo(x):
y = x * 2
print "in foo(): y = ", y
foo(1)
foo(2)
print y
foo() の中で変数yに代入していますが、外側の変数yには影響しません(外側のyが指すオブジェクトは変わりません。)(図3)。このプログラムの実行結果は、次のようになります。
in foo(): y = 2 in foo(): y = 4 100
関数内でのみ有効な変数をローカル変数といいます。言い換えると、ローカル変数のスコープは関数です。なお、すべての関数のなかから参照できる変数をグローバル変数といいます。
ただし、関数呼び出しでは、名前(変数)が新しく取られるだけで、オブジェクトがコピーされるわけではありません。引数として与えられた変数を介してオブジェクトを操作すると、その影響は呼び出した側にまで及びます。
次のプログラムは、リストを関数の中で変更します。メソッドの外側で生成したオブジェクトが変更されます。
def hoge(ary):
ary.append(10)
ary = [1, 2, 3]
hoge(ary)
print ary #=> [1, 2, 3, 10]
@ Tkinter
これまでのサンプルでは、ラベル、ボタンをウィンドウに貼り付けてきました。Tkinterには、それ以外にも、いろいろなものがあります。
■ウィジェット
ウィンドウに貼れるものをウィジェット (widget) といいます。
Note.
|
いくつかのウィジェットをウィンドウに並べてみます(リスト5)。実行すると、画面1のように表示されます。
ウィジェットのクラスとその内容は表1のとおりです。それぞれのクラスの使い方は、紙面もないので、An Introduction to 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>」は、マウスの左ボタンを押す、というイベントです。実際にイベントが発生したときには、イベントの情報を格納したオブジェクトを引数として、登録しておいた関数が呼び出されます。
表2に、イベントの一部を掲げます。
| イベント | 内容 |
|---|---|
| <Button-x> | マウスのボタンが押された。xはボタン番号で、1=左ボタン、2=中ボタン、3=右ボタン |
| <Bx-Motion> | マウスがドラッグされた。xはボタン番号。 |
| <ButtonRelease-x> | マウスのボタンが離された。xはボタン番号 |
| <Double-Button-x> | マウスがダブルクリックされた。xはボタン番号 |
| <Enter> | マウスポインタがウィジェットに重なった(入ってきた)。キーボードのEnterキーが押されたにあらず。 |
| <Leave> | マウスポインタがウィジェットから離れた |
@ クラスを定義する
関数は、引数として与えられたオブジェクトと、関数内で生成したオブジェクト、グローバル変数しか参照できません。大きな仕事をするには、多くの関数が協力・分担して、いろいろなことができなければなりません。
すでに見てきたように、それぞれのオブジェクトは多くのメソッドを持ち、これらがオブジェクトの内部情報を共有しています。外側から見ると、メソッドに対して指図することで(メソッド呼び出し)、オブジェクトを操作します(図4)。
我々もこのようなオブジェクトを作っていい頃合いです。オブジェクトの振る舞い(メソッド)は、クラスで定義します。
クラスを定義する文法は、次のようになります。
class 定義するクラス名 :
メソッド定義など...
■メソッドを定義する
メソッドを定義する文法は、関数とほとんど同じで、次のようになります。
def 定義するメソッド名 ( 自オブジェクト変数名, 引数, ... ) :
文...
関数定義と違うのは、最初の引数として操作対象となるオブジェクトを指す変数を用意することだけです。この変数の名前は何でもいいのですが、慣例的にselfとします。
字下げして文を書いたり、呼び出されるたびに新しい名前空間が導入されるのも同じです(ローカル変数のスコープも同じ)。
実際の書き方をリスト8に示します。__init__メソッドは、特別なメソッドで、オブジェクトが生成されるときに呼び出されます。
■インスタンス変数
オブジェクトの内部状態を保持する変数をインスタンス変数といいます。インスタンス変数は、次のように書きます。
オブジェクト . 変数名 # 文法
上記のリスト8では、self.value がインスタンス変数です。インスタンス変数は、オブジェクトが生成されるたびに作られ、オブジェクトのメソッドで共有できます。オブジェクトが消滅するまで生き続けます。
@ サンプル:お絵かきプログラム
それでは、これまでに見てきたことを踏まえて、ごく短いお絵かきプログラムを書いてみます(リスト9)。実行結果は、画面2のようになります。
どのようなことをしているか、順に見ていきましょう。
(1) Scribble#__init__() (リスト9囲み3)
お絵かきのためのキャンバスオブジェクトと、プログラムを終了するためのボタンオブジェクトを作ります。キャンバスオブジェクトは、bg引数で背景を白に、widthとheightで大きさを指定します。
ボタンオブジェクトには、Tkinter.Tk#quitメソッドを登録しておきます。クリックされたら、このメソッドが呼び出され、プログラムが終了します。
キャンバスにはbind()でイベントを結び付けます。ユーザーがマウスの左ボタンを押したとき、マウスをドラッグしたときに、それぞれon_pressedメソッド、on_draggedメソッドが呼び出されるようにします。
(2) Scribble#on_pressed() (リスト9囲み1)
ボタンが押されたら点を描きます。Tkinter.Canvas#create_oval()は楕円を描くメソッドです。この上下左右の座標を1点にすることで、点を描きます。
インスタンス変数sx、syに現在位置を入れておきます。on_dragged()でこれを使います。
(3) Scribble#on_dragged() (リスト9囲み2)
マウスがドラッグされたときは、直前の座標との間に線を描けばうまくいきます。on_pressed()で保存した座標との間に、Tkinter.Canvas#create_line()で線を描きます。
その後、最後の座標(sx, sy)を更新しておきます。こうすることで、さらにマウスを動かしたときに、今度はここから次の座標まで線を引けます。
@ 色を変更する(オプションメニュー)
前節リスト9では、線の色と太さが一定でした。オプションメニューとスケールを使って、これらを変更できるようにしてみましょう。
Scribbleクラスに新しいメソッドを追加します(リスト10)。
このメソッドは、オプションメニュー(OptionMenuクラス)オブジェクトを生成し、それをウィンドウに配置します。
このメソッドの動作を順に説明すると、
- オプションメニューに表示する色の選択肢のリストを生成します。色は、"red", "green"など色の名前を指定することも、"#FF00FF"のように赤、緑、青それぞれ00〜255(16進数で00〜FF)の範囲で指定することもできます。
- StringVarオブジェクトを生成します。このオブジェクトに、オプションメニューで選択された項目の文字列が格納されます。StringVarオブジェクトは、setメソッドで値を設定でき、getメソッドで値を読み取ることができます。
- オプションメニューで最初に表示する値をStringVar#set()で設定します。COLORS[n]で (n + 1) 番目の値が得られるので、ここでは2番目の"green"を設定することになります。
- OptionMenuオブジェクトを生成します。最初の引数として貼り付けるウィンドウのオブジェクトを、2番目の引数としてオプションメニューで選択された項目の値を格納するオブジェクトを指定します。3番目以降の引数で選択肢を指定します。COLORSの前の「*」は、リストの要素を展開する記号で、次のように書いたかのように動きます。
Tkinter.OptionMenu(window, self.color, "red", "green", "blue", "#FF00FF", "black")
- OptionMenuオブジェクトのpackメソッドで、左詰めにします。
メソッドを作ったら、Scribble#__init__メソッドに、このメソッドを呼び出す文を追加します(リスト11)。
さらに、点を打つところ、線を引くところを修正しておきます(リスト12)。
これで色を選べるようになりました(画面3)。
@ 線の太さを変更する(スケール)
今度は線の太さをスケールを使って変えられるようにしてみましょう。また新しいメソッドを作ります(リスト13)。
このメソッドは、スケールオブジェクトを生成し、ウィンドウに貼り付けます。
- Tkinter.Scale()でスケールオブジェクトを生成します。最初の引数にウィンドウのオブジェクトを指定します。キーワード引数from_とtoで、スケールの範囲を指定します。また、orientで方向を指定します。Tkinter.HORIZONTALだと水平方向のスケールになります。
- 最初の値(初期値)をScaleオブジェクトのsetメソッドで指定します。
- Scaleオブジェクトのpackメソッドで、左詰めで詰めます。
新しいメソッドを作成したら、Scribble#__init__()で、width_chooser()を呼び出す文を追加します。color_chooser()を呼び出す行の次がいいでしょう(リスト14)。
加えて、on_pressed(), on_dragged()も修正します。self.width.get()で現在のスケールの値を得ます。これでペンの太さと色を変えられるようになりました(画面4)。
Note.