user13622968
user13622968

Reputation:

Change color of different widgets altogether in tkinter

I want to change the different configuration colors of different widgets altogether to a same color when the user clicks on the entry widgets.

I've created a function "change_color(color)" where all my widgets are getting configured to the color passed as an argument.

The problem is the code has a lot of widgets and I've to manually add every widget to the function to keep them updated. I can't use a list as some widgets options are different, for eg: changing foreground of entry widget, background of labels, and much more. Please let me know if there is a better approach of doing this.

Here is a small example of my program. My main code is very long and is not suitable to post here.

import tkinter as tk

def change_color(color):
    "Change color of widgets."
    window.config(bg=color)
    user_label.config(bg=color)
    pass_label.config(bg=color)
    user_entry.config(highlightbackground=color)
    pass_entry.config(highlightbackground=color)
    user_entry.config(fg=color, insertbackground=color)
    pass_entry.config(fg=color, insertbackground=color)

window = tk.Tk()
# username
user_label = tk.Label(window, text='Username')
user_entry = tk.Entry(window, bg='black')
# password
pass_label = tk.Label(window, text='Password')
pass_entry = tk.Entry(window, bg='black')
user_label.grid(row=0, column=0)
user_entry.grid(row=0, column=1)
pass_label.grid(row=1, column=0)
pass_entry.grid(row=1, column=1)
# changes color
user_entry.bind("<1>", lambda _: change_color("#99c9ff"))
pass_entry.bind("<1>", lambda _: change_color("#ffaf99"))
window.mainloop()

I hope you can get an idea from this example. If something is not clear please ask me from the comment section.

Upvotes: 2

Views: 1925

Answers (3)

Skopyk
Skopyk

Reputation: 33

Changing color for many widgets simultaneously, regardless of their place in the app hierarchy, is the exact problem I've solved. I was adding an ability for my app to change its color theme in pure tkinter. Consider an example application below, and the brief explanation that follows.

the app in action

green-themed app red-themed app blue-themed app

main.py

import tkinter as tk
import themed_widgets as th # where magic happens

root = tk.Tk()
root.geometry("200x200")

frame1 = th.Frame(root, pady=10, borderwidth=6, relief=tk.SUNKEN)
frame1.pack(fill=tk.X)

# btn will NOT support recoloring, 
# since it was made with 'tk', not 'th'
btn = tk.Button(frame1, text="change theme", 
                command=lambda: th.change_theme(root))
btn.pack()

frame2 = th.Frame(root, pady=10, borderwidth=12, relief=tk.SUNKEN)
frame2.pack(fill=tk.BOTH, expand=True)

label = th.Label(frame2, text="random label")
label.pack(fill=tk.X, expand=True)

tk.Radiobutton()
rb_var = tk.IntVar(value=0)
for i in range(3):
    rb = th.Radiobutton(frame2, text=f"radiobutton {i}",
                        variable=rb_var, value=i, indicatoron=False)
    rb.pack()

root.mainloop()

See how it's enough to replace tk with th in the existing app where you wish to support recoloring. In order for it to work, all the tk widgets you want to recolor must have their counterparts in the themed_widgets.py file.

themed_widgets.py

import tkinter as tk

active = 0
THEMES = [
    # colors                # themes
    # primary ,  secondary  #
    ["#0000FF", "#00FF00"], # 1st
    ["#00FF00", "#FF0000"], # 2nd
    ["#FF0000", "#0000FF"], # 3rd
]
def primary(): return THEMES[active][0]
def secondary(): return THEMES[active][1]

def Frame(*args, **kwargs):
    fr = tk.Frame(*args, **kwargs)
    fr.recolor = lambda: fr.configure(bg=secondary())
    fr.recolor()
    return fr

def Label(*args, **kwargs):
    lbl = tk.Label(*args, **kwargs)
    lbl.recolor = lambda: lbl.configure(bg=secondary(), fg=primary())
    lbl.recolor()
    return lbl

def Radiobutton(*args, **kwargs):
    rb = tk.Radiobutton(*args, **kwargs)
    # notice how '.configure' differs drastically between widgets
    # especially here, with 'selectcolor' exclusive to a Radiobutton
    rb.recolor = lambda: rb.configure(bg=secondary(), selectcolor=primary())
    rb.recolor()
    return rb

def change_theme(container):
    """
    Recursively iterate over all children of a given container.
    Typically call with 'root' as an argument.
    """
    global active
    active += 1
    if active > len(THEMES) - 1:
        active = 0

    do_recolor(container)

def do_recolor(container):
    for child in container.winfo_children():
        try:
            child.recolor()
        except AttributeError as e:
            pass
        if child.winfo_children():
            do_recolor(child)

See how for each widget a function with its name is created. There, a recoloring lambda, specific to the widget's needs, is assigned to the recolor attribute of the tk widget. With this approach, you don't need to check widget's class to decide what to do in .configure. This allows for a more simple recursive recoloring, as can be seen in do_recolor.

Recoloring relies on a list of constants, which ensures a uniform look for your app, and makes expanding template colors and adding themes that much easier.

Set themes for individual widgets as opposed to whole widget classes

Don't want a widget instance to participate in recoloring? Simply create it with tk instead of th. What if, however, you have two themed widgets of the same class, and they need to be recolored differently?

The solution as it is can be tweaked to solve this case. Consider a more elegant solution. With closures, custom theme colors to cycle through can be used for individual themed widgets. Here is an example for labels.

Add this to themed_widgets.py

def Unique_Label(theme_colors):
    l = len(theme_colors)
    def primary(): return theme_colors[active % l][0]
    def secondary(): return theme_colors[active % l][1]
    def label(*args, **kwargs):
        lbl = tk.Label(*args, **kwargs)
        lbl.recolor = lambda: lbl.configure(bg=secondary(), fg=primary())
        lbl.recolor()
        return lbl
    return label

Add this below the lbl in main.py

ulabel_theme = [['lightblue', 'darkgreen'],['darkblue', 'lightgreen']]
ulabel = th.Unique_Label(ulabel_theme)(frame2, text="unique label")
ulabel.pack(fill=tk.X, expand=True)

And it works!

green-themed app, custom label is dark red-themed app, custom label is light

Upvotes: 0

Daniel Huckson
Daniel Huckson

Reputation: 1217

Here is one approach to doing it.

import tkinter as tk

def change_color(color):
    "Change color of widgets."

    for wdg in window.children():
        wdg = window.nametowidget(wdg)
        if isinstance(wdg, tk.Label):
            wdg.config(bg=color)
        elif isinstance(wdg, tk.Entry):
            wdg.config(fg=color, insertbackground=color, highlightbackground=color)


window = tk.Tk()
# username
user_label = tk.Label(window, text='Username')
user_entry = tk.Entry(window, bg='black')
# password
pass_label = tk.Label(window, text='Password')
pass_entry = tk.Entry(window, bg='black')
user_label.grid(row=0, column=0)
user_entry.grid(row=0, column=1)
pass_label.grid(row=1, column=0)
pass_entry.grid(row=1, column=1)
# changes color
user_entry.bind("<1>", lambda _: change_color("#99c9ff"))
pass_entry.bind("<1>", lambda _: change_color("#ffaf99"))

window.mainloop()

Upvotes: 0

acw1668
acw1668

Reputation: 46669

You can go through all widgets recursively using winfo_children():

def change_color(color, container=None):
    if container is None:
        container = window  # set to root window
    container.config(bg=color)
    for child in container.winfo_children():
        if child.winfo_children():
            # child has children, go through its children
            change_color(color, child)
        elif type(child) is tk.Label:
            child.config(bg=color)
        elif type(child) is tk.Entry:
            child.config(highlightbackground=color)
            child.config(fg=color, insertbackground=color)
        # check for other widget types ...

Upvotes: 2

Related Questions