Entry widget validation difference between classic and ttk widgets - python

I have discovered an unexpected difference in Entry widget validation between classic and ttk widgets in Python 3.5.
Using classic widgets:
from tkinter import *
def validate(reason):
print("--> validate:", reason)
return(True)
def change():
var.set("data")
root = Tk()
vc = root.register(validate)
var = StringVar()
Entry(root, textvariable = var, validate = "all", validatecommand = (vc, "%V")).pack()
Button(root, text = "Change", command = change).pack()
root.mainloop()
Using ttk widgets:
from tkinter import *
from tkinter.ttk import *
... same code as above
With classic widgets, when the "Change" button is pressed, the validate function is called with reason == "forced", which seems to comply with the Tk doc. With ttk widgets, when the "Change" button is pressed, the validate function is not called. Otherwise the validate function seems to have the equivalent behavior for both cases. Anybody have an idea if this is a bug or a feature?

It's a feature. According to the official ttk documentation:
DIFFERENCES FROM TK ENTRY WIDGET VALIDATION
The standard Tk entry widget automatically disables validation (by
setting -validate to none) if the -validatecommand or -invalidcommand
modifies the entry's value. The Tk themed entry widget only disables
validation if one of the validation scripts raises an error, or if
-validatecommand does not return a valid boolean value. (Thus, it is not necessary to re-enable validation after modifying the entry value
in a validation script).
In addition, the standard entry widget invokes validation whenever the
linked -textvariable is modified; the Tk themed entry widget does not.

Related

How to configure map for all ttk widgets except TButton?

I am creating a GUI with Tkinter and ttk, and I'm trying to create a custom map to make ttk widgets blue on hover. I could apply that to all widgets with passing "." as the first argument to ttk.Style().map(...).
import tkinter as tk
import tkinter.ttk as ttk
root = tk.Tk()
style = ttk.Style()
style.map(".", background=[("active", "blue")])
button = ttk.Button(root, text="An example button")
button.pack()
scrollbar = ttk.Scrollbar(root)
scrollbar.pack()
root.mainloop()
But now I want to exclude TButton from this query. That is, I need to make all widgets but TButton blue on hover. How can I do that?
Passing ".!TButton" as well as "!TButton" instead of "." has no effect.
There is a root style whose name is '.'. To change some feature's default appearance for every widget, you can configure this style. For example, let's suppose that you want all text to be 12-point Helvetica (unless overriden by another style or font option).
So we can override it by simply adding another style:
style.map('TButton',background=[('active','#f0f0f0')])
Keep a note that you might want to do this for every style that you wish to set to default too.
Take a read here for more info.

Update on the fly a ttk.OptionMenu without affecting its previous callback/command

I am doing a large GUI with different ttk.OptionMenu widgets that need to be updated often depending on the interaction with the user. Each time the user selects another value from the menu, there is a function callback by the widget via the command input. The command works fine until I need to update the possible values within the OptionMenu widget. After updating using add_command, my previous callback does not work anymore.
Is there any way of updating the possible inputs of a ttk.OptionMenu without affecting its original callback? Let me put a dummy example to illustrate my problem. My original code would be like
from tkinter import *
from tkinter import filedialog, messagebox, ttk
def printing(variable,*args):
print('ok')
if variable:
print(variable)
root=Tk()
List=['Madrid', 'Paris', 'Brussels']
variable = StringVar()
optionmenu = ttk.OptionMenu(root,variable,'', *List,
command=printing)
optionmenu.pack()
root.mainloop()
Then the user would update the list dynamically, and the menu needs to be updated on the fly, using:
newList = ['Madrid', 'Paris', 'Brussels', 'London']
menu = optionmenu["menu"]
menu.delete(0, "end")
for string in newlist:
menu.add_command(label=string,
command=lambda value=string: variable.set(value))
but doing this operation, the callback printing would not work anymore. Also important to mention that one clicked in one option it should become the value showed by the OptionMenu ( so the StringVar variable is settled to that input)
I hope it is clear
There is an internal class _setit in tkinter module which is used by OptionMenu internally to populate the values. You can use this class as below:
from tkinter import filedialog, messagebox, ttk, _setit
...
for value in newList:
menu.add_command(label=value, command=_setit(variable, value, printing))

Checkbutton and Scale conflict

I have a problem with my Checkbutton widget. Every time I select it the slider on the scale widget above moves by itself to 1, deselecting the Checkbutton widget will set the Scale widget to 0. Both widgets are not intended to be related with each other in any way yet for some reason changing values in one of them affect the other. Can anyone explain to me why this is happening and how can I avoid such problems in the future?
tk.Label(f7, text=("Jakość")).grid(row=3, column=0)
self.jakosc=tk.Scale(f7, orient='horizontal', variable=jakosc)
self.jakosc.grid(row=3, column=1)
self.rozpinany_sweter=tk.IntVar()
tk.Checkbutton(f7, text='Rozpinany',variable=rozpinany_sweter).grid(row=4, column=1)
In this example
the slider is set to 56, after checking the checkbox on the slider sets itself to 1.
EDIT: MCVE provided:
import tkinter as tk
from tkinter import ttk as ttk
RS=0
Q=0
class Aplikacja(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.grid()
self.create_widgets()
def create_widgets(self):
self.jakosc=tk.Scale(root, orient='horizontal', variable=Q)
self.jakosc.grid()
self.rozpinany_sweter=tk.IntVar()
tk.Checkbutton(root, variable=RS).grid()
root= tk.Tk()
app= Aplikacja(root)
root.mainloop()
The variable= parameter to Tk widgets MUST be a Tk variable (created by Tk.IntVar() or similar calls). Your code passes Q and RS, which are ordinary Python variables; the one Tk variable you create is pointless, because you never use it anywhere. Tk variables have a special ability not possessed by Python variables: they can have watchers attached to them, which allows widgets to automatically update themselves when the variable is modified.
The Python representation of a Tk variable is basically just the Tk name of the variable. Both Q and RS happen to have the same value, so they're both referring to the same variable on the Tk side - that's why your scale and checkbox appear to be linked.

Preventing a tkinter Entry from gaining focus when its associated StringVar changes

I have a tkinter Entry with a validate command, to be executed when the entry gets focus ("focusin"). This Entry is associated with a StringVar. It seems that whenever the StringVar changes value, the Entry gets focus, triggering the validation command. For example:
import Tkinter as tk
window = tk.Tk()
var = tk.StringVar()
def validate(*args):
print("Validation took place")
return True
entry = tk.Entry(validate="focusin", validatecommand=validate)
print("Entry created. Associating textvariable")
entry.config(textvariable=var)
print("textvariable associated. Changing value")
var.set("Text")
print("Value changed")
entry.pack()
tk.mainloop()
This code generates the following output:
Entry created. Associating textvariable
textvariable associated. Changing value
Validation took place
Value changed
Note that the validation command was executed, caused by a call to var.set.
Is there a way for me to change the value of the StringVar without causing its associated Entry to gain focus? I can't temporarily disassociate the StringVar from the Entry, because when re-associating them, the Entry also gains focus.
I think your observation is incorrect: the focus does not change when you set the value of the StringVar. This is likely just an edge case that only happens when your application first starts up. The widget probably gets focus when it is initially created. Once the GUI is up and running, setting the variable won't change the focus.
The official tk documentation discourages the use of both validation and the use of a variable. From the documentation:
In general, the textVariable and validateCommand can be dangerous to mix. Any problems have been overcome so that using the validateCommand will not interfere with the traditional behavior of the entry widget.
There's no reason to use a StringVar in this case. My recommendation is to remove it. The use of a StringVar is generally only useful if you use the same variable for more than one widget, or you're putting a trace on the variable. You are doing neither so just stop using the StringVar. It adds an extra object to manage without providing much extra benefit.
One possible workaround is to disable the validation event before setting the stringvar, and re-enable it afterwards.
import Tkinter as tk
window = tk.Tk()
var = tk.StringVar()
def validate(*args):
print("Validation took place")
return True
entry = tk.Entry(validate="focusin", validatecommand=validate)
print("Entry created. Associating textvariable")
entry.config(textvariable=var)
print("textvariable associated. Changing value")
entry.config(validate="none")
var.set("Text")
entry.config(validate="focusin")
print("Value changed")
entry.pack()
tk.mainloop()
If you expect to do this a lot, you could even write a context manager so you don't accidentally forget to re-enable validation after you're finished altering the text.
import Tkinter as tk
from contextlib import contextmanager
#contextmanager
def temp_change(widget, **kargs):
old_values = {key:widget.cget(key) for key in kargs}
widget.config(**kargs)
try:
yield #wait for `with` block to finish executing...
finally:
widget.config(**old_values)
window = tk.Tk()
var = tk.StringVar()
def validate(*args):
print("Validation took place")
return True
entry = tk.Entry(validate="focusin", validatecommand=validate)
print("Entry created. Associating textvariable")
entry.config(textvariable=var)
print("textvariable associated. Changing value")
with temp_change(entry, validate="none"):
var.set("Text")
print("Value changed")
entry.pack()
tk.mainloop()

Disabling tkinter ttk scale widget

I am trying to disable all of the (ttk) widgets in a frame, but it appears that the scale widget is giving me some trouble, as it throws the following exception:
_tkinter.TclError: unknown option "-state"
Some relevant code:
import tkinter as tk
from tkinter import ttk
def disable_widgets(parent):
for child in parent.winfo_children():
child.config(state = 'disabled')
root = tk.Tk()
# Frame full of widgets to toggle
frame_of_widgets = ttk.Frame(root)
frame_of_widgets.pack()
# Button to be disabled
button_to_disable = ttk.Button(frame_of_widgets)
button_to_disable.pack()
# Entry to be disabled
entry_to_disable = ttk.Entry(frame_of_widgets)
entry_to_disable.pack()
# Scale to be disabled
scale_to_disable = ttk.Scale(frame_of_widgets)
scale_to_disable.pack()
# Button that disables widgets in frame
disable_button = ttk.Button(root,text="Disable",command= lambda: disable_widgets(frame_of_widgets))
disable_button.pack()
root.mainloop()
It works for the button and entry, but not for the scale. I thought one of the benefits of ttk was making widgets more uniform with common methods and attributes, so I am guessing perhaps I am accessing all three of these widgets incorrectly?
For ttk widgets you use the state method. The state method for buttons and entry widgets are just a convenience function to mimic the standard button and entry widgets.
You can rewrite your function like this:
def disable_widgets(parent):
for child in parent.winfo_children():
child.state(["disabled"])
ttk states are mentioned in the ttk documentation here (though the description borders on useless): https://docs.python.org/3.1/library/tkinter.ttk.html#widget-states
another way:
scale_to_disable.configure(state='disabled') # 'normal'
You can consider that set the breakpoint at the configure of the class Scale (from tkinter.ttk import Scale) may get some helpful.
The following is part of the code to intercept the class Scale
class Scale(Widget, tkinter.Scale):
...
def configure(self, cnf=None, **kw):
if cnf:
kw.update(cnf)
Widget.configure(self, **kw)

Categories