2

In the following code, the show_widget_validity() function either applies a custom style that has just a change to the background color of a widget's existing style, or restores the original style. This is a library routine, so does not take complete control of the style. The background color appears to be reconfigured correctly, as shown by the background style description reported in the entry widget after each change. However, the actual background color of the widget does not change.

This behavior is seen on Linux with Python 2.7 and 3.6, and on Windows with Python 2.7; I haven't tested other environments.

Any clues as to the cause of this behavior, or code changes necessary to account for it, would be appreciated.

EDIT: Using "fieldbackground" instead of "background" is effective on Linux but not Windows, and does not allow revision of the background color in the disabled state.

try:
    import Tkinter as tk
except:
    import tkinter as tk
try:
    import ttk
except:
    from tkinter import ttk


def show_widget_validity(widget, is_valid):
    invalid_color = "#fff5ff"
    invalid_disabled_color = "#f6f0f6"
    sname = widget["style"] or widget.winfo_class()
    if sname.startswith("Invalid."):
        if is_valid:
            widget['style'] = sname[8:]
    else:
        if not is_valid:
            invname = "Invalid." + sname
            ttk.Style().configure(invname, background=[('disabled', invalid_disabled_color), ('active', invalid_color)])
            # Simpler, but also ineffective:
            #ttk.Style().configure(invname, background=invalid_color)
            widget['style'] = invname

def show_invalid():
    show_widget_validity(entry, False)
    entry_var.set(ttk.Style().lookup(entry["style"] or entry.winfo_class(), "background"))

def show_valid():
    show_widget_validity(entry, True)
    entry_var.set(ttk.Style().lookup(entry["style"] or entry.winfo_class(), "background"))

root = tk.Tk()
root.title("Testing of Style Customization")

appframe = tk.Frame(root, padx=12, pady=12)
appframe.pack(expand=True, fill=tk.BOTH)

entry_var = tk.StringVar()
entry = ttk.Entry(appframe, textvariable=entry_var, width=40, exportselection=False)
entry.grid(row=0, column=0, padx=3, pady=3, sticky=tk.EW)

btnframe = ttk.Frame(appframe)
btnframe.grid(row=1, column=0)
invalid_btn = ttk.Button(btnframe, text="Make invalid", command=show_invalid)
valid_btn = ttk.Button(btnframe, text="Make valid", command=show_valid)
invalid_btn.grid(row=0, column=0, padx=3, pady=3)
valid_btn.grid(row=0, column=1, padx=3, pady=3)

root.mainloop()
stovfl
  • 14,998
  • 7
  • 24
  • 51
rd_nielsen
  • 2,407
  • 2
  • 11
  • 18

1 Answers1

4

It appears you are trying to make an entry field show the validity of the content by changing the background. However you are trying to reconfigure the style each time which is not the right way to go. Instead you should configure the dynamic properties according to the widget state. This is how the disabled/!disabled and pressed/!pressed aappearance changes are handled. The ttk widgets have a state method where you can change a number of flags. disabled is the most common and pressed for button-type widgets. The other is active for when the widget changes its appearance when the pointer is hovering over it making it look 'hot'. For your purpose there is a handy invalid state defined. We just need to add it to the style map for the widget style. As we don't want to affect all Entry widgets we can copy the current style to a new Custom.Entry style:

style = ttk.Style()
style.layout("Custom.Entry", style.layout('TEntry'))
style.configure("Custom.Entry", **style.configure('TEntry'))
style.map("Custom.Entry", **style.map('TEntry'))
style.map("Custom.Entry",
    fieldbackground=[(['invalid','!disabled'], '#ff4040'),
                     (['invalid','disabled'], '#ffc0c0')])

entry = ttk.Entry(root, style='Custom.Entry')

On the Tk-based themes this will be sufficient to have the background of the widget change colour according to the widget invalid state. ie: entry.state(['invalid']) will make it use a red background. On themes that use native drawing elements like the Windows and MacOS themes this needs a bit more work. We can't necessarily change the look of a native themeing engine drawn element unless the native engine supports the invalid state already. If it does not then we can override the elements that make up the widget presentation by cloning in new ones from one of the Tk-based themes. To illustrate this see the createCustomEntry function below which copies the fieldbackground from the 'default' theme so that we can change the look on Windows.

On Linux it looks like this now:

enter image description here

On Windows 7: enter image description here

Modified code

try:
    import Tkinter as tk
except:
    import tkinter as tk
try:
    import ttk
except:
    from tkinter import ttk

def createCustomEntry(style):
    if 'Custom.Entry.field' not in style.element_names():
        style.element_create('Custom.Entry.field', 'from', 'default')
    if style.theme_use() in ['alt', 'clam', 'default', 'classic']:
        style.layout('Custom.Entry', style.layout('TEntry'))
    else:
        style.layout("Custom.Entry", [
            ("Custom.Entry.field", {'sticky': 'nswe', 'border': '1', 'children': [
                ("Custom.Entry.padding", {'sticky':'nswe', 'children': [
                    ("Custom.Entry.textarea", {'sticky':'nswe'})
                ]})
            ]})
        ])
    style.configure('Custom.Entry', **style.configure('TEntry'))
    style.map('Custom.Entry', **style.map('TEntry'))
    style.map('Custom.Entry',
        fieldbackground=[(['invalid','!disabled'], '#ff4040'),
                            (['invalid','disabled'], '#ffc0c0')])

def show_invalid():
    [w.state(['invalid']) for w in (entry, entry2)]

def show_valid():
    [w.state(['!invalid']) for w in (entry,entry2)]

root = tk.Tk()

# Simple version:
style = ttk.Style()
style.layout("Custom.Entry", style.layout('TEntry'))
style.configure("Custom.Entry", **style.configure('TEntry'))
style.map("Custom.Entry", **style.map('TEntry'))
style.map("Custom.Entry",
    fieldbackground=[(['invalid','!disabled'], '#ff4040'),
                     (['invalid','disabled'], '#ffc0c0')])

#createCustomEntry(style)

root.title("Testing of Style Customization")

appframe = tk.Frame(root, padx=12, pady=12)
appframe.pack(expand=True, fill=tk.BOTH)

entry_var = tk.StringVar()
entry = ttk.Entry(appframe, textvariable=entry_var, width=40,
                  exportselection=False, style="Custom.Entry")
entry.grid(row=0, column=0, padx=3, pady=3, sticky=tk.EW)

entry2 = ttk.Entry(appframe, textvariable=entry_var, width=40,
                   exportselection=False, style="Custom.Entry")
entry2.grid(row=1, column=0, padx=3, pady=3, sticky=tk.EW)
entry2.state(['disabled'])

btnframe = ttk.Frame(appframe)
btnframe.grid(row=2, column=0)
invalid_btn = ttk.Button(btnframe, text="Make invalid", command=show_invalid)
valid_btn = ttk.Button(btnframe, text="Make valid", command=show_valid)
invalid_btn.grid(row=0, column=0, padx=3, pady=3)
valid_btn.grid(row=0, column=1, padx=3, pady=3)

root.mainloop()
patthoyts
  • 32,320
  • 3
  • 62
  • 93
  • Thank you for the extremely comprehensive answer. It's not clear to me, though, why the layout of the new style needs to be explicitly specified--wouldn't that be copied from the default? And if the layout were not explicitly specified, would the custom style then be applicable to other ttk widgets such as Treeview and Checkbutton? – rd_nielsen Feb 22 '18 at 23:39
  • If you just copy the layout you don't include the custom element as the layout just requests 'Entry.field'. Ideally you want to copy the current theme layout and substitute the field element for the new more specfic one as we currently are loosing the theme specific border as well. You can see on the Windows screenshot the lower entry is a standard one and has a different border due to the slightly different layout in the vsapi theme. – patthoyts Feb 23 '18 at 00:51
  • If you want to see how a style is set for the whole widget set or group of widgets you will really need to look at the Tk library code. Look at the ttk/* files defining the alt theme and vista themes for example. eg `/usr/share/tcltk/tk8.6/ttk/vistaTheme.tcl` – patthoyts Feb 23 '18 at 00:53
  • Note that this example does not work with the style set to vista (style.theme_use("vista")), which is the default on Windows. – Watusimoto Nov 27 '21 at 02:57