I am trying to animate a ball, that is be moving forward. In addition, if the user presses up or down button, it should move respectively.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.update()
self.frame.bind_all("<Up>", lambda event: self.move_up())
self.frame.bind_all("<Down>", lambda event: self.move_down())
pass
def move_up(self):
self.y -= 10
pass
def move_down(self):
self.y += 10
pass
def update(self):
canvas.delete("obj")
self.count += 10
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
root.after(1000, self.update)
pass
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
There's an issue, though. Given the fact that the ball is moving every 1 second, it doesn't respond to the key press fast enough. As a result, if you click the up kick, the screen is updating after one second.
I know there's a .move() function inside Tkinter. However, due to many reason I cannot use that.
My main concern is the interval it take for the screen to be updated.
From what I understand you want to be able to keep the timer moving forward consistent but also be able to move up and down at any point even outside of the timer.
to accomplish this I moved the timer to its own function and allowed the up down functions to call the object destroy/create function.
I have updated you code to reflect that.
Take a look at this code and let me know if it helps.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.object_timer()
self.frame.bind_all("<Up>",lambda event: self.move_up())
self.frame.bind_all("<Down>",lambda event: self.move_down())
def move_up(self):
self.y -= 10
self.update()
def move_down(self):
self.y += 10
self.update()
def update(self):
canvas.delete("obj")
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
def object_timer(self):
self.update()
self.count += 10
root.after(1000, self.object_timer)
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
It's all just math. You can call update twice as much, and move the ball half as much if you want to keep the ball moving at the same speed while reacting to keypresses faster.
For example, instead of moving 10 pixels every second, have it move 1 pixel every tenth of a second. The end result is still 10 pixels every second. The animation will be smoother, and it will react to keypresses much faster.
By the way, there's no need to delete and recreate the oval every iteration. It's possible to move items on the canvas. Deleting and recreating will eventually cause performance problems do to how the canvas is implemented internally. Each object you create gets a new unique id, and the canvas will slow down when it has a large number of ids. The canvas does not re-use an id.
I would also recommend against using lambda. Unless you need it, it adds complexity without adding any value. Just add an extra parameter to move_up and move_down:
self.frame.bind_all("<Up>",self.move_up)
self.frame.bind_all("<Down>", self.move_down)
...
def move_up(self, event=None):
...
def move_down(self, event=None):
...
Related
suppose I have code like the following. I want to black blob to end the game when it touched the frame, in other words, the wall. I actually am a beginner to Tkinter Canvas so if you give a code explanation too complex I can not understand it.
# Modules
from tkinter import *
# Classes
class Player:
def __init__(self, master=None):
self.master = master
self.x = 0 # To take care of movement in "x" direction.
self.y = 0 # To take care of movement in "y" direction.
self.canvas = Canvas(master) # Canvas object to create shape.
self.rectangle = self.canvas.create_rectangle(40, 40, 25, 25, fill="black") # Creating rectangle.
self.canvas.pack() # Packing the canvas.
self.movement() # Calling the movement method to move the rectangle.
def movement(self):
self.canvas.move(self.rectangle, self.x, self.y) # This is where the move() method is called.
"""
⬆ This moves the rectangle to x, y coordinates.
"""
self.canvas.after(100, self.movement) # Gives the character a cooldown time.
def left(self): # For motion in negative "x" direction.
self.x = -5
self.y = 0
def right(self): # For motion in positive "x" direction.
self.x = 5
self.y = 0
def down(self): # For motion in positive "y" direction.
self.x = 0
self.y = 5
def up(self): # For motion in negative "y" direction.
self.x = 0
self.y = -5
# Main Window
print("How To Play\nTry to not collid the frame.")
master = Tk()
master.title("Easy Game")
master.geometry("300x75")
master.resizable(True, True)
engine = Player(master)
master.bind("<KeyPress-Left>", lambda e: engine.left())
master.bind("<KeyPress-Right>", lambda e: engine.right())
master.bind("<KeyPress-Up>", lambda e: engine.up())
master.bind("<KeyPress-Down>", lambda e: engine.down())
# Mainloop
mainloop()
Thank you for further explanation.
I am making a draggable point(a circle). The code works, however, while dragging, if the mouse motion is quick the point stops moving. I have taken help from this code for making this program. I will be using this point later on for other purposes. Here is my full code,
from tkinter import *
import sys,os,string,time
class Point():
def __init__(self,canvas,x,y):
self.canvas = canvas
# It could be that we start dragging a widget
# And release it while its on another
# Hence when we land on a widget we set self.loc to 1
# And when we start dragging it we set self.dragged to 1
self.loc = self.dragged = 0
self.x = x
self.y = y
self.radius = 5
self.point = canvas.create_oval(self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius,fill="green",tag="Point")
canvas.tag_bind("Point","<ButtonPress-1>",self.down)
canvas.tag_bind("Point","<ButtonRelease-1>",self.chkup)
canvas.tag_bind("Point","<Enter>",self.enter)
canvas.tag_bind("Point","<Leave>",self.leave)
def down(self,event):
self.loc = 1
self.dragged = 0
event.widget.bind("<Motion>",self.motion)
canvas.itemconfigure(self.point,fill = "red")
def motion(self,event):
root.config(cursor = "exchange")
cnv = event.widget
cnv.itemconfigure(self.point,fill = "red")
self.x,self.y = cnv.canvasx(event.x), cnv.canvasy(event.y)
got = canvas.coords(self.point,self.x-self.radius,self.y-self.radius,self.x+self.radius,self.y+self.radius)
def enter(self,event):
canvas.itemconfigure(self.point,fill="blue")
self.loc = 1
if self.dragged == event.time:
self.up(event)
def up(self,event):
event.widget.unbind("<Motion>")
canvas.itemconfigure(self.point,fill="green")
self.canvas.update()
def chkup(self,event):
event.widget.unbind("<Motion>")
root.config(cursor = "")
canvas.itemconfigure(self.point,fill="green")
if self.loc: # is button released in the same widget as pressed
self.up(event)
else:
self.dragged = event.time
def leave(self,event):
self.up(event)
root = Tk()
root.title("Drag and Drop")
canvas = Canvas(root,width = 256, height = 256, borderwidth = 1)
point = Point(canvas,128,128)
canvas.pack()
root.mainloop()
Your problem is that your <Leave> binding can fire if you move the mouse outside of the tiny circle faster than you can process the move. That causes the binding for <Motion> to be disabled.
My recommendation is to a) don't bind on <Leave> to disable the binding, and b) bind on <B1-Motion> so that the binding is active only while the button is pressed.
from Tkinter import *
import random
root = Tk()
width = 700
height = 600
canvas = Canvas(root, width = width, height = height, bg = "light blue")
canvas.pack()
pipes = []
class NewPipe:
def __init__(self, pipe_pos, pipe_hole):
self.pipe_pos = list(pipe_pos)
def update(self):
self.pipe_pos[0] -= 3
self.pipe_pos[2] -= 3
def draw(self):
canvas.create_rectangle(self.pipe_pos, fill = "green")
def get_pos(self):
return self.pipe_pos
def generate_pipe():
pipe_hole = random.randrange(0, height)
pipe_pos = [width - 100, 0, width, pipe_hole]
pipes.append(NewPipe(pipe_pos, pipe_hole))
draw_items()
canvas.after(2000, generate_pipe)
def draw_items():
for pipe in pipes:
if pipe.get_pos()[2] <= 0 - 5:
pipes.remove(pipe)
else:
pipe.draw()
pipe.update()
canvas.after(100, draw_items)
def jump(press):
pass
canvas.bind("<Button-1>", jump)
canvas.after(2000, generate_pipe)
draw_items()
mainloop()
Right now I am trying to make a game where you have to dodge rectangles, which are pipes. It is basically Flappy Bird, but on Tkinter. In this code I am trying to generate pipes and move them, but the pipes I have drawn before do not leave and they just stay there. This means that when the pipe moves, the position it was just in doesnt change and that shape stays there. Is there any way to delete past shapes, or another way to move them?
canvas.create_rectangle(self.pipe_pos, fill = "green") returns an ID.
You can use this ID to put it into methods like
canvas.coords
canvas.delete
canvas.itemconfigure
canvas.scale
canvas.type
...
Have a look at help(canvas).
The canvas is not a framebuffer on which you paint stuff for one frame. The painted stuff does not go away and you can move it and change all the parameters you can use when creating.
I have a number of text widgets floating on a scrollable canvas widget. I want to allow users to resize them by dragging their edges and/or corners, possibly moving them if they drag the upper left edges or corner. I'm open to making them into frames with text widgets inside them, since I'm likely to do that anyway.
I figure I can handle the events manually if I can just get it to show resize handles. Do I need to catch mouseovers and clicks on a border myself?
Adding a "<Configure>" event binding does nothing, as one would expect. ttk.Sizegrip allegedly only works on top-level windows. There are lots of resources for preventing resizes, but very few for facilitating them, and they all seem to be for top-level windows.
I ended up making a frame around the widget with a fat border, catching mouse events, and handling all the dirty resize logic myself.
I had to record both the initial location of the location of the click and the position of each successive mouse-move, and use them for top/left and bottom/right resizes, respectively.
Edit: Here's a handy (relatively) encapsulated implementation.
from Tkinter import *
class ResizableCanvasFrame(Frame):
'''
Class that handles creating resizable frames on a canvas.
Don't pack it.
Set save_callback to whatever you want to happen when the mouse
lets up on the border. You can catch <Configure> too, but at least
in my case I didn't want to save the new position on every mouse move.
'''
def __init__(self, master, x, y, w, h, *args, **kwargs):
# master should be a Canvas
self.frame_thickness = 5
Frame.__init__(
self,
master,
*args,
borderwidth = self.frame_thickness,
cursor = 'fleur',
**kwargs
)
self.canvas = master
self.resize_state = None
self.bind('<Button-1>', self.mousedown)
self.bind('<B1-Motion>', self.mousemove)
self.bind('<ButtonRelease-1>', self.mouseup)
self.bind('<Destroy>', self.delete_item)
# add self to canvas
self.itemid = self.canvas.create_window(
x,
y,
window=self,
anchor="nw",
width=w,
height=h
)
self.save_callback = None
def canvas_coords(self):
return map(int, self.canvas.coords(self.itemid))
def move(self, dx, dy):
# strictly, this is out of the range of RCF,
# but it helps with the law of demeter
self.canvas.move(self.itemid, dx, dy)
def mousedown(self, event):
window_width = self.winfo_width()
window_height = self.winfo_height()
self.resize_state = {
'start_coords': (event.x, event.y),
'last_coords': (event.x, event.y),
'left_edge': (0 <= event.x < self.frame_thickness),
'right_edge': (window_width - self.frame_thickness <= event.x < window_width),
'top_edge': (0 <= event.y < self.frame_thickness),
'bottom_edge': (window_height - self.frame_thickness <= event.y < window_height),
}
def mousemove(self, event):
if self.resize_state:
resize = self.resize_state # debug var
event_x = event.x
event_y = event.y
# distance of cursor from original position of window
delta = map(int, (event.x - self.resize_state['start_coords'][0],
event.y - self.resize_state['start_coords'][1]))
# load current pos, size
new_x, new_y = self.canvas_coords()
new_width = int(self.canvas.itemcget(self.itemid, 'width'))
new_height = int(self.canvas.itemcget(self.itemid, 'height'))
# handle x resize/move
if self.resize_state['left_edge']:
# must move pos and resize
new_x += delta[0]
new_width -= delta[0]
elif self.resize_state['right_edge']:
new_width += (event.x - self.resize_state['last_coords'][0])
# handle y resize/move
if self.resize_state['top_edge']:
new_y += delta[1]
new_height -= delta[1]
elif self.resize_state['bottom_edge']:
new_height += (event.y - self.resize_state['last_coords'][1])
# save new settings in item, not card yet
self.resize_state['last_coords'] = (event.x, event.y)
self.canvas.coords(self.itemid, new_x, new_y)
self.canvas.itemconfig(self.itemid, width=new_width, height=new_height)
def mouseup(self, event):
if self.resize_state:
self.resize_state = None
if self.save_callback:
self.save_callback()
def delete_item(self, event):
self.canvas.delete(self.itemid)
You could use the PanedWindow widget, or a combination of a few of them, inside your canvas. They are designed to do that. Getting the PanedWindow to stretch like the "sticky" command inside the canvas is an unknown though.
That is what I was looking for when stumbled across this post.
The program I am writing has a tkinter window that is constantly being fed with data manually rather than being part of a mainloop. It also needs to track mouse location. I havn't found a workaround for tracking the mouse outside of mainloop yet, but if you have one please do tell.
from Tkinter import *
import random
import time
def getCoords(event):
xm, ym = event.x, event.y
str1 = "mouse at x=%d y=%d" % (xm, ym)
print str1
class iciclePhysics(object):
def __init__(self, fallrange, speed=5):
self.speed = speed
self.xpos = random.choice(range(0,fallrange))
self.ypos = 0
def draw(self,canvas):
try:
self.id = canvas.create_polygon(self.xpos-10, self.ypos, self.xpos+10, self.ypos, self.xpos, self.ypos+25, fill = 'lightblue')
except:
pass
def fall(self,canvas):
self.ypos+=self.speed
canvas.move(self.id, 0, self.ypos)
root = Tk()
mainFrame = Frame(root, bg= 'yellow', width=300, height=200)
mainFrame.pack()
mainCanvas = Canvas(mainFrame, bg = 'black', height = 500, width = 500, cursor = 'circle')
mainCanvas.bind("<Motion>", getCoords)
mainCanvas.pack()
root.resizable(0, 0)
difficulty = 1500
#root.mainloop()
currentIcicles = [iciclePhysics(difficulty)]
root.update()
currentIcicles[0].draw(mainCanvas)
root.update_idletasks()
time.sleep(0.1)
currentIcicles[0].fall(mainCanvas)
root.update_idletasks()
tracker = 0
sleeptime = 0.04
while True:
tracker+=1
time.sleep(sleeptime)
if tracker % 3 == 0 and difficulty > 500:
difficulty -= 1
elif difficulty <= 500:
sleeptime-=.00002
currentIcicles.append(iciclePhysics(difficulty))
currentIcicles[len(currentIcicles)-1].draw(mainCanvas)
for i in range(len(currentIcicles)):
currentIcicles[i].fall(mainCanvas)
root.update_idletasks()
for i in currentIcicles:
if i.ypos >= 90:
currentIcicles.remove(i)
root.update_idletasks()
There is no way. Mouse movement is presented to the GUI as a series of events. In order to process events, the event loop must be running.
Also, you should pretty much never do a sleep inside a GUI application. All that does is freeze the GUI during the sleep.
Another hint: you only need to create an icicle once; to make it fall you can use the move method of the canvas.
If you are having problems understanding event based programming, the solution isn't to avoid the event loop, the solution is to learn how event loops work. You pretty much can't create a GUI without it.