Кастомизация кнопок в 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 шт):
Полностью работоспособный вариант на основе 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)
Собственно это и требовалось!
Работоспособный вариант на основе 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, у конопок созданных по этой схеме:
- Нет подсветки "в фокусе";
- Дополнительные функции (например,
anchorи,foreground); - Наследование от
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.




