Tkinter create_image() retains PNG transparency but Button(image) does not - python

TkVersion = 8.6, Python version 3.7.3
Im trying to create a button in python with tkinter using a PNG image. The transparent corners of the image are transparent depending on which widget I use. It seems canvas.create_image is the only widget retaining the transparency.
First I added the image on a canvas using create_image(0,0, image=button) and it works fine - the rounded corners are transparent.
But then when I try to implement it as an actual button using Button() and create_window() widgets, the corners are filled with white.
button = ImageTk.PhotoImage(file="button.png")
canvas = tk.Canvas(width=200, heigh=200, borderwidth=0, highlightthickness=0)
canvas.grid()
canvas.create_rectangle(0,0,199,199, fill="blue")
canvas.create_image(0,0, image=button, anchor="nw")
[]
button = ImageTk.PhotoImage(file="button.png")
canvas = tk.Canvas(width=200, heigh=200, borderwidth=0, highlightthickness=0)
canvas.grid()
canvas.create_rectangle(0,0,199,199, fill="blue")
buttonWidget = tk.Button(root, image=button)
canvas.create_window(0,0, window=buttonWidget, anchor="nw")
How can I make the PNG button corners transparent?
Here is also the button image:

You can make your own custom button class inherited from canvas and use it just like you use Button(). I made one for you hopefully you find it helpful.
Custom Button Class:
Save this class in a separate file as imgbutton.py and then import it to your main file. Also make sure it is in the same directory as the main file is in. Or you can just keep it on the top of the main file after your imports.
import tkinter as tk
class Imgbutton(tk.Canvas):
def __init__(self, master=None, image=None, command=None, **kw):
# Declared style to keep a reference to the original relief
style = kw.get("relief", "groove")
if not kw.get('width') and image:
kw['width'] = image.width()
else: kw['width'] = 50
if not kw.get('height') and image:
kw['height'] = image.height()
else: kw['height'] = 24
kw['relief'] = style
kw['borderwidth'] = kw.get('borderwidth', 2)
kw['highlightthickness'] = kw.get('highlightthickness',0)
super(Imgbutton, self).__init__(master=master, **kw)
self.set_img = self.create_image(kw['borderwidth'], kw['borderwidth'],
anchor='nw', image=image)
self.bind_class( self, '<Button-1>',
lambda _: self.config(relief='sunken'), add="+")
# Used the relief reference (style) to change back to original relief.
self.bind_class( self, '<ButtonRelease-1>',
lambda _: self.config(relief=style), add='+')
self.bind_class( self, '<Button-1>',
lambda _: command() if command else None, add="+")
Here is an Example how to use it
import tkinter as tk
from imgbutton import Imgbutton # Import the custom button class
root = tk.Tk()
root['bg'] = 'blue'
but_img = tk.PhotoImage(file='button.png')
but = Imgbutton(root, image=but_img, bg='blue',
command=lambda: print("Button Image"))
but.pack(pady=10)
root.mainloop()

Related

Custom tkinter label background

I am making a school project, and right now I'm having a problem with the label's backgrounds.
the objective is to make the label's background the same as the frame in both dark and light themes, and if possible, remove the little different color on the edges of the buttons.
import tkinter
import customtkinter
root_tk = customtkinter.CTk() # create CTk window like you do with the Tk window
root_tk.geometry("1280x720")
root_tk.minsize(1280, 720)
class GUI:
def __init__(self, root_tk):
Frame1 = customtkinter.CTkFrame(root_tk)
Frame1.pack
frame2 = customtkinter.CTkFrame(Frame1, width=100, height=720)
frame2.pack(pady=20,padx=20)
frame3 = customtkinter.CTkFrame(root_tk, width=250, height=725)
frame3.pack()
frame3.place(anchor="w", relx=0.0, rely=0.5, relwidth=0.2, relheight=1)
self.test()
def test(self):
self.switch = customtkinter.CTkSwitch(master=root_tk, text="Dark Mode", command=self.theme_change)
self.switch.toggle(1)
self.switch.place(relx=0.05, rely=0.05)
self.label_width = customtkinter.CTkLabel(root_tk, text="glasses width:")
self.label_width.place(relx=0.1, rely=0.67, anchor=tkinter.CENTER)
self.label_height = customtkinter.CTkLabel(root_tk, text="glasses height:")
self.label_height.place(relx=0.1, rely=0.77, anchor=tkinter.CENTER)
self.button_add_Face = customtkinter.CTkButton(root_tk, width=200, height=50, border_width=0, corner_radius=8, hover=True, text="Adicionar Rostos", command=print("added"))
self.button_add_Face.place(relx=0.1, rely=0.6, anchor=tkinter.CENTER)
def theme_change(self):
if self.switch.get() == 1:
customtkinter.set_appearance_mode("dark")
else:
customtkinter.set_appearance_mode("light")
start = GUI(root_tk)
root_tk.mainloop()
According to the source code of the CTkBaseClass for the widgets. The background color should be the same as the fg_color of your master as long as you haven't defined explicitly a bg_color for your widget.
Look at the configure method of the BaseClass and follow it to the detect_color_of_master.
the objective is to make the label's background the same as the frame
in both dark and light themes
So this should do the trick:
Frame1 = customtkinter.CTkFrame(root_tk,fg_color=root_tk.fg_color)
Frame1.pack()
self.button_add_Face = customtkinter.CTkButton(Frame1, width=200, height=50, border_width=0, corner_radius=8, hover=True, text="Adicionar Rostos", command=print("added"), border_color=root_tk.fg_color)
But you may are more interested in the ThemeManager. There are some Themes and you could just do another .json file in the form of the examples and add it in this directory to apply your own style.

How do you get a second canvas to appear on a tkinter Toplevel window when inside a class?

I'm using Tkinter to create an application, and with this Dashboard class, I'm trying to get a pop out window to show another canvas, so I can use create_image and tag_bind on the pop out. The result I'm currently getting is that the second canvas appears over the first canvas instead of in the pop out window.
import tkinter as tk
from tkinter import *
class Dashboard(tk.Tk):
"""
Configures, and displays the Dashboard
"""
def __init__(self):
tk.Tk.__init__(self)
self.config(width=1440, height=1024)
canvas = tk.Canvas(self, bg="#343333", height=1024, width=1440, bd=0, highlightthickness=0, relief="ridge")
canvas.place(x=0, y=0)
# Captures the background image for the canvas
image_path = "dashboard_background.png"
self.background_img = tk.PhotoImage(file=image_path)
canvas.create_image(0, 0, anchor='nw', image=self.background_img)
def logoutbuttonClicker():
pop = Toplevel(self)
pop.geometry('537x273')
pop.config(height=273, width=537)
logout_canvas = tk.Canvas(canvas, bg="#ffffff", height=273, width=537, bd=0, highlightthickness=0, relief="ridge")
logout_canvas.place(x=0, y=0)
self.logout_background_img = PhotoImage(file=f"logout_background.png")
logout_canvas.create_image(268.5, 136.5, anchor='nw', image=self.logout_background_img)
self.logout_yes_img = PhotoImage(file=f"logout_yes.png")
self.logout_no_img = PhotoImage(file=f"logout_no.png")
logout_image_path = "dashboard_logout.png"
self.logout_image = tk.PhotoImage(file=logout_image_path)
logoutButton = canvas.create_image(45, 950, anchor='nw', image=self.logout_image)
canvas.tag_bind(logoutButton, "<ButtonRelease-1>", lambda event: logoutbuttonClicker())
def main():
app = Dashboard()
app.mainloop()
if __name__ == '__main__':
main()
This is how it currently appears in the UI

Error scrolling in main window after top level window was closed

I am using Tkinter for building an app and i need scrollable windows. I created a scrollable container using Canvas: ScrollContainer . After this I incorporated the main logic of my program into this container where I put a button that opens another separate TopLevel window. This separate window also has to be scrollable . Therefore i also include it in the same container class
Now, the problem: When I run the program my main window scrolls fine. I open the TopLevel window after clicking the button. Secondary window scrolls fine. After I close the secondary window and hover the mouse again over the main window, now it doesn't scroll and i get an error in the console:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\mirel.voicu\Anaconda3\envs\gis385\lib\tkinter\__init__.py", line 1883, in __call__
return self.func(*args)
File "C:/Users/mirel.voicu/Desktop/python_projects/TKINTER/standalone_program/test.py", line 45, in _on_mousewheel
self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
File "C:\Users\mirel.voicu\Anaconda3\envs\gis385\lib\tkinter\__init__.py", line 1929, in yview_scroll
self.tk.call(self._w, 'yview', 'scroll', number, what)
_tkinter.TclError: invalid command name ".!toplevel.!frame.!canvas"
Note:
Instead of self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel) i also tried with self.my_canvas.bind("<MouseWheel>", self._on_mousewheel) and now there is no error .However, the scrolling changes. You cannot scroll anymore if you hover over the labels. You only can scroll if you enlarge the window and hover mouse a bit on the right . I guess it's because you have to bring the mouse over the canvas as it is the only entity scrollable
ScrollContainer class:
from tkinter import *
from tkinter import ttk
class ScrollContainer (ttk.Frame):
def __init__(self, container,w,h,*args, **kwargs):
super().__init__(container, *args, **kwargs)
# Create a main frame
self.main_frame = Frame(container, width=w, height=h)
self.main_frame.pack(side=TOP,fill=BOTH, expand=1) # expand frame to the size of the container
# create a canvas
self.my_canvas = Canvas(self.main_frame)
self.my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# add h and v scrollbar to canvas
self.my_vscrollbar = ttk.Scrollbar(self.main_frame, orient=VERTICAL, command=self.my_canvas.yview)
self.my_vscrollbar.pack(side=RIGHT, fill=Y)
self.my_hscrollbar = ttk.Scrollbar(container, orient=HORIZONTAL, command=self.my_canvas.xview)
self.my_hscrollbar.pack(side=BOTTOM, fill=X)
# configure canvas
self.my_canvas.configure(yscrollcommand=self.my_vscrollbar.set, xscrollcommand=self.my_hscrollbar.set)
self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox('all')))
# create another frame inside the canvas
self.second_frame = Frame(self.my_canvas)
# add that new frame to a window in the canvas
self.my_canvas.create_window((0, 0), window=self.second_frame, anchor='nw')
def _on_mousewheel(self, event):
self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
Main program logic:
def open():
w=Toplevel()
SecondContainer=ScrollContainer(w,1000,768)
for thing in range(40):
Label(SecondContainer.second_frame, text=f"It's Friday {thing} ").grid(row=thing, column=0)
root=Tk()
MainContainer=ScrollContainer(root,1000,768)
btn=Button(MainContainer.second_frame, text="New Window",bg='yellow',command=open)
btn.grid(row=0,column=0)
for thing in range(1,30):
Label(MainContainer.second_frame,text=f"It's Friday {thing} ").grid(row=thing,column=0)
# frame design
root.mainloop()
It is because you used bind_all() which is application-wise binding. So the binding in the Toplevel() will override the one in root. When the toplevel is destroyed, the binding function is still referring the canvas in the toplevel and so the exception.
You should use window-wise binding:
class ScrollContainer(ttk.Frame):
def __init__(self, container, w, h, *args, **kwargs):
super().__init__(container, *args, **kwargs)
container.bind("<MouseWheel>", self._on_mousewheel) # bind on the parent window
# Create a main frame
self.main_frame = Frame(container, width=w, height=h)
self.main_frame.pack(side=TOP, fill=BOTH, expand=1) # expand frame to the size of the container
# create a canvas
self.my_canvas = Canvas(self.main_frame)
self.my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
#self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# add h and v scrollbar to canvas
self.my_vscrollbar = ttk.Scrollbar(self.main_frame, orient=VERTICAL, command=self.my_canvas.yview)
self.my_vscrollbar.pack(side=RIGHT, fill=Y)
self.my_hscrollbar = ttk.Scrollbar(container, orient=HORIZONTAL, command=self.my_canvas.xview)
self.my_hscrollbar.pack(side=BOTTOM, fill=X)
# configure canvas
self.my_canvas.configure(yscrollcommand=self.my_vscrollbar.set, xscrollcommand=self.my_hscrollbar.set)
self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox('all')))
# create another frame inside the canvas
self.second_frame = Frame(self.my_canvas)
# add that new frame to a window in the canvas
self.my_canvas.create_window((0, 0), window=self.second_frame, anchor='nw')
def _on_mousewheel(self, event):
self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
I noticed you already solved the problem but the solution #acw1668 gave to you, in my case, worked partially. So, I decided to post here my code in case someone was unlucky just like me.
I used #acw1668 class but modified it a bit.
The replacement of 'bind_all' with 'bind' had the mere effect to deactivate the scroll with the mouse everywhere besides of the scrolling bar zone so I had to define the behaviour of the widget (self.canvas) when the event ('MouseWheel') happens. So I made some searches and I ended up on another question HERE answered by #Saad who explained how to change a widget behaviour when the cursor is over it by means of a new method (set_mousewheel). Then, I putted all together and that's what I got:
class ScrollableFrame(Frame):
"""
Defines a ScrollbarFrame class
"""
def __init__(self, container, width=800, height=700, bg = "white", *args, **kwargs):
super().__init__(container, *args, **kwargs)
# to hide the border
highlightthickness = 0
# Create a main frame
self.main_frame = Frame(container, width=width, height=height, bg=bg)
self.main_frame.pack(side=TOP, fill=BOTH, expand=1) # expand frame to the size of the container
#create a canvas
#highlightthickness = 0 to hide the border
self.canvas = Canvas(self.main_frame, width=width, height=height, bg=bg, highlightthickness=highlightthickness)
self.canvas.pack(side=LEFT, fill=BOTH, expand=1)
self.scrollbar = Scrollbar(self.main_frame, orient=VERTICAL, command=self.canvas.yview)
self.scrollbar.pack(side=RIGHT, fill=Y)
self.scrollbar_x = Scrollbar(container, orient=HORIZONTAL, command=self.canvas.xview)
self.scrollbar_x.pack(side=BOTTOM, fill=X)
self.canvas.configure(yscrollcommand=self.scrollbar.set, xscrollcommand=self.scrollbar_x.set)
self.canvas.bind(
"<Configure>",
lambda e: self.canvas.configure (
scrollregion=self.canvas.bbox("all")
)
)
self.scrollable_frame = Frame(self.canvas, bg=bg)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
#DO NOT PUT bind_all!!!
self.canvas.bind("<MouseWheel>", self._on_mousewheel)
self.canvas.bind("<MouseWheel>", self.set_mousewheel(self.canvas, self._on_mousewheel))
def _on_mousewheel(self, event):
"""
Allowes to scroll the mouse
"""
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def set_mousewheel(self, widget, command):
"""Activate / deactivate mousewheel scrolling when
cursor is over / not over the widget respectively."""
widget.bind("<Enter>", lambda _: widget.bind_all('<MouseWheel>', command))
widget.bind("<Leave>", lambda _: widget.unbind_all('<MouseWheel>'))

How to put widgets inside a ttk.Button

Is there a proper way to nest widgets inside of a ttk.Button? It supports specifying a label (a str) and image (a PhotoImage) which I assume is implemented using children widgets.
Here's an example where I'm adding a left-aligned and a right-aligned label to a button.
import tkinter as tk
import tkinter.ttk as ttk
root = tk.Tk()
normal_button = ttk.Button(root, text="Normal Button")
normal_button.pack(fill=tk.X)
custom_button = ttk.Button(root)
custom_button.pack(fill=tk.X)
left_label = ttk.Label(custom_button, text="Left")
left_label.pack(side=tk.LEFT, padx=16, pady=4)
right_label = ttk.Label(custom_button, text="Right")
right_label.pack(side=tk.RIGHT, padx=16, pady=4)
root.mainloop()
This sort of works, but there are some quirks:
When hovering over the button, the button's background is highlighted but the nested labels keep their unhighlighted backgrounds.
If I click within either nested label, the button will press down, but will not become unpressed.
When the button is pressed, the nested labels will not shift giving the illusion of a button being pressed.
Is there a proper way to pack widgets inside of a button?
As I said in a comment you can create your own widget.
Here is a simple example with tk.Frame and tk.Label (ttk.Label needs more work with ttk.Style).
I bind events <Enter> and <Leave> to change frame and labels backgrounds.
For more widgets you could keep them in a list and use a for loop to change the background.
import tkinter as tk
import tkinter.ttk as ttk
class MyButton(tk.Frame):
def __init__(self, master, bg_hover='red', bg_normal=None, **options):
tk.Frame.__init__(self, master, **options)
self.bg_normal = bg_normal
self.bg_hover = bg_hover
# use default color if bg_normal is `None`
if not self.bg_normal:
self.bg_normal = self['bg']
# add first label
self.left_label = tk.Label(self, text="Left")
self.left_label.pack(side=tk.LEFT, padx=16, pady=4)
# add second label
self.right_label = tk.Label(self, text="Right")
self.right_label.pack(side=tk.RIGHT, padx=16, pady=4)
# bind events
self.bind('<Enter>', self.on_enter)
self.bind('<Leave>', self.on_leave)
def on_enter(self, event=None):
# change all backgrounds on mouse enter
self['bg'] = self.bg_hover
self.left_label['bg'] = self.bg_hover
self.right_label['bg'] = self.bg_hover
def on_leave(self, event=None):
# change all backgrounds on mouse leave
self['bg'] = self.bg_normal
self.left_label['bg'] = self.bg_normal
self.right_label['bg'] = self.bg_normal
root = tk.Tk()
normal_button = ttk.Button(root, text="Normal Button")
normal_button.pack(fill=tk.X)
my_button = MyButton(root)
my_button.pack()
root.mainloop()
There is no proper way to pack widgets inside a button. Buttons aren't designed for that feature. As you've seen, you can indeed use pack or grid to put widgets inside of buttons. However, you'll have to add custom bindings to make it appear as if it's all one button widget.

tkinter image button not working inside a function [duplicate]

I am trying to place two image buttons on my image background in a certain position, but my buttons are not appearing. I think their images are behind the background.
I tried to use place and pack, both did not work. What could be the problem?
from tkinter import*
import tkinter as tk
import settings
class Application(Frame):
def __init__ (self, master):
Frame.__init__(self,master)
self.grid()
self.create_widgets()
def create_widgets(self):
button1 = PhotoImage(file ="button1.gif")
button2 = PhotoImage(file ="button2.gif")
settings_button = Button(self, image = button1,
command = self.mult_command, width = 15)
settings_button.place(x=1, y=1)
rules_button = Button(self, image = button2,
command = self.the_rules, width = 15)
rules_button.place(x=50, y=50)
def main_code():
window = Tk()
window.title("The Bouncer")
bg_image = PhotoImage(file ="pic.gif")
x = Label (image = bg_image)
x.image = bg_image
x.place(x = 0, y = 0, relwidth=1, relheight=1)
window.geometry("600x300")
app = Application(window)
window.mainloop()
main_code()
thanks
It is likely that your image is being garbage collected before it is displayed. This is a common Tkinter gotcha. Try changing the lines:
button1 = PhotoImage(file ="button1.gif")
button2 = PhotoImage(file ="button2.gif")
to
self.button1 = PhotoImage(file ="button1.gif")
self.button2 = PhotoImage(file ="button2.gif")
and use
settings_button = Button(self, image = self.button1, command = self.mult_command, width = 15)
etc.
This should keep a reference to your image, stopping it from getting garbage collected.
In addition to keeping a reference to the image, you have a problem with this line:
self.grid()
in the __init__ method of Application. It's gridding the Frame into the window, but since nothing is ever packed or gridded into the frame, it doesn't ever expand past a little, tiny frame, so you just don't see the Buttons inside it. A simple fix here would be the pack method, with arguments to fill the window and expand when needed:
self.pack(fill=BOTH, expand=1)

Categories