GUI für einen Bildbetrachter

Programmierung für GNOME und GTK+, GUI-Erstellung mit Glade.
Antworten
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Hallo liebes Python Forum,

da dies mein erster Post ist, möchte ich mich vorstellen. Mein Name ist Christopher, 28 Jahre jung und gerade in einer Umschulung zum Fachinformatiker Anwendungsentwicklung. Python gehört nicht zu meinem Schwerpunkt in der Umschulung, allerdings möchte ich dies aus eigenem Interesse erlernen. Meinen Erfahrungsgrad kann ich selber schwer einschätzen, ich habe in meiner Jugend mit VB6 programmiert und danach gelegentlich kleine Erfahrungen in Java sammeln können. OOP, Iterationen, usw. sind mir nicht fremd.

Python ist an sich ganz angenehm zu schreiben, wie ich finde, allerdings tu ich mich mit der GUI schwer. Wie sich bei Recherche rausstellte, ist das bei den meisten Anfängern so.

Mein Startprojekt soll ein Programm sein, welches über einen FileOpenDialog eine Grafik lädt und anzeigt. Das klappt soweit auch schon ganz gut, allerdings gefällt mir das Layout gar nicht. Habe schon darüber nachgedacht, die GUI per Glade zu erstellen und einzubinden, aber da hapert es dann doch an den "Skills".

Ich kann euch hier mal den Code einspeisen, den ich bisher habe:

Code: Alles auswählen

#!/usr/bin/python

from gi.repository import Gtk, GdkPixbuf, Gdk
import os, sys

class GUI:
    def __init__(self):
        
        #creating the window itself
        window = Gtk.Window()
        window.set_title ("cCloudImageViewer by Christopher Barkey")
        window.set_default_size (300,150)
        window.set_position(Gtk.WindowPosition.CENTER)
        window.connect_after('destroy', self.destroy)
        
        #creating a box
        wBox = Gtk.HBox(False, 0)
        mbox = Gtk.HBox(False, 5)
        box = Gtk.HBox()
        box.set_spacing (5)
        box.set_orientation (Gtk.Orientation.HORIZONTAL)
        #adding the box to the window
        window.add (wBox)
        wBox.add (mbox)
        wBox.add (box)
        
        self.Image = Gtk.Image()
        box.pack_start (self.Image, False, False, 0)
        
        #creating the buttons
        buttonL = Gtk.Button ("<<")
        buttonL.connect_after ('clicked', self.prev_image)
        button = Gtk.Button ("Choose a file...")
        button.connect_after ('clicked', self.on_open_clicked)
        buttonR = Gtk.Button (">>")
        buttonR.connect_after ('clicked', self.next_image)
        mbox.pack_start (button, False, False, 0)
        mbox.pack_start (buttonL, False, False, 0)
        mbox.pack_start (buttonR, False, False, 0)
        #show all
        window.show_all()
    
    #defining what happens when buttons are clicked
    def prev_image (self, button):
        print ("Previous Image")
        
    def next_image (self, button):
        print ("Next Image")
        
    def on_open_clicked (self, button):
        dialog = Gtk.FileChooserDialog ("Open Image", button.get_toplevel(), Gtk.FileChooserAction.OPEN);
        dialog.add_button (Gtk.STOCK_CANCEL, 0)
        dialog.add_button (Gtk.STOCK_OK, 1)
        dialog.set_default_response(1)
        
        filefilter = Gtk.FileFilter()
        filefilter.add_pixbuf_formats()
        dialog.set_filter(filefilter)
        
        if dialog.run() ==1:
            self.Image.set_from_file(dialog.get_filename())
            
        dialog.destroy()
        
    def destroy(window, self):
        Gtk.main_quit()        
def main():
    app = GUI()
    Gtk.main()

if __name__ == "__main__":
    sys.exit(main())
Wenn ich nun ein Bild geladen habe, sieht das Ganze so aus:

Bild

Ich wünsche mir eine Anordnung wie diese:

Bild

Wenn ich versuche mehrere Boxen direkt auf das Window anzuwenden, dann bekomme ich die Meldung, dass nur ein Widget ginge. Packe ich 2 Boxen in 1 andere, dann funktioniert es zwar, aber ich bekomme die Anordnung nicht hin.

Ich hoffe ich konnte mein Problem deutlich schildern und vielleicht hat ja der Ein oder Andere Zeit, mir bei der Lösung zu helfen.

Mit freundlichem Gruß

Christopher

EDIT: Die "<<" und ">>" sollen später durch die Bilder in einem Verzeichnis browsen, das möchte ich allerdings erstmal hinten anstellen.
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Du setzt eine `HBox` auf das Fenster und wunderst Dich dann das der Inhalt *H*orizontal in dieser Box angeordnet wird‽ :-)

Ansonsten fällt am Quelltext auf das Du oft Leerzeichen zwischen Funktion oder Methode und der öffnenden Klammer der Argumente setzt. Das ist unüblich und liest sich deshalb komisch. Und Du machst das auch noch nicht einmal konsistent.

Wenn das Python 2-Quelltext ist, und da spricht erst einmal nichts gegen, dann ist ``print`` auch keine Funktion und da gehören keine Klammern hin. Man sollte in dem Fall die GUI-Klasse auch von `object` erben lassen, damit man eine new-style-Klasse bekommt. Frag nicht nach dem Unterschied — man will eigentlich immer new-style-Klassen haben. Das es old-style-Klassen in Python 2 noch gibt, und dass die Default sind, hat nur mit Rückwärtskompatibiltät zu tun.

Einige Namen halten sich nicht an den Style Guide for Python Code.

Man sollte auch Abkürzungen in Namen verwenden die nicht allgemein bekannt sind. Also so etwas wie `wBox`, mbox (sofern es sich nicht um eine Mailbox im MBOX-Format handelt), oder `buttonL` sind nicht gut, weil sie den Leser eher zum Rätselraten bringen als ihm klar zu sagen was der Wert bedeutet der an den Namen gebunden ist. Bei `buttonL` und `buttonR` wäre auch die nicht abgekürzte Version `button_left` und `button_right` noch nicht besonders gut gewählt weil sie immer noch nicht die Bedeutung der Schaltflächen für das Programm wiedergibt, sondern nur ihre (vermutliche) relative Position in der GUI.

In der `destroy()`-Methode sind die Argumentnamen vertauscht und `window` ist wohl auch kein passender Name. Man könnte(/sollte?) die Methode als `staticmethod()` definieren:

Code: Alles auswählen

    @staticmethod
    def destroy(_button=None):
        Gtk.main_quit()
Edit: Die Kommentare sind übrigens so gut wie alle überflüssig. Die beschreiben nur in Worten was direkt danach ganz offensichtlich im Quelltext passiert. Die haben also überhaupt keinen Mehrwert. Faustregel: Nicht kommentieren *was* passiert, das sollte man im Code lesen können, sondern wenn etwas unklar sein könnte *warum* der Code das tut was er tut.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Hi BlackJack,

vielen lieben Dank für deinen ausführlichen Kommentar. Ich habe schon befürchtet, dass mein Code wie ein durcheinander gewürfelter Haufen aussieht. Habe mir das, was da jetzt so an Code steht, in ein paar Tagen mit verschiedensten Dokumentationen erarbeitet. Konnte mich da wohl nicht auf eine Quelle fixieren und es durchziehen, weil ich immer wieder auf Verständlichkeitsprobleme stieß.

Danke auch für den Link zum Python Code Guide, werde mir den auch mal zu Gemüte ziehen.

Also erster Schritt für mich jetzt - Code neu, besser schreiben!? ;)

EDIT: Ach Mensch, ist doch zum Haare raufen...also ich verstehe deinen Teil mit den vertauschten Argumentnamen nicht und den Codeschnipsel, den du eingeworfen hast.
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Wegen der paar Kleinigkeiten lohnt es sich nicht das komplett neu zu schreiben. Man müsste doch nur ein paar Leerzeichen und Kommentare weglassen, und an anderen Stellen ein paar Leerzeichen hinzufügen. Und Namen ändern.

Und aus der ersten `HBox` für das Fenster mal eine `VBox` machen, damit die beiden anderen Boxen nicht horizontal, sondern vertikal arrangiert werden.

Zum Codeschnippsel mal in mehreren Schritten. Dein Code:

Code: Alles auswählen

    def destroy(window, self):
        Gtk.main_quit()
Da sind die Namen der Argumente verstauscht. Das *erste* Argument bei Methoden heisst per Konvention `self`, weil da das Objekt selbst übergeben wird, auf dem die Methode aufgerufen wird. Das ist sozusagen das ``this`` in Java, nur dass es bei Python nicht auf magische Weise in einer Methode definiert ist, sondern als Argument übergeben wird. Richtig wäre also:

Code: Alles auswählen

    def destroy(self, window):
        Gtk.main_quit()        
Da das Objekt für diese Methode gar nicht verwendet wird, es sich also gar nicht wirklich um eine (normale) Methode handelt, sollte man das entsprechend kenntlich machen, in dem man aus `destroy()` eine „statische Methode” macht (``static`` in Java) was ja im Grunde eine Funktion ist, die in einer Klasse definiert ist:

Code: Alles auswählen

    @staticmethod    
    def destroy(window):
        Gtk.main_quit()
Nun sind wir das unbenutzte `self` losgeworden. `window` wird allerdings auch gar nicht benutzt und steht dort nur weil es mit einem Argument vom GUI-Toolkit aufgerufen wird. Um zu zeigen dass das kein Versehen vom Programmierer ist, sondern das Argument absichtlich nicht verwendet wird, gibt es eine Konvention solche absichtlich unbenutzten lokalen Namen mit einem führenden Unterstrich zu versehen. Da muss ich allerdings dazu sagen, dass diese Konvention nicht so weit verbreitet ist wie die Sachen aus PEP8. Dann wären wir bei:

Code: Alles auswählen

    @staticmethod    
    def destroy(_window):
        Gtk.main_quit()
Jetzt kann man `destroy()` noch nicht ohne Argument aufrufen, wie Du das zum Beispiel mit ``dialog.destroy`` gemacht hast. Da das Argument sowieso nicht verwendet wird, kann es auch einfach mit dem Default `None` versehen werden:

Code: Alles auswählen

    @staticmethod    
    def destroy(_window=None):
        Gtk.main_quit()
Das ich `_window` in meinem Beispiel `_button` genannt habe war ein Fehler von mir. Eigentlich sollte man es `_widget` nennen, denn wenn man der GUI noch eine Schaltfläche oder einen Menüpunkt zum Beenden hinzufügen würde, dann stimmt weder `_window` noch `_button` an der Stelle.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Hm, habe es jetzt wie folgt angepasst (HBox und VBox jetzt mal ausgeblendet, habe einfach nur Boxen genommen...)

Code: Alles auswählen

#!/usr/bin/python

from gi.repository import Gtk, GdkPixbuf, Gdk
import os, sys

class GUI:
    def __init__(self):
        
        cCloudImageViewer = Gtk.Window()
        cCloudImageViewer.set_title("cCloudImageViewer by Christopher Barkey")
        cCloudImageViewer.set_default_size(300,150)
        cCloudImageViewer.set_position(Gtk.WindowPosition.CENTER)
        cCloudImageViewer.connect_after('destroy', self.destroy)
        
        windowBox = Gtk.Box(False, 0)
        menuBox = Gtk.Box(False, 5)
        imageBox = Gtk.Box()
        windowBox.set_spacing (5)
        windowBox.set_orientation(Gtk.Orientation.HORIZONTAL)

        cCloudImageViewer.add(windowBox)
        windowBox.add(menuBox)
        windowBox.add(imageBox)
        
        self.Image = Gtk.Image()
        imageBox.pack_start(self.Image, False, False, 0)
        

        btn_prev_img = Gtk.Button("<<")
        btn_prev_img.connect_after('clicked', self.prev_image)
        btn_choose_img = Gtk.Button("Choose a file...")
        btn_choose_img.connect_after('clicked', self.on_open_clicked)
        btn_next_img = Gtk.Button(">>")
        btn_next_img.connect_after('clicked', self.next_image)
        menuBox.pack_start(btn_choose_img, False, False, 0)
        menuBox.pack_start(btn_prev_img, False, False, 0)
        menuBox.pack_start(btn_next_img, False, False, 0)

        cCloudImageViewer.show_all()
    
    def prev_image(self, button):
        print "Previous Image"
        
    def next_image(self, button):
        print "Next Image"
        
    def on_open_clicked(self, button):
        dialog = Gtk.FileChooserDialog("Open Image", button.get_toplevel(), Gtk.FileChooserAction.OPEN);
        dialog.add_button(Gtk.STOCK_CANCEL, 0)
        dialog.add_button(Gtk.STOCK_OK, 1)
        dialog.set_default_response(1)
        
        filefilter = Gtk.FileFilter()
        filefilter.add_pixbuf_formats()
        dialog.set_filter(filefilter)
        
        if dialog.run() ==1:
            self.Image.set_from_file(dialog.get_filename())
            
        dialog.destroy()
    
    @staticmethod
    def destroy(_cCloudImageViewer=None):
        Gtk.main_quit()        
def main():
    app = GUI()
    Gtk.main()

if __name__ == "__main__":
    sys.exit(main())
Ich komme leider noch nicht so ganz hinter den Sinn der Anpassung mit "staticmethod" und der Änderung des destroy()-Dingens. Werde mir deine Worte wohl noch mehrfach durchlesen müssen! :D
Bin auf jeden Fall über die Hilfsbereitschaft sehr dankbar, echt klasse! :)
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Tut mir leid, wenn ich jetzt doppelt poste, aber ich kann da soviel HBox oder VBox machen wie ich möchte, das Bild wird immer neben den Buttons angezeigt. Einzig die Art, wie die Buttons angeordnet sind, ändert sich.

Hach jeee...unter VB6 Zeiten war man so verwöhnt von dem GUI Editor! :D
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Du musst Dir mal klar machen was diese Boxen sind. Also `HBox` und `VBox`. In `HBox` werden die enthaltenen Elemente *h*orizontal*, also nebeneinander angeordnet, und in `VBox` *v*ertikal, also untereinander. Die Schaltflächen sollen nebeneinander angeordnet werden, gehören also in eine `HBox` und diese Box mit den Schaltflächen und das Bild sollen untereinander angeordnet werden, das gehört dann in eine `VBox`. Mal bildlich:

Code: Alles auswählen

 +-VBox-----------------+
 |+-HBox---------------+|
 ||+--++----------++--+||
 |||<<||Bild laden||>>|||
 ||+--++----------++--+||
 |+--------------------+|
 |+--------------------+|
 ||                    ||
 ||                    ||
 ||       Image        ||
 ||                    ||
 ||                    ||
 ||                    ||
 |+--------------------+|
 +----------------------+
Das wäre beim GUI-Editor genau so, auch dort müsstest Du die richtigen Boxen auswählen und dort die Elemente in der richtigen Reihenfolge hinein stecken. Ein GUI-Editor wie in VisualBasic wo man die Elemente absolut platziert hat, funktioniert heutzutage nicht mehr. Die Bildschirmgrössen und Grafikauflösungen variieren so stark das eine GUI die auf einem Rechner mit absoluten Positionen normal aussieht, auf einem anderen unbenutzbar sein kann, weil entweder die Texte alle nicht mehr passen, oder die GUI-Elemente so ineinander geschoben sind, dass man nicht mehr alles lesen oder benutzen kann.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Hahaaaa! BlackJack, du bist genial! Vom Prinzip hatte ich schon verstanden, wie das mit den Boxen ist, allerdings hatte ich wohl einen starken logischen Fehler drin. Jetzt funktioniert es auf jeden Fall erstmal, auch wenn die Buttons jetzt einfach stumpf links floaten. Den Rest werde ich jetzt erstmal wieder auf eigene Faust erforschen und ein wenig rumdameln.

Hier jetzt nochmal der Codeschnipsel:

Code: Alles auswählen

        windowBox = Gtk.VBox(False, 0)
        menuBox = Gtk.HBox(False, 5)
        imageBox = Gtk.Box()

        cCloudImageViewer.add(windowBox)
        windowBox.add(menuBox)
        windowBox.add(imageBox)
        
        self.Image = Gtk.Image()
        imageBox.pack_start(self.Image, False, False, 0)
        

        btn_prev_img = Gtk.Button("<<")
        btn_prev_img.connect_after('clicked', self.prev_image)
        btn_choose_img = Gtk.Button("Choose a file...")
        btn_choose_img.connect_after('clicked', self.on_open_clicked)
        btn_next_img = Gtk.Button(">>")
        btn_next_img.connect_after('clicked', self.next_image)
        menuBox.pack_start(btn_choose_img, False, False, 0)
        menuBox.pack_start(btn_prev_img, False, False, 0)
        menuBox.pack_start(btn_next_img, False, False, 0)

        cCloudImageViewer.show_all()
So sieht das Ganze jetzt aus, wenn ich das Programm starte:

Bild

Und so, wenn ich ein Bild geladen habe:

Bild

Buttons werden natürlich entsprechend gezerrt, je nachdem wie ich das Fenster skaliere...

Ich melde sich, sobald ich neue Fortschritte gemacht habe oder mich wieder festgefressen habe! :D
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Tja...wie es so kommen musste, weiß ich mal absolut nicht weiter. Knöppe verzerren immer noch und ich habe auch keine Idee, wie ich durch die Dateien browsen soll. Hatte gehofft, dass jede Datei in einem Ordner so eine Art ID hat, über die ich die dann ansprechen kann.

In VB habe ich damals alle Dateien (bzw den Pfad mit Dateinamen) in eine Liste geladen, dann hatten die natürlich IDs von 0 bis XXX...Einfach die nächste ID gewählt und den Pfad daraus gelesen und schon konnte man die anzeigen. Wüsste aber nicht wie ich das in Python bewerkstelligen kann... :(

Würde mich freuen wenn mir jemand den Stups in die richtige Richtung geben könnte.
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Dateien haben eine ID: den Dateinamen. :-)

Du kannst doch auch in Python die Dateinamen eines Ordners in eine Liste stecken und dann über den Index ansprechen. Wenn Du das nicht weisst, solltest Du die GUI erst einmal auf Eis legen und die grundlegenden Datentypen von Python kennen lernen. Und eine kleine Tour durch die Standardbibliothek machen, also welche Funktionen es dort so gibt um mit Dateien und Dateinamen/-pfaden zu arbeiten. So als Stups: `os.listdir()` und `os.path.join()`. Eventuell könntest/müsstest Du auch den `Gtk.FileFilter` verwenden um die Dateien nach anzeigbaren Bildern zu filtern.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Es ist schon frustend, wenn es dann heißt "auf GUI verzichten"... =/

Würde halt schon gerne dort einsteigen und es auf diesem Wege lernen. Das es in Python wahrscheinlich auch auf diesem "Umweg" mit einer Liste ginge, habe ich mir ja fast gedacht, war mir aber nicht sicher, ob es so elegant wäre.

Den FileFilter verwende ich ja:

Code: Alles auswählen

        def on_open_clicked(self, button):
             dialog = Gtk.FileChooserDialog("Open Image", button.get_toplevel(), Gtk.FileChooserAction.OPEN);
             dialog.add_button(Gtk.STOCK_CANCEL, 0)
             dialog.add_button(Gtk.STOCK_OK, 1)
             dialog.set_default_response(1)
        
             filefilter = Gtk.FileFilter()
             filefilter.add_pixbuf_formats()
             dialog.set_filter(filefilter)
        
             if dialog.run() ==1:
                 self.Image.set_from_file(dialog.get_filename())
            
             dialog.destroy()
*seufz* Ist mir alles echt mal leichter gefallen... :(
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Du musst ja nicht auf die GUI verzichten, was bei Bildbetrachtern ja auch irgendwo nicht so viel Sinn macht, sondern nur mal eine kleine Pause *damit* einlegen, bis der Rest von der Sprache bis inklusive objektorientierter Programmierung soweit sitzt, dass GUI das einzige zusätzliche neue ist.

*Du* verwendest den `FileFilter` da nicht wirklich, sondern Du erstellst einen damit der Dialog den verwenden kann. Wenn Du selber eine Liste mit Pfaden filtern möchtest, dann musst Du da ja die Bilder filtern, und dafür wäre das Objekt dann auch praktisch.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Du hast wohl Recht! Allerdings fällt mir der Einstieg sehr schwer, wenn ich nicht ein, aus meiner Sicht, "vernünftiges" Projekt habe, an dem ich arbeite.

Was schlägst du mir vor, wie ich jetzt weiter vorgehen soll? Wo liegt der Denkfehler bei mir? Was kann ich tun, damit ich von dieser Denkweise abkomme?

Nebenbei übe ich ja schon auch noch bei www.codeacademy.com

Komme mir jetzt echt dämlich vor! :D
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Wie gesagt Du musst ja nicht komplett mit dem Bildbetrachter aufhören oder den auf den Sankt Nimmerleins Tag verschieben. Praktisch wäre jetzt ein kleines Miniprojekt: Eine Modellklasse erstellen der man einen Pfad übergibt und die dann einen dort alle Namen nach einem bestimmten Muster filtert (siehe `glob`-Modul aus der Standardbibliothek) und intern eine Liste mit dem Ergebnis hält. Die braucht dann noch eine Methode (oder ein `property()`) um den aktuellen Pfad zurückzugeben und zwei Methoden um einen Pfad weiter und einen Pfad zurück zu gehen. Das kann man ohne GUI entwickeln und testen und wenn es funktioniert in die GUI einbauen. `glob` zum Filtern dann vielleicht noch durch die `FileFilter`-Klasse ersetzen.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Puh okay, klingt sinnvoll. Dann werde ich mich wohl mal auf meinen Hintern setzen und schauen, dass ich das hinbekomme! *sigh*

Vielen lieben Dank nochmal für deine konstruktiven Antworten und deine Geduld! :)
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Antworten