How do I prevent focus change if an entry is invalid? - python

I want if one field has invalid entry, message to be displayed on moving out of that field and focus should remain on that field. But in following code validation of next field is also triggered when moving out of first field. I have commented set focus, otherwise, it gets into an infinite loop.
from tkinter import *
from tkinter import ttk, messagebox
root = Tk()
def callback1():
if (len(e1.get()) <4):
messagebox.showinfo("error", "Field 1 length < 4")
#e1.focus_set()
return False
else:
return True
def callback2():
if (len(e2.get()) <4):
messagebox.showinfo("error", "Field 2 length < 4")
#e2.focus_set()
return False
else:
return True
e1 = Entry(root, validate="focusout", validatecommand=callback1)
e1.grid()
e2 = Entry(root, validate="focusout", validatecommand=callback2)
e2.grid()
root.mainloop()

When you place your cursor in e1, type something which does not satisfy the validatecommand condition and then try to place your cursor in e2, the following sequence of events takes place:
e1 loses focus and calls callback1
Meanwhile, the cursor is placed in e2 and e2 gets focus
The msgbox window for e1 takes focus and is ready to pop up just when...
e2 loses focus and calls callback2
The msgbox window for e2 pops up
This is followed by the pop up for e1 which was waiting in the queue
The root cause for the problem is that the messagebox window takes focus, which in turn triggers the other entry box. This approach seems to be highly fragile due to the strong interplay of events.
Notice that if you just print the information to the terminal, everything works perfectly as the terminal doesn't take focus.
So, I would recommend you display the information using an alternate method where the widget showing the information won't steal focus. One option is to display the information using a label.
Now coming to your second problem, if you want the entry to retain focus if the entered text is not valid, you could use a global variable (hanging) to keep track of whether the user is in the process of filling an entry successfully.
If the user is in the process of filling an entry, he/she will not be able to place the cursor in the other entry because FocusIn1 and FocusIn2 return "break" when hanging equals True.
You can replace the print statements in the below working code using a label.
Working Code:
from tkinter import *
from tkinter import ttk, messagebox
root = Tk()
hanging = False #True implies that the user is in the process of filling an entry successfully
def onFocusOut1():
global hanging
hanging = True
if (len(e1.get()) <4):
print("error", "Field 1 length < 4")
e1.focus_set()
return False
else:
hanging = False
return True
def onFocusOut2():
global hanging
hanging = True
if (len(e2.get()) <4):
print("error", "Field 2 length < 4")
e2.focus_set()
return False
else:
hanging = False
return True
def onFocusIn1():
if hanging:
return "break"
e1.configure(validate="focusout", validatecommand=onFocusOut1)
def onFocusIn2():
if hanging:
return "break"
e2.configure(validate="focusout", validatecommand=onFocusOut2)
e1 = Entry(root)
e1.grid()
e2 = Entry(root)
e2.grid()
#Binding both the entries to FocusIn
e1.bind("<FocusIn>", lambda e: onFocusIn1())
e2.bind("<FocusIn>", lambda e: onFocusIn2())
root.mainloop()
PS: It turns out that you can actually use messagebox.showinfo itself in place of print in the working code. The first problem got solved automatically along with the second one. So, this gives the complete solution to your problem.

Related

How to insert a scrollbar to a menu?

I want to add another widget, in this case a scale widget, to a menu widget in tkinter.
Right now the only solutions I see are creating a new command and open a new window with the scale widget or creating the scale widget elsewhere. Both don't seem too appealing to me.
Any ideas how to archive this are welcome :)
You cant add a scrollbar to it, but I have coded something similar to this. Its a hacky way and maybe its hard to understand but I can try to explain.
Note as Bryan mentioned in the linked Thread, this seems to be a a Windows only solution.
import tkinter as tk
def my_first_function():
print('first')
def my_second_function():
print('second')
def check_for_scroll(event):
check = root.call(event.widget, "index","active")
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
if check == file_menu.index('end'):
scroll_down()
root.after(100,lambda e=event:check_for_scroll(e))
def scroll_up():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_first_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
previous_command_label = list(dict_of_commands)[i-1]
previous_command = list(dict_of_commands.values())[i-1]
if i != 0: #avoid to get the last as first
file_menu.delete(index_of_first_command) #first before pull down button
file_menu.insert_command(index_of_first_command,
label=previous_command_label,
command=previous_command)
except Exception as e:
print(e)
def scroll_down():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_last_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
next_command_label = list(dict_of_commands)[i+1]
next_command = list(dict_of_commands.values())[i+1]
file_menu.delete(index_of_first_command) #first after pull up button
file_menu.insert_command(index_of_last_command,
label=next_command_label,
command=next_command)
except:
pass
space = ' '
dict_of_commands = {'first' : my_first_function,
'second': my_second_function}
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
file_menu = tk.Menu(menubar,tearoff=0)
menubar.add_cascade(label='File', menu=file_menu)
file_menu.bind('<<MenuSelect>>', check_for_scroll)
file_menu.add_command(label=space+u'\u25B2'+space, font=["Arial", 8,'bold'])
file_menu.add_command(label='first', command=my_first_function)
file_menu.add_command(label=space+u'\u25BC'+space, font=["Arial", 8,'bold'])
root.mainloop()
So this code creates your window and a menubar on it as usal:
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
file_menu = tk.Menu(menubar,tearoff=0)
menubar.add_cascade(label='File', menu=file_menu)
file_menu.add_command(label=space+u'\u25B2'+space, font=["Arial", 8,'bold'])
file_menu.add_command(label='first', command=my_first_function)
file_menu.add_command(label=space+u'\u25BC'+space, font=["Arial", 8,'bold'])
root.mainloop()
Important for you, is this line here:
file_menu.bind('<<MenuSelect>>', check_for_scroll)
This line binds the event MenuSelect and it happens/triggers if your cursor hovers over a command of your menu. To this event I have bound a function called check_for_scroll and it looks like this:
def check_for_scroll(event):
check = root.call(event.widget, "index","active")
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
if check == file_menu.index('end'):
scroll_down()
root.after(100,lambda e=event:check_for_scroll(e))
The line below checks for the index of the command that has triggered the event. With this we check if its button of our interest like the first or last, with the arrows.
check = root.call(event.widget, "index","active")
if its the first for example this code here is executed:
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
it calls/triggers the function scroll_up and uses then the after method of tkinter to retrigger itself, like a loop. The scroll_up function is build like the scroll_down just in the opposite direction. Lets have a closer look:
def scroll_up():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_first_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
previous_command_label = list(dict_of_commands)[i-1]
previous_command = list(dict_of_commands.values())[i-1]
if i != 0: #avoid to get the last as first
file_menu.delete(index_of_first_command) #first before pull down button
file_menu.insert_command(index_of_first_command,
label=previous_command_label,
command=previous_command)
except Exception as e:
print(e)
In this function we need to know the first and the last position of commands, because we want to delete one and insert another on that position/index. To achieve this I had created a dictionary that contains the label and the the function of the command item of tkinter like below. (This could be created dynamically, but lets keep it for another question)
dict_of_commands = {'first' : my_first_function,
'second': my_second_function}
So we iterate over this enumerated/indexed dictionary in our function and check if the k/key/label is our item of interest. If true, we get the previous_command by listing the dictionary keys and get the extract the key before by this line:
next_command_label = list(dict_of_commands)[i+1]
similar to the value of the dictionary with this line:
next_command = list(dict_of_commands.values())[i+1]
After all we can delete one and insert one where we like to with this:
file_menu.delete(index_of_first_command) #first after pull up button
file_menu.insert_command(index_of_last_command,
label=next_command_label,
command=next_command)
I know that this code can improved by a lot but it seems hard enough to understand as it is. So dont judge me please.
I hope this solves your question, even if it isnt in the way you wanted to. But this code avoids you from hardly code a own menubar.
If there are questions left on my answer, let me know.

Creating an Entry Widet with Limited Input

In an attempt to create a Python Entry widget with limited input(3 or 4 characters), I found this
Knowing nothing yet about validation, my question is this: can the 'subclass' for max length in that tutorial be used as its own class, referencing the entry widget as its parent instead of 'ValidatingEntry', or is all the legwork above it (validating) necessary? Is there any shorter way to accomplish this?
Then I saw this question and its answer:
Considering doing something like that. Then I discovered the builtin 'setattr' function. Is it possible to apply this to a new instance of the class 'Entry' and use it to limit characters?
I should clarify- I'm trying to apply this limit to 3 entry widgets- two with a 3 character limit and one with a 4 character limit (a phone number)
Thanks.
Regarding your stated concern of length validation in an Entry widget. An example of input length validation.
# further reference
# http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html
import tkinter as tk
root = tk.Tk()
# write the validation command
def _phone_validation(text_size_if_change_allowed):
if len(text_size_if_change_allowed)<4:
return True
return False
# register a validation command
valid_phone_number = root.register(_phone_validation)
# reference the validation command
entry = tk.Entry(validate='all',
validatecommand=(valid_phone_number,'%P' ))
entry.pack()
root.mainloop()
suggestion in response to comment on focus switching between entry widgets, change validate 'all'-> 'key', and use focus_set()
import tkinter as tk
root = tk.Tk()
# write the validation command
def _phone_validation(text_size_if_change_allowed):
if len(text_size_if_change_allowed) == 3:
entry2.focus_set()
if len(text_size_if_change_allowed)<4:
return True
return False
# register a validation command
valid_phone_number = root.register(_phone_validation)
# reference the validation command
entry = tk.Entry(validate='key',
validatecommand=(valid_phone_number,'%P' ))
entry2 = tk.Entry()
entry.pack()
entry2.pack()
root.mainloop()

Python Tkinter entry not loading content correct

i am facing a problem. I'm runnig this code.
import tkinter as tk
root = tk.Tk()
def check():
if len(e.get().split("a")) > 1:
print("contains a")
e = tk.Entry(frame1)
e.grid(row=4,column=1,columnspan=2,padx = (10,10), pady=(5,10), sticky="w e")
e.bind("<Key>",check)
when i type "a" to the entry I wont get nothing printed. I'll get the result by tiping a second character. I think that it happens because the function gets executed before the content has actualy changed. I tried to add a timer on the beginning of the function but it does nothing.
I want get the result by entering the first "a". What should I do?
I think that it happens because the function gets executed before the content has actualy changed.
You're right. If you want the callback to be able to see the character you just typed, you should create a StringVar and bind to that instead of binding to a "<Key>" event on the widget.
import tkinter as tk
frame1 = tk.Tk()
def check(*args):
if "a" in s.get():
print("contains a")
s = tk.StringVar()
e = tk.Entry(frame1, textvariable=s)
s.trace("w", check)
e.grid(row=4,column=1,columnspan=2,padx = (10,10), pady=(5,10), sticky="w e")
frame1.mainloop()

capturing tkinter checkbox input

I am running a script with tkinter that captures user input and then opens a second and possibly a third window based on the input. The issue I am having is capturing user input from the third and final window. Each window is divided up into it's own python class on execution.
Here is the code that calls the third window, which executes properly:
test_assign = TestAssign(mylist)
Here is the third window code:
class TestAssign:
def __init__(self, mylist):
self.mylist = mylist
self.selected_values = []
self.master = Tk()
for i in range(len(mylist)):
setattr(self, 'item'+mylist[i], IntVar())
ch = Checkbutton(master, text='item'+mylist[i], variable=getattr(self, 'item'+mylist[i])
ch.pack()
b = Button(master, text='Next', command=self.get_selected_values)
b.pack()
mainloop()
def get_selected_values(self):
for i in range(len(self.mylist)):
if getattr(self, 'item'+self.mylist[i]) == 1:
self.selected_values.append(self.mylist[i])
self.master.destroy()
Control then returns to the call point (at least I believe it does). Where I attempt to print the selected values:
test_assign = TestAssign(mylist)
while not test_assign.selected_values:
pass
print test_assign.selected_values
Everytime execution gets to the print statement it prints an empty list whether there are boxes checked or not. If I call dir(test_assign) for testing purposes, the checkbox attrs are there. Not sure why I am not able to capture it like this.
Can anyone see the flaw in my code?
Two things:
1)
ch = Checkbutton(master, text='item'+mylist[i], variable=getattr(self, 'item'+mylist[i])
and
b = Button(master, text='Next', command=self.get_selected_values)
I think master should be self.master (but honestly, that almost certainly just a copy/pasting error.)
2) The important one:
if getattr(self, 'item'+self.mylist[i]) == 1:
should be
if getattr(self, 'item'+self.mylist[i]).get() == 1:
(you need to call get on your IntVars to read the value.)

First letters of a Tkinter input

My program should check if the first three letters of the input word are similar to a predefined word.
I've made a GUI with Tkinter and want to get the letters of the input field.
Somehow I can't implement it in like I would do without Tkinter.
That's how I do it just for the shell:
text = raw_input('Enter a word: ')
if (text[0] + text[1] + text[2] == 'sag'):
print "sagen"
else:
print "error"
So, when I input the word "sagst" it checks the first three letters and should put out "sagen". Works fine.
I learned that e.g. inputfield.get() gets the input of the entry "inputfield".
But how can I check the first letters of that "inputfield"?
A small selection:
from Tkinter import*
root = Tk()
def check():
if (text[0] + text[1] + text[2] == 'sag'):
print "True"
else:
print "False"
inputfield = Entry(root)
inputfield.pack()
but = Button(root,text='Check!', command = check)
but.pack()
text = inputfield.get()
root.mainloop()
Does not work...
I hope you can understand my question and will answer soon. (Sorry for my bad english and my bad Python skills) ;-)
Thanks!
Your check function will have to retrieve the textfield after the button has been pressed:
def check():
text = inputfield.get()
print text.startswith('sag')
I've changed your test a little, using .startswith(), and directly printing the result of that test (print will turn boolean True or False into the matching string).
What happens in your code is that you define inputfield, retrieve it's contents (obviously empty), and only then show the TKInter GUI window by running the mainloop. The user never gets a chance to enter any text that way.
You can also check this without the need for a button (Now it will check whenever the user presses "Enter"):
from Tkinter import *
root = Tk()
def check(*event):
text = inputfield.get()
print text.startswith('sag')
inputfield = Entry(root)
inputfield.bind('<Return>',check)
inputfield.pack()
root.mainloop()
You can also do other things to have your widget validate the entry as you type. (The link is old, but it also points to newer features that allow you to do this without subclassing).
You're not actually putting the value in the input field into the text variable.
I renamed the value from text to input_text because it was confusing to me. I also changed from using text[0] + text[1] + text[2] to using startswith(). This will keep you from getting IndexErrors on short strings, and is much more pythonic.
from Tkinter import*
root = Tk()
def check():
input_text = inputfield.get()
if input_text.startswith('sag'):
print "True"
else:
print "False"
inputfield = Entry(root)
inputfield.pack()
input_text = inputfield.get()
print input_text # Note that this never prints a string, because it only prints once when the input is empty.
but = Button(root, text='Check!', command=check)
but.pack()
root.mainloop()
The key change is that the check function needs to actually get the value in the inputfield.
Here is a version which uses an Entry widget which validates its contents as the user types (so the user does not have to click a button or even press Return).
import Tkinter as tk
class MyApp(object):
'''
http://effbot.org/zone/tkinter-entry-validate.htm
http://effbot.org/tkinterbook/entry.htm
http://www.tcl.tk/man/tcl8.5/TkCmd/entry.htm#M-validate
'''
def __init__(self, master):
vcmd = (master.register(self.validate),
'%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
self.entry = tk.Entry(master, validate = 'key',
validatecommand = vcmd)
self.entry.pack()
self.entry.focus()
def validate(self, action, index, value_if_allowed,
prior_value, text, validation_type, trigger_type, widget_name):
dtype = {'0':'delete', '1':'insert', '-1':'other'}[action]
n = min(3, len(value_if_allowed))
valid = False
if dtype == 'insert':
if value_if_allowed[:n] == 'sag'[:n]: valid = True
else: valid = False
else: valid = True
print(valid)
return True
root = tk.Tk()
app = MyApp(root)
root.mainloop()

Categories

Resources