Tom Hosker
Tom Hosker

Reputation: 722

Why is Python still garbage-collecting my Tkinter image?

I'm aware that this is a question that has been asked before on this site. However, I've made an honest attempt to implement the solutions put forward in those answers, and I'm still running into the same problem: Python seems to keep garbage-collecting my image, and I get an empty slot in my window where the image ought to be, as shown in the attached screenshot.

screenshot

This is the part of my code where I attempt to import an image:

def make_top_title(self):
    result = Frame(self.top)
    text_lbl = Label(result, text="Proserpine",
                     font=(main_font, title_pt, "bold"))
    arms_image = PhotoImage("icon.png")
    arms_lbl = Label(result, image=arms_image, height=200)
    arms_lbl.image = arms_image
    arms_lbl.pack()
    text_lbl.pack()
    return result

You'll notice that I've already attempted to use the trick of preserving the image by anchoring it onto a property of a label object. You may also notice, from the attached screenshot, that I have no problem importing a custom icon - using the same image file - for this window.

I'm happy to share any more code from this program, as required.


A couple of people have suggested I share a bit more of my code. This is the whole file in which the code above is located:

### This code holds a class which allows the user to inspect a the coldstore
### object attached to its parent.

# GUI imports.
from tkinter import *

# Custom imports.
from sibstructures import data_storage
from sibstructures.numerals import index_to_label_column, \
                                   index_to_label_row, \
                                   index_to_label_layer

# Imports
import time
from threading import Thread

# Local constants.
main_font = "Arial"
title_pt = 60
subtitle_pt = 30
big_pt = 20
standard_pt = 15
diddy_pt = 10
standard_pad = 10
inner_pad = 5
tile_width = 15
details_height = 10
details_width = 30
grid_border = 5
spot_stack_width = tile_width+5

##############
# MAIN CLASS #
##############

# The class in question.
class CS_Viewer:
  def __init__(self, parent):
    self.parent = parent
    self.parent_window = self.parent.get_top()
    self.code = self.parent.coldstore.code
    self.parent.coldstore.reconstruct()
    self.max_max_layers = self.parent.coldstore.get_max_max_layers()
    self.layer = 0
    self.top = Frame(self.parent_window)
    self.top_title = self.make_top_title()
    self.subtitle = Label(self.top, text="code="+self.code,
                          font=(main_font, subtitle_pt, "bold"))
    self.main_container = Frame(self.top)
    self.spot_grid = Spot_Grid(self, self.main_container)
    self.box_details = Text(self.main_container,
                            height=details_height, width=details_width,
                            state=DISABLED)
    self.spot_stack = Spot_Stack(self.main_container, None,
                                 self.box_details)
    self.add_headings()
    self.arrange()

  # Ronseal.
  def make_top_title(self):
    result = Frame(self.top)
    text_lbl = Label(result, text="Proserpine",
                     font=(main_font, title_pt, "bold"))
    arms_image = PhotoImage("icon.png")
    arms_lbl = Label(result, image=arms_image, height=200)
    arms_lbl.image = arms_image
    arms_lbl.pack()
    text_lbl.pack()
    return result

  # Add headings to the holster widgets.
  def add_headings(self):
    spot_grid_label = Label(self.main_container, text="Coldstore",
                            font=(main_font, big_pt, "bold"))
    spot_stack_label = Label(self.main_container, text="Spot",
                             font=(main_font, big_pt, "bold"),
                             width=spot_stack_width)
    box_details_label = Label(self.main_container, text="Box",
                              font=(main_font, big_pt, "bold"))
    spot_grid_label.grid(column=0, row=0)
    spot_stack_label.grid(column=1, row=0)
    box_details_label.grid(column=2, row=0)

  # Ronseal.
  def place_spot_stack(self):
    self.spot_stack.get_top().grid(column=1, row=1,
                                   padx=standard_pad, pady=standard_pad)

  # Arrange the object's elements.
  def arrange(self):
    self.top_title.pack()
    self.subtitle.pack()
    self.spot_grid.get_top().grid(column=0, row=1, sticky=N,
                                  padx=standard_pad, pady=standard_pad,
                                  ipadx=inner_pad, ipady=inner_pad)
    self.place_spot_stack()
    self.box_details.grid(column=2, row=1, sticky=N,
                          padx=standard_pad, pady=standard_pad)
    self.main_container.pack()

  # Replace the spot stack widget.
  def replace_spot_stack(self):
    self.spot_stack.get_top().grid_forget()
    self.place_spot_stack()

  # Ronseal.
  def get_top(self):
    return self.top

################################
# HELPER CLASSES AND FUNCTIONS #
################################

# A class which holds the grid of spots.
class Spot_Grid:
  def __init__(self, parent, parent_window):
    self.parent = parent
    self.parent_window = parent_window
    self.top = Frame(self.parent_window, borderwidth=grid_border,
                     relief="solid")
    Thread(target=self.make_grid).start()

  # Fill the grid with boxes.
  def make_grid(self):
    cs = self.parent.parent.coldstore
    for i in range(len(cs.columns)):
      column_label = Label(self.top, text=str(index_to_label_column(i)),
                           font=(main_font, big_pt, "bold"))
      column_label.grid(column=(i+1), row=0, padx=standard_pad)
      for j in range(len(cs.columns[0].spots)):
        if i == 0:
          row_label = Label(self.top, text=str(index_to_label_row(j)),
                            font=(main_font, big_pt, "bold"))
          row_label.grid(column=0, row=(j+1), padx=standard_pad)
        tile = Spot_Tile(self, self.parent, cs.columns[i].spots[j],
                         self.parent.box_details)
        tile.get_top().grid(column=(i+1), row=(j+1))

  # Ronseal.
  def get_top(self):
    return self.top

# A class which holds a clickable representation of a spot.
class Spot_Tile:
  def __init__(self, parent, main_ref, spot_obj, box_details_ref):
    self.parent = parent
    self.main_ref = main_ref
    self.spot_obj = spot_obj
    self.box_details_ref = box_details_ref
    self.parent_window = self.parent.get_top()
    self.top = Frame(self.parent_window)
    Thread(target=self.make_filling).start()

  # Fill the object with either a tile or a label.
  def make_filling(self):
    if self.spot_obj.max_layers == 0:
      filling = Label(self.top, text="VOID", font=(main_font, diddy_pt),
                      width=tile_width)
    elif self.spot_obj.layers() == 0:
      filling = Button(self.top, text="free", command=None,
                       font=(main_font, diddy_pt, "italic"),
                       width=tile_width, state=DISABLED)
    else:
      filling = self.make_filling_button()
    filling.pack()

  # Make the filling object if it is a button.
  def make_filling_button(self):
    result = Button(self.top, text=self.make_filling_button_text(),
                    command=self.inspect,
                    font=(main_font, diddy_pt), width=tile_width)
    return result

  # Make the text portion of the filling button.
  def make_filling_button_text(self):
    growers = set()
    varieties = set()
    fields = set()
    for box in self.spot_obj.boxes:
      current_epc = box.epc
      current_data = data_storage.fetch_most_recent_crop(current_epc)
      growers.add(current_data["grower"])
      varieties.add(current_data["variety"])
      fields.add(current_data["field"])
    result = (set_to_string(growers)+"\n"+set_to_string(varieties)+"\n"+
              set_to_string(fields)+"\n"+str(len(self.spot_obj.boxes)))
    return result

  # Inspect a given spot.
  def inspect(self):
    self.main_ref.spot_stack = Spot_Stack(self.main_ref.main_container,
                                          self.spot_obj,
                                          self.box_details_ref)
    self.main_ref.replace_spot_stack()

  # Ronseal.
  def get_top(self):
    return self.top

# A class which holds a representation of the boxes on a given spot.
class Spot_Stack:
  def __init__(self, parent_window, spot_obj, box_details_ref):
    self.parent_window = parent_window
    self.spot_obj = spot_obj
    self.box_details_ref = box_details_ref
    self.top = Frame(self.parent_window)
    if self.spot_obj is None:
      self.fill_empty()
    else:
      Thread(target=self.add_boxes).start()

  # "Fill in" the representation if the spot object is empty.
  def fill_empty(self):
    label = Label(self.top, text="Select spot",
                  font=(main_font, standard_pt, "italic"), width=tile_width)
    label.pack()

  # Add representations of the spot's boxes.
  def add_boxes(self):
    no_of_boxes = len(self.spot_obj.boxes)
    if no_of_boxes == 0:
      empty_label = Label(self.top, text="Empty spot",
                          font=(main_font, standard_pt, "italic"),
                          width=tile_width)
      empty_label.pack()
    else:
      for i in range(no_of_boxes):
        backwards_index = (no_of_boxes-1)-i
        box_tile = Box_Tile(self.top, self.spot_obj.boxes[backwards_index],
                            backwards_index, self.box_details_ref)
        box_tile.get_top().pack()

  # Ronseal.
  def get_top(self):
    return self.top

# A class which holds a clickable representation of a box.
class Box_Tile:
  def __init__(self, parent_window, box, index, box_details_ref):
    self.parent_window = parent_window
    self.box = box
    self.index = index
    self.box_details_ref = box_details_ref
    self.top = Frame(self.parent_window)
    self.make_filling()

  # Fill the object with either a tile or a label.
  def make_filling(self):
    label = Label(self.top, text=str(index_to_label_layer(self.index)),
                  font=(main_font, standard_pt))
    filling = Button(self.top, text=self.box.epc, command=self.inspect,
                     font=(main_font, standard_pt), width=tile_width)
    label.grid(column=0, row=0, padx=standard_pad)
    filling.grid(column=1, row=0)

  # Ronseal.
  def get_top(self):
    return self.top

  # Inspect the data for this particular box in more detail.
  def inspect(self):
    text_to_insert = data_storage.fetch_most_recent_crop(self.box.epc)
    self.box_details_ref.config(state=NORMAL)
    self.box_details_ref.delete("1.0", END)
    self.box_details_ref.insert(END, text_to_insert)
    self.box_details_ref.config(state=DISABLED)

# Turns a set into a string, with items thereof separated by commas.
def set_to_string(the_set):
  result = ""
  the_list = list(the_set)
  # Reversal is necessary, since .add() seems to add items to the FRONT of
  # the set.
  the_list.reverse()
  for item in the_list:
    if the_list.index(item) == 0:
      result = item
    else:
      result = result+", "+item
  return result

This is the other file, which, with its fellow, makes up the whole program:

### This code holds a class which manages transitions between windows, and
### also oversees their interactions with the Coldstore object.

# Imports.
from pathlib import Path

# GUI imports.
from tkinter import *

# Custom imports.
from sibstructures.coldstore import Coldstore

# Local imports.
from cs_viewer import CS_Viewer

# Constants.
path_to_db = str(Path.home())+"/cseye/source/proserpine/data.db"

##############
# MAIN CLASS #
##############

# The class in question.
class Comptroller:
  def __init__(self):
    self.coldstore = Coldstore(proserpine_mode=True,
                               proserpine_path=path_to_db)
    self.gui = Tk()
    self.top = Frame(self.gui)
    self.window = CS_Viewer(self)
    self.arrange()

  # Return the top-level GUI object.
  def get_top(self):
    return self.top

  # Arrange the widgets.
  def arrange(self):
    self.window.get_top().pack()
    self.top.pack()

  # Run the "mainloop" method on the GUI object.
  def run_me(self):
    self.gui.title("Proserpine")
    self.gui.iconphoto(True, PhotoImage(file="icon.png"))
    self.gui.mainloop()

###################
# RUN AND WRAP UP #
###################

def run():
  comptroller = Comptroller()
  comptroller.run_me()

if __name__ == "__main__":
  run()

Upvotes: 3

Views: 514

Answers (1)

acw1668
acw1668

Reputation: 46707

The argument to PhotoImage("...") is wrong. It should be PhotoImage(file="...").

Upvotes: 3

Related Questions