Modul für GUI-Programmierung

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.
barisoezcan
User
Beiträge: 73
Registriert: Freitag 15. März 2013, 19:38

Ich steh hier grad bisschen auf dem Schlauch..

Ich will eine GUI erzeugen, wo Bilder angezeigt werden.
Wenn auf ein bestimmtes Bild geklickt wird, soll ein bestimmtes anderes Bild angezeigt werden (statt allen anderen Bildern).

Mein bisheriger Lösungsansatz:

Code: Alles auswählen

selectedImage = None

def button_click(event):
    selectedImage = n
    print("selectedImage:", selectedImage) #Hier sollte die Nummer "n" vom geklickten Bild ausgegeben werden (aber Ausgabe: "15")
    
            
root = tk.Tk()
        
photo_imgs = getPhotoImages()

for n, im in enumerate(photo_imgs):
    l = tk.Label(root, image=im)
    l.grid(column=int(n/4),row=n%4)
    l.bind("<Button>", button_click)
        
root.mainloop()   
Das Problem ist erstmal: Wenn geklickt wird, wird immer nur "selectedImage: 15" ausgegeben.
(Es gibt insgesamt 16 Bilder. Der Zähler "n" läuft also von 0 bis 15 und dementsprechend wird immer nur die 15 ausgegeben.)

Aber wie schaffe ich es, dass die Nummer des geklickten Bildes angezeigt wird?
BlackJack

@barisoezcan: Ich glaube ich habe `functools.partial()` hier schon einmal erwähnt.

Und dann solltest Du den Code auf Modulebene mal einer Funktion verschwinden lassen, dann hättest Du gar nicht erst auf das `n` zugreifen können, was *natürlich* den Wert 15 hat, nachdem die ``for``-Schleife über 16 Bilder gelaufen ist. Du musst Dir mal klar machen wann welcher Code ausgeführt wird. Diese Schleife läuft durch, `n` hat dann den Wert 15, Benutzer klickt auf Bild und dann wird die `button_click()`-Funktion ausgeführt. Wie kann man dann etwas anderes erwarten als das `n` immer noch 15 ist? Welche Stelle im Quelltext sollte das zu diesem Zeitpunkt an einen anderen Wert binden?
barisoezcan
User
Beiträge: 73
Registriert: Freitag 15. März 2013, 19:38

Ich hatte es mit der "functools.partial()"-Methode versucht, aber es hatte nicht geklappt, weil ich bei der zu bindenden Methode den Parameter "event" an erster Stelle hatte.
Nun funktioniert es und sieht auch recht kompakt aus =)
Danke.
barisoezcan
User
Beiträge: 73
Registriert: Freitag 15. März 2013, 19:38

Code: Alles auswählen

import tkinter as tk
import numpy
from PIL import Image, ImageTk
from functools import partial


class Map:
    
    def __init__(self, nodesMatchingPictures, dimensions, size):
        self.dimensions = dimensions
        self.size = size
        self.nodesMatchingPictures = nodesMatchingPictures


    def getPhotoImages(self, nodes = None):
        if nodes == None:
                nodes_float = numpy.load("trained_map.npy")
                nodes = (nodes_float*255).astype(numpy.uint8)
        
        photo_imgs = []
        
        for row in nodes:
            for node in row:
                photo_imgs.append(ImageTk.PhotoImage(Image.fromarray(node, "RGB"))) 

        return photo_imgs


    def showNodes(self):
        
        root = tk.Tk()
        root.title("Nodes")
        
        photo_imgs = self.getPhotoImages()
        
        for n, im in enumerate(photo_imgs):
            l1 = tk.Label(root, image=im)
            l2 = tk.Label(root, text=str(len(self.nodesMatchingPictures[n])))
            l1.grid(column=int(n/self.size[0]),row=n%self.size[0])
            l2.grid(column=int(n/self.size[0]),row=(n%self.size[0])+5)
            l1.bind("<Button-1>", partial(self.showMatchingPicturesToNode, n))
        
        root.mainloop()   
        
    
    def showMatchingPicturesToNode(self, selectedNode, event):
        
        root2 = tk.Toplevel()
        
        column = str(int(selectedNode/self.size[0] + 1))
        row = str(selectedNode%self.size[1] + 1)
        
        root2.title("Matching pictures to node: column: " + column + ", row: " + row)

        pictures = []
        
        for im in self.nodesMatchingPictures[selectedNode]:
            pictures.append((im*255).astype(numpy.uint8).reshape(1, self.dimensions[0], self.dimensions[1], 3))
        
        photo_imgs = self.getPhotoImages(pictures)
        
        for n, im in enumerate(photo_imgs):
            l = tk.Label(root2, image=im)
            l.grid(column=int(n/(self.size[0]*4)),row=n%(self.size[0]*4))
        
        root2.mainloop()
So müsste die Klasse doch aber einem sinnvollen Klassen-Design entsprechen, oder?
Verbesserungsvorschläge?
barisoezcan
User
Beiträge: 73
Registriert: Freitag 15. März 2013, 19:38

Kann/Will mir keiner weiterhelfen?
BlackJack

@barisoezcan: `getPhotoImages()` ist eine Funktion und keine Methode.

Namen sollte man nicht ohne Not abkürzen und ein- oder zweibuchstabige Namen wie `im`, `l`, `l1`, `l2` gehen in lokal sehr beschränkten Gültigkeitsbereichen wie „list comprehensions”, Generatorausdrücken, oder anonymen Funktionen; aber allgemein sollte man sie vermeiden.

Wo kommt in `showNodes()` diese „magische” 5 beim `row`-Argument für das Textlabel her? Gleiche Frage zum Faktor 4 in `showMatchingPicturesToNode()`.

Was soll die 2 bei `root2` in `showMatchingPicturesToNode()` bedeuten?

Das am Ende der Methode *nochmal* eine Hauptschleife auf dem `Toplevel`-Exemplar aufgerufen werden muss, liegt nur daran, dass das lokale `photo_imgs` „am Leben” gehalten werden muss, damit die Bilder angezeigt werden können. Das ist aber IMHO unschön gelöst.

Ich komme dann ungefähr bei so etwas raus (ungetestet):

Code: Alles auswählen

try:
    import Tkinter as tk
except ImportError:
    import tkinter as tk
from functools import partial
import numpy
from PIL import Image, ImageTk


TRAINED_MAP_FILENAME = 'trained_map.npy'


def get_photo_images(nodes=None, filename=TRAINED_MAP_FILENAME):
    if nodes is None:
        nodes = (numpy.load(filename) * 255).astype(numpy.uint8)
    return [
        ImageTk.PhotoImage(Image.fromarray(node, 'RGB'))
        for node in row for row in nodes
    ]


class Map(object):
    def __init__(self, nodes_matching_pictures, dimensions, size):
        self.nodes_matching_pictures = nodes_matching_pictures
        self.dimensions = dimensions
        self.size = size

    def show_nodes(self):
        root = tk.Tk()
        root.title('Nodes')
        photo_images = get_photo_images()
        for n, (image, nodes) in enumerate(
            zip(photo_images, self.nodes_matching_pictures)
        ):
            image_label = tk.Label(root, image=image)
            image_label.grid(column=n // self.size[0], row=n % self.size[0])
            image_label.bind(
                '<Button-1>', partial(self.show_matching_pictures_to_node, n)
            )
            text_label = tk.Label(root, text=str(len(nodes)))
            # 
            # XXX: Magic 5.
            # 
            text_label.grid(column=n// self.size[0], row=(n % self.size[0]) + 5)
        root.mainloop()   
        
    
    def show_matching_pictures_to_node(self, selected_node, _event):
        root = tk.Toplevel()
        root.title(
            'Matching pictures to node: column: {0}, row: {1}'.format(
                selected_node // self.size[0] + 1,
                selected_node % self.size[1] + 1
            )
        )
        pictures = (
            (im * 255)
                .astype(numpy.uint8)
                .reshape(1, self.dimensions[0], self.dimensions[1], 3)
            for im in self.nodes_matching_pictures[selected_node]
        )
        photo_images = get_photo_images(pictures)
        for n, image in enumerate(photo_images):
            column, row = divmod(n, self.size[0] * 4)  # XXX: Magic 4.
            tk.Label(root, image=image).grid(column=column, row=row)
        # 
        # FIXME: Strange and only necessary to keep local `photo_images` alive.
        # 
        root.mainloop()
Zum Klassenentwurf: Es ist im Grunde eine Datenklasse, die Methoden hat, in denen GUIs erzeugt werden. Das würde man eigentlich trennen in Programmlogik, also eine Datenklasse die man nach den benötigten Daten befragen kann und die Operationen auf den Daten zur Verfügung stellt und GUI-Code der das dann benutzt. Also eigentlich umgekehrt zum jetzigen Entwurf.

Wenn man es aus Sicht der Daten betrachtet scheint es einen Zusammenhang zwischen den geladenen `photo_images()` und `self.nodes_matching_pictures` zu geben, der aber nicht wirklich abgebildet wird. Zum Beispiel in dem beides als Attribute auf dem Objekt gehalten wird und vielleicht auch irgendwo mal sichergestellt wird, dass die wenigstens von der Länge her zusammen passen.
barisoezcan
User
Beiträge: 73
Registriert: Freitag 15. März 2013, 19:38

Klappt wunderbar...Danke.
Nur bei der verschachtelten for-Schleife

Code: Alles auswählen

for node in row for row in nodes
meckert er bei "row".

Die "magic" 5 ist dafür da um die "text_labels" unter die "image_labels" hinzuzufügen.
Es gibt 5 Reihen mit Bilder, deshalb werden die "text_labels" um 5 nach unten verschoben.
Bild
So sieht das dann aus.

Und die "magic" 4 wäre dafür da um im 2.Fenster maximal "self.size[0] * 4" Reihen zu haben.
Also in diesem Fall 20 Reihen. Damit auch alles auf dem Bildschirm dargestellt werden kann.
Antworten