Bild durch Button wechseln

Fragen zu Tkinter.
Antworten
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

Hallo,
ich möchte ein Bild mit einem klick auf einen Button wechseln.
Das Fenster sieht gestartet so aus.
Bild
Wenn der Button Quadrieren angeklickt wird, soll sich das Bild ändern.
Leider verschwindet das Bild bei mir und die Fläche wird einfach grau.
Bild
So sieht mein Code aus:

Code: Alles auswählen

import tkinter
from tkinter import *

def ende():
    main.destroy()

def quad():
    eingabe = e.get()
    try:
        zahl = float(eingabe)
        lb["text"] = "Ergebnis von " + str(zahl) + "² =" + str(zahl * zahl)
    except:
        lb["text"] = "Bitte Zahl eingeben"

def trip():
    eingabe = e.get()
    try:
        zahl = float(eingabe)
        lb1["text"] = "Ergebnis von "+str(zahl)+ "³ =" + str(zahl * zahl * zahl)
    except:
        lb1["text"] = "Bitte Zahl eingeben"

def ccolor():
    e["bg"] = "#FFEA00"

def cpic():
    im2 = PhotoImage(file = "Bild2.gif")
    p["image"] = im2
    

main = tkinter.Tk()


p = Label(main)
im = PhotoImage(file = "Bild.gif")
p["image"] = im
p.pack()

e = tkinter.Entry(main)
#e["bg"] = "#000021"
e.pack()

#bquad = tkinter.Button(main, text = "Quadrieren", command = quad)
bquad = tkinter.Button(main, text = "Quadrieren", command = lambda:[quad(), trip(), ccolor(), cpic()])
bquad.pack()

lb = tkinter.Label(main, text = "Ergebnis: ")
lb.pack()

lb1 = tkinter.Label(main, text = "Ergebnis: ")
lb1.pack()


bende = tkinter.Button(main, text= "Ende", command = ende)
bende.pack()

main.mainloop()
Warum wird das Bild nicht durch das neue erstzt ?

Gruß Kai
Benutzeravatar
sparrow
User
Beiträge: 4540
Registriert: Freitag 17. April 2009, 10:28

Dein ganzer Code steht auf Modulebene. Der gehört da nicht hin.
Dann werde deine globalen Variablen los. Verwende Funktionen so, wie sie gedacht sind: sie bekommen alle Werte, die sie benötigen als Parameter und geben ein Ergebnis mit return zurück.

Bei jeder nicht trivialen GUI-Programmierung muss man sich auch mit Objektorientierung auseinandersetzen.
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

Danke für die schnelle Antwort.

Ok ich werde es damit versuchen. Das ist allerdings eines der ersten Beispiel im Buch, daher wird noch alles in die Modulebene geschrieben (denke ich zumindest).

Ist es denn möglich das Bild so zu ändern ?

Gruß Kai
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

Keine *-Importe. Du mischst auch den Zugriff auf die tkinter-Namen direkt mit denen über das Modul tkinter.Button vs. Label. Tkinter wird üblicherweise als `import tkinter as tk` importiert, damit man sich etwas Lesearbeit spart.
`ende` ist unnötig, da Du unten direkt main.destroy verwenden könntest.
In `quad`: niemals nakte except benutzen. Du willst hier ja nur den ValueError abfangen, den float werfen könnte. Keine Strings mit + zusammenstückeln, dafür gibt es Formatstrings und alles was eine Methode braucht, sollte sie auch über ihre Argumente bekommen, und zwar mit aussagekräftigen Namen, e ist einbuchstabig, sagt also nichts; lb ist eine Abkürzung, sollte wohl ausgabelabel heißen.

Code: Alles auswählen

def quad(eingabefeld, ausgabelabel):
    eingabe = eingabefeld.get()
    try:
        zahl = float(eingabe)
    except ValueError:
        ausgabelabel["text"] = "Bitte Zahl eingeben"
    else:
        ausgabelabel["text"] = f"Ergebnis von {zahl}² ist {zahl ** 2}"
Für trip gilt das selbe.

In `cpic` stolperst Du über die übliche Hürde, dass man immer eine Referenz von PhotoImage behalten muß, sonst räumt der Garbage-Collector von Python das Bild einfach weg. cpic sollte wohl change_picture heißen.

Code: Alles auswählen

def change_picture(picturelabel):
    picturelabel.image = tk.PhotoImage(file="Bild2.gif")
    picturelabel["image"] = picturelabel.image
Da Argumente an die Funktionen kannst Du mit functools.partial übergeben. Denn alles ab Zeile 30 gehört auch in eine Funktion, die üblicherweise main heißt, so dass man gar nicht erst in Verlegenheit gerät, einfach so auf globale Variablen zuzugreifen.

Das was Du da als Argument command an den Quadrieren-Button übergibst, ist reichlich seltsam, Du rufst vier Funktionen auf und packst deren Rückgabewerte (vier mal None) in eine Liste und gibst diese beim Drücken des Knopfes an den Mainloop zurück, der sie kommentarlos verwirft. Was wolltest Du eigentlich damit bewirken?
Der Auskommentierte Code ist der richtige, oder mit Argumenten und ohne die Zuweisung an eine Variable, die man später nicht mehr braucht:

Code: Alles auswählen

tk.Button(main, text="Quadrieren", command=partial(quad, eingabefeld, ausgabelabel)).pack()
Benutzeravatar
__blackjack__
User
Beiträge: 14087
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Kahnbein.Kai: Also wenn das so als Beispiel in einem Buch steht, würde ich mir ein anderes Buch suchen.

Sternchen-Importe sin Böse™, auch wenn die in manchen Büchern oder Tutorials gerade im Zusammenhang mit `tkinter` verwendet werden, aber das neben dem Sternchem-Import dann auch noch das `tkinter`-Modul selbst importiert wird und im Code dann beide Wege verwendet werden um auf Werte aus diesem Modul zuzugreifen, sollte in keinem Buch oder Tutorial vorkommen. Also also als erstes mal sollte der Sternchem-Import weg. `tkinter` kann man dann als `tk` importieren, damit es etwas weniger Tipparbeit gibt und der Code übersichtlich bleibt.

Auf Modulebene sollte wie sparrow ja schon anmerkte kein Code stehen, also keiner der nicht Konstanten, Funktionen, oder Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Womit der Name `main` für das `Tk`-Objekt ungünstig ist, weil das zu Verwirrungen beim Leser führen kann.

Namen sollten keine kryptischen Abkürzungen oder Prä-/Suffixe enthalten und schon gar nicht nur aus einer Abkürzung oder einem Buchstaben bestehen. `p`, `im`, `e`, `bquad`, `lb`, `lb1`, und `bende` sind keine guten Namen an denen der Leser erkennen kann was der Wert, der dahinter steckt, im Kontext des Programms bedeutet.

Wenn man das `PhotoImage`-Objekt vor dem `Label` in dem es angezeigt werden soll erstellt, dann braucht man das nicht nachträglich ändern sondern kann das Bild gleich beim Erstellen des `Label` übergeben.

Um Gleichheitszeichen von Schlüsselwortargumenten setzt man üblicherweise keine Leerzeichen. Siehe auch den Style Guide for Python Code.

Die `ende()`-Funktion ist überflüssig weil da nur eine Methode aufgerufen wird, die man auch direkt als `command` bei dem `Button` angeben kann. Wobei ich da auch nicht `destroy()` verwenden würde, sondern einfach die `quit()`-Methode zum verlassen der GUI-Hauptschleife.

Die Funktionsnamen sind auch schlecht. `trip()` ist eine Reise und `quad()` eine Fahrzeugart. Wenn man `quadrieren()` und `triple()` meint, sollte man das auch schreiben. Wobei da die Sprachwahl ein wenig inkonsistent ist und `triple()` auch falsch ist, denn es wird ja nicht verdreifacht, sondern exponenziert.

Zudem sind die beiden Funktionen fast gleich. Das sollte so nicht sein, da würde man *eine* Funktion schreiben, bei der der Exponent als Argument übergeben wird.

Apropos Argument(e): Da es auf Modulebene keine Variablen gibt, werden Funktionen und Methoden alle Werte ausser Konstanten, als Argument(e) übergeben.

Man verwendet keine nackten ``except:``\s ohne konkrete Ausnahmen. Damit werden dann *alle* Ausnahmen behandelt, auch solche die man gar nicht erwartet. Es gibt nur wenige Möglichkeiten wirklich *alle* Ausnahmen *sinnvoll* zu behandeln. Wenn Du darauf reagieren möchtest, dass eine Zeichenkette nicht in eine Zahl umgewandelt werden kann, dann behandle a) explizit die Ausnahmen die dabei ausgelöst wird, und b) sollte der ``try``-Block nur wenig Code umfassen, nicht das die gleiche Ausnahme auch anders ausgelöst werden kann, dann aber etwas anderes bedeutet.

Werte und Zeichenketten stückelt man in Python nicht mit ``+`` und `str()` zusammen. Das ist eher BASIC als Python. In Python gibt es dafür die `format()`-Methode auf Zeichenketten und ab Python 3.6 f-Zeichenkettenliterale.

Statt eine Liste zu missbrauchen um in einem Lambda-Ausdruck mehr als eine Funktion aufzurufen, schreibt man eine Funktion die mehrere andere Funktionen aufruft.

Nun zum Problem warum das Bild verschwindet: Es gibt keine Möglichkeit bei Tk in Erfahrung zu bringen ob ein Bild noch benötigt wird oder nicht. Es liegt also an Python ob das Bildobjekt aus dem Speicher entfernt wird. Damit das nicht passiert, muss man dafür Sorgen, dass eine Referenz auf das Bild in Python erreichbar bleibt. Ein Hack um das ohne eigene Klasse zu erreichen ist das Bild einfach als Attribut irgendwo anders dran zu heften von dem man weiss, dass es besteht solange das Bild gebraucht wird. Da bietet sich das `Label`-Objekt an in dem das Bild angezeigt wird, denn solange das existiert, wird auch das Bild gebraucht.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial

DIGIT_TO_SUPERSCRIPT_DIGIT = str.maketrans("0123456789", "⁰ⁱ²³⁴⁵⁶⁷⁸⁹")


def exponentiate(entry, result_label, exponent):
    try:
        number = float(entry.get())
    except ValueError:
        result = "Bitte eine Zahl eingeben"
    else:
        exponent_text = str(exponent).translate(DIGIT_TO_SUPERSCRIPT_DIGIT)
        result = f"Ergebnis von {number}{exponent_text} = {number ** exponent}"

    result_label["text"] = result


def on_exponentiate(
    image_label, entry, squared_result_label, cubed_result_label
):
    exponentiate(entry, squared_result_label, 2)
    exponentiate(entry, cubed_result_label, 3)
    entry["background"] = "#FFEA00"
    image = tk.PhotoImage(file="Bild2.gif")
    image_label.image = image
    image_label["image"] = image


def main():
    root = tk.Tk()

    image = tk.PhotoImage(file="Bild.gif")
    image_label = tk.Label(root, image=image)
    image_label.pack()

    entry = tk.Entry(root)
    entry.pack()

    exponentiate_button = tk.Button(root, text="Exponenzieren")
    exponentiate_button.pack()

    squared_result_label = tk.Label(root, text="Ergebnis: ")
    squared_result_label.pack()

    cubed_result_label = tk.Label(root, text="Ergebnis: ")
    cubed_result_label.pack()

    exponentiate_button["command"] = partial(
        on_exponentiate,
        image_label,
        entry,
        squared_result_label,
        cubed_result_label,
    )

    tk.Button(root, text="Ende", command=root.quit).pack()

    root.mainloop()


if __name__ == "__main__":
    main()
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

Vielen Dank für die sehr ausführlichen Antworten. Ich werde mich morgen mit meinem Programm und euren Verbesserungen beschäftigen.
Das mit der lambda Funktion habe ich hier her: https://www.delftstack.com/de/howto/pyt ... er-button/

Gruß Kai
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

Ich mein Murmeltier! Eine neue Seite wo totaler Schrott verbreitet wird.
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

Ok, so ich habe versucht den Code aufzuräumen, und bessere Variablennamen zu verwenden.
Desweiteren habe ich die Funktionen 'trip()' und 'quad()' wie ihr geschreiben habt, in eine Funktion geschrieben.
Das klappt auch soweit. Für die Potenzfunktion ist der auskommentierte Button zuständig.

Code: Alles auswählen

import tkinter as tk
from functools import partial

def berechnung(eingabefeld, eingabefeld2, ausgabelabel):
    eingabe = eingabefeld.get()
    eingabeExponent = eingabefeld2.get()
    try:
        zahl = float(eingabe)
        zahlExp = float(eingabeExponent)
    except ValueError:
        ausgabelabel["text"] = "Bitte eine Zahl eingeben"
    else:
        Potenzwert = zahl ** zahlExp
        ausgabelabel["text"] = f"Ergebnis von Basis {zahl} mit dem Exponenten {zahlExp} ist {Potenzwert}"    

def bild_wechsel(image_label):
    Bild2 = tk.PhotoImage(file="Bild2.gif")
    image_label = Bild2

root = tk.Tk()

Bild = tk.PhotoImage(file="Bild.gif")
image_label = tk.Label(root, image=Bild)
image_label.pack()

eingabefeld = tk.Entry(root)
eingabefeld.pack()

eingabefeld2 = tk.Entry(root,)
eingabefeld2.pack()

ausgabelabel = tk.Label(root, text = "Meldung")
ausgabelabel.pack()

#bquad = tk.Button(root, text = "Potenzieren", command = partial(berechnung, eingabefeld, eingabefeld2, ausgabelabel))
bquad = tk.Button(root, text = "Bild wechseln", command = partial(bild_wechsel, image_label))
bquad.pack()

ende = tk.Button(root, text= "Ende", command = root.destroy)
ende.pack()

root.mainloop()
Jetzt zum Bild wechsel.
Bzgl. der Referenzen des Bildes im ersten versuch von mir, zeigt nicht im auf das Bild.gif und im2 auf das Bild2.gif ? Oder schmeiße ich da was durcheinander ?
Ich habe versucht die Funktion für den Bild wechsel aus beiden Beiträgen zu übernehmen. Wenn ich die beiden Parameter in den Button Potenzieren schreibe, kommt eine Fehlermeldung, dass die Funktion 'berechnung' nur 3 Argumente braucht und 5 übergeben werden.
Bei dem Button Bild wechseln passiert leider gar nichts.
Gruß Kai
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

So das Bild kann gewechselt werden, so sieht der Code der Funktion aus, was da passiert verstehe ich aber nicht.

Code: Alles auswählen

def bild_wechsel(image_label):
    Bild2 = tk.PhotoImage(file="Bild2.gif") 
    image_label.configure(image=Bild2) 
    image_label.image = Bild2 

Code: Alles auswählen

  Bild2 = tk.PhotoImage(file="Bild2.gif") 
Die Datei Bild2.gif wird als Bild2 ins Programm geladen.

Code: Alles auswählen

  image_label.configure(image=Bild2) 
Das Bild2 wird dem image_label zugewiesen ? Was bedeutet hier genau configure, ohne funktioniert es nicht.

Code: Alles auswählen

image_label.image = Bild2
Das Bild2 wird dem image_label nochmal zugewiesen ? Ohne die Zeile funktioniert es auch nicht. Was bedeutet hier .image ? Das Bild2 ein Image ist wurde doch schon in der vorherigen Zeile definiert durch

Code: Alles auswählen

(image=Bild2)
oder?
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

Configure ist das selbe wie die Zuweisung mit den eckigen Klammern: man setzt eine Eigenschaft eines tk-Widgets.
Das reicht aber bei Bildern nicht. Zusätzlich muss man sich in Python auch noch eine Referenz merken. Und das geht an geschicktesten als Attribut des Python-Objekts label. Zwei verschiedene Welten. Sieht komisch aus, muss aber so.
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

Hallo Sirius3, vielen Dank für deine Antwort. Stimmt es geht auch mit dem eckigen Klammern.

Ich bin auf eine neue Idee gekommen. Die Umsetzung schien mir leicht, jedoch bekomme ich es nicht hin. Der Button soll zwischen Bild1 und Bild2 hin und her schalten können.

Dazu soll die Variable i den Startwert 0 bekommen.
Wenn i = 0 ist:
soll von Bild1 auf Bild2 gewechselt werden, dabei soll i + 1 gerechnet werden.
Wenn i = 1 ist:
zurück von Bild2 auf Bild1 und i -1

Leider weiß ich nicht wie ich i = 0 deklarieren soll. Ich habe es am Anfang meiner Funktion stehen. Dort wird es bei jeden Ausführen wieder auf i = 0 gesetzt, sodass immer nur von Bild1 auf Bild2 gewechselt wird.
Da es sich um eine lokale Variable handelt, kann ich sie nicht Global setzten.

Gibt es einen Weg, das Bild über i zu wechseln, oder ist das komplizierter als gedacht ?

Gruß Kai

Code: Alles auswählen

import tkinter as tk
from functools import partial

def berechnung(eingabefeld, eingabefeld2, ausgabelabel):
    eingabe = eingabefeld.get()
    eingabeExponent = eingabefeld2.get()
    try:
        zahl = float(eingabe)
        zahlExp = float(eingabeExponent)
    except ValueError:
        ausgabelabel["text"] = "Bitte eine Zahl eingeben"
    else:
        Potenzwert = zahl ** zahlExp
        ausgabelabel["text"] = f"Ergebnis von Basis {zahl} mit dem Exponenten {zahlExp} ist {Potenzwert}"    

def bild_wechsel(image_label):
    i=0
    print(f"Bild1 Startwert i: {i}")
    if i == 0:
        Bild2 = tk.PhotoImage(file="Bild2.gif") 
        #image_label.configure(image=Bild2) 
        image_label["image"] = Bild2
        image_label.image = Bild2
        i = i + 1
        print(f"Bild2 Wert i: {i}")
        
    else:
        #Bild = tk.PhotoImage(file="Bild.gif") 
        #image_label.configure(image=Bild) 
        #image_label["image"] = Bild
        #image_label.image = Bild
        i = i - 1
        print(i)
        print(f"Bild1 Wert i: {i}")
        
def main():
    root = tk.Tk()

    Bild = tk.PhotoImage(file="Bild.gif")
    #image_label = tk.Label(root, image=Bild)
    image_label =tk.Label(root)
    image_label["image"] = Bild
    image_label.pack()

    eingabefeld = tk.Entry(root)
    eingabefeld.pack()

    eingabefeld2 = tk.Entry(root)
    eingabefeld2.pack()

    ausgabelabel = tk.Label(root, text = "Meldung")
    ausgabelabel.pack()

    berB = tk.Button(root, text = "Potenzieren", command = partial(berechnung, eingabefeld, eingabefeld2, ausgabelabel))
    bilW = tk.Button(root, text = "Bild wechseln", command = partial(bild_wechsel, image_label))
    berB.pack()
    bilW.pack()

    ende = tk.Button(root, text= "Ende", command = root.destroy)
    ende.pack()

    root.mainloop()

if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 18289
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn man Zustand (i) über verschiedene Aufrufe hinweg speichern will, kommt man um die Definition von Klassen nicht drumrum, das ist bei jeder nicht-trivialen GUI so.
Konvention ist, dass man alle Variablennamen klein schreibt.
Wenn Du eh nur zwei fixe Bilder hast, ist es sinnvoll, beide am Anfang zu laden und nur jeweils die Anzeige zu wechseln. Dann brauchst Du auch keine Tricks mit dem Image-Label. Auch partial ist nicht mehr nötig. Es wird also alles viel einfacher:

Code: Alles auswählen

import tkinter as tk
from itertools import cycle


class MainWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.bilder = [
            tk.PhotoImage(file="Bild.gif"),
            tk.PhotoImage(file="Bild2.gif") 
        ]
        self.bilder_karusell = cycle(self.bilder)
        self.image_label =tk.Label(self)
        self.image_label["image"] = next(self.bilder_karusell)
        self.image_label.pack()

        self.eingabefeld = tk.Entry(self)
        eingabefeld.pack()

        self.eingabefeld2 = tk.Entry(self)
        self.eingabefeld2.pack()

        self.ausgabelabel = tk.Label(self, text="Meldung")
        self.ausgabelabel.pack()

        tk.Button(self, text="Potenzieren", command=self.berechnung).pack()
        tk.Button(self, text="Bild wechseln", command=self.bild_wechsel).pack()
        tk.Button(self, text="Ende", command=self.destroy).pack()
        
    def berechnung(self):
        eingabe = self.eingabefeld.get()
        eingabe_exponent = self.eingabefeld2.get()
        try:
            zahl = float(eingabe)
            exponent = float(eingabe_exponent)
        except ValueError:
            self.ausgabelabel["text"] = "Bitte eine Zahl eingeben"
        else:
            potenzwert = zahl ** exponent
            self.ausgabelabel["text"] = f"Ergebnis von Basis {zahl} mit dem Exponenten {exponent} ist {potenzwert}"    

    def bild_wechsel(self):
        self.image_label["image"] = next(self.bilder_karusell)
        
def main():
    root = MainWindow()
    root.mainloop()

if __name__ == "__main__":
    main()
Kahnbein.Kai
User
Beiträge: 104
Registriert: Mittwoch 24. Juni 2015, 14:12
Wohnort: Bochum

Vielen Dank, sirus3, es klappt wunderbar.

Jetzt muss ich mich in die Klassen Thema einlesen.

Schönes WE und Gruß
Kai
Antworten