Кастомизация кнопок в Python

Столкнулся с вопросом кастомизации кнопок (захотелось более аутентичного их вида, вместо стандартных). В ходе поисков нашёл такой вот вариант:

import tkinter as tk

class CustomButton(tk.Button):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.config(
            bd=0,
            highlightthickness=0,
            image=_default)
        self.bind("<Enter>", self.on_hover)
        self.bind("<ButtonPress>", self.on_press)
        self.bind("<ButtonRelease>", self.on_release)
        self.bind("<Leave>", self.on_leave)

    def on_hover(self, event):
        self.config(image=_over)
    def on_press(self, event):
        self.config(image=_press)
    def on_release(self, event):
        self.config(image=_over)
    def on_leave(self, event):
        self.config(image=_default)

root = tk.Tk()
root.title("Custom Button Example")
width = 300
height = 300
x = int((root.winfo_screenwidth() / 2) - (width / 2))
y = int((root.winfo_screenheight() / 2) - (height / 2))
root.geometry(f'{width}x{height}+{x}+{y}')
root.resizable(width=False, height=False)
root['background'] = '#d4dbe4'

_default = tk.PhotoImage(file=r'путь до изображения кнопки')
_over = tk.PhotoImage(file=r'путь до изображения кнопки')
_press = tk.PhotoImage(file=r'путь до изображения кнопки')

custom_button = CustomButton(root)
custom_button.pack(pady=120)

root.mainloop()

Это изображения состояний кнопки:

введите сюда описание изображения

введите сюда описание изображения

введите сюда описание изображения

В таком варианте при анимации нажатия происходит вот что:

введите сюда описание изображения

Т.е. картинка съезжает на пиксель по x и y, что меня не устраивает.

ДОПОЛНЕНИЕ 1:

Проблема с белыми линиями решена (см. ответ). Но, вариант с состояниями, реализованный с помощью tk.Label, выглядит предпочтительней, т.к. в нем изображение нажатой кнопки не обрезается и не смещается:

введите сюда описание изображения

Работоспособный код в первом ответе.

ДОПОЛНЕНИЕ 2:

Получилось заставить работать вариант с tk.Label, с помощью наследования:

import tkinter as tk


class States(tk.Label):
    def __init__(self, master=None):
        super().__init__(master)
        self.config(bd=0, image=_default)
        self.bind("<Enter>", self.on_hover)
        self.bind("<ButtonPress>", self.on_press)
        self.bind("<ButtonRelease>", self.on_release)
        self.bind("<Leave>", self.on_leave)

    def on_hover(self, event):
        self.config(image=_over)

    def on_press(self, event):
        self.config(image=_press)

    def on_release(self, event):
        self.config(image=_over)

    def on_leave(self, event):
        self.config(image=_default)


root = tk.Tk()
root.title("Custom Button Example")
width = 300
height = 300
x = int((root.winfo_screenwidth() / 2) - (width / 2))
y = int((root.winfo_screenheight() / 2) - (height / 2))
root.geometry(f'{width}x{height}+{x}+{y}')
root.resizable(width=False, height=False)
root['background'] = '#d4dbe4'

_default = tk.PhotoImage(file=r'_btnB_default.png')
_over = tk.PhotoImage(file=r'_btnB_over.png')
_press = tk.PhotoImage(file=r'_btnB_press.png')


def function1():
    print('function1')


def function2():
    print('function2')


class MyButton1(States):
    def on_release(self, event):
        super().on_release(self)
        print('function1')


custom_button = MyButton1(root)
custom_button.place(relx=0.5, y=100, anchor='center')


class MyButton2(States):
    def on_release(self, event):
        super().on_release(self)
        print('function2')


custom_button = MyButton2(root)
custom_button.place(relx=0.5, y=160, anchor='center')

root.mainloop()

При таком подходе все работает. Я попробовал назначить событие не только на on_release, но и на on_press и тогда получается залипание анимации, пока курсор не увести с кнопки.


Ответы (2 шт):

Автор решения: KLYSTRON

Полностью работоспособный вариант на основе class States(tk.Button):

from tkinter import *
import tkinter as tk


class States(tk.Button):
    def __init__(self, master, command, **kwargs):
        super().__init__(master, command=command)
        self.config(border=0,
                    relief=FLAT,
                    background='#d4dbe4',
                    activebackground='#d4dbe4',
                    image=_default)
        self.bind('<Enter>', self.on_hover)
        self.bind('<ButtonPress>', self.on_press)
        self.bind('<ButtonRelease>', self.on_release)
        self.bind('<Leave>', self.on_leave)

    def on_hover(self, event):
        self.config(image=_over)

    def on_press(self, event):
        self.config(image=_press)

    def on_release(self, event):
        self.config(image=_over)

    def on_leave(self, event):
        self.config(image=_default)


root = tk.Tk()
root.title('Custom Button Example')
width = 300
height = 300
x = int((root.winfo_screenwidth() / 2) - (width / 2))
y = int((root.winfo_screenheight() / 2) - (height / 2))
root.geometry(f'{width}x{height}+{x}+{y}')
root.resizable(width=False, height=False)
root['background'] = '#d4dbe4'

_default = tk.PhotoImage(file=r'_btnB_default.png')
_over = tk.PhotoImage(file=r'_btnB_over.png')
_press = tk.PhotoImage(file=r'_btnB_press.png')


def function1():
    print('function1')


def function2():
    print('function2')


custom_button = States(root, function1)
custom_button.place(relx=0.5, y=100, anchor='center')

custom_button = States(root, function2)
custom_button.place(relx=0.5, y=160, anchor='center')

root.mainloop()

Смещение в анимации нажатия tk.Button, убирается добавлением к свойствам relief=FLAT или relief=SUNKEN:

self.config(border=0,
            relief=FLAT, #relief=SUNKEN
            background='#d4dbe4',
            activebackground='#d4dbe4',
            image=_default)

Собственно это и требовалось!

→ Ссылка
Автор решения: KLYSTRON

Работоспособный вариант на основе tk.Label:

import tkinter as tk


class States(tk.Label):
    def __init__(self, master=None):
        super().__init__(master)
        self.config(bd=0, image=_default)
        self.bind("<Enter>", self.on_hover)
        self.bind("<ButtonPress>", self.on_press)
        self.bind("<ButtonRelease>", self.on_release)
        self.bind("<Leave>", self.on_leave)

    def on_hover(self, event):
        self.config(image=_over)

    def on_press(self, event):
        self.config(image=_press)

    def on_release(self, event):
        self.config(image=_over)

    def on_leave(self, event):
        self.config(image=_default)


root = tk.Tk()
root.title("Custom Button Example")
width = 300
height = 300
x = int((root.winfo_screenwidth() / 2) - (width / 2))
y = int((root.winfo_screenheight() / 2) - (height / 2))
root.geometry(f'{width}x{height}+{x}+{y}')
root.resizable(width=False, height=False)
root['background'] = '#d4dbe4'

_default = tk.PhotoImage(file=r'_btnB_default.png')
_over = tk.PhotoImage(file=r'_btnB_over.png')
_press = tk.PhotoImage(file=r'_btnB_press.png')


def function1():
    print('function1')


def function2():
    print('function2')


class MyButton1(States):
    def on_release(self, event):
        super().on_release(self)
        print('function1')


custom_button = MyButton1(root)
custom_button.place(relx=0.5, y=100, anchor='center')


class MyButton2(States):
    def on_release(self, event):
        super().on_release(self)
        print('function2')


custom_button = MyButton2(root)
custom_button.place(relx=0.5, y=160, anchor='center')

root.mainloop()

Не так компактно по коду, как вышеописанный вариант на tk.Button, зато работает. Ещё в таком исполнении при попытке выполнить bind на on_press:

class MyButton1(States):
    def on_press(self, event):
        super().on_release(self)
        print('function1')

...происходит залипание изображения и анимация нажатия не происходит, пока ищу способы решения, если найду, сделаю правку.

ДОПОЛНЕНИЕ:

В процессе поиска решений, нашел такую тему, где frarugi87 описывает способ создания практически полноценной кнопки на основе tk.Label, с детальными комментариями по коду.

В отличие от tk.Button, у конопок созданных по этой схеме:

  1. Нет подсветки "в фокусе";
  2. Дополнительные функции (например, anchorи, foreground);
  3. Наследование от tk.TLable.

Получается довольно тяжеловесная по коду конструкция, но, возможно, кому-то пригодится.

Еще по поводу bind в одной из тем (не помню какая, если попадется прикреплю ссылку), видел такой комментарий:

Using a bind isn't a particularly good solution IMO. This is exactly what the -command option is for. Plus, by doing this in a binding you lose the ability to have the callback called via keyboard traversal unless you also add key bindings. It gets pretty messy pretty quick, so stick with -command.

→ Ссылка