HOME Python download 書き込む

10. キャンバスにドラッグできるオブジェクトを描こう


1. 初めに

今回は、大きめのキャンバスの作り方と、オブジェクトをつまんで動かしたりする方法について述べます。 題材は、知育教材 (toy.py) です。要するに子供だましですが、童心に還って遊んでみてください。

2. Tk.Canvas へのスクロールバーのつけ方

以下のコードは toy.py からの抜粋です。 スクロールバーのつけ方は以下の通りです。
  1. まず、Tk.Canvas を作ります (self.cvs)。そのとき、 オプションで scrollregion を指定します。
    下の例は縦横に 0--40 cm スクロールするという意味です。widthheight は表示する幅と高さです。
  2. self.cvs を grid します。縦横にスクロールバーをつけるときは必ず grid してください。
    sticky= Tk.N+Tk.E+Tk.W+Tk.S を指定して、上下左右に広がるようにします。
  3. 次に横スクロールバーを作ります (xscroll)。 オプションに command=self.cvs.xview を指定します。
  4. xscroll を grid します。 sticky=Tk.E+Tk.W を指定して、左右に伸びるようにします。
  5. 同様に縦スクロールバー (yscroll) を作って grid します。
  6. その後 config メソッドを使って、self.cvs の属性 xscrollcommnadxscroll.set に、 yscrollcommnadyscroll.set にします。
  7. 親 widget の grid_rowconfigure, grid_columncofigure メソッドを使って、row=0, column=0 の weight を 1 にします。 こうすることによって、キャンバスが親 widget の伸縮に伴って伸縮します。
  8. 親 widget を pack するときは fill=Tk.BOTH, expand=1 を指定します。
以上で縦横スクロールバー付伸縮自在キャンバスが出来上がりです。

[code 1] Tk.Canvas を作る部分

01:         def __init__(self, master=None):
02:             Tk.Frame.__init__(self, master)
03:             self.master.title("Toy")
04:             self.master.geometry("+20+20")
05:             self.cvs = Tk.Canvas(self, scrollregion=("0c", "0c",  "40c",  "40c"), width="20c", height="20c",   
06:                                        relief=Tk.SUNKEN, borderwidth=2, bg='#FFEFD5') 
07:             self.cvs.grid(row=0, column=0, sticky= Tk.N+Tk.E+Tk.W+Tk.S)
08:             
09:             xscroll = Tk.Scrollbar(self, orient=Tk.HORIZONTAL, command=self.cvs.xview)
10:             xscroll.grid(row=1, column=0, sticky=Tk.E+Tk.W)
11:     
12:             yscroll = Tk.Scrollbar(self, orient=Tk.VERTICAL, command=self.cvs.yview)
13:             yscroll.grid(row=0, column=1, sticky=Tk.N+Tk.S)
14:             
15:             self.cvs.config(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
16:             self.grid_rowconfigure(0, weight=1, minsize=0)     
17:             self.grid_columnconfigure(0, weight=1, minsize=0)  
[code 2] Frame を pack する部分
01:     if __name__ == '__main__':
02:         f = Frame()
03:         f.pack(fill=Tk.BOTH, expand=1)
04:         f.mainloop()

3. 知育教材

図1に示す知育教材 (toy.py) を作ります。この知育教材は以下の4つの部分からなっています。カッコ内はキャンバス上の位置です。
(a)
(b)
(c)
(d)

図1:
a) 色分けして箱にしまう課題(左上)
b) 正三角形を大きさ順に並べる課題(右上)
c) 番号を線で結ぶ課題(左下)
d) しりとりになるように単語を線で結ぶ課題(右下)

3.1. ドラッグできるキャンバスオブジェクト CanvasItem の定義

toy.py のコードを示す前に、ドラッグできるキャンバスオブジェクトの定義の仕方について述べます。

刺激に反応するオブジェクトを表すのには新しいクラスを定義するのが OOP の定石です。 ここでもそれに従って class CanvasItem を定義します。
CanvasItem の定義は以下の通りです。

[code 3]

01:     class CanvasItem:
02:         canvas = None
03:         
04:         def make_binds(self):
05:             CanvasItem.canvas.tag_bind(self.id, '<1>', self.drag_start)
06:             CanvasItem.canvas.tag_bind(self.id, '<Button1-Motion>', self.dragging)
07:     
08:         def drag_start(self, event):
09:             self.x = event.x
10:             self.y = event.y
11:     
12:         def dragging(self, event):
13:             x1 = event.x
14:             y1 = event.y
15:             CanvasItem.canvas.move(self.id, x1-self.x, y1-self.y)
16:             self.x = x1
17:             self.y = y1

3.2. CanvasItem サブクラス

このスクリプトでは、CanvasItem のサブクラスとして、CanvasOval, CanvasRectangle, CanvasTriangle, CanvasText, CanvasCircleNumber を定義しています。その部分のコードを以下に示します。

[code 4]

01:     class CanvasOval(CanvasItem):
02:         def __init__(self, x0, y0, x1, y1, **key):
03:             self.id = CanvasItem.canvas.create_oval(x0, y0, x1, y1, **key)
04:             self.make_binds()
05:             
06:     class CanvasRectangle(CanvasItem):
07:         def __init__(self, x0, y0, x1, y1, **key):
08:             self.id = CanvasItem.canvas.create_rectangle(x0, y0, x1, y1, **key)
09:             self.make_binds()
10:     
11:     class CanvasTriangle(CanvasItem):
12:         def __init__(self, x, y, r, color):
13:             self.id = CanvasItem.canvas.create_polygon(str(x)+'c', str(y-r*SQRT3)+'c', str(x-r)+'c', str(y)+'c',
14:                                                        str(x+r)+'c', str(y)+'c', fill=color)
15:             self.make_binds()
16:     
17:     class CanvasText(CanvasItem):
18:         def __init__(self, *pos, **key):
19:             self.id = CanvasItem.canvas.create_text(*pos, **key)
20:             self.make_binds()
21:            
22:     
23:     class CanvasCircleNumber(CanvasItem):
24:         def __init__(self, x, y, num, session):
25:             s = session + str(num)
26:             self.circle = CanvasItem.canvas.create_oval('%fc' % (x-0.7), '%fc' % (y-0.7), '%fc' % (x+0.7), '%fc' % (y+0.7),
27:                                                         width=2, outline='red', fill='#FFFFF0', tag=s)
28:             self.number = CanvasItem.canvas.create_text(str(x)+'c', str(y)+'c', text=str(num),
29:                                                         font=('Helvetica', '14', 'bold'), tag=s)
30:             CanvasItem.canvas.tag_bind(s, '<1>', self.drag_start)
31:             CanvasItem.canvas.tag_bind(s, '<Button1-Motion>', self.dragging)
32:     
33:         def dragging(self, event):
34:             x1 = event.x
35:             y1 = event.y
36:             CanvasItem.canvas.move(self.circle, x1-self.x, y1-self.y)
37:             CanvasItem.canvas.move(self.number, x1-self.x, y1-self.y)
38:             self.x = x1
39:             self.y = y1
  1. CanvasOval, では、CanvasItem.canvas 上に楕円を描き、その ID を self.id に代入しています。 その後、self.make_binds メソッドによって、ドラッグができるようにします。 CanvasRectangle, CanvasText も同様です。
  2. CanvasTriangle では、CanvasItem.canvas 上に create_polygon メソッドを使って正三角形を描きその ID を self.id に代入しています。
  3. CanvasCircleNumber では、円と数字を同時に移動させなければならないので、 make_bind を使わず、自前の bind をしています。また、dragging メソッドも再定義しています。

3.3. toy.py 全体の簡単な説明

最後に toy.py の全コードを示します。

[code 5]

 01:     #! /usr/bin/env python
 02:     # -*- coding: shift_jis -*-
 03:     """
 04:     toy.py
 05:     
 06:     July 08, 2005
 07:     """
 08:     import Tkinter as Tk
 09:     import random as R
 10:     import math
 11:     
 12:     
 13:     
 14:     TITLE_COLOER = '#FF6600'
 15:     COLOR = ['red', 'gold', 'blue']
 16:     TITLE_FONT = ('Helvetica', '20', 'bold')
 17:     CAPPING = [u'あさがお', u'おけ', u'けいと', u'とり', u'リス', u'スイカ', u'かき', u'きもの', u'のはら']
 18:     SQRT3 = math.sqrt(3)
 19:     
 20:     
 21:     class CanvasItem:
 22:         canvas = None
 23:         
 24:         def make_binds(self):
 25:             CanvasItem.canvas.tag_bind(self.id, '<1>', self.drag_start)
 26:             CanvasItem.canvas.tag_bind(self.id, '<Button1-Motion>', self.dragging)
 27:     
 28:         def drag_start(self, event):
 29:             self.x = event.x
 30:             self.y = event.y
 31:     
 32:         def dragging(self, event):
 33:             x1 = event.x
 34:             y1 = event.y
 35:             CanvasItem.canvas.move(self.id, x1-self.x, y1-self.y)
 36:             self.x = x1
 37:             self.y = y1
 38:     
 39:     
 40:     class CanvasOval(CanvasItem):
 41:         def __init__(self, x0, y0, x1, y1, **key):
 42:             self.id = CanvasItem.canvas.create_oval(x0, y0, x1, y1, **key)
 43:             self.make_binds()
 44:             
 45:     class CanvasRectangle(CanvasItem):
 46:         def __init__(self, x0, y0, x1, y1, **key):
 47:             self.id = CanvasItem.canvas.create_rectangle(x0, y0, x1, y1, **key)
 48:             self.make_binds()
 49:     
 50:     class CanvasTriangle(CanvasItem):
 51:         def __init__(self, x, y, r, color):
 52:             self.id = CanvasItem.canvas.create_polygon(str(x)+'c', str(y-r*SQRT3)+'c', str(x-r)+'c', str(y)+'c',
 53:                                                        str(x+r)+'c', str(y)+'c', fill=color)
 54:             self.make_binds()
 55:     
 56:     class CanvasText(CanvasItem):
 57:         def __init__(self, *pos, **key):
 58:             self.id = CanvasItem.canvas.create_text(*pos, **key)
 59:             self.make_binds()
 60:            
 61:     
 62:     class CanvasCircleNumber(CanvasItem):
 63:         def __init__(self, x, y, num, session):
 64:             s = session + str(num)
 65:             self.circle = CanvasItem.canvas.create_oval('%fc' % (x-0.7), '%fc' % (y-0.7), '%fc' % (x+0.7), '%fc' % (y+0.7),
 66:                                                         width=2, outline='red', fill='#FFFFF0', tag=s)
 67:             self.number = CanvasItem.canvas.create_text(str(x)+'c', str(y)+'c', text=str(num),
 68:                                                         font=('Helvetica', '14', 'bold'), tag=s)
 69:             CanvasItem.canvas.tag_bind(s, '<1>', self.drag_start)
 70:             CanvasItem.canvas.tag_bind(s, '<Button1-Motion>', self.dragging)
 71:     
 72:         def dragging(self, event):
 73:             x1 = event.x
 74:             y1 = event.y
 75:             CanvasItem.canvas.move(self.circle, x1-self.x, y1-self.y)
 76:             CanvasItem.canvas.move(self.number, x1-self.x, y1-self.y)
 77:             self.x = x1
 78:             self.y = y1
 79:     
 80:     
 81:     
 82:            
 83:     class Frame (Tk.Frame):       
 84:         
 85:         def __init__(self, master=None):
 86:             Tk.Frame.__init__(self, master)
 87:             self.master.title("Toy")
 88:             self.master.geometry("+20+20")
 89:             self.cvs = Tk.Canvas(self, scrollregion=("0c", "0c",  "40c",  "40c"), width="20c", height="20c",
 90:                                        relief=Tk.SUNKEN, borderwidth=2, bg='#FFEFD5')  
 91:             self.cvs.grid(row=0, column=0, sticky= Tk.N+Tk.E+Tk.W+Tk.S)
 92:             
 93:             xscroll = Tk.Scrollbar(self, orient=Tk.HORIZONTAL, command=self.cvs.xview)
 94:             xscroll.grid(row=1, column=0, sticky=Tk.E+Tk.W)
 95:     
 96:             yscroll = Tk.Scrollbar(self, orient=Tk.VERTICAL, command=self.cvs.yview)
 97:             yscroll.grid(row=0, column=1, sticky=Tk.N+Tk.S)
 98:             
 99:             self.cvs.config(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
100:             self.grid_rowconfigure(0, weight=1, minsize=0)     
101:             self.grid_columnconfigure(0, weight=1, minsize=0)  
102:     
103:             # assign a CanvasItem class parameter
104:             CanvasItem.canvas=self.cvs
105:     
106:             # binding
107:             self.cvs.bind('<3>', self.draw_start)
108:             self.cvs.bind('<Button3-Motion>', self.drawing)
109:             self.cvs.bind('<Double-Button-1>', self.delete_line)
110:     
111:             
112:             # Display a 2x2 rectangular grid.
113:             self.cvs.create_line('0c', '20c', '40c', '20c', width=2)
114:             self.cvs.create_line('20c', '0c', '20c', '40c', width=2)
115:     
116:     
117:             # First Toy
118:             self.cvs.create_text('10c', '1.5c', text=u'色別に整理して、しまいましょう。\n左ボタンドラッグで動きます。',
119:                                   font=TITLE_FONT, fill=TITLE_COLOER)
120:             for i, c in enumerate(COLOR):
121:                 self.cvs.create_line('%dc' % (i*6+1), '16c', '%dc' % (i*6+1), '19c', '%dc' % (i*6+6),
122:                                     '19c', '%dc' % (i*6+6), '16c', fill=c, width=3)
123:     
124:             for i in range(5):
125:                 x=R.randint(200, 1800) * 0.01
126:                 y=R.randint(300, 1500) * 0.01
127:                 r=R.randint(30, 150) * 0.01
128:                 c=R.randint(0,2)
129:                 CanvasOval('%fc' % (x-r), '%fc' % (y-r), '%fc' % (x+r), '%fc' % (y+r), fill=COLOR[c], width=0)
130:     
131:             for i in range(5):
132:                 x=R.randint(100, 1700) * 0.01
133:                 y=R.randint(300, 1500) * 0.01
134:                 w=R.randint(50, 150) * 0.01
135:                 h=R.randint(50, 150) * 0.01
136:                 c=R.randint(0,2)
137:                 CanvasRectangle('%fc' % (x), '%fc' % (y), '%fc' % (x+w), '%fc' % (y+h), fill=COLOR[c], width=0)
138:     
139:     
140:             # Second Toy
141:             self.cvs.create_text('30c', '1.5c', text=u'三角形を大きい順に並べましょう。\n左ボタンドラッグで動きます。',
142:                                   font=TITLE_FONT, fill=TITLE_COLOER)
143:             for i in range(10):
144:                 x=R.randint(2200, 3800) * 0.01
145:                 y=R.randint( 300, 1950) * 0.01
146:                 r=R.randint(3000, 20000) * 0.0001
147:                 c=R.randint(0,2)
148:                 CanvasTriangle(x, y, r, COLOR[c])
149:                                   
150:             # Third Toy
151:             self.cvs.create_text('10c', '21.5c', text=u'数字を順番に線で結びましょう。\n右ボタンドラッグで線が引けます。',
152:                                   font=TITLE_FONT, fill=TITLE_COLOER)
153:             self.cvs.create_text('10c', '22.8c', text=u'数字が重なっていたら、ドラッグして位置を少しずらしましょう。',
154:                                   font=('Helveticla', '12'))
155:             for i in range(12):
156:                 x=R.randint(200, 1800) * 0.01
157:                 y=R.randint(2400, 3600) * 0.01
158:                 CanvasCircleNumber(x, y, i+1, 'third')
159:                 
160:     
161:             # Fourth Toy
162:             self.cvs.create_text('30c', '21.5c', text=u'「しりとり」になるように単語を線で結びましょう。\n'
163:                                                       u'右ボタンドラッグで線が引けます。',
164:                                   font=TITLE_FONT, fill=TITLE_COLOER)
165:             self.cvs.create_text('30c', '22.8c', text=u'単語が重なっていたら、ドラッグして位置を少しずらしましょう。',
166:                                   font=('Helveticla', '12'))
167:     
168:                                   
169:             for txt in CAPPING:
170:                 x=R.randint(2200, 3800) * 0.01
171:                 y=R.randint(2400, 3600) * 0.01
172:                 r=R.randint(0,200)
173:                 g=R.randint(0,200)
174:                 b=R.randint(0,200)
175:                 CanvasText(str(x)+'c', str(y)+'c', text=txt, font=('Helvetica', '18', 'bold'), fill='#%02X%02X%02X' % (r, g, b))
176:                 
177:     
178:     
179:     
180:     ###
181:         def draw_start(self, event):
182:             self.x = self.cvs.canvasx(event.x, '0.2m')
183:             self.y = self.cvs.canvasy(event.y, '0.2m')
184:     
185:         def drawing(self, event):
186:             x1 = self.cvs.canvasx(event.x, '0.2m')
187:             y1 = self.cvs.canvasy(event.y, '0.2m')
188:             self.cvs.create_line(self.x, self.y, x1, y1, width=2, fill='#CC3300', tag='line')
189:             self.x = x1
190:             self.y = y1
191:     
192:         def delete_line(self, event):
193:             self.cvs.delete('line')
194:         
195:                                   
196:     
197:     ##-----------------------------------------------------
198:     if __name__ == '__main__':
199:         f = Frame()
200:         f.pack(fill=Tk.BOTH, expand=1)
201:         f.mainloop()

簡単な説明

以下に簡単な説明を示します。カッコ内は行数です。

5. 終わりに

toy.py は Tkinter demo にある items.py を簡略化したものです。そちらも見てください。
Tk.Canvas を使うといろいろと楽しいアプリケーションが書けることがお分かりいただけたと思います。
次回は Tk.Canvas を使って花火を打ち上げて、 Tk.Canvas の説明の締めくくりにしたいと思います。