My final solution is this:
All the behaviour about color is encapsulated in the button widget.
I control the event with a handler which changes the background color for the active state with a lighter color.
Whenever the color changes, it does through a function of mine, so I trigger the '' event with .generate_event(), change the color and unbind the current handler for highlighting, and then bind a new handler for highlighting replacing the former.
I've made an auxiliar, reusable module for style-related methods and functions:
styleUtils
import tkinter as tk
import tkinter.ttk as ttk
import random as rnd
style = None
def random_color():
"""
Returns a random color as a string
:return: a color
"""
def r():
return rnd.randint(0, 0xffff)
return '#{:04x}{:04x}{:04x}'.format(r(), r(), r())
def get_style(master=None):
"""
Returns the style object instance for handling styles
:param master: the parent component
:return: the style
"""
global style
if not style:
style = ttk.Style(master) if master else ttk.Style()
return style
def get_style_name(widget):
"""
Returns the the name of the current style applied on this widget
:param widget: the widget
:return: the name of the style
"""
# .config('style') call returns the tuple
# ( option name, dbName, dbClass, default value, current value)
return widget.config('style')[-1]
def get_background_color(widget):
"""
Returns a string representing the background color of the widget
:param widget: a widget
:return: the color of the widget
"""
global style
color = style.lookup(get_style_name(widget), 'background')
return color
def highlighted_rgb(color_value):
"""
Returns a slightly modified rgb value
:param color_value: one of three possible rgb values
:return: one of three possible rgb values, but highlighted
"""
result = (color_value / 65535) * 255
result += (255 - result) / 2
return result
def highlighted_color(widget, color):
"""
Returns a highlighted color from the original entered
:param color: a color
:return: a highlight color for the one entered
"""
c = widget.winfo_rgb(color)
r = highlighted_rgb(c[0])
g = highlighted_rgb(c[1])
b = highlighted_rgb(c[2])
return ("#%2.2x%2.2x%2.2x" % (round(r), round(g), round(b))).upper()
def change_highlight_style(event=None):
"""
Applies the highlight style for a color
:param event: the event of the styled widget
"""
global style
widget = event.widget
current_color = get_background_color(widget)
color = highlighted_color(event.widget, current_color)
style.map(get_style_name(widget), background=[('active', color)])
It may be necessary to change the calling code a little bit to remove the unnecessary code now, but this will work straight away.
widgets.py (code for the button)
import os
import tkinter as tk
import tkinter.ttk as ttk
from PIL.ImageTk import PhotoImage
from user.myProject.view import styleUtils
class ImgButton(ttk.Button):
"""
This has all the behaviour for a button which has an image
"""
def __init__(self, master=None, **kw):
super().__init__(master, **kw)
self._img = kw.get('image')
# TODO Replace this temporal test handler for testing highlight color
self.bind('<Button-1>', self.change_color)
def change_color(self, __=None):
"""
Changes the color of this widget randomly
:param __: the event, which is no needed
"""
import random as rnd
#Without this, nothing applies until the mouse leaves the widget
self.event_generate('<Leave>')
self.set_background_color(rnd.choice(['black', 'white', 'red', 'blue',
'cyan', 'purple', 'green', 'brown',
'gray', 'yellow', 'orange', 'cyan',
'pink', 'purple', 'violet']))
self.event_generate('<Enter>')
def get_style_name(self):
"""
Returns the specific style name applied for this widget
:return: the style name as a string
"""
return styleUtils.get_style_name(self)
def set_background_color(self, color):
"""
Sets this widget's background color to that received as parameter
:param color: the color to be set
"""
styleUtils.get_style().configure(self.get_style_name(), background=color)
# If the color changes we don't want the current handler for the old color anymore
self.unbind('<Enter>')
# We replace the handler for the new color
self.bind('<Enter>', self.change_highlight_style)
def get_background_color(self):
"""
Returns a string representing the background color of the widget
:return: the color of the widget
"""
return styleUtils.get_style().lookup(self.get_style_name(), 'background')
def change_highlight_style(self, __=None):
"""
Applies the highlight style for a color
:param __: the event, which is no needed
"""
current_color = self.get_background_color()
# We get the highlight lighter color for the current color and set it for the 'active' state
color = styleUtils.highlighted_color(self, current_color)
styleUtils.get_style().map(self.get_style_name(), background=[('active', color)])
Calling code
import tkinter as tk
import tkinter.ttk as ttk
from widgets import ImgButton
class DiceFrame(ttk.Frame):
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
current_style = 'Die.TButton'
style = ttk.Style()
style.configure(current_style,
borderwidth=6,
)
button = ImgButton(master, style=current_style)
button.pack(side=tk.LEFT)
if __name__ == "__main__":
root = tk.Tk()
DiceFrame(root).pack(side="top", fill="both", expand=True)
root.mainloop()