I have a function that reads locations from a text file, parses them, then moves the respective objects to the locations listed on a tkinter canvas using the coords function. The data is being read from the file and parsed correctly but for some reason the coords function is only moving the objects to the last location listed in the file on the last iteration of the loop.
Do I need to update the canvas somehow after each iteration of my loop? Thanks!
Here is my code:
def playback():
fptr = tkFileDialog.askopenfilename()
filename = open(fptr,"rU")
if not filename:
return
stat.set('REPLAY IN PROGRESS')
gamestatus[0] = 2
for line in filename:
line = line.strip()
#Example line from input file: 'B:#,#,#,#|L:#,#,#,#|R:#,#,#,#'
line = line.split('|')
B_loc = line[0].split(':')[1].split(',')
L_loc = line[1].split(':')[1].split(',')
R_loc = line[2].split(':')[1].split(',')
#Converting strings to ints and lists to tuples to simplify code below
B_tup=(int(B_loc[0]),int(B_loc[1]),int(B_loc[2]),int(B_loc[3]))
L_tup=(int(L_loc[0]),int(L_loc[1]),int(L_loc[2]),int(L_loc[3]))
R_tup=(int(R_loc[0]),int(R_loc[1]),int(R_loc[2]),int(R_loc[3]))
#Moving objects to locations from input file
playingField.coords(pongball.ball,B_tup)
playingField.coords(leftpaddle.paddle,L_tup)
playingField.coords(rightpaddle.paddle,R_tup)
time.sleep(.02)
filename.close()
gamestatus[0] = 0
stat.set('-------Pong-------')
A very good rule of thumb in GUI development is to never call sleep. This freezes the GUI, and even if it's just for a few milliseconds it's still a bad practice.
The proper way to do animation in Tkinter is to write a function that displays a single frame, then reschedules itself using after. This allows the event loop to constantly service events while doing the animation.
For example, take the entire body of the for statement -- minus the sleep -- and put it into a method. Let's call this "refresh". Have this function re-schedule itself using after, like this:
def refresh():
line = get_next_line()
line = line.split('|')
B_loc = line[0].split(':')[1].split(',')
...
# call this function again in 20ms
root.after(20, refresh)
Now all you need to do is implement get_next_line as a function and you're set. This will automatically allow the GUI to redraw itself each time yo update the coordinates.
Of course, you'll need to put in checks for when input is exhausted, and you might want to have a flag the user can set via a button that requests that the animation stops, etc.
Related
I am actually trying to create an application but I encountered a big issue
Specifically, it should check a folder filled with pictures for it's items and for each item it should create an ImageButton(already defined) in a Scollview GridLayout list. It works so far..
But when one of the displayed pictures is pressed, I want it to change the path of the picture on the main screen
This is my Python code
#This is outside the MDApp class
class ImageButton(ButtonBehavior, Image):
#Inside the MDApp class
def item_choser(self, item)
.
.
.
elif item == "car":
#It first clears the grid layout
self.root.ids.pictures_grid.clear_widgets()
#Then, the path folder of the items(pictures) is being defined
pictures = glob.glob(f"./images/items/cars/*.jpg")
#For loop created to assign
for i in pictures:
#This "z" is the number after path in format <path\02_test_golf7_tez.jpg>
#Different for every picture (01, 02, 03 etc)
z = i[-21:-19]
#Creating button with bind
z = ImageButton(source=i, allow_stretch=True, keep_ratio=True)
z.bind(on_release=lambda x:self.chosen(i)) <<--- Here is my actual problem
print(z)
#Adding it into the grid
self.root.ids.pictures_grid.add_widget(z)
def chosen(self, selectedCar):
print(selectedCar)
self.root.ids.main_image_display.source = selectedCar
This is what the path folder contains:
...cars\01_test_golf8_tez.jpg
...cars\02_test_golf7_tez.jpg
...cars\03_test_passat_te.jpg
...cars\04_test_crafter_t.jpg
All photos are rightly placed. For every " z " printed after the bind, it shows a different object in memory so until here everything is fine
But here is where my problem starts.
Let's say you want to pick the passat so you press the picture of it but somehow, whatever picture you press, the chosen function will always print "...cars\04_test_crafter_t.jpg"
How could this code be made in order to bind 1 button to it's function?
This was the closest answer found but either I don't understand how to use and integrate it in my code or it simply doesn't help [lambda x, i=i : i * x for i in range(len(pictures))] because it didn't work
This may not be the full answer, but I can't squeeze much code in a comment.
Make gala a list then append each picture to it:
gala = []
for i in pictures:
z = i[-21:-19]
tmp = ImageButton(source=i, allow_stretch=True, keep_ratio=True)
tmp.bind(on_release=lambda x, i=i:self.chosen(i)) # closure hack
self.root.ids.pictures_grid.add_widget(tmp)
gala.append(tmp) # can reference later > gala[0]
Concerning the 'hack'
The target function (lamda) does not run immediately. When defining the function, python uses late binding. With this, python stores the method address (lambda) and the parameter address (i). It does not store the value of the parameter. When the function is later called, the current (latest) value of the parameter variable is used.
Take this example. When chosen is called, i will equal 2 because that is the value of i when the loop is completed but before the function is actually called.
for i in range(3): 0-2
tmp.bind(on_release=lambda i:self.chosen(i)) # address of i is stored, not value
The hack resolves this issue by forcing early binding. Using i=i tells python there is a default value for that parameter (like def myfunc(i=5)) and early binding should be used. With early binding, the (default) parameter values are stored when the method is created, resolving the loop issue.
for i in range(3): 0-2
tmp.bind(on_release=lambda x, i=i:self.chosen(i)) # value of i is stored
The x parameter may not be needed. You can experiment to confirm.
#inclement provided a useful link in his comment: https://docs.python-guide.org/writing/gotchas/#late-binding-closures
I would assume that the problem is in the ImageButton Class - you need to check there if exactly that button was pressed.
class ImageButton(Button):
# ....
def on_touch_down(self, touch):
"""
Check which of the buttons was pressed
:param touch:
:return:
"""
if self.collide_point(*touch.pos):
print(self.source)
# You could then trigger an event from here and pass the Source/ID whatever as parameter
else:
# This Button wasn't pressed
pass
You can check that with the touch parameter of the on_touch_down function. Check with which button collider the touch.pos collides. The on_touch_down event is triggered for all ImageButtons even when only one is pressed!
I always use the Button id to identify the button - and link that with more data.
recently im working using Python with Tkinter gui. The problem is , i want to show some lines by changing its state from hidden to normal within some interval, let's say it 1s.
So , the line will show up 1 by 1 within 1 second.
When i tried, the commandline works perfectly(i print some text on windows cmd) But, the gui frozen until the end of the computation then all of the lines showed up(not 1 by 1), idk why? im new with python :(
here's my dummy code
def delay():
allline=mainframe.find_withtag('line')
for i in allline:
tags=mainframe.gettags(i)
print(tags[0])
root.after(1000, mainframe.itemconfigure(tags[0],state='normal'))
......
mainframe.create_line((50,50,100,100),...,tags=('line1','line'),state='hidden')
mainframe.create_line((150,150,100,100),...,tags=('line2','line'),state='hidden')
let's say i have a button which trigger delay function.
Thanks for ur help! sorry for my bad english :)
Rather than using a for loop like this, get all the lines into a list and call a function which removes the first line from the list, shows the line and then have the function call itself using after. Next time the function is called the next item is removed from the list, line made visible etc etc. This way tkinter has time to update the screen.
import tkinter as tk
lines = []
def showNextLine():
global lines
try:
nextLine = lines.pop(0)
canvas.itemconfigure(nextLine,state='normal')
root.after(1000,showNextLine)
except IndexError:
pass
#No more items in list.
root = tk.Tk()
canvas = tk.Canvas()
canvas.grid()
canvas.create_line((50,50,100,100),fill='red',tags=('line1','line'),state='hidden')
canvas.create_line((50,60,100,110),fill='blue',tags=('line2','line'),state='hidden')
canvas.create_line((50,70,100,120),fill='yellow',tags=('line3','line'),state='hidden')
canvas.create_line((50,80,100,130),fill='green',tags=('line4','line'),state='hidden')
lines = list(canvas.find_withtag('line'))
print(type(lines),lines)
root.after(1000,showNextLine)
root.mainloop()
all. I'm working on a simple Notepad-like program that saves files and closes the program when the escape key is pressed. I mention this because it is in this method that the program runs into problems. textpad is a ScrolledText object.
This line:
`contents = self.textPad.get(self, 1.0, END)`
results in the following error:
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python2.7/lib-tk/Tkinter.py", line 1535, in __call__
return self.func(*args)
File "todopad.py", line 24, in save_and_quit
contents = self.textPad.get(self, 1.0, END)
AttributeError: Event instance has no attribute 'textPad'
I know this is the problem, because the program executes and terminates without issue when this line is commented out. Although I don't understand the error at all.
This has been a very long-winded way of asking: How can I retrieve the contents of a ScrolledText text pad and save it to a variable or directly write it to a file? And also an explanation about the error message?
Thank you in advance.
EDIT: As requested, here is the code for the entire thing.
import sys
import Tkinter
from Tkinter import *
from ScrolledText import *
root = Tkinter.Tk(className = "TodoPad");
textPad = ScrolledText(root, width = 80, height = 20)
def start_and_open():
textFile = open('/home/colin/documents/prog/py/todopad/todo', 'r')
contents = textFile.read()
textPad.insert('1.0', contents)
textFile.close()
def save_and_quit(self):
textFile = open('/home/colin/documents/prog/py/todopad/todo', 'w')
#contents = self.textPad.get(self, 1.0, END) # The line in question
#textFile.write(contents)
textFile.close()
root.destroy()
textPad.pack()
root.bind('<Escape>', save_and_quit)
root.after(1, start_and_open)
root.mainloop()
Since I have posted the whole thing I may as well explain the rationale behind everything. It's supposed to be a fast little thing that opens a to-do list and displays what's already on the list in the text box. I make whatever edits I like, then it saves before closing when I hit escape, problem being is that it doesn't like closing because of the line that I mentioned previously in my post.
First of all, kudos on identifying the problem.
Placing the Widget
To answer what is going wrong: you need to actually place the widget into the window frame. You have a choice between .grid() and .pack(). The first allows you to pick exactly where you want it to go, the second puts in a (technically) default location.
Right now, the instance of your widget is not preset, so your program has no idea where to pull the value from. You have to set a location. i would recommend using .grid(), but for the example .pack() will work as well.
textPad = ScrolledText(root, width = 80, height = 20)
textPad.pack()
Try this, and see if it works. This should fix it, but I could be wrong.
Do NOT just do
textPad = ScrolledText(root, width = 80, height = 20).pack()
The pack() function returns a NULL and will nullify your widget.
Your Issue With Self
Finally, why are you using self at all? You are not using any classes--you need to globalize the variable. The error that is thrown is a result of your program not knowing what class you are pulling the self instance from. Remove the self variables from the program, and put this into the function:
global textPad
This will make it global and all functions will be able to use it.
This should solve all the problems you have right now. However, give it a try and report what happens.
Here are some resources on global variables, getting input from widgets, and saving to files;
http://www.python-course.eu/python3_global_vs_local_variables.php
http://effbot.org/tkinterbook/text.htm
http://www.afterhoursprogramming.com/tutorial/Python/Writing-to-Files/
Happy coding, and best of luck!!
I'm writing my own code editor and I want it to have numbered lines on left side. Based on this answer I wrote this sample code:
#!/usr/bin/env python3
import tkinter
class CodeEditor(tkinter.Frame):
def __init__(self, root):
tkinter.Frame.__init__(self, root)
# Line numbers widget
self.__line_numbers_canvas = tkinter.Canvas(self, width=40, bg='#555555', highlightbackground='#555555', highlightthickness=0)
self.__line_numbers_canvas.pack(side=tkinter.LEFT, fill=tkinter.Y)
self.__text = tkinter.Text(self)
self.__text['insertbackground'] = '#ffffff'
self.__text.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=True)
def __update_line_numbers(self):
self.__line_numbers_canvas.delete("all")
i = self.__text.index('#0,0')
self.__text.update() #FIX: adding line
while True:
dline = self.__text.dlineinfo(i)
if dline:
y = dline[1]
linenum = i[0]
self.__line_numbers_canvas.create_text(1, y, anchor="nw", text=linenum, fill='#ffffff')
i = self.__text.index('{0}+1line'.format(i)) #FIX
else:
break
def load_from_file(self, path):
self.__text.delete('1.0', tkinter.END)
f = open(path, 'r')
self.__text.insert('0.0', f.read())
f.close()
self.__update_line_numbers()
class Application(tkinter.Tk):
def __init__(self):
tkinter.Tk.__init__(self)
code_editor = CodeEditor(self)
code_editor.pack(fill=tkinter.BOTH, expand=True)
code_editor.load_from_file(__file__)
def run(self):
self.mainloop()
if __name__ == '__main__':
app = Application()
app.run()
Unfortunately something is wrong inside __update_line_numbers. This method should write line numbers from top to bottom on my Canvas widget but it prints only the number for the first line (1) and then exits. Why?
The root problem is that you're calling dlineinfo before returning to the runloop, so the text hasn't been laid out yet.
As the docs explain:
This method only works if the text widget is updated. To make sure this is the case, you can call the update_idletasks method first.
As usual, to get more information, you have to turn to the Tcl docs for the underlying object, which basically tell you that the Text widget may not be correct about which characters are and are not visible until it's updated, in which case it may be returning None not because of any problem, but just because, as far as it's concerned, you're asking for the bbox of something that's off-screen.
A good way to test whether this is the problem is to call self.__text.see(i) before calling dlineinfo(i). If it changes the result of dlineinfo, this was the problem. (Or, if not that, at least something related to that—for whatever reason, Tk thinks everything after line 1 is off-screen.)
But in this case, even calling update_idletasks doesn't work, because it's not just updating the line info that needs to happen, but laying out the text in the first place. What you need to do is explicitly defer this call. For example, add this line to the bottom of load_from_file and now it works:
self.__text.after(0, self.__update_line_numbers)
You could also call self.__text.update() before calling self.__update_line_numbers() inline, and I think that should work.
As a side note, it would really help you to either run this under the debugger, or add a print(i, dline) at the top of the loop, so you can see what you're getting, instead of just guessing.
Also wouldn't it be easier to just increment a linenumber and use '{}.0'.format(linenumber) instead of creating complex indexes like #0,0+1line+1line+1line that (at least for me) don't work. You can call Text.index() to convert any index to canonical format, but why make it so difficult? You know that what you want is 1.0, 2.0, 3.0, etc., right?
The root cause of the problem is that the text hasn't been drawn on the screen yet, so the call to dlineinfo will not return anything useful.
If you add a call to self.update() before drawing the line numbers, your code will work a little better. It won't work perfectly, because you have other bugs. Even better, call the function when the GUI goes idle, or on a Visibility event or something like that. A good rule of thumb is to never call update unless you understand why you should never call update(). In this case, however, it's relatively harmless.
Another problem is that you keep appending to i, but always use i[0] when writing to the canvas. When you get to line 2, i will be "1.0+1line". For line three it will be "1.0+1line+1line", and so on. The first character will always be "1".
What you should be doing is asking tkinter to convert your modified i to a canonical index, and using that for the line number. For example:
i = self.__text.index('{0}+1line'.format(i))
This will convert "1.0+1line" to "2.0", and "2.0+1line" to "3.0" and so on.
folks! So, thanks to you guys I was able to figure out what it was I was doing wrong in my previous script of staggering animation for selected objects in a scene. I am now on part two of this little exercise: Creating a UI for it.
This involves creating a window with a button and user input of how much the animation will be staggered by. So, instead of me putting how much the stagger should increment by (which was two in my previous script), I'd now allow the user to decide.
The script I have so far created the window, button, and input correctly, though I am having some trouble with getting the UI to properly execute, meaning when I click on the button, no error pops up; in fact, nothing happens at all to change the scene. I get the feeling it's due to my not having my increment variable in the correct spot, or not utilizing it the right way, but I'm not sure where/how exactly to address it. Any help would be greatly appreciated.
The code I have (with suggested edits) is as follows:
import maya.cmds as cmds
spheres = cmds.ls(selection=True)
stagWin = cmds.window(title="Stagger Tool", wh=(300,100))
cmds.columnLayout()
button = cmds.button(label="My Life For Aiur!")
count = cmds.floatFieldGrp(fieldgroup, query=True, value=True)
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
cmds.showWindow(stagWin)
def stagger(fieldgroup):
for i in spheres:
cmds.selectKey(i)
cmds.keyframe(edit=True, relative=True, timeChange=count)
print "BLAH"
Moving the comments into an answer because I think I've got it all figured out finally:
First of all, the better practice is to pass the stagger object to the button command rather than the string. so that would be:
cmds.button(label="My Life For Aiur!", command=stagger)
Secondly, the count isn't getting updated, so it stays 0 as per your third line. To update that:
count = cmds.floatFieldGrp(fieldgroup, query=True, value=True)
But wait, where did fieldgroup come from? We need to pass it into the function. So go back to your button code and take out the command entirely, also saving the object to a variable:
button = cmds.button(label="My Life For Aiur!")
Now store the object for the fieldgroup when you make it:
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
Now that you have fieldgroup, you can pass that in the function for the button, like this:
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
I had to wrap the function in a lambda because we're passing fieldgroup, but if I just put stagger(fieldgroup) it would call that and pass the result of that into the command for the button
Also update stagger def with fieldgroup argument:
def stagger(fieldgroup):
One final note that won't actually affect this, but good to know:
when you shift the keyframes inside stagger you're using a DIFFERENT count variable than the one you declared as 0 up above. The outer one is global, and the inner is local scope. Generally it's best to avoid global in the first place, which fortunately for you means just taking out count = 0
Putting that all together:
import maya.cmds as cmds
spheres = cmds.ls(selection=True)
stagWin = cmds.window(title="Stagger Tool", wh=(300,100))
cmds.columnLayout()
button = cmds.button(label="My Life For Aiur!")
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
cmds.showWindow(stagWin)
def stagger(fieldgroup):
count = 0
increment = cmds.floatFieldGrp(fieldgroup, query=True, value=True)[0]
print count
for i in spheres:
cmds.selectKey(i)
cmds.keyframe(edit=True, relative=True, timeChange=count)
count += increment
print "BLAH"