I'm using tkinter with Python to create a user interface for a program that converts Excel files to CSV.
I created a label to act as a status bar, and set statusBarText as a StringVar() as the textvariable. inputFileEntry and outputFileEntry are textvariables that contain the input and output file paths.
def convertButtonClick():
statusBarText.set('Converting...')
if inputFileEntry.get() == '' or outputFileEntry.get() == '':
statusBarText.set('Invalid Parameters.')
return
retcode = subprocess.('Program.exe' ,shell=true)
if retcode == 0:
statusBarText.set('Conversion Successful!')
else:
statusBarText.set('Conversion Failed!')
This function gets called when you click the convert button, and everything is working fine EXCEPT that the status bar never changes to say 'Converting...'.
The status bar text will get changed to invalid parameters if either the input or output are empty, and it will change to success or failure depending on the return code. The problem is it never changes to 'Converting...'
I've copied and pasted that exact line into the if statements and it works fine, but for some reason it just never changes before the subprocess runs when its at the top of the function. Any help would be greatly appreciated.
Since you're doing all of this in a single method call, the GUI never gets a chance to update before you start your sub process. Check out update_idletasks() call...
from http://infohost.nmt.edu/tcc/help/pubs/tkinter/universal.html
w.update_idletasks()
Some tasks in updating the display, such as resizing and redrawing widgets, are called idle tasks because they are usually deferred until the application has finished handling events and has gone back to the main loop to wait for new events.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
How are you creating your Label?
I have this little test setup:
from Tkinter import *
class LabelTest:
def __init__(self, master):
self.test = StringVar()
self.button = Button(master, text="Change Label", command=self.change)
self.button.grid(row=0, column=0, sticky=W)
self.test.set("spam")
self.testlabel = Label(master, textvariable = self.test).grid(row = 0,column = 1)
def change(self):
self.test.set("eggs")
root = Tk()
root.title("Label tester")
calc = LabelTest(root)
root.mainloop()
And it works.
Did you make sure to use "textvariable = StatusBarText" instead of "text=StatusBarText.get()"?
Related
I'm building a tkinter app and as a part of it the user has to upload a file, write a message and then press a Button whose command runs in another thread.
self.sidebar_button = Button(self.sidebar_frame, text="send your message",
command=threading.Thread(target=send_msg).start)
If the Button is pressed in the right condition then everything is fine.
However, if the user doesn't upload the file and write the message before pressing the Button then i show the user an error message. The problem here is that since the first Button press has started the thread, it can't start again.
Can you think of a workaround for this problem?
Is it possible to disable the Button before the right conditions are met?
Look at this:
import tkinter as tk
# This is called each time there is a change in the entry
def check_conditions(*args):
message_text = message_var.get()
# Here we check the conditions:
if message_text == "":
button.config(state="disabled")
else:
button.config(state="normal")
def send():
message_text = message_var.get()
print(f"Send: {message_text!r}")
root = tk.Tk()
message_var = tk.StringVar(root)
# When the value of the `message_var` has changed, call `check_conditions`
message_var.trace("w", check_conditions)
# Create the entry and attach `message_var`
message = tk.Entry(root, textvariable=message_var)
message.pack()
button = tk.Button(root, text="Send", command=send, state="disabled")
button.pack()
root.mainloop()
It uses <tk.StringVar>.trace("w", <function>) to call check_conditions each time the user changes the entry. For more info, read this.
tkinter doesn't always like being called from other threads, so avoid using threading when using tkinter.
So I have this code:
try:
# for Python2
from Tkinter import *
except ImportError:
# for Python3
from tkinter import *
class Injector():
def __openInjector(self):
root = Tk()
root.geometry('600x400')
root.title('Toontown Rewritten Injector')
root.resizable(False, False)
def __init__(self):
self.code = ''
self.__openInjector()
def runInjectorCode(self):
exec(self.code.get(1.0, 'end'), globals())
def __openInjector(self):
root = Tk()
root.geometry('600x400')
root.title('Toontown Rewritten Injector')
root.resizable(False, False)
frame = Frame(root)
self.code = Text(frame, width=70, height=20)
self.code.pack(side='left')
Button(root, text='Inject!', command=self.runInjectorCode).pack()
scroll = Scrollbar(frame)
scroll.pack(fill='y', side='right')
scroll.config(command=self.code.yview)
self.code.config(yscrollcommand=scroll.set)
frame.pack(fill='y')
Injector()
In the IDLE console it works fine and does everthing I want it to do. But whenever I run the .py file on my Desktop. The black window appears, then just closes and nothing happens. Any help?
First, you have two methods in your class with the same name. The first one gets overwritten by the second one. At the end of that second one, you need the following line:
root.mainloop()
This will actually run the GUI. It's needed when running from a script, but not when running within the interactive interpreter.
Add it at the end of the second __openInjector:
...
self.code.config(yscrollcommand=scroll.set)
frame.pack(fill='y')
root.mainloop()
At the end of your second __openInjector method, add the line: root.mainloop().
This is necessary for Tkinter to run your code. mainloop is really nothing more than an infinite loop that waits for events. An event may be a user interaction, such as clicking a button.
My guess is you don't need mainloop when running interactively for purely convenience reasons.
So, I used Tkinter to create a widget that allows the user to input some information and click the run button, which will begin running a test that is defined elsewhere. Here is the code. It is far from perfected, this is just a prototype:
from tkinter import*
import controller
root = Tk()
#create labels
label = Label(text = "text you don't need to know")
label.pack()
remind = Label(text = "more text you don't need to know")
remind.pack()
#create text fields
name = Entry(root)
name.pack()
name.insert(0, "Name")
name.focus_set()
testName = Entry(root)
testName.pack()
testName.insert(0, "Test name")
duration = Entry(root)
duration.pack()
duration.insert(0, "Duration in minutes")
def runTest():
controller.main(testName.get(), name.get(), float(duration.get()))
#create run button
run = Button(root, text = "Run", fg = "red", width = 10, command = runTest)
run.pack()
root.mainloop()
So, here is my issue. Once this project is implemented, the duration will likely be set for something like 1-4 hours. So, what I would like to do is have a countdown appear on the widget, so the users can reference that timer at any time to see how long until their data is produced. The problem is that as soon as my test is running, the widget locks up until it is complete. Everything I've tried is put on hold until it finishes running the test, then it does what I wanted. It doesn't help very much at that point.
Anybody have some experience at implementing something like this?
Thanks.
You'll need to fork off your work in runTest. The threading module will be your friend (e.g. from threading import Thread).
Then rewrite your runTest method:
def runTest():
# pack your arguments in a tuple
mainArgs = (testName.get(), name.get(), float(duration.get()))
# create a thread object armed with your function and the args to call it with
thread = Thread(target=controller.main, args=mainArgs)
# launch it
thread.start()
#and remember, never set state (directly or indirectly) from separate threads without taking appropriate precautions!
I have a program where I check if the user has an active network connection or not. If not, the program displays a frame that says to turn on the internet connection. The the program checks to see if the state has changed, if so, the login screen is shown. But I can't get rid of the noNetworkConnectionScreen.
The loadFrame is the frame that shows the 'splashscreen'.
class AppUI(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.parent.title("Redux")
self.loadFrame = Frame(self)
self.initLogin()
def initLogin(self):
internet_is_on = False
#check 3 times to be sure
for i in range(3):
#check the internet connection by pinging to google (returns True or False)
if db.internetOn():
internet_is_on = True
if internet_is_on:
self.initLoginScreen()
else:
self.initLoadScreen()
thread1 = threading.Thread(target=self.checkNetwork)
thread1.start()
I also tried grid_forget, which crashes:
def checkNetwork(self):
internet_is_off = True
while internet_is_off:
if db.internetOn():
internet_is_off = False
self.loadFrame.pack_forget()
self.loadFrame.destroy()
self.initLoginScreen()
else:
time.sleep(2)
And here I initialize the loadscreen:
def initLoadScreen(self):
self.loadFrame.grid(row=0, column=0, pady=(150,0))
self.lblUser = Label(self.loadFrame, text="HI", font=('Arial', 60), foreground="#666666")
self.lblUser.grid(row=0, column=0)
self.canvas = Canvas(self.loadFrame, width = 121, height = 81)
self.canvas.grid(row=1, column=0)
self.loader = PhotoImage(file = 'loader.gif')
self.canvas.create_image(121, 81, image = self.loader, anchor = NW)
self.lblNetwork = Label(self.loadFrame, text="please make sure you have an active network connection", font=('Arial', 16), foreground="#666666")
self.lblNetwork.grid(row=2, column=0)
I can see that the letters from lblNetwork are cut off at the sides, but the frame doesn't disappear. Any ideas?
Destroying a widget and/or "forgetting" it are the right ways to remove a widget from the screen. Almost certainly, the problem has to do with your use of threading. Tkinter isn't thread safe, and should only be used in one thread. If you create widgets in one thread, you shouldn't try to use them from any other thread. In this case you're trying to destroy a frame from a thread other than where it was created.
What you'll need to do is set up some sort of communication between the threads -- a queue or a shared (non-Tkinter) variable. Since you are simply checking a boolean flag, a simple shared variable will work. In your main program you can check for this variable using a simple after-based loop in your main thread:
def check_network(self):
if the_network_is_down:
<display a message>
else:
<remove the message>
# check once a second
self.after(1000, self.check_network)
Also, if you call destroy on a widget, there's no need to call grid_forget or pack_forget -- once the widget is destroyed there's nothing else you need to do.
Could it be that you have to force the screen refresh ?
pyGTK sometimes needs this (but are you using pyGTK ?) :
while gtk.events_pending(): # this forces the refresh of the screen
gtk.main_iteration()
I'm developing a GUI application that models an essay. Among other things, the user can create a new topic and then populate that topic with notes. At the moment, I have two ways of creating new topics: through a dropdown option in the menu (the menu command) and through a button on the main screen (the button command). The button starts life with the text "New Topic". When the user presses the button, the program makes a new topic, asks the user to name the topic using tkSimpleDialog.askstring, and then sets the button's text to be the name of the topic and the number of notes in that topic. The button's command then changes to be adding a note to that topic.
While developing the program, I first verified that the menu command worked. It calls askstring successfully, creating a new popup window that handles input in the way I wanted. However, as soon as I added the button command, the call to askstring failed, even when called via the menu command. The window that should have the askstring dialog is whited out and the program hangs. If I comment out the button command, it works again. If I comment out the menu command, it hangs.
Here's the code where I add the command to the menu:
TopicBtn.menu.add_command(label="New Topic", underline=0,
command=self.newTopic)
Here's the code for newTopic():
def newTopic(self, button=None):
""" Create a new topic. If a Button object is passed, associate that Button
with the new topic. Otherwise, create a new Button for the topic. """
topicPrompt = "What would you like to call your new topic?"
topicName = tkSimpleDialog.askstring("New Topic", topicPrompt)
if topicName in self.topics.keys():
print "Error: topic already exists"
else:
newTopic = {}
newTopic["name"] = topicName
newTopic["notes"] = []
newTopic["button"] = self.newTopicButton(newTopic, button)
self.topics[topicName] = newTopic
self.addToTopicLists(newTopic)
Here's the code for newTopicButton():
def newTopicButton(self, topic, button=None):
""" If a Button object is passed, change its text to display the topic name.
Otherwise, create and grid a new Button with the topic name. """
if button is None:
button = Button(self.topicFrame)
index = len(self.topics)
button.grid(row=index/self.TOPICS_PER_ROW, column=(index %
self.TOPICS_PER_ROW), sticky=NSEW, padx=10, pady=10)
else:
button.unbind("<Button-1>")
buttonText = "%s\n0 notes" % topic["name"]
button.config(text=buttonText)
button.config(command=(lambda s=self, t=topic: s.addNoteToTopic(t)))
return button
And, finally, here's the code for the button command:
for col in range(self.TOPICS_PER_ROW):
button = Button(self.topicFrame, text="New Topic")
button.bind("<Button-1>", (lambda e, s=self: s.newTopic(e.widget)))
button.grid(row=0, column=col, sticky=NSEW, padx=10, pady=10)
Anybody have any idea why binding the lambda expression to the button makes askstring hang?
Edit: Thanks for the comments. Here's a minimal example that exhibits the behavior:
from Tkinter import *
import tkSimpleDialog
class Min():
def __init__(self, master=None):
root = master
frame = Frame(root)
frame.pack()
button = Button(frame, text="askstring")
button.bind("<Button-1>", (lambda e, s=self: s.newLabel()))
button.grid()
def newLabel(self):
label = tkSimpleDialog.askstring("New Label", "What should the label be?")
print label
root = Tk()
m = Min(root)
root.mainloop()
Note that switching from button.bind("<Button-1>", (lambda e, s=self: s.newLabel())) to button = Button(frame, text="askstring", command=(lambda s=self: s.newLabel())) fixes the bug (but doesn't give me a reference to the button that was pressed). I think the problem has something to do with capturing the event as one of the inputs to the lambda.
The problem you encountered here is due to the call to wait_window in the dialog you are using (you never call it yourself, but the code that implement the dialog does). For instance, the following code replicates the problem after (likely) two button clicks:
import Tkinter
def test(event=None):
tl = Tkinter.Toplevel()
tl.wait_window(tl)
root = Tkinter.Tk()
btn = Tkinter.Button(text=u'hi')
btn.bind('<Button-1>', test)
btn.pack(padx=10, pady=10)
root.mainloop()
This call to wait_window effectively does what the update command does, and is a typical example of why calling update is a bad thing to do. It enters in conflict with the <Button-1> event being handled, and hangs. The problem is that you will have to live with wait_window being used, since it belongs to the dialog's code. Apparently, if you bind to <ButtonRelease-1> then this conflict never happens. You could also use the command parameter in the button, which works fine too.
Lastly, I suggest the following to create the buttons in a cleaner manner based on what you want to achieve:
for i in range(X):
btn = Tkinter.Button(text=u'%d' % i)
btn['command'] = lambda button=btn: some_callback(button)
I figured out a workaround. From the minimum-example testing, it appears that the problem comes from making a separate call to bind and thereby accepting the event as an input to the lambda. If anyone can explain why that might be happening, I'll accept their answer over mine, but I'll accept this one for now.
The workaround is not to use a separate bind function but to create an array of buttons and then pass the correct entry in the array as the parameter to the lambda function (you can't pass the button itself, since it's being created in the line that has the lambda function).
Here's the code:
from Tkinter import *
import tkSimpleDialog
class Min():
def __init__(self, master=None):
root = master
frame = Frame(root)
frame.pack()
buttons = [None] * 2
for i in range (2):
buttons[i] = Button(frame, text="askstring",
command=(lambda s=self, var=i: s.newLabel(buttons[var])))
buttons[i].grid()
def newLabel(self, button):
label = tkSimpleDialog.askstring("New Label", "What should the label be?")
button.config(text=label)
print label
root = Tk()
m = Min(root)
root.mainloop()