I am slowly learning my way through Python and tkinter. :)
In a game I'm making there are animations of images displayed within widgets (namely buttons).
Animating frame-by-frame is mundane, so I came up with a function to help me automate a 10-frame animation.
In a loop of range(10) (as I have 10 frames) the function calls the after() method which has a function callback, each displaying next frame of animation.
Since time within the after method is larger for each consecutive iteration of the loop, each new frame should be displayed nicely after given time (here it's 34ms).
That's all fine in theory, however when I run the code and appropriate functions are called, the button does not animate properly. Only the last frame seems to pop out.
The way I see it, after some reading on how tkinter works, is that each after in a loop should set independent callback in tkinter's "timeline" to be called after some time. Thus in my opinion this code should work.
What do you make of it? What've I got wrong, is my logic about after() in a loop off?
#Python 3.4.3
def animateMine(object):
global firstAnimateMineCall
for frame in range(10):
frame += 1
time = 34 * frame
root.after(time, lambda: mineAnimationFrame(object, frame))
if firstAnimateMineCall and frame == 10:
root. after(500 , lambda: animateAllMines(object))
firstAnimateMineCall = False
In the doubtful event this'd be useful:
def mineAnimationFrame(object, frame):
tempDir = "Resources/Mine/saperx_mine_%s.png" % (frame)
tempImage = PhotoImage(file=tempDir)
object.configure(image=tempImage)
object.image = tempImage
object.disabled = True
A simplistic, good-looking and easy to implement solution to this problem I came up with (thanks to CurlyJoe's advice).
*A major pro of this design is that it's easy to adjust it to your frames quantity... you got 5? Just change 1 value and it's good to go! Got 900? Still easy. 6,02*10^23 frames? Still just 1 change ;]*
To adjust to your frame size, just change the list comprehension range(10) to whatever quantity you wish, the code will take care of the rest.
from tkinter import Tk, PhotoImage, Button, FLAT
root = Tk()
mineImagesList = [PhotoImage(file="Resources/Mine/saperx_mine_%s.png" % (frame)) for frame in range(1, 11)]
button = Button(root, bd=0, relief=FLAT, command= lambda: func(button))
def func (object, frame=0):
object.configure(image=mineImagesList[frame])
object.image = mineImagesList[frame]
print("Object image:", object.image)
if frame+1 < len(mineImagesList):
frame += 1
root.after(34, lambda frame=frame, object=object: func(object=object, frame=frame))
button.pack()
root.mainloop()
Related
I want to have a calculator with 4 buttons and 2 entry. What is my problem? I don't know how to fix that it can't multiply sequence by non-int of type str nor can't multiply entry * entry.
Also I don't know where is my output and how use that.
from tkinter import ttk
from tkinter import *
import tkinter as tk
#the environment
calc=tk.Tk()
calc.title("Calculator")
#-------------------------------------
#lables and their data entry
tk.Label(calc,text="enter your first number:").grid(row=0)
tk.Label(calc,text="enter your second number:").grid(row=1)
e1=tk.Entry(calc)
e2=tk.Entry(calc)
e1.grid(row=0, column=1)
e2.grid(row=1, column=1)
x1=e1.get()
x2=e2.get()
tk.Label(calc,text="your result is:").grid(row=3)
# a free variable for get output,is this really need?
lbl=list()
#---------------------------
#the functions but my entry are entry type
def prod():
lbl=print(x1*x2)
def div():
lbl=print(x1/x2)
def sum():
lbl=print(x1+x2)
def min():
lbl=print(x1-x2)
#-------------------------------
#buttons and function in them as a command
btn1=tk.Button(calc,text="*",command=prod()).grid(row=0,column=2)
btn2=tk.Button(calc,text="/",command=div()).grid(row=1,column=2)
btn3=tk.Button(calc,text="+",command=sum()).grid(row=2,column=2)
btn4=tk.Button(calc,text="-",command=min()).grid(row=3,column=2)
#--------------------------------------------------------------------
#The answer which i need it
print("your answer is:")
calc.mainloop()
In GUI programming, you need to get the value of widgets at the time you need them, not at the time you create them.
Also, widgets return string values. If you are doing math on them, you need to convert them to numbers first.
If you want to display the result in the window, you can update a label widget with the configure method
For example, your prod definition could look something like this:
def prod():
x1 = int(e1.get())
x2 = int(e2.get())
result = x1 * x2
result_label.configure(text=str(result))
You then need to do two other things. First, you need to create the label for the result, and store the widget in a variable. To do that you must separate the creation of the widget from the layout since calling grid would cause the variable to be set to None
result_label = tk.Label(calc,text="your result is:")
result_label.grid(row=3)
Finally, your buttons need to be given a reference to the function. What you're currently doing is calling the function and assigning the result to the command attribute.
Your button should look like the following. Notice that prod does not have () after it:
btn1 = tk.Button(calc, text="*", command=prod)
...
btn1.grid(row=0,column=2)
On a side note, I think that for all widgets with a common parent, grouping all widget creation code together and then all calls to grid or pack together makes the code easier to read, easier to maintain, and easier to visualize.
For example:
btn1 = tk.Button(...)
btn2 = tk.Button(...)
btn3 = tk.Button(...)
btn4 = tk.Button(...)
btn1.grid(row=0,column=2)
btn2.grid(row=1,column=2)
btn3.grid(row=2,column=2)
btn4.grid(row=3,column=2)
Thank you for reading this.
I'm working on a simple animation that is based on one of two examples from the PysimpleGUI cookbook. The attached code, of course, it is not doing anything. I've looked through many examples trying to figure out how to update the canvas but without success.
My first attempt was based on the sine wave plot example. I have an endless while loop and a display function. The display on the graph area shows the first iteration through the loop but is never updated after that.
The display function contains:
graph.DrawCircle((i,j), 5, line_color='black', etc
A second related question, should I be using the canvas or the graph method (as in the sine wave plot example), or doesn't it matter?
I don't want to overwhelm the reader with too much code. If I can get the following to work then I may have a good chance with the real code.
import PySimpleGUI as sg
import time
layout = [
[sg.Canvas(size=(100, 100), background_color='red', key= 'canvas')],
[sg.T('Change circle color to:'), sg.Button('Red'), sg.Button('Blue')]
]
window = sg.Window('Canvas test')
window.Layout(layout)
window.Finalize()
canvas = window.FindElement('canvas')
cir = canvas.TKCanvas.create_oval(50, 50, 100, 100)
while True:
event, values = window.Read()
'''
if event is None:
break
if event == 'Blue':
canvas.TKCanvas.itemconfig(cir, fill="Blue")
elif event == 'Red':
canvas.TKCanvas.itemconfig(cir, fill="Red")
'''
# this is the part that I need to sort out
for i in range(10):
if i % 2 == 0:
canvas.TKCanvas.itemconfig(cir, fill="Blue")
else:
canvas.TKCanvas.itemconfig(cir, fill="Red")
time.sleep(1)
I discovered the answer and that is, window.Read(timeout = 0).
In order for changes to show up in a window after making changes, you much either call Read or Refresh. I think all you need to do is down in your bottom loop, add the line:
window.Refresh()
From the docs at http://www.PySimpleGUI.org:
Refresh()
Cause changes to the window to be displayed on the screen.
Normally not needed unless the changes are immediately required or if
it's going to be a while before another call to Read.
I am a beginner messing around in python 3.6 with tkinter canvas and have built a function that generates a solar system at random using elipses and the random method.
I would like to be able to save these items and all their attributes like their tags and bindings so that I can clear the canvas and draw a new random system. I would then like to go back to the first generated system if I need to.
I do not want to save the canvas as an image as I am binding the items to functions. Any ideas on how to achieve this?
As far as I know, you only get the items "id" as a handle to that item, and while you can delete an item with a given ID, you can not recreate it just with that ID.
What you could do is to given all those elements a common tag and use tag_lower and tag_raise to hide and show the items below and above the "background" pane. The objects are still on the canvas, but can not be seen and do not react to e.g. mouse events.
import tkinter, random
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
space = canvas.create_rectangle(0, 0, 200, 200, fill="#000000")
for _ in range(10):
x, y = random.randint(0, 200), random.randint(0, 200)
s = canvas.create_oval(x, y, x+10, y+10, fill="#ffff00", tags="star")
canvas.tag_bind(s, "<Button>", lambda e: print("twinkle"))
root.after(3000, lambda: canvas.tag_lower("star", space))
root.after(6000, lambda: canvas.tag_raise("star", space))
root.mainloop()
Update: As suggested by OP in comments, one can also set the items' state to hidden, being probably the clearer option and not needing some obscure (-ing) background item.
root.after(3000, lambda: canvas.itemconfig("star", state="hidden"))
root.after(6000, lambda: canvas.itemconfig("star", state="normal"))
So basically, I have text which is typed out character by character. with the code:
text = "test"
delta = 40
delay = 0
for i in range(len(text) + 1):
s = test_string[:i]
update_text = lambda s=s: canvas.itemconfigure(variable, text=s)
canvas.after(delay, update_text)
delay += delta
This is all inside of a function, lets call: def NewEvent(). What I want to do is create a text button with the text "Skip" which changes delta to a lower number, thus speeding up the animation upon click. I cant seem to figure it out, normally when you make text clickable, it has something along the lines of:
skipbutton = canvas.create_text((400,100), activefill="Medium Turquoise", text="Skip", fill="White", font=('Arial', 30), tags="skip")
canvas.tag_bind('skip', '<ButtonPress-1>', function)
The problem is, it needs to stay within the same function. So I thought of creating an if statement similar like this:
if delta is 40 and skip is ____:
delta = 10
However, I dont know what would come after- (skip is) for it to work, or even if this would work at all... Any help would be appreciated.
You are doing animation in a way that makes your problem very difficult to solve. The problem is that you are scheduling all of the frames of animation before you display the first frame. In order to change the speed you would have to cancel all of the pending jobs and recreate new jobs. This is not the proper way to do animation in Tkinter.
A better solution is to only have a single job active at one time. You do this by having a function that displays one frame of animation and then schedules itself to run again in the future.
The general structure looks like this:
def animate():
<draw one frame of animation>
if <there are more frames>:
root.after(delay, animate)
In your case, each "frame" is simply adding one character to a character string, and your condition at the end is to simply check to see if there are more characters.
A simple implementation is to pass a string into the animate function, have it pull the first character off of the string and append it to the display.
For example:
def update_text(text):
char = text[0]
remainder = text[1:]
current_string = canvas.itemcget(variable, "text")
new_string = current_string + char
canvas.itemconfigure(variable, text=new_string)
if len(remainder) > 0:
canvas.after(delay, update_text, remainder)
To start the animation, give it the string you want to display:
update_text("Hello, world")
This this function depends on a global variable for the delay, writing a function to speed the animation up or slow it down only requires that you modify this delay:
def speedup():
global delay
delay = int(delay/2)
You can call it from a button quite easily:
tk.Button(root, text="Speed up!", command=speedup)
I am trying to make a simple space invaders game and a problem I have run into is getting things t happen at the same time. I have binded the shooting action to the canvas of the game so that when you click a function is called. I would like it so that this function can be called multiple times at once so that multiple "lasers/bullets" can be seen on the screen at any one time. At the minute when you click and a "laser/bullet" is already on screen, the previous one disappears and a new one appears. CODE:
class Game1():
def __init__(self, xcoord1=380, ycoord1=550, xcoord2=400, ycoord2=570):
self.Master = Master
self.Master.geometry("800x600+300+150")
Game1Canvas = Canvas(self.Master, bg="black", height=600, width=800)
Game1Canvas.place(x=0, y=0)
self.Canvas = Game1Canvas
self.Canvas.bind("<Button-1>", self.Shoot)
self.Ship = self.Canvas.create_rectangle(self.xcoord1, self.ycoord1, self.xcoord2, self.ycoord2, fill = "red")
def Shoot(self):
self.LaserLocation = 0
for self.LaserLocation in range(0 , 112):
Master.after(1, self.Canvas.create_rectangle(self.xcoord1, self.ycoord1 - (self.LaserLocation * 5), self.xcoord2 - 5, self.ycoord2 - (self.LaserLocation * 5), fill = "pink", tag=str(CurrentTag)))
Master.update()
self.Canvas.delete(str(CurrentTag))
This is a much more "dumbed" down version of the code at the minute because I've been trying a bunch of different ways to get this working and it's a mess. I am aware of the multiprocessing and threading imports and I have tried them both but am unable to get them working for my code. If someone could reply back with a solution I would be very grateful. Cheers.
You don't need to use multithreading or multiprocessing. You also don't need (nor want) to be drawing new rectangles every millisecond, or multiple times per millisecond.
The solution is to have your Shoot function merely create a single rectangle, and add it to a list. Then, using a simple animation mechanism, iterate over the list and move each bullet up one or two pixels. You do this by creating a function that calls itself every 20-30 ms.
The solution looks something like this:
def Shoot(self):
laser = self.Canvas.create_rectangle(...)
self.lasers.append(laser)
def do_animation(self):
# make a copy of the list of lasers to iterate
# over, so we can remove items from the original
# list when they go off screen
lasers = self.lasers[:]
for laser in lasers:
# get current coordinates of this laser
(x0,y0,x1,y1) = self.canvas.bbox(laser)
if x1 < self.canvas.winfo_height():
# if it is not off screen, move it up
self.canvas.move(laser, 0, -10)
else:
# if it IS off screen, delete it
self.canvas.delete(laser)
self.lasers.remove(laser)
self.after(30, self.do_animation)
The above will move the lasers every 30 milliseconds (about 33 fps).