Tkinter - image zur Laufzeit innerhalb der Klasse laden.

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
derHoepp
User
Beiträge: 4
Registriert: Donnerstag 9. Januar 2020, 22:21

Hallo zusammen,

ich stehe vor einem Problem, bei dem mir derzeit die Ideen ausgehen. Langfristiges Ziel ist eine kleine Applikation, mit der ich Fotos markieren kann. Später soll es möglich sein, das Foto so zu kennzeichnen, dass entweder eine erste Seite oder eine Folgeseite abgebildet ist. Dies wird später dazu verwendet, einzelne PDF-Files aus den jpg-Dateien zu generieren, von der ersten Seite bis zur nächsten ersten Seite.
Um zu entscheiden, ob eine erste Seite abgebildet ist, muss ich mir das Foto anschauen. Das ganze soll in einer sehr einfachen GUI passieren, die außer einem Label für die Darstellung der Fotos nur zwei Buttons für erste oder zweite Seite beinhaltet.
In meiner Testumgebung habe ich vier kleine jpg-Bilder mit der Bezeichnung image001.jpg bis image004.jpg.
Gebe ich bei der Instanzierung eine Liste mit bereits geladenen und in PhotoImage-Objekten mit, lassen sich diese in der Callback-Funktion wunderbar dem Label zuweisen (im Code mit der show_random_image()-Funktion). Es gelingt mir allerdings nicht, ein PhotoImage-Objekt zur laufzeit direkt in der Callback-Funktion zu erzeugen und dem Label zuzuweisen (im Code mit der load_image()-Funktion). Das Label fällt in dem Fall auf den Alternativ-Text zurück.

Code: Alles auswählen

import tkinter as tk
import tkinter.ttk as ttk
from random import choice
from PIL import ImageTk, Image

class App(tk.Frame):
    def __init__(self, master, images):
        super().__init__(master)
        self.master = master
        self.images = images
        self.master.grid()
        
        self.imagebox = ttk.Label(master, image = self.images[0], compound = "top", text="Alternative text")
        self.imagebox.grid(column = 0, row = 0)
        
        self.right_container = tk.Frame(self.master)
        self.right_container.grid(column = 1, row = 0)
        
        self.random_button = ttk.Button(self.right_container, text = "Show random image from list")
        self.random_button.pack()
        self.random_button.bind("<Button>", self.show_random_image)
        
        self.load_from_file_button=ttk.Button(self.right_container, text= "Load image from file and show")
        self.load_from_file_button.pack()
        self.load_from_file_button.bind("<Button>", self.load_image)
    
    def show_random_image(self, event):
        print(event)
        self.imagebox.configure(image = choice(self.images))
    
    def load_image(self,event):
        print(event)
        self.imagebox.configure(image = ImageTk.PhotoImage(Image.open("image004.jpg")))
    


def main():
    root = tk.Tk()
    photo_images = []
    
    photo_images.append(ImageTk.PhotoImage(Image.open("image001.jpg")))
    photo_images.append(ImageTk.PhotoImage(Image.open("image002.jpg")))
    photo_images.append(ImageTk.PhotoImage(Image.open("image003.jpg")))
    
    image_application = App(root, photo_images)
    image_application.mainloop()

if __name__ == "__main__":
    main()
Später sollen natürlich deutlich mehr Bilder verarbeitet werden; diese Vollständig als fertige PhotoImage-Objekte zu übergeben halte ich für vermeidbaren Speicherverbrauch. Kann mir jemand das Verhalten erläutern und mir Hinweise geben, was ich ändern sollte? Sonstige Hinweise zum Code sind natürlich ebenfalls willkommen.

Vielen Dank im vorhinein
derHoepp
derHoepp
User
Beiträge: 4
Registriert: Donnerstag 9. Januar 2020, 22:21

Ah, ich habe es selbst gefunden (viewtopic.php?p=422704#p422704):
__deets__ hat geschrieben:Einer der halben dutzend Klassiker
Wäre ich mal direkt in das TKinter Unterforum gewechselt. Ich binde das Image nun an ein self.current_image bevor ich es lade.

Vielen Dank fürs mitlesen!
derHoepp
Benutzeravatar
__blackjack__
User
Beiträge: 14000
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@derHoepp: Man muss auf Python-Seite eine Referenz auf das Bild behalten, sonst gibt Python den Speicher frei und Tk hat nix mehr zum anzeigen.

Sonstige Anmerkungen:

Der `ttk`-Import ist komisch gelöst.

Eine leere Liste erstellen und dann drei `append()`-Aufrufe ist auch komisch. Warum die drei Elemente nicht gleich in die Liste schreiben, statt `append()` zu bemühen? Also:

Code: Alles auswählen

    photo_images = []
    photo_images.append(ImageTk.PhotoImage(Image.open("image001.jpg")))
    photo_images.append(ImageTk.PhotoImage(Image.open("image002.jpg")))
    photo_images.append(ImageTk.PhotoImage(Image.open("image003.jpg")))

    # ->

    photo_images = [
        ImageTk.PhotoImage(Image.open("image001.jpg")),
        ImageTk.PhotoImage(Image.open("image002.jpg")),
        ImageTk.PhotoImage(Image.open("image003.jpg")),
    ]
Wobei da ein bisschen zu viel Code wiederholt wird, also eigentlich wäre das eine „list comprehension“ über eine Liste mit Dateinamen (oder man generiert die auch noch aus den Zahlen in den Dateinamen).

`App` ist ein `Frame` aber der wird letztlich gar nicht verwendet weil alles direkt ins Fenster gesetzt wird. Da würde ich eher `App` von `Tk` ableiten.

`self.master` wäre unnötig weil Widgets ihr übergeordnetes Containerwidget kennen und man das schon abfragen kann wenn man das wirklich braucht. Was man normalerweise nicht tut, denn am Master hat der Untergebene nichts zu ändern. Kein vorhandenes Widget layoutet sich selbst.

`self.master.grid()` überrascht mich jetzt, denn `master` ist ja das Hauptfenster. Wo wird *das* denn in welchem Grid gesetzt‽ Das hat keinen Effekt. (Ist ein bisschen blöd das `Tk`-Objekte diese Methode überhaupt haben.)

`right_container` ist als Name nicht so gut. Es interessiert ja nicht wo der angezeigt wird, sondern was der Inhalt bedeutet. Das muss auch nicht wirklich ein Attribut sein. Ebenso die Buttons. Die müsste man nicht mal überhaupt an einen Namen binden, denn `bind()` ist hier das falsche Mittel. Dann verhalten sich die Buttons nicht so wie der Benutzer das von anderen Buttons in anderen Programmen kennt.

Ungetestet:

Code: Alles auswählen

import tkinter as tk
from random import choice
from tkinter import ttk

from PIL import Image, ImageTk


def load_photo_image(filename):
    return ImageTk.PhotoImage(Image.open(filename))


class App(tk.Tk):
    def __init__(self, images):
        super().__init__()
        self.image = None
        self.images = images
        self.imagebox = ttk.Label(
            self, compound=tk.TOP, text="Alternative text"
        )
        self.imagebox.grid(column=0, row=0)

        frame = tk.Frame(self)
        frame.grid(column=1, row=0)
        ttk.Button(
            frame,
            text="Show random image from list",
            command=lambda: self.show_image(choice(self.images)),
        ).pack()
        ttk.Button(
            frame,
            text="Load image from file and show",
            command=lambda: self.show_image(load_photo_image("image004.jpg")),
        ).pack()

        self.show_image(self.images[0])

    def show_image(self, image):
        self.image = self.imagebox["image"] = image


def main():
    filenames = ["image001.jpg", "image002.jpg", "image003.jpg"]
    App(list(map(load_photo_image, filenames))).mainloop()


if __name__ == "__main__":
    main()
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
derHoepp
User
Beiträge: 4
Registriert: Donnerstag 9. Januar 2020, 22:21

Hallo,

vielen Dank blackjack. Das gibt mir wertvolle Einsichten! Insbesondere, dass ein gesondertes bind nur notwendig zu sein scheint, wenn ich Wert auf das konkret auslösende Event lege und dass ich Widgets nicht an eigene Namen binden muss, wenn ich sie nicht mehr ändern will. Insbesondere der Fallstrick, dass master auch in meinem Kopf als Frame gedacht war, der magischerweise gleichzeitig ein window erzeugt. Frame als vererbende Klasse habe ich mir aus dem tkinter Bereich von docs.python.org abgeschaut.
Die leere Liste stammte daher, dass ich mir erst überlegte hatte, die Liste der Dateinamen in einer for schleife zusammenzustellen, dann aber dazu übergegangen bin, die einfach direkt zu tippen.

Vielen Dank noch einmal!
derHoepp
Benutzeravatar
__blackjack__
User
Beiträge: 14000
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@derHoepp: Der Unterschied `command` vs. `bind()` ist mehr als nur das Ereignis-Objekt. Buttons kann man beispielsweise drücken und mit der Maus wieder verlassen, ohne das die Aktion ausgelöst wird. Bei `bind()` kann man das nicht machen. Und man kann die GUI auch mit der Tastatur bedienen. Zum Beispiel mit Tab zum gewünschten Button navigieren und den dann mit der Leertaste auslösen um den `command`-Rückruf auszuführen. Davon weiss `bind()` auch nichts.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Antworten