2

According to the accepted answer on this post the use of .configure(highlightbackground='red') on a button should apply a color around the button however in testing I cannot reproduce what the poster has demonstrated in their gif recording.

Here is my test case: (Note even copy pasting the posters code I cannot get the highlight effect they are showing)

import tkinter as tk


root = tk.Tk()

btn = tk.Button(root, text='test', bg="#000000", fg="#ffffff", highlightthickness=4, activebackground="#ffffff",
                activeforeground="#000000", highlightbackground='red', highlightcolor='red')
btn.pack()
btn.focus_set()
root.mainloop()

Resulting app:

enter image description here

With some extensive searching I am not finding much on highlightbackground in the way of Q/A about the same issue so maybe something is missing. I have also tried to set the focus as this documentation states the widget needs focus with the same non result.

Maybe it could be version or OS related...

OS - Windows 10 Pro

Python - 3.6.2

Updated example using Krrr's post. So this does kinda work now however the issue at hand here is that it is resizing the button and not providing the correct highlighted color.

import tkinter as tk


def ResponsiveWidget(widget, *args, **kwargs):
    bindings = {
        '<FocusIn>': {'highlightbackground': 'red', 'highlightcolor':'red'},
        '<FocusOut>': {'highlightbackground': '#d9d9d9', 'highlightcolor':'SystemButtonFace'},
        '<Enter>': {'state': 'active'},
        '<Leave>': {'state': 'normal'}
    }
    for k, v in bindings.items():
        root.bind_class('Button', k, lambda e, kwarg=v: e.widget.config(**kwarg))


def update_active(event):
    global previous_button
    if previous_button != event.widget:
        previous_button.config(default='normal')
        event.widget.config(default='active')
        previous_button = event.widget


root = tk.Tk()
button_list = []
previous_button = None

for i in range(5):
    if i == 0:
        button_list.append(tk.Button(root, text='test', bg="#000000", fg="#ffffff", highlightthickness=5,
                                     activebackground="#ffffff", activeforeground="#000000", default='active'))
        previous_button = button_list[-1]
    else:
        button_list.append(tk.Button(root, text='test', bg="#000000", fg="#ffffff", highlightthickness=5,
                                     activebackground="#ffffff", activeforeground="#000000", default='normal'))
    button_list[-1].pack(padx=5, pady=5)
    button_list[-1].bind('<ButtonRelease-1>', update_active)

root.mainloop()

Results:

enter image description here

Expectation:

enter image description here

Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • Your code works on OSX: I see the red highlight color. – Bryan Oakley Oct 11 '19 at 19:44
  • @BryanOakley Hum. Not working on my work PC. I will test on 3.7 when I get home. – Mike - SMT Oct 11 '19 at 20:02
  • For what it's worth, it's not working on my Windows 10 Python 3.7.4 as well. I don't think you're going to have much luck. Seems OS related. – r.ook Oct 11 '19 at 20:03
  • @Krrr thanks for the input. At this point it sounds like an OS issue. – Mike - SMT Oct 11 '19 at 20:04
  • I'll add an observation - the button sizing seems suspect to me. On the answer you linked to, the button have spacing between them but when I run the code, the buttons are expanded to the fullest without gaps. Same with your current sample. – r.ook Oct 11 '19 at 20:05
  • @Krrr I have also noticed that issue. If you copy the posters code and run that I do not see any spacing between the buttons but in their images thee is spacing. That said the spacing is configured in the creation of the button not the grid so maybe that is worth testing. – Mike - SMT Oct 11 '19 at 20:06
  • @Krrr well changing the padding to be in the grid method did not fix it either but did add the expected spacing between buttons seen in their image. – Mike - SMT Oct 11 '19 at 20:09
  • Yeah I tried the padding and height/width, doesn't seem to make a difference. But I feel like the button is expanding to the fullest... or the highlight is actually drawing on top of the button instead. – r.ook Oct 11 '19 at 20:11

2 Answers2

3

Unfortunately it seems Windows OS doesn't seem to be triggering the state and default widget configs properly. However this is achievable by doing your own bindings.

If you only have a handful of widgets that need this behaviour, you can have create a widget wrapper:

def ResponsiveWidget(widget, *args, **kwargs):
    bindings = {
        '<FocusIn>': {'default':'active'},    # for Keyboard focus
        '<FocusOut>': {'default': 'normal'},  
        '<Enter>': {'state': 'active'},       # for Mouse focus
        '<Leave>': {'state': 'normal'}
    }
    # Create the widget instance
    w = widget(*args, **kwargs)

    # Set the bindings for the widget instance
    for k, v in bindings.items():
        w.bind(k, lambda e, kwarg=v: e.widget.config(**kwarg))

    # Remember to return the created and binded widget
    return w

btn = ResponsiveWidget(tk.Button, root, text='test3', bg="#000000", fg="#ffffff", highlightthickness=10, activebackground="#ffffff",
                activeforeground="#000000", highlightbackground='red', highlightcolor='green')

btn2 = ResponsiveWidget(tk.Button, root, text='test4', bg="#000000", fg="#ffffff", highlightthickness=10, activebackground="#ffffff",
                activeforeground="#000000", highlightbackground='green', highlightcolor='red')

On the other hand, if you wanted the entire class of the widget to always trigger the default/state properly, you can use bind_class instead:

bindings = {
    '<FocusIn>': {'default':'active'},    # for Keyboard focus
    '<FocusOut>': {'default': 'normal'},  
    '<Enter>': {'state': 'active'},       # for Mouse focus
    '<Leave>': {'state': 'normal'}
}
for k, v in bindings.items():
    root.bind_class('Button', k, lambda e, kwarg=v: e.widget.config(**kwarg))

This seem to trigger the events just fine.

If you just want to replicate the functionality of the highlight colour, a less desirable method would be to change the highlightcolor config on focus instead:

bindings = {
        '<FocusIn>': {'highlightcolor':'red'},
        '<FocusOut>': {'highlightcolor': 'SystemButtonFace'},
        '<Enter>': {'state': 'active'},
        '<Leave>': {'state': 'normal'}
    }
for k, v in bindings.items():
    root.bind_class('Button', k, lambda e, kwarg=v: e.widget.config(**kwarg))

# Note this method requires you to set the default='active' for your buttons

btn = tk.Button(root, text='test', bg="#000000", fg="#ffffff", highlightthickness=10, activebackground="#ffffff",
                activeforeground="#000000", highlightcolor='SystemButtonFace', default='active')

# ...

I'd consider this more a hacky method.

Edit: For completeness, here's a MCVE using bind_class:

import tkinter as tk

root = tk.Tk()
bindings = {
        '<FocusIn>': {'highlightcolor':'red'},
        '<FocusOut>': {'highlightcolor': 'SystemButtonFace'},
        '<Enter>': {'state': 'active'},
        '<Leave>': {'state': 'normal'}
    } 

for k, v in bindings.items():
    root.bind_class('Button', k, lambda e, kwarg=v: e.widget.config(**kwarg))

btns = list(range(5))
for btn in btns:
    btns[btn] = tk.Button(root, text='test', bg="#000000", fg="#ffffff", highlightthickness=5, activebackground="#ffffff",
        activeforeground="#000000", highlightcolor='SystemButtonFace', default='active', padx=5, pady=5)
    btns[btn].pack()

btns[0].focus_set()
root.mainloop()

And MCVE using ResponsiveWidget function:

import tkinter as tk

root = tk.Tk()
def ResponsiveWidget(widget, *args, **kwargs):
    bindings = {
        '<FocusIn>': {'highlightcolor':'red'},    # for Keyboard focus
        '<FocusOut>': {'highlightcolor': 'SystemButtonFace'},  
        '<Enter>': {'state': 'active'},       # for Mouse focus
        '<Leave>': {'state': 'normal'}
    }
    # Create the widget instance
    w = widget(*args, **kwargs)

    # Set the bindings for the widget instance
    for k, v in bindings.items():
        w.bind(k, lambda e, kwarg=v: e.widget.config(**kwarg))

    # Remember to return the created and binded widget
    return w

btns = list(range(5))
for btn in btns:
    btns[btn] = ResponsiveWidget(tk.Button, root, text=f'test{btn}', bg="#000000", fg="#ffffff", highlightthickness=10, activebackground="#ffffff",
        activeforeground="#000000", highlightcolor='SystemButtonFace', default='active', padx=5, pady=5)
    btns[btn].pack()

btns[0].focus_set()
root.mainloop()
r.ook
  • 13,466
  • 2
  • 22
  • 39
  • Hum. Well I am just getting multiple buttons without borders unless I use `focus()`. The idea is that the highlight will change on button click. – Mike - SMT Oct 15 '19 at 14:09
  • You mean you wanted the `highlightbackground` colour to show when the focus is moved away? Yeah that bit I was trying to figure out as well, not sure why that particular setting wasn't triggered. – r.ook Oct 15 '19 at 14:10
  • Ya. That is the main struggle. I have added a print statement to your lambda for testing it may help with troubleshooting. `w.bind(k, lambda e, kwarg=v: (e.widget.config(**kwarg), print('test{} - {}'.format(e, kwarg))))` – Mike - SMT Oct 15 '19 at 14:12
  • So the goal is to replicate the intended functionality of this post. https://stackoverflow.com/questions/53647343/how-to-change-which-tkinter-button-is-highlighted-via-arrow-keys/53648642#53648642. Without being able to show the highlight on windows when it should then it wont work. – Mike - SMT Oct 15 '19 at 14:14
  • If all you need is functionality, a dirty method would be to keep the `default='active'` for the buttons and just change `highlighcolor` on focus, it would achieve the same effect albeit not the intended method. – r.ook Oct 15 '19 at 14:14
  • That is the problem though. The colors are not showing up. Even when telling specific buttons to have color in a tracked function. – Mike - SMT Oct 15 '19 at 14:15
  • Changing the `highlight` colour on focus definitely works, but not desirable IMO. I've edited the answer with a sample FYI. – r.ook Oct 15 '19 at 14:51
  • I will try to work this into my code to see if I can get the buttons to dynamically update on selection. – Mike - SMT Oct 15 '19 at 14:57
  • Ok so I got it working with a list of buttons however the highlighting seams to resize the button and then fill the highlight in the area the button was in before. This is not the behavior we are looking for. We need the highlight to be outside of the button without resizing the button itself. Here is the [link to the example results](https://i.imgur.com/OItl3uD.png). Also the color does not change to red. – Mike - SMT Oct 15 '19 at 15:09
  • The highlight exists outside of the button, so the thickness of the highlight + the button size would be the full widget size. I think this bit is consistent across platforms as seen in the linked question, so you might just need to factor in the `highlightthickness` for sizing. The colours worked on my end though, did you try my last sample with the `''` set to `{'highlightcolor': 'red'}`? I'm on Windows 10 - Python 3.7.4 – r.ook Oct 15 '19 at 15:18
  • If you review the post I referenced in my question you will see the highlight does not affect the size of the button in the posters gif example. That tells me it is not consistent across platforms. I have updated my question. I did try `highlightcolor` as well as `highlightbackground` and both at the same time. – Mike - SMT Oct 15 '19 at 15:22
  • I did review the post before and after my answer, but I'm pretty sure the `highlightthickness` definitely affects the button sizing as the sample code also produced much bigger buttons than the gif when ran on my OS. The only difference I see is that on Windows the thickness is filled by the button size itself instead of the `highlightbackground` as expected. Observe the size of these two buttons: `tk.Button(root, text='active', highlightthickness=10, default='active').pack(); tk.Button(root, text='normal', highlightthickness=10).pack()` – r.ook Oct 15 '19 at 15:32
  • Note: on your updated code, you seem to have mixed up my solution: You've put `root.bind_class(...)` within the `ResponsiveWidget` and it's never called, so the bindings never took effect. Try just `root.bind_class` on the global scope. Otherwise, make sure you follow my `ResponsiveWidget` function to return the created and binded widget properly. – r.ook Oct 15 '19 at 15:36
  • Ya there was some confusion before you posted your MCVE. I think I have it now just need to go over it step by step to really understand what the `bind_class()` is doing. This is a solution that is far removed from anything I have tried and frankly am still lacking in understand what exactly it is doing but with your MCVE I can recreate the behavior I need. Thanks. It is odd that we have to do this in windows. I wonder if this is a bug that needs to be reported. – Mike - SMT Oct 15 '19 at 15:43
  • Re: `bind_class`, from the [documentation](https://effbot.org/tkinterbook/tkinter-events-and-bindings.htm): *You could use the `bind_class` method to modify the bindings on the class level, but that would change the behavior of all text widgets in the application.* So it effectively changes how the entire `Button` class behaves in this case. If you only wanted a specific subset of `Button`s to have these binding, the `ResponsiveWidget` function would be easier to manage. But yeah, `tkinter` have some weird behaviours on Windows... but that's not too surprising for a cross platform module. – r.ook Oct 15 '19 at 15:58
0

Thank you for your question, your code is good and to solve your problem just use default="active"

import tkinter as tk


root = tk.Tk()

btn = tk.Button(root, text='test', bg="#000000", fg="#ffffff", highlightthickness=4,  
activebackground="#ffffff",
                activeforeground="#000000", highlightbackground='red', 
default="active", highlightcolor='red')
btn.pack()
btn.focus_set()
root.mainloop()