I was working with Tkinter in OOP and I came across an odd bug: the button here goes completely unresponsive, despite being fully active and the mainloop running.
Code:
import tkinter as tk
def main():
global root, button
root = tk.Tk()
pixel = tk.PhotoImage(width=1, height=1)
button = tk.Button(root, text="Hmm",
image=pixel,
compound="c")
button.pack()
main()
root.mainloop()
I used the hidden image as I need to set the specific pixel height and width of the button, but when I remove the image=pixel
the button works fine.
This button is unpressable, both with mouse and with tab
+enter
:
I was working with Tkinter in OOP and I came across an odd bug: the button here goes completely unresponsive, despite being fully active and the mainloop running.
Code:
import tkinter as tk
def main():
global root, button
root = tk.Tk()
pixel = tk.PhotoImage(width=1, height=1)
button = tk.Button(root, text="Hmm",
image=pixel,
compound="c")
button.pack()
main()
root.mainloop()
I used the hidden image as I need to set the specific pixel height and width of the button, but when I remove the image=pixel
the button works fine.
This button is unpressable, both with mouse and with tab
+enter
:
The bug here is subtle, but it happens because the image pixel
gets garbage collected by python when the main
function finishes. The button no longer works if pixel
doesn't exist (Not entirely sure why, likely due to tkinter internals), but never raises an error or tells you it's broken.
The solution is to save pixel
in memory anywhere, so it never gets GCed or deleted:
import tkinter as tk
def main():
global root, button, pixel
root = tk.Tk()
pixel = tk.PhotoImage(width=1, height=1)
button = tk.Button(root, text="Hmm",
image=pixel,
compound="c")
button.pack()
main()
root.mainloop()
If you want to use OOP, use class:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
# use attribute to store the reference of the image
# to avoid from garbage collection
self.pixel = tk.PhotoImage(width=1, height=1)
self.button = tk.Button(self, text='Hmm', image=self.pixel, compound='c')
self.button.pack()
App().mainloop()
TkInter Button Unpressable - No Errors Raised, Not Disabled
@acw1668 emphasizes that globals like pixel, button, and root are not necessary.
I observed that you placed root.mainloop()
outside of a function. This is due to the fact that you initialized tk.Tk()
within a function. Consequently, the widget is not responsive.
Place tk.tk() outside of the function.
The problem can be fixed.
Include a button.picture = button
before button.pack()
.
Code (no minor alterations):
import tkinter as tk
root = tk.Tk()
def main():
pixel = tk.PhotoImage(width=1, height=1)
button = tk.Button(root, text="Hmm",
image=pixel,
compound="c")
button.image = pixel
button.pack()
main()
root.mainloop()
Most likely, there is an error, but this error is not caught by the tkinter wrapper. As you find out, the image object is garbage-collected. The image is no longer in the image list for your root.
If we try to change some button option using configure
, we will get the error _tkinter.TclError: image "pyimage1" doesn't exist
.
import tkinter as tk
def main():
global root, button
root = tk.Tk()
pixel = tk.PhotoImage(width=1, height=1)
button = tk.Button(root, text="Hmm",
image=pixel,
compound="c")
button.pack()
print("Before:", root.tk.call("image", "names"))
main()
print("After:", root.tk.call("image", "names"))
# _tkinter.TclError: image "pyimage1" doesn't exist
button.configure(text="New")
root.mainloop()
Output:
Before: ('pyimage1', '::tk::icons::information', '::tk::icons::error', '::tk::icons::warning', '::tk::icons::question')
After: ('::tk::icons::information', '::tk::icons::error', '::tk::icons::warning', '::tk::icons::question')
Traceback (most recent call last):
...
_tkinter.TclError: image "pyimage1" doesn't exist
We can create something similar to what you get (with Python code) by intentionally deleting the image after creating the button in a Tcl script.
package require Tk
proc go {} {
.msg configure -text "works for ttk button"
#.b configure -width 1; # Error: image "img" doesn't exist
}
proc main {} {
image create photo img -width 1 -height 1
button .b -image img -text "Hello" -compound "center" -command go
#ttk::button .b -image img -text "Hello" -compound "center" -command go
pack .b
label .msg -text ""
pack .msg
}
main
puts "Before:"
puts [image names]
image delete img
puts "After:"
puts [image names]
#.b configure -width 1; # Error: image "img" doesn't exist
Output:
Before:
img ::tk::icons::information ::tk::icons::error ::tk::icons::warning ::tk::icons::question
After:
::tk::icons::information ::tk::icons::error ::tk::icons::warning ::tk::icons::question
When I run this script, the tk button
fails. It does not handle events.
If I hover over or click on the button, I get an application error, Error: image "img" doesn't exist
.
I didn't dig into why. This goes beyond Python and tkinter.
Most likely, your Python code example is silently failing because of something like this.
ttk button
executes a command after being clicked unless we change the button options using configure
.
As suggested in other answers, the most reliable solution is to have an additional reference to the image object (global variable, button attribute, or class instance attribute).
We can also recreate the image under the same name to make it work.
import tkinter as tk
def go():
print("ok")
def main():
global root, button
root = tk.Tk()
pixel = tk.PhotoImage(width=1, height=1)
button = tk.Button(root, text="Hmm",
image=pixel,
compound="c",
command=go)
button.pack()
main()
print(button["image"]) # pyimage1
# recreate image
root.tk.call("image", "create", "photo", "pyimage1", "-width", "1", "-height", "1")
button.configure(bg="blue")
print(root.tk.call("pyimage1", "cget", "-width")) # 1
root.mainloop()
Out of curiosity, we can explore other options, such as creating our own button bindings and/or using ttk.Button
with ttk.Style
.
However, this limits your ability to change button options. Calling button.configure(text="New")
or button["text"] = "New"
will result in errors.
I doubt such workarounds will be necessary.