How can I update Entry without a "submit" button in Tkinter? - python

So I have Entries which have some values assigned to them from a CFG File. I want to modify the CFG file when the Entry is updated, live, without a submit button;
Using <Key> binding will work but will take only the previous value, not the current one, as the last key pressed is not taken into consideration as a value, but as a key-press.
For example:
class EntryBox:
def __init__(self, value, option, section, grid_row, master_e):
self.section = section
self.option = option
self.box = Entry(master_e)
self.box.grid(column=0, row=grid_row)
self.serial_no = grid_row
self.box.insert(0, value)
self.box.bind("<Key>", lambda event: update_cfg(event, self, self.get_value()))
def get_value(self):
return self.box.get()
def update_cfg(evt, entry_box,new_value):
global config_file
config_file.set(entry_box.section, entry_box.option, new_value)
print "Config file modified. "+entry_box.section+" "+entry_box.option+" "+new_value
If the value in the entry is 05R when I click on the entry and press 6, it will print Config file modified. CURRENT_MEASUREMENT_EXAMPLE_HP shunt_resistance 05R; after I press 7, it will print Config file modified. CURRENT_MEASUREMENT_EXAMPLE_HP shunt_resistance 0R56 and so on, always with one keypress behind. The only way to live update it after the value has been changed is to press the TAB or arrow buttons.

You can use either
FocusOut
tab or enter Key
KeyRelease
bindings to achieve that.
Also validation functions can help as they have previous and new values available. Please read the docs for more information on that matter.
It is IMHO the most "pythonic" / "tkinter" way of achieving what is a "check and submit" functionality.
Edit
As stated by OP, binding focusout could lead to problems here an example how it does indeed work:
import Tkinter as tk
import sys
def focus_out_event(event):
print >> sys.stderr, "Focus-Out event called with args: %s"%event
print >> sys.stderr, "Entry Widget Content: %s"%event.widget.get()
def key_release_event(event):
print >> sys.stderr, "Key-Release event called with args: %s"%event
print >> sys.stderr, "Entry Widget Content: %s"%event.widget.get()
if __name__ == "__main__":
root = tk.Tk()
entry1 = tk.Entry(root)
entry1.bind("", key_release_event)
entry1.bind("", focus_out_event)
entry1.grid()
entry2 = tk.Entry(root)
entry2.bind("", key_release_event)
entry2.bind("", focus_out_event)
entry2.grid()
root.mainloop()
Test:
- enter text ("asd") to entry1
- click into entry2
The last line of output is from changing to screenshot (event that fired a focusout)

You have this key-press in event.char so you can add it to text.

I decided that <Key> was not the right option in my case and instead used <FocusOut>. This way, if you either change the value using the mouse or keyboard TAB, on focus out it will update it.

Related

How can I reprogram the 'Ok' button in the Tkinter Messagebox module

I'm working on my very first python GUI, and I want to close all the previous windows from the code after clicking on the 'OK' button of the Message
messagebox.showinfo('Access Granted', 'Your data has been retrieved.')
The tkinter dialogs return a string representing what the user clicked on, so it's just a matter of saving that value and checking it afterwards. However, since showinfo only gives the user one option it's always going to return "ok", so there's no need to check the value. Just call your function after the dialog has been displayed:
def some_function():
messagebox.showinfo('Access Granted', 'Your data has been retrieved.')
root.destroy()
...
button = tk.Button(root, text="Quit", command=some_function)
So, say if your window was called root you would want to first define a function to 'destroy' the window
def closeWindow():
root.destroy()
Then you'd want to add that command to the button -
btn = tkinter.Button(text="Click Me!" command=closeWindow)
If you get any more errors, let me know!

Binding all keyboard keys to entry.get() tkinter

I'm trying to create a dynamically-updating searchbox and instead of polling the the entry widget every x milliseconds, I am trying to use keyboard bindings to get the contents of the entry widget whenever a key on the keyboard is pressed:
from tkinter import *
root = Tk()
def callback():
result = searchbox.get()
print(result)
#do stuff with result here
searchbox = Entry(root)
searchbox.pack()
searchbox.bind("<Key>", lambda event: callback())
root.mainloop()
my problem is that searchbox.get() is always executed before the key the user pressed is added to the searchbox, meaning the result is the content of the searchbox BEFORE the key was pressed and not after.
For example if I were to type 'hello' in the searchbox, I would see the following printed:
>>>
>>> h
>>> he
>>> hel
>>> hell
Thanks in advance.
There's no need to use bindings. You can associate a StringVar with the widget, and put a trace on the StringVar. The trace will call your function whenever the value changes, no matter if it changes from the keyboard or the mouse or anything else.
Here's your code, modified to use a variable with a trace:
from tkinter import *
root = Tk()
def callback(*args):
result = searchbox.get()
print(result)
#do stuff with result here
var = StringVar()
searchbox = Entry(root, textvariable=var)
searchbox.pack()
var.trace("w", callback)
root.mainloop()
For information about the arguments passed to the callback see What are the arguments to Tkinter variable trace method callbacks?
For a better understanding of why your bindings seem to always be one character behind, see the accepted answer to Basic query regarding bindtags in tkinter

Why does Tkinter hang when I call tkSimpleDialog.askstring from a lambda?

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()

tkinter: stopping event propagation in text widgets tags

I'm currently writing a color scheme editor. For the preview of the scheme, I use a text widget, where I insert text with the corresponding color tags (which I generate programmatically).
What I want is the following behaviour:
click anywhere on the text widget where no text is: change background color
click on text inserted with a tag: change tags corresponding foreground color
Now here's my problem:
When I click on a tagged text, the callback of the tag is called. So far so good. But then, the callback of the text widget is called as well, although I return "break" in the tags callback method (which should stop further event handling). How can I stop this?
To illustrate this specific problem, I wrote this working example (for Python 2 & 3):
#!/usr/bin/env python
try:
from Tkinter import *
from tkMessageBox import showinfo
except ImportError:
from tkinter import *
from tkinter.messagebox import showinfo
def on_click(event, widget_origin='?'):
showinfo('Click', '"{}"" clicked'.format(widget_origin))
return 'break'
root = Tk()
text = Text(root)
text.pack()
text.insert(CURRENT, 'Some untagged text...\n')
text.bind('<Button-1>', lambda e, w='textwidget': on_click(e, w))
for i in range(5):
tag_name = 'tag_{}'.format(i)
text.tag_config(tag_name)
text.tag_bind(tag_name, '<Button-1>',
lambda e, w=tag_name: on_click(e, w))
text.insert(CURRENT, tag_name + ' ', tag_name)
root.mainloop()
Any help is appreciated!
Edit: Tried Python 2 as well.
Thanks for posting this question and for providing a solution. I can't count how many hours I lost trying to fix up the symptoms created by this behaviour. Weird Tk design decision that tag_bind is insenstive to return "break".
Following your idea to hijack the Text widget by binding it with the same event sequence as tag_bind, I have improved the solution, which enables now to simulate the expected return "break" behaviour of Tk's other bind+callback pairs. The idea is the following (full source below):
create a wrapper around the wished callback, i.e. a callable class instance
when the class instance is called, run callback and check its result.
if the result is "break", temporarily hijack the event propagation: bind the Text widget to the same event bound to tag_bind, with an empty callback. Then, after an idle time, unbind.
if the result is not "break": do nothing, the event will propagate to Text automatically
Here is a full working example. My specific problem was to get some sort of hyper text behaviour: ctrl-clicking on a hyper-text should not move the insertion point to the click's location. The example below shows that within the same callback wrapped in tag_bind, we can propagate or not the event to the Text widget, simply by returning "break" or another value.
try:
# Python2
import Tkinter as tk
except ImportError:
# Python3
import tkinter as tk
class TagBindWrapper:
def __init__(self, sequence, callback):
self.callback=callback
self.sequence=sequence
def __call__(self, event):
if "break" == self.callback(event):
global text
self.bind_id=text.bind(self.sequence, self.break_tag_bind)
return "break"
else:
return
def break_tag_bind(self, event):
global text
# text.after(100, text.unbind(self.sequence, self.bind_id))
text.after_idle(text.unbind, self.sequence, self.bind_id)
return "break"
def callback_normal(event):
print "normal text clicked"
return "break"
def callback_hyper(event):
print "hyper text clicked"
if event.state & 0x004: # ctrl modifier
return "break" # will not be passed on to text widget
else:
return # will be passed on to text widget
# setup Text widget
root=tk.Tk()
text = tk.Text(root)
text.pack()
text.tag_config("normal", foreground="black")
text.tag_config("hyper", foreground="blue")
text.tag_bind("hyper", "<Button-1>", TagBindWrapper("<Button-1>", callback_hyper))
text.tag_bind("normal", "<Button-1>", callback_normal)
# write some normal text and some hyper text
text.insert(tk.END, "normal text, ", "normal")
text.insert(tk.END, "hyper text (try normal-click and ctrl-click).", "hyper")
root.mainloop()
There is one simplification I couldn't find how to do: replace the wrapper call TagBindWrapper("<Button-1>", callback_hyper) by TagBindWrapper(callback_hyper), i.e. get the information of the event 'sequence' string ("<Button-1>") simply from the event object passed to __call__. Is it possible?
Okay, after tkint tinkering around a bit, I was able to figure out a working solution. I think the problem is that tags are probably not subclassed from BaseWidget.
My workaround:
Make a seperate callback for the tags; set a variable there which keeps track of which tag was clicked
Let the event handler of the text widget decide what to do depending on the content of this variable
The workaround in code (sorry for using global here, but I just modified my questions simple example...):
#!/usr/bin/env python
try:
from Tkinter import *
from tkMessageBox import showinfo
except ImportError:
from tkinter import *
from tkinter.messagebox import showinfo
tag_to_handle = ''
def on_click(event, widget_origin='?'):
global tag_to_handle
if tag_to_handle:
showinfo('Click', '"{}" clicked'.format(tag_to_handle))
tag_to_handle = ''
else:
showinfo('Click', '"{} " clicked'.format(widget_origin))
def on_tag_click(event, tag):
global tag_to_handle
tag_to_handle = tag
root = Tk()
text = Text(root)
text.pack()
text.insert(CURRENT, 'Some untagged text...\n')
text.bind('<Button-1>', lambda e, w='textwidget': on_click(e, w))
for i in range(5):
tag_name = 'tag_{}'.format(i)
text.tag_config(tag_name)
text.tag_bind(tag_name, '<Button-1>',
lambda e, w=tag_name: on_tag_click(e, w))
text.insert(CURRENT, tag_name + ' ', tag_name)
root.mainloop()
I hope this is helpful for people having the same problem.
I'm still open to nicer solutions of course!

Python tkinter label won't change at beginning of function

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()"?

Categories