this post was submitted on 12 May 2024
14 points (100.0% liked)

Learn Programming

1643 readers
1 users here now

Posting Etiquette

  1. Ask the main part of your question in the title. This should be concise but informative.

  2. Provide everything up front. Don't make people fish for more details in the comments. Provide background information and examples.

  3. Be present for follow up questions. Don't ask for help and run away. Stick around to answer questions and provide more details.

  4. Ask about the problem you're trying to solve. Don't focus too much on debugging your exact solution, as you may be going down the wrong path. Include as much information as you can about what you ultimately are trying to achieve. See more on this here: https://xyproblem.info/

Icon base by Delapouite under CC BY 3.0 with modifications to add a gradient

founded 1 year ago
MODERATORS
14
submitted 6 months ago* (last edited 6 months ago) by Hammerheart to c/learn_programming
 

I'm working on a little gui app that will eventually (hopefully) add a watermark to a photo. But right now I'm focused on just messing around with tkinter and trying to get some basic functionality down.

I've managed to display an image. Now I want to change the image to whatever is in the Entry widget (ideally, the user would put an absolute path to an image and nothing else). When I click the button, it makes the image disappear. I made it also create a plain text label to see if that would show up. It did.

Okay, time to break out the big guns. Add a breakpoint. py -m pdb main.py. it works. wtf?

def change_image():
    new_image = Image.open(image_path.get()).resize((480, 270))
    new_tk_image = ImageTk.PhotoImage(new_image)
    test_image_label.configure(image=new_tk_image)
    breakpoint()

with the breakpoint, the button that calls change_image works as expected. But without the breakpoint, it just makes the original image disappear. Please help me understand what is happening!

edit: all the code

import io
import tkinter as tk
from pathlib import Path
from tkinter import ttk

from PIL import ImageTk
from PIL import Image

from LocalImage import Localimage
from Layout import Layout

class State:
    def __init__(self) -> None:
        self.chosen_image_path = ""

    def update_image_path(self):
        self.chosen_image_path = image_path.get()



def change_image():
    new_image = Image.open(image_path.get()).resize((480, 270))
    new_tk_image = ImageTk.PhotoImage(new_image)
    test_image_label.configure(image=new_tk_image)
    breakpoint()

TEST_PHOTO_PATH = "/home/me/bg/space.png"
PIL_TEST_PHOTO_PATH = "/home/me/bg/cyberpunkcity.jpg"
pil_test_img = Image.open(PIL_TEST_PHOTO_PATH).resize((480,270))
# why does the resize method call behave differently when i inline it
# instead of doing pil_test_img.resize() on a separate line?


root = tk.Tk()

root.title("Watermark Me")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky="NWES")

layout = Layout(mainframe)

image_path = tk.StringVar()
tk_image = ImageTk.PhotoImage(pil_test_img)
test_image_label = ttk.Label(image=tk_image)

entry_label = ttk.Label(mainframe, text="Choose an image to watermark:")
image_path_entry = ttk.Entry(mainframe, textvariable=image_path)
select_button = ttk.Button(mainframe, text="Select",
                           command=change_image)
hide_button = ttk.Button(mainframe, text="Hide", command= lambda x=test_image_label:
                  layout.hide_image(x))
test_text_label = ttk.Label(mainframe, text="here i am")
empty_label = ttk.Label(mainframe, text="")

for child in mainframe.winfo_children():
    child.grid_configure(padx=5, pady=5)

entry_label.grid(column=0, row=0)
image_path_entry.grid(column=1, row=0)
hide_button.grid(column=0, row=3)
select_button.grid(column=0, row=4)
test_image_label.grid(column=0, row=5)
empty_label.grid(column=0, row=6)


image_path_entry.insert(0,TEST_PHOTO_PATH)
image_path_entry.focus()
breakpoint()



root.mainloop()
top 9 comments
sorted by: hot top controversial new old
[–] [email protected] 6 points 6 months ago* (last edited 6 months ago) (3 children)

I've used Tkinter in a number of projects, though not the PIL specifically. At least not recently?

What I have found with Tkinter in general is that it can be rather quirky in terms of dealing with object ownership. I don't know why this is? You shouldn't have to worry about tracking object lifetimes in a garbage-collected language like Python, but Tkinter is just…weird about it.

My wager would be that for whatever reason, once your change_image() function returns, the garbage collector is coming around and releasing new_tk_image (or maybe even new_image?), even though test_image_label should hold a reference to it by that point? And the breakpoint() stops the function from returning, so you are "safe" (at least temporarily).

What I would try then, as a workaround, would be to store your own references to some of these and see if it works? You can begin by just making them globals to see if this is indeed the problem, though it's probably more elegant to group them together in a class with anything else to do with the window.

[–] Hammerheart 7 points 6 months ago (1 children)

That was it! I moved the two lines above the .configure() call into the outermost scope, and it works now.

Should probably just bite the bullet and OOP it out. I was putting that off until I figured out how I wanted the whole thing to work, but it will probably be easier to just do it and change it as I go instead of trying to raw dog it for too long.

[–] [email protected] 4 points 6 months ago* (last edited 6 months ago)

Splendid! Glad I was able to help. I know I've hit this same problem in some other context? Maybe it was font-handling or something? And it was the same thing. You change the font and it disappears. So I got into the habit of subclassing some higher level object like Frame or whatever and making all these things into methods that store self.current_font etc. to make sure I don't let that last reference go bye-bye!

[–] [email protected] 5 points 6 months ago (1 children)

You shouldn't have to worry about tracking object lifetimes in a garbage-collected language like Python, but Tkinter is just…weird about it.

Don't know if this is the case with TK, but this is 100% the cause of a lot of bugs working with Qt in python. If you don't explicitly keep a reference to all your python objects, they can get GCd, and then when the Qt loop tries to access them you get a crash.

[–] [email protected] 2 points 6 months ago

Ah so it's not only Tkinter then. Interesting. Maybe this is one of the reasons why Python-specific GUIs like Kivy came about? When I first started learning Python though, I came under the impression that Tkinter was the "official" Python GUI with privileged status in the standard library, so I started there and was surprised at how it actually doesn't play all that nice with the language.

Oh well. I'm glad OP brought it up because I'm sure everyone who's tried to write a GUI in Python for the first time hits this before long and has that same wtf moment. :)

[–] [email protected] 3 points 6 months ago (1 children)

What I have found with Tkinter in general is that it can be rather quirky in terms of dealing with object ownership. I don't know why this is? You shouldn't have to worry about tracking object lifetimes in a garbage-collected language like Python, but Tkinter is just…weird about it.

My understanding is very limited, but I think the reason is that TKinter isn't Python, it's Tk (i.e., Tcl and/or C). It's a real thin binding around non-garbage-collected stuff.

[–] [email protected] 2 points 6 months ago

This sounds highly plausible. Somehow, Tkinter objects are not always first class Python objects, and the object management (such as it is) is happening outside the scope of Python.

[–] [email protected] 3 points 6 months ago (1 children)
[–] Hammerheart 2 points 6 months ago

okay, added the rest of the code.