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)
Related
I'm trying to make a mode for a simple game where you catch items as they fall down using tkinter.
In this mode, you have 60 seconds to catch as many items as you can. All the timer methods I've tried pause the whole program
...tried using an empty label, but the .after pauses the whole program
timerlabel = tkinter.Label(text="")
def timer():
global t, timerdisplay
while t > 0:
t -= 1
timerlabel.after(1000)
c.delete(timerdisplay)
timerdisplay = c.create_text(200, 12, text=t)
c.update()
any idea how to do this?
This is the better way to do it, specifically because after(n) freezes the program for the given time period. Create a function that accepts a number and displays that number. Then, it subtracts one and then reschedules itself to run one second in the future until the number becomes zero.
def timer(t):
global timerdisplay
c.delete(timerdisplay)
timerdisplay = c.create_text(200, 12, text=t)
if t >= 1:
c.after(1000, timer, t-1)
timer(timerdisplay, 10)
To optimize this, you can pass in the canvas item along with the number. You can also just reconfigure the text item rather than deleting and restoring it.
def timer(timerdisplay, t):
c.itemconfigure(timerdisplay, text=t)
if t >= 1:
c.after(1000, timer, timerdisplay, t-1)
timerdisplay = c.create_text(200, 12)
timer(timerdisplay, 10)
I'm working on creating a stopwatch that is given two values. It starts with value A and counts down to 0, then changes to value B, counts down to 0, then goes back to value A, counts down to 0 etc until I close the program (I'll probably add a pause button at some point) and overall it's working really well. However, when it updates the label with the new text, It seems to just be making a new text item and putting it as a layer on top of the previous. I can see then when I go from a single double-digit number to a single digit, and the sentence is shortened, the part of the old sentence that isn't covered can still be seen. So I'm hoping I'm just missing something incredibly simple. The newWindow.update() is what I thought would update the window but it does not appear to be doing that. Below is my snippet of code that handles the logic.
def countdown(timer_count,count_type):
counter = timer_count
count_type = count_type
while counter >= 0:
timer = tk.Label(newWindow, text=f"{count_type} for: {counter}")
timer.config(font=("TkDefaultFont",30))
timer.grid(row=0,column=2)
newWindow.update()
time.sleep(1)
counter -= 1
print(counter)
if count_type == "work":
count_type = "rest"
elif count_type == "rest":
count_type = "work"
return count_type
def interval():
counter_type = "work"
while True:
if counter_type == "work":
counter_type = countdown(int(exer_var.get()),counter_type)
elif counter_type == "rest":
counter_type = countdown(int(rest_var.get()),counter_type)
You are creating a new Label widget each time through your while loop instead of changing the text inside the while loop. That is why it is layering one widget on top of the other, so you need to create your widget, then run the while loop, and set the text to change in the timer.config inside the loop. You should also declare the font in the original tk.Label, no need to change that each trip through the loop. For "some_starting value" it would probably be text = counter
timer = tk.Label(newWindow, font=("TkDefaultFont",30), text="some_starting_value")
while counter >= 0:
timer.config(text=f"{count_type} for: {counter}")
timer.grid(row=0,column=2)
Its hard to say from your code where this is taking place, but here is how its usually done:
Make the label outside all functions in the main block.
timer = tk.Label(newWindow,font=("TkDefaultFont",30)) # Adding the font here itself
Then now inside the function just change its value using config:
def countdown(timer_count,count_type):
counter = timer_count
count_type = count_type
while counter >= 0:
timer.config(text=f"{count_type} for: {counter}") # Update the options for the created label.
timer.grid(row=0,column=2)
So now each time function/loop is running, new labels wont be created and overwritten, instead existing label will be configured.
On a side note, using time.sleep() and while loop is not the very best practice, even with update() it will still cause some kind of disturbance to GUI. Instead, re-arrange your code to use after(ms,func) method, which will not freeze the GUI. You can ask a new question on that if you face any trouble, later.
I am trying to create a delay based on the length of a string. But the delay has to happen before the string is printed in the console. The goal is to add a bit of realism to an adventure game.
This is the current code I have come up with. I know it isn't right, but this is the first week I've been using Python.
import time
def calculate_text_speed(delay):
delay = text.len() / 20
def start_game():
calculate_text_speed(delay)
time.sleep(calculate_text_speed.delay)
print("Test message.")
Should I create a list with dialogues to calculate the the strings beforehand? This will probably decrease the readability of the code, right?
Perhaps this is what you mean by not declaring text?
def print_with_delay(text):
time.sleep(len(text) / 20)
print(text)
print_with_delay("Some very exciting things are happening....")
Original answer:
You can create very readable code without generating the text list ahead of time.
The main issue is you are passing the wrong thing to calculate_text_speed. Try this.
import time
text = "Some very exciting things are happening...."
#pass text to the calculate_text_speed function
def calculate_text_speed(text):
return len(text) / 20
time.sleep(calculate_text_speed(text))
print(text)
First you will have to return something from the first function, in your case it is:
In python for string length is len(text) not text.len()
return (len(text)() / 20)
then the function would look like this:
def calculate_text_speed(text):
return (len(text) / 20)
Now to calculate text length,
you can pass the text variable globally or locally. I do now what you would want. I assume that you want it locally so in this case your second function would lok like:
def start_game(text):
time.sleep(calculate_text_speed(text))
print("Test message.")
if for you want the text variable globally defined, then:
text = "something"
def calculate_text_speed():
return (len(text) / 20)
def start_game():
time.sleep(calculate_text_speed())
print("Test message.")
start_game()
I was making an application using tkinter and came across an error. I wanted people to input a variable, which I have made, and then have that many Entry boxes popup on the screen for input. I was wondering what is wrong with my code, if it is possible, or if there is a better way. Thanks in advance!
p.s. the NoOfBoxes has been predefined
int(NoOfBoxes)
x = 1
while(NoOfBoxes>=x):
a = a + 50
fill_empty(a)
x = x + 1
def fill_empty():
empty = tk.Entry(self)
empty.grid(row=200,column=a)
return empty
In first line of shown code, you are converting NoOfBoxes to an integer but you are not assigning back it to NoOfBoxes hence, when while line comes, NoOfBoxes is still not an integer. Also, there is no parameter on your fill_empty definition.
Most likely you will need those Entry widgets at some point in your code, so it'll be much better if you keep references.
listOfEntries = [fill_empty(idx) for idx in range(int(NoOfBoxes))]
def fill_empty(a):
empty = tk.Entry(self)
empty.grid(row=200,column=a)
return empty
When you want to make any operation on those, you can easily do something like:
listOfEntries[0].get()
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()