I've started coding a Connect 4 game (a click triggers a disc to fall) and I wanted to animate the discs' fall.
So I start by creating a new disc image :
discList[y][x] = can.create_image((xCan,yCurrent), image=imgRedDisc, anchor="nw")
The disc is created at the top of the canvas at xCan; yCurrent. What I want to do now is to animate the disc falling to its destination, yCan. I call my drop function which keeps incrementing the disc's yCurrent until it reaches yCan with recursivity and the after() method :
def drop(disc):
global yCan, yCurrent
can.move(disc, 0, 1)
yCurrent += 1
if yCurrent < yCan:
can.after(5, drop(disc))
Now what my problem is is that the disc's intermediary positions don't display, it only shows up directly at the bottom after a few seconds.
After some research I added a line to update the canvas :
def drop(disc):
global yCan, yCurrent
can.move(disc, 0, 1)
yCurrent += 1
can.update()
if yCurrent < yCan:
can.after(5, drop(disc))
Now I get to see my disc falling, but it gets laggy after each play ; the discs only start falling a few seconds after I've clicked (which triggers their fall). Another problem is that if I trigger two discs to fall almost silmutaneously by double-clicking very quickly, the first one stops its fall midway then simply falls out of the canvas.
My question is, how do I display every step of my discs' fall without can.update() ?
Also I store each disc id in a list (discList), is there a more convenient way to store dynamically created canvas images ?
The proper way to do animation in tkinter is to have a function that does one frame of animation, and then have it call itself repeatedly until some terminal condition.
For example:
def move_object(obj_id):
can.move(obj_id, 0, 1)
x0,y0,x1,y1 = can.coords(obj_id)
if y0 < yCan:
can.after(5, move_obj, obj_id)
The main difference to what you wrote is that you were using after incorrectly. You were doing this:
can.after(5, drop(disc))
... which is exactly the same as this:
result = drop(disc)
can.after(5, drop)
Notice the difference? Like with the command attribute, you must supply a reference to a function. You however were calling the function and then giving the result to after.
Here's a very simple example to experiment with:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=200, height=200)
canvas.pack(fill="both", expand=True)
ball = canvas.create_oval(50,0,100,50, fill="red")
def animate(obj_id):
canvas.move(obj_id, 0, 3)
x0,y0,x1,y1 = canvas.coords(obj_id)
if y0 > canvas.winfo_height():
canvas.coords(obj_id, 50,-50, 100, 0)
canvas.after(50, animate, obj_id)
animate(ball)
root.mainloop()
Related
Here is a rough example of what I want to do(my code is too long and messy to share here): -
import tkinter as tk
app = tk.Tk()
w, h = 600, 600
canvas = tk.Canvas(app, width = w, height = h)
canvas.pack()
Rec1 = canvas.create_rectangle(0, 0, 100, 100, fill = 'blue', tag = 'move_to_next_window')
Rec2 = canvas.create_rectangle(100, 100, fill='green', tag = 'dont_move_to_next_window')
app.mainloop()
I am sorry if I messed up a couple lines but this program should run by creating 2 rectangles. What I need help with is if I initiate a brand new window which is running off different code, how would I move Rec1 and its position to the other window. If its possible, could I copy all of the object's properties in the second window? Thank you for taking the time to read this (the second window can also use the tkinter canvas).
What I need help with is if I initiate a brand new window which is running off different code, how would I move Rec1 and its position to the other window.
You can't move canvas items from one canvas to another. Your only option is to delete the item in the first canvas and add a new item in the other canvas.
If its possible, could I copy all of the object's properties in the second window?
Yes, you can use the itemconfigure method to get all of the properties of a canvas object, you can use coords to get the coordinates, and you can get the type with type method.
Here's an example function that copies a rectangle from one canvas to another.
def copy_canvas_item(source, item_id, destination):
item_type = source.type(item_id)
coords = source.coords(item_id)
# N.B. 'itemconfigure' returns a dictionary where each element
# has a value that is a list. The currently configured value
# is at index position 4
attrs = {x[0]: x[4] for x in source.itemconfigure(item_id).values()}
if item_type == "rectangle":
item_id = destination.create_rectangle(*coords, attrs)
return item_id
The objective is to move alien1, atarts from 0,0 then moves all the way to the right, goes down and then all the way to the left, and then down.
from tkinter import *
import random
def enemigos():
global Enemigos #Enemigos downloads the image for alien1
n = random.randint(1,3)
if n == 1:
def movalien1():
alien1 = CanvasJuego.create_image(0,0, anchor = NW, image = Enemigos[0], tags= ('alien1'))
RIGHT1 = True
CoordsAlien1 = CanvasJuego.coords(alien1)
if (CoordsAlien1[0] < 1000 and RIGHT1==True):
CanvasJuego.coords(alien1, CoordsAlien1[0]+5, CoordsAlien1[1])
if ((CoordsAlien1[0]+5)==1000):
RIGHT1 = False
CanvasJuego.coords(alien1, CoordsAlien1[0], CoordsAlien1[1]+50)
elif (CoordsAlien1[0]>0 and RIGHT1==False):
CanvasJuego.coords(alien1, CoordsAlien1[0]-5, CoordsAlien1[1])
if ((CoordsAlien1[0]-5)==0):
RIGHT1 = True
CanvasJuego.coords(alien1, CoordsAlien1[0], CoordsAlien1[1]+50)
def rec():
movalien1()
root.after(20,rec)
root.after(20,movalien1())
Alien1 does appear at (0,0), but it won't move.
The problem is that you create a new "alien" every 20 milliseconds. You should be creating alien1 exactly once outside of movalien1. What is happening is that you create it at 0,0, then move it to 5.0. The alien is at 5,0. The next time through the loop you create a new alien at 0,0, and then move it to 5,0. You keep creating new aliens over and over and moving the new alien to 5,0.
Also, you can use the move method to move an item instead of adjusting its coordinates.
Finally, even though it doesn't actually matter in this code, you are calling after incorrectly here: root.after(20, movealien1()). It needs to be either root.after(20, movealien1) or just directly call movealien1() without using after.
So, what I am trying to do is when you open the window, it starts a process in which every 0.2 seconds it changes the first and 3rd value of the color (in which it converts the elements of the range into a hex value and then a string) to go from rgb( 86, 32, 86) to rgb(126, 32, 126). Although I thought this might just work, it doesn't. I only get a background of the first color and that's all.
from tkinter import *
import time
root = Tk()
for i in range(86,126):
h = hex(i)
h = str(h)
h = h[2] + h[3]
root.configure(background=("#" + h + "32" + h ))
time.sleep(0.2)
root.mainloop()
You must use the after function to give the window system time to process updates. Calling window update functions in a loop like that on the main thread will lock up the window until the loop terminates.
Try moving the code in the for loop into a new function, say updateBackground, and making it call itself recursively using after:
def updateBackground(i):
# ...
if i < 126:
root.after(200, lambda: updateBackground(i + 1))
Note that I used a lambda in order to increment i.
Credit: https://stackoverflow.com/a/36670519/1757964
Your main issue with this code is the use of sleep(). Because Tkinter is a single thread application and is event driven what ends up happening when you use sleep() the entire Tkinter instance freezes.
To work around this Tkinter provides a method called After() that is designed to schedule an event to happen after a time. So to get the same affect you are trying to get with sleep we can instead create a function that can call itself after 0.2 sec and provide it with the starting number and ending number.
from tkinter import *
root = Tk()
def do_something(start_number, end_number):
if start_number <= end_number:
h = str(hex(start_number))[2:] # combined all your work on h to one line
root.configure(background=("#{}32{}".format(h, h)))
start_number += 1
root.after(200, do_something, start_number, end_number)
do_something(86, 126)
root.mainloop()
Note that the color change is mild and hard to see. If you increase the `end_number thought it will become more obvious.
I can't figure out how can I finish one simple program written in Python. Program basically generates array of ten random numbers and then sorts them using bubblesort algorithm. Whole shorting process should be shown on screen - such as this one
My current code is this:
import tkinter
import random
canvas = tkinter.Canvas(bg='white',width='800',height='400')
canvas.pack()
c = []
for i in range(0,10):
c=c+[random.randrange(10)]
print(c)
print('Zoradenie...', c)
def sort(c):
x=300
for i in range(0,10):
for j in range(0,len(c)-1-1):
if c[j+1]<c[j]:
c[j+1],c[j]=c[j],c[j+1]
canvas.create_text(300,80,text=c[j],fill='Red')
x+=25
canvas.update()
canvas.after(1000)
print(c)
return c
sort(c)
But I can't figure out how to show numbers on screen. Any ideas?
To display the digits on the canvas, you must create a text item for each digit. See the end of my code. The harder part is moving the digits. One way is to delete and recreate; the other is to move. I choose the latter.
The hardest part, perhaps, is the time delays. If one uses mainloop, one should use after rather than time.sleep (which blocks the looping) and not use for-loops for animation. The problem is that the function (here sort) that naturally contains for-loops must be broken into pieces whose joint operation may be hard to understand. If one is running just one function and does not care about user interaction (for instance, a pause button), one can use time.sleep and update. I have done so here to make what is going on clearer.
from random import randrange
from time import sleep
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, bg='white', width='800', height='400')
canvas.pack()
ndigits = 10
digits = [randrange(10) for _ in range(ndigits)]
tdelta1, tdelta2 = .8, .2
xstart = 300
xdelta = 25
y = 80
def color(i, swap):
"Temporarily color digits i and i+i according to swap needed."
x = xstart + xdelta * i
dcolor = 'Red' if swap else 'green'
canvas.itemconfigure(items[i], fill=dcolor)
canvas.itemconfigure(items[i+1],fill=dcolor)
canvas.update()
sleep(tdelta1)
canvas.itemconfigure(items[i], fill='Black')
canvas.itemconfigure(items[i+1], fill='Black')
canvas.update()
sleep(tdelta2)
def swap(i):
digits[i], digits[i+1] = digits[i+1], digits[i]
canvas.move(items[i], xdelta, 0)
canvas.move(items[i+1], -xdelta, 0)
items[i], items[i+1] = items[i+1], items[i]
def bubsort():
"Sort digits and animate."
for stop in reversed(range(1, ndigits)):
# stop = index of position whose entry will be determined.
for i in range(stop):
swap_needed = digits[i] > digits[i+1]
color(i, swap_needed)
if swap_needed:
swap(i)
color(i, False)
# Create display items and pause.
items = [canvas.create_text(xstart + xdelta*i, y, text=str(digit))
for i, digit in enumerate(digits)]
canvas.update()
sleep(tdelta1)
bubsort()
This code makes it fairly easy to replace the text digit display with, for instance, a colored bar display. To develop this further, I would define a class of items combining int values and display items as attributes. There would them be only one array of combined items. With comparison methods defines, the array could be passed to any sort function.
I am a high school programming student and I have a small question. I have been tasked with writing a simple game in Tkinter where an icicle falls from the ceiling and you have to avoid it with your mouse. Simple enough. However, I have hit an issue. Whenever I run a loop in a Tkinter application, it won't open. I've tried with a for loop that pauses every .5 seconds using time.sleep() and the window opens as soon as the loop finishes. Is there some special thing I need to do to make loops work in Tkinter?
from Tkinter import *
import time
import random
class App:
def __init__(self, parent):
self.frame = Frame(root, bg= '#1987DF', width=800, height=800)
self.frame.bind("<Motion>", self.motionevent)
self.frame.pack()
#self.run()
def randhex(self):
b = "#"
for i in range(1, 7):
a = random.randint(0, 15)
if a == 10:
a = "A"
elif a == 11:
a = "B"
elif a == 12:
a = "C"
elif a == 13:
a = "D"
elif a == 14:
a = "E"
elif a == 15:
a = "F"
b = b+str(a)
return b
def motionevent(self, event):
xpos, ypos, bg = event.x, event.y, self.randhex()
str1 = "X : %d Y : %d BG : %s" % (xpos, ypos, bg)
root.title(str1)
x,y, delta = 100, 100, 10
self.frame.config(bg=bg)
def run(self):
for i in range(0, 10):
time.sleep(.5)
print 'i'
self.frame.config(bg=self.randhex())
root = Tk()
app = App(root)
root.mainloop()
Currently all it is supposed to do is change the background when the mouse moves. When the line in init that says self.run() is uncommented it will print 'i' 10 times then the window will open. Help?
Writing an event based program is not the same as writing traditional programs. There is already an infinite loop running, and like any infinite loop, if you place another loop inside, the outer loop can't continue until the inner loop finishes. Since the outer loop is what causes the screen to refresh and events to be processed, inner loops effectively freeze your app until they are done.
Since there is already a loop running you don't need to create another loop. All you need to do is add little jobs to the event queue one at a time. Each job is, in effect, one iteration of your inner loop.
For example, if you wanted to write an inner loop like this:
for i in range(10):
print "i:", i
... you would instead add an event to the event queue and each time the event loop iterates (or more precisely, each time it finishes processing any other events) it will do one iteration of your loop. You do it like this:
def do_one_iteration(i):
print "i:", i
if i < 9:
root.after_idle(do_one_iteration, i+1)
Then, after the first time you call do_one_iteration, it will place the next iteration of itself on the event queue, and continue to do so until it decides it is done.
Typically you would call do_one_iteration when the user presses a button (eg: the "start" button). Call it once, then it does one bit of work (ie: moving the icicle down a couple of pixels) and then reschedules itself.
In game development you might have a function called update_display which is in charge of redrawing everything. It could, for example, subtract 1 from the Y coordinate of each icicle. You can add bindings for the left and right arrows to move the player (by incrementing or decrementing the X coordinate), and your update function would use these new coordinates to redraw the player.
By the way, you can slow down your program by using after instead of after_idle to call the function after a slight delay. This delay can be used to control the frame rate. For example, assuming the update is nearly instantaneous (and it probably will be in your case), calling after with an argument of 41 (milliseconds) yields a framerate of approximately 24 fps (24 frames times 41 milliseconds equals 984 milliseconds, or roughly 24 frames per second)
It may sound complicated but it's really pretty easy in practice once you do it once or twice.