Fenster aus einem thread

Programmierung für GNOME und GTK+, GUI-Erstellung mit Glade.
Antworten
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

Ich weiss, dass GUI und threads ziemlich kompliziert sind, habs aber bisher immer hinbekommen nun habe ich jedoch das Problem, dass wenn ich aus einem Thread ein neues Fenster starten möchte mir die ganze Gui einfriert. Leider muss das Fenster aus dem Thread aus gestartet werden und je nach antwort muss sich das Programm dann entsprechend verhalten.

Vereinfachter code:

Code: Alles auswählen

import gtk
import threading

class MainWindow(object):
    '''
    Fenster mit button, welcher einen Thread startet
    '''
    def __init__(self):
        gtk.gdk.threads_init()
        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        window.set_position(gtk.WIN_POS_CENTER)
        window.set_title("Hauptfenster")
        window.connect("delete_event", gtk.main_quit)
        button = gtk.Button("Start timer")
        button.connect("clicked", self.buttonEvent)
        buttonBox = gtk.HButtonBox()
        buttonBox.add(button)
        window.add(buttonBox)
        window.show_all()
    
    def startGtk(self):
        gtk.main()
    
    def buttonEvent(self, widget, *args):
        '''
        Startet Timer, welcher nach Ablauf von 2 Sek. den callback ausfuehrt 
        '''
        print "Starte Timer und warte 2 Sekunden"
        timer = threading.Timer(2, self.callback)
        timer.start()
    
    def callback(self, *args):
        '''
        Startet den Dialog, friert jedoch ein
        '''
        dialog = MyDialog()
        ret = dialog.runMyDialog()
        print ret
        
class MyDialog():
    '''
    Klasse in der ein Dialog generiert und gestartet wird
    '''
    def runMyDialog(self):
        dialog = gtk.Dialog("My dialog", None,
                  gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,  #binary flags or'ed together
                  (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO))
        hbox = gtk.HBox(False, 0)
        hbox.set_border_width(10)
        hbox.pack_start(gtk.Label("It works"))
        dialog.vbox.pack_start(hbox)
        dialog.vbox.set_border_width(10)
        dialog.show_all()
        response = dialog.run()
        dialog.destroy()
        
        return response

if __name__ == "__main__":
    
    programm = MainWindow()
    gtk.gdk.threads_enter()
    programm.startGtk()
    gtk.gdk.threads_leave()
    
Wäre schön, wenn jemand eine Idee hätte. Habe schon versucht mit den gtk.Dialog parametern herumzuspielen, hat aber nichts gebracht.

EDIT: Benutze python 2.7
deets

Yaso hat geschrieben:Leider muss das Fenster aus dem Thread aus gestartet werden
Wenn das stimmen wuerde, dann waerst du der erste, dem das so geht. Und das ist ja ein bisschen unwahrscheinlich, oder? In deinem Code kommt jedenfalls nichts vor, was ueberhaupt multi-threading rechtfertigt.

Und es wird auch niemals so funktionieren, weil du ja schon selbst bemerkt hast, dass es nicht geht.

Dein Test wuerde genausogut mit normalen Timern arbeiten, nicht mit Threads. Solange wir also nicht mehr wissen ueber dein eigentliches Problem, sind GTK-Timer die Loesung.
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

Dein Test wuerde genausogut mit normalen Timern arbeiten, nicht mit Threads. Solange wir also nicht mehr wissen ueber dein eigentliches Problem, sind GTK-Timer die Loesung.
Ja, das weiss ich schon selber, aber mein Chef hat bei diesem Problem irgentwie ein Brett vor dem Kopf und will umbedingt, dass es mit threads gelöst wird, auch wenn diese völlig sinnlos angelegt werden => Je mehr Threads desto besser und flüssiger läuft das Programm :?: :K Ich glaube er vermischt das mit Multiprocessing, aber erklären oder zuhören will er nicht.
Wenn das stimmen wuerde, dann waerst du der erste, dem das so geht. Und das ist ja ein bisschen unwahrscheinlich, oder? In deinem Code kommt jedenfalls nichts vor, was ueberhaupt multi-threading rechtfertigt.
Aber als Gedankenspiel könnte man schon vorstellen, dass es gebraucht wird:
Über einen GUI button wird eine rechenintensive Anwendung gestartet, welche während dem Rechnen bestimmte Inputs vom User braucht welche durch einen Dialog eingegeben werden. Dann müsste das Dialogfenster aus dem Thread gestartet werden oder? Und darum dachte ich, dass es auch irgendwie funktionieren müsste, oder wie müsste man dieses UseCase "elegant" abdecken?
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Yaso hat geschrieben:Aber als Gedankenspiel könnte man schon vorstellen, dass es gebraucht wird:
Über einen GUI button wird eine rechenintensive Anwendung gestartet, welche während dem Rechnen bestimmte Inputs vom User braucht welche durch einen Dialog eingegeben werden. Dann müsste das Dialogfenster aus dem Thread gestartet werden oder? Und darum dachte ich, dass es auch irgendwie funktionieren müsste, oder wie müsste man dieses UseCase "elegant" abdecken?
Dann gibt man einfach dem Haupthread eine Queue, welche Anweisungen empfangen kann. Braucht ein Thread ein Fenster, so teilt er dies der Warteschlange mit und bleibt erstmal stehen. In der Hauptschleife wird die Queue abgearbeitet, indem zum Beispiel das Eingabefenster angezeigt wird. Wurden die Werte abgearbeitet, schickt das Hauptprogramm diese an die Queue des Threads, welcher dann mit seiner eigentlichen Arbeit fortfahren kann.
Das Leben ist wie ein Tennisball.
deets

Beratungsresistent Chefs sind natuerlich ein Problem.

Aber ein anderes, als das hier vorliegende ;)

Der konzeptionelle Denkfehler von dir ist der, dass du unterstellst, dass ein worker-thread die GUI erstellen & managen muss. Muss er aber nicht.

Was stattdessen passiert ist, dass dieselbe Aktion, die den Worker-thread startet (zB ein Button-callback, wie bei dir)

- den Thread startet
- den Dialog oeffnet

Danach kann man dann verschiedene Sachen machen. ZB kannst du einen GTK-Timer setzen, der jede Sekunde den Worker fragt, ob er schon fertig ist, und solange eine "still running"-Nachricht oder so schreibt.

Es gibt auch Wege, von einem nicht-GUI-Thread Events in die Event-Queue des GUI-threads zu inijizieren. Das benutzt man, um durch den Worker-Thread Fortschritte oder sonstige Zustandsaenderungen an die GUI zu kommunizieren.

Ich wuerde dir aber fuer den ersten Anlauf mal ein simples timer-basiertes polling an's Herz legen. Sobald du das laufen hast, kannst du dich mal mit der anderen Moeglichkeit beschaeftigen.
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

Danke, werde das mal ausprobieren, erst mit polling und dann die Queue.

Kennt ihr vielleicht gerade ein gutes beispiel mit GTK und Queue?
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

Hmmmm, nun komme ich leider nicht weiter. Im Prinzip ist die Idee folgende:
- Das Programm startet und checkt jede x Sekunden ein File ob dieses sich in der Grösse verändert hat.
- Wenn ja, soll es einen Dialog zeigen und der Benutzer soll entscheiden ob er eine Funktion starten soll oder nicht.
- Wenn ja, soll diese Funktion in einem neuen Thread gestartet werden (damit das GUI immer noch bedienbar ist)

Soweit der Prototyp:

Code: Alles auswählen

import gtk
import time
import threading

class MainWindow(object):
    '''
    Fenster mit button
    Start eines Timers welcher alle 3 Sek einen Dialog aufruft
    '''
    def __init__(self):
        
        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        window.set_position(gtk.WIN_POS_CENTER)
        window.set_title("Hauptfenster")
        window.connect("delete_event", gtk.main_quit)
        button = gtk.Button("Click me!")
        button.connect("clicked", self.buttonEvent)
        buttonBox = gtk.HButtonBox()
        buttonBox.add(button)
        window.add(buttonBox)
        window.show_all()
        print "Starte Timer und warte 3 Sekunden"
        timer = gtk.timeout_add(3*1000, self.checkFile)
        
    
    def startGtk(self):
#        gtk.gdk.threads_init()
#        gtk.gdk.threads_enter()
        gtk.main()
#        gtk.gdk.threads_leave()
        
    def buttonEvent(self, widget, *args):
        '''
        Sollte während der rechenintensiven Funktion anklickbar sein
        '''
        print "Button clicked"
        
        
    def checkFile(self, *args):
        '''
        Startet den Ja/Nein Dialog
        '''
        dialog = MyDialog()
        ret = dialog.runMyDialog()
        print "Dialog response = " + str(ret)
        if ret == -8:
            threading.Thread(target=self.rechenintensiveFunktion)
        
        return True
        
    def rechenintensiveFunktion(self):
        print "starte rechenintensive Funktion"
        time.sleep(3)
        print "rechenintensive Funktion abgeschlossen"
        
class MyDialog():
    '''
    Klasse in der ein Dialog generiert und gestartet wird
    '''
    def runMyDialog(self):
        dialog = gtk.Dialog("File changed", None,
                  gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,  #binary flags or'ed together
                  (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO))
        hbox = gtk.HBox(False, 0)
        hbox.set_border_width(10)
        hbox.pack_start(gtk.Label("Rechenintensive Funktion starten?"))
        dialog.vbox.pack_start(hbox)
        dialog.vbox.set_border_width(10)
        dialog.show_all()
        response = dialog.run()
        dialog.destroy()
        
        return response

if __name__ == "__main__":
    
    programm = MainWindow()
    programm.startGtk()
    
Wenn ich aber die auskommentierten Linien wieder einfüge (welche ja fürs threading erforderlich sind) funktioniert der Dialog nicht mehr.
Kann mir jemand den (Denk)fehler erklären?
BlackJack

@Yaso: Für Threading generell sind `gdk.threads_enter()`/`gdk.threads_leave()` das nicht nötig, sondern nur um kritische Abschnitte, die auf Gdk-Funktionen zugreifen, gegeneinander zu schützen. Nur ein Thread darf gleichzeitig in so einem Abschnitt aktiv sein. Und Du erklärst da im Grunde das gesamte Programm zu einem kritischen Abschnitt. Keine gute Idee.

Das sind übrigens keine „Linien” sondern „Zeilen”, die dort auskommentiert sind. ;-)

``if ret == -8:`` ist hoffentlich nichts, was Du in echtem Quelltext verwendest.
deets

Also, das offensichtliche ist natuerlich dass du den Thread erst gar nicht startest...

So geht's fuer mich. In dem Moment, wo ich aber den gtk.thread_init-quatsch versucht habe, ist alles in die Hose gegangen. Ich bin kein GTK-Experte, aber es hat sich definitiv nicht so verhalten, wie ich das von der Doku her gedacht haette.

Code: Alles auswählen

# -*- coding: utf-8 -*-
import gtk
import time
import threading

class MainWindow(object):
    '''
    Fenster mit button
    Start eines Timers welcher alle 3 Sek einen Dialog aufruft
    '''
    def __init__(self):
        self.check_for_file = True
        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        window.set_position(gtk.WIN_POS_CENTER)
        window.set_title("Hauptfenster")
        window.connect("delete_event", gtk.main_quit)
        button = gtk.Button("Click me!")
        button.connect("clicked", self.buttonEvent)
        buttonBox = gtk.HButtonBox()
        buttonBox.add(button)
        window.add(buttonBox)
        window.show_all()
        print "Starte Timer und warte 3 Sekunden"
        timer = gtk.timeout_add(3*1000, self.checkFile)


    def startGtk(self):
        gtk.main()


    def buttonEvent(self, widget, *args):
        '''
        Sollte während der rechenintensiven Funktion anklickbar sein
        '''
        print "Button clicked"


    def checkFile(self, *args):
        '''
        Startet den Ja/Nein Dialog
        '''
        if not self.check_for_file:
            return
        dialog = MyDialog()
        ret = dialog.runMyDialog()
        print "Dialog response = " + str(ret)
        if ret == -8:
            threading.Thread(target=self.rechenintensiveFunktion).start()

        return True

    def rechenintensiveFunktion(self):
        self.check_for_file = False
        print "starte rechenintensive Funktion"
        time.sleep(3)
        print "rechenintensive Funktion abgeschlossen"
        self.check_for_file = True

class MyDialog():
    '''
    Klasse in der ein Dialog generiert und gestartet wird
    '''
    def runMyDialog(self):
        dialog = gtk.Dialog("File changed", None,
                  gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,  #binary flags or'ed together
                  (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO))
        hbox = gtk.HBox(False, 0)
        hbox.set_border_width(10)
        hbox.pack_start(gtk.Label("Rechenintensive Funktion starten?"))
        dialog.vbox.pack_start(hbox)
        dialog.vbox.set_border_width(10)
        dialog.show_all()
        response = dialog.run()
        dialog.destroy()

        return response

if __name__ == "__main__":

    programm = MainWindow()
    programm.startGtk()
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

@Yaso: Für Threading generell sind `gdk.threads_enter()`/`gdk.threads_leave()` das nicht nötig, sondern nur um kritische Abschnitte, die auf Gdk-Funktionen zugreifen, gegeneinander zu schützen. Nur ein Thread darf gleichzeitig in so einem Abschnitt aktiv sein. Und Du erklärst da im Grunde das gesamte Programm zu einem kritischen Abschnitt. Keine gute Idee.
Ich weiss, wurde aber in vielen Foren so empfohlen und ohne diese hats gar nicht funktioniert.
Das sind übrigens keine „Linien” sondern „Zeilen”, die dort auskommentiert sind. ;-)
Wird da jemand pingelig? ;-) - Nein, hast natürlich Recht
``if ret == -8:`` ist hoffentlich nichts, was Du in echtem Quelltext verwendest.
Nein, mach ich nicht, wollte nur dass sich die Ausgabe mit dem If Statement deckt.

@deets: Hmm bei mir hat es erst nach ein paar Versuchen geklappt, sonst immer eingefroren, darum hab ich's immer mit dem "gtk.thread_init-quatsch" versucht. Aber werds mal so weiterverfolgen. Und es hat sich definitiv nicht nach der Doku (meinem Verständnis nach) verhalten.

Danke zusammen

EDIT: Ein paar Rechtschreibfehler, alles andere gewollt und in der CH erlaubt ;)
BlackJack

@Yaso: Ich kann mir nicht vorstellen, dass wirklich empfohlen wurde Deinen *gesamten Code* in einen kritischem Abschnitt zu verwandeln. Das ist totaler Unsinn, weil es dann ja nur *einen* Thread geben kann. *Dann* braucht man die Funktionen aber auch nicht.
deets

@BlackJack

was ich interessant fand: wenn ich nur den thread-init-call eingebaut habe (natuerlich ohne die kritische Sektion), hat es auch nicht funktioniert. Und selbst *mit* der kritischen Sektion verstehe ich ueberhaupt nicht, warum es einen Unterschied macht: der Worker-Thread benutzt ja gar keinen GTK-Code.
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

Hab noch folgende Änderungen gemacht

Code: Alles auswählen

    def checkFile(self, *args):
        '''
        Startet den Ja/Nein Dialog
        '''
        if not self.check_for_file:
            return True # <= Damit der Timer wieder startet
        dialog = MyDialog()
        ret = dialog.runMyDialog()
        print "Dialog response = " + str(ret)
        if ret == -8:
            threading.Thread(target=self.rechenintensiveFunktion).start()

        return True
Nun habe ich das komische Verhalten, dass die Funktion nach dem ich beim Dialog auf Ja klicke, nach dem sleep einfach nichts macht bis ich den Click Me Button drücke. Kannst du das nachvollziehen?
@BlackJack

was ich interessant fand: wenn ich nur den thread-init-call eingebaut habe (natuerlich ohne die kritische Sektion), hat es auch nicht funktioniert. Und selbst *mit* der kritischen Sektion verstehe ich ueberhaupt nicht, warum es einen Unterschied macht: der Worker-Thread benutzt ja gar keinen GTK-Code.
Geht mir genau so
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

So, bin schon fast fertig mit meinem Vorhaben, jedoch habe ich immer noch das Problem, dass mein Programm öfters mal nichts macht, erst nach dem ich eine Aktion durchführe (button click) fährt es fort.

Weiss jemand wieso das so ist?

Hier nochmals der kleine Entwurf: http://paste.pocoo.org/show/452465/
LivingOn
User
Beiträge: 33
Registriert: Montag 11. August 2008, 07:53

versuch es mal mit:

Code: Alles auswählen

...
import gobject
...
if __name__ == "__main__":
    gobject.theads_init()
    programm = MainWindow()
    programm.startGtk()
Bei der Gelegenheit könntest Du die als "deprecated" markierte Funktion gtk.timeout_add(...) gegen gobject.timeout(...) ersetzen ;-)
Yaso
User
Beiträge: 30
Registriert: Freitag 1. April 2011, 08:00

:mrgreen: Cool, danke jetzt klappt's!!
Bei der Gelegenheit könntest Du die als "deprecated" markierte Funktion gtk.timeout_add(...) gegen gobject.timeout(...) ersetzen
Ups, habs nicht gesehen, danke.
Antworten