QtCore.pyqtSignal mit eigenen Klassen bestücken

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Hallo allerseits,

ich bin gerade dabei mi die new style Methoden der Signalverarbeitung von Qt anzugucken.

Dabei will ich jetzt folgendes machen:

Code: Alles auswählen

class ModelFiller(QtCore.QThread):
    
    # Initialize various signals.
    progress = QtCore.pyqtSignal(ModelFiller)
Wenn ich das richtig verstanden haben soll ich als Parameter von pyqtSignal() gerade die Typen reinschreiben, wie zum Beispiel int.

Leider führt der obige Code zu einem NameError, der sagt: "name 'ModelFiller' is not defined"

Wie kann ich das umgehen? Beim old Style musste ich einfach eine Insatz übergeben, die ja leider zur Zeit der Definition im obigen Beispiel ja noch nicht exisitert.

Vielen Dank für eure Hilfe!

Grüße,
anogayales
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

So,

habs rausgefunden:
Das ganze statt zum Teil im "old style" Teil der Dokumentation, was mich ein bisschen verwundert

Code: Alles auswählen

class ModelFiller(QtCore.QThread):
    
    # Initialize various signals.
    progress = QtCore.pyqtSignal("PyQt_PyObject")
Wichtig!
Man darf old-style und new-style Signalpassing nicht vermischen, das gibt ärger!
lunar

Die Lösung für das Problem ist doch trivial. Du musst mit der Deklaration des Signals eben warten, bis die Klasse erzeugt ist:

Code: Alles auswählen

class ModelFiller(QThread):
    # …

ModelFiller.progress = pyqtSignal(ModelFillter)
Falls Du damit einfach nur das Sender-Exemplar an den Slot durchreichen möchtest, ist das allerdings unnötig. In von QObject abgeleiteten Klassen kannst Du im Slot einfach "self.sender()" aufrufen, um den Auslöser des Signals zu erhalten. Alternativ kannst Du partielle Funktionen verwenden:

Code: Alles auswählen

filler.progress.connect(functools.partial(bar.progress_slot, filler)))
Das ist der schönere Ansatz, denn er nutzt die Möglichkeiten von Python und vermeidet, dass man immer explizit "self" beim Auslösen des Signals übergeben muss.

Ohne die spezielle Situation zu kennen, würde ich allerdings pauschal sagen, dass so ein "Durchgriff" an den Sender ein Design Smell sein könnte, und vielleicht auf ungenügende Kapselung der Komponenten schließen lässt. Im Idealfall sollte dem Empfänger der Sender vollkommen unbekannt und egal sein.

PS: Dem Namen nach geht es darum, ein Modell asynchron zu befüllen. Das ist eine Situation, in der C++ tatsächlich von Vorteil sein könnte, da man solche asynchronen Aufgaben mit QtConcurrent mitunter sehr elegant ausdrücken kann. QtConcurrent ist unter Python allerdings nicht verfügbar ...
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Vielen Dank!

Ja es geht darum mehrere Tabellen die jeweils ihr eigenes Model haben zur Laufzeit über den Modelfiller zu füllen.

Vielleicht kannst du mir bei der beseitigung des code smells helfen. Ich starte mehrer Threads und diese benachrichtigen eine progressbar.

Die progressbar speichert sich in einem dictionary von welchem thread sie wie viel aufgaben zu lösen hat und passt dementsprechend ihre anzeige an.

Am ende des Befüllens wird dann

Code: Alles auswählen

 self.finished.emit(self)  
aufgerufen und die progressbar nimmt den Thread aus dem dictionary raus.

Wie würdet ihr das Problem lösen? Mit Timern arbeite ich auch noch nicht, ich denke dir wäre hier angebracht, nur wo und wie :)

Grüße,
anogayales
lunar

Du hast einen(!) Fortschrittsbalken für mehrere Threads bzw. Prozesse?
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Korrekt

In den Threads wird jeweils eine längere Operation durchgeführt und die progressbar zeigt nunmal an wie weit alle threads voran geschritten sind. Was ist daran so abwegig?
lunar

Ich kann mir nicht vorstellen, dass Du das vernünftig implementiert hast. Schließlich muss bei diesem Ansatz jeder Thread wissen, wie weit alle anderen Threads fortgeschritten sind, damit der Balken korrekt aktualisiert werden kann.
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Nein, das verwaltet alles die Progressbar, das ganze sieht so aus:

Das sind alles Methoden der Progressbar:

Hiermit meldet sich der Thread an:

Code: Alles auswählen

    def update_bar(self, thread, current, maximum):        
        self.workers[thread] = [current, maximum]
        self.refresh_progressbar()
Und diese Methode ruft der Thread während er arbeitet auf:

Code: Alles auswählen

    def refresh_progressbar(self):
        current, maximum = 0, 0
        for item in self.workers.items():
            current += item[1][0]
            maximum += item[1][1]

        self.setValue(current)
        self.setMaximum(maximum) 
Und das funktioniert auch soweit, ich würde eben nur wissen, wie ich das ganze ohne code smell implementieren könnte.
lunar

Ich hab in meinem ersten Beitrag im Konjunktiv gesprochen. Es kann, aber es muss auch eben nicht schlecht sein, wenn man auf den Sender eines Signals zugreift. Solange Dir klar ist, dass die Komponenten so recht eng koppelst, halte ich das nicht für problematisch. Du wirst schon wissen, was Du tust :)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@anogayales:
Kannst Du mal zeigen, wie die Threads den Aufruf von 'update_bar()' und 'refresh_progressbar()' auslösen? (Stichwort Threadsicherheit)
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

Das ganze sieht, ein wenig vereinfacht, so aus:

Code: Alles auswählen

    def run(self):     
        counter = 0

        # Make the progress bar idle
        self.waiting.emit()

        for item, maximum_number in self.model.generator:            
            self.model.add_item(item)
            # update every 8th item
            if counter % 8 == 0:
                self.progress.emit(self, counter, maximum_number)   
            counter += 1     
     
        self.finished.emit(self)
Was hier der eigentlich code smell ist, dass jeder Thread progress emitiert und daraufhin update_bar aufgerufen wird, also:

Code: Alles auswählen

job.progress.connect(self.progressbar.update_bar, type = QtCore.Qt.QueuedConnection)
Irgendwelche Einwände? Mir ist schon klar, dass jeder thread die update_bar methode aufruft, unter umständen wenige millisekunden hinter einander, was die gui ein wenig träge wirken lässt. Habt ihr für das Problem eine Idee?

Grüße,
anogayales
Benutzeravatar
DaMutz
User
Beiträge: 202
Registriert: Freitag 31. Oktober 2008, 17:25

ich frage mich ob es wirklich nötig ist, bei jedem update den thread, den current Wert und den maximal Wert zu übergeben. Wäre es nicht einfacher zu Begin das Maximum zu übermitteln und alle maxima zusammenzuzählen und dann jeweils die Zunahme (delta) zu übertragen. Dadurch würde doch alles viel einfacher.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Du könntest refresh_progressbar() aus update_bar() herausnehmen und via QTimer regelmäßig updaten lassen.

refresh_progressbar() ließe sich auch kürzer schreiben:

Code: Alles auswählen

    def refresh_progressbar(self):
        current, maximum = map(sum, zip(*self.workers.values()))
        self.setValue(current)
        self.setMaximum(maximum)
anogayales
User
Beiträge: 456
Registriert: Mittwoch 15. April 2009, 14:11

DaMutz hat geschrieben:ich frage mich ob es wirklich nötig ist, bei jedem update den thread, den current Wert und den maximal Wert zu übergeben. Wäre es nicht einfacher zu Begin das Maximum zu übermitteln und alle maxima zusammenzuzählen und dann jeweils die Zunahme (delta) zu übertragen. Dadurch würde doch alles viel einfacher.
Das Problem ist, dass ich den maximalen Wert erst durch self.model.generator bekomme. Natürlich würde ich das irgendwie aufspalten können, aber dazu habe ich ehrlich gesagt momentan keine Lust :P

Das mit dem QTimer werde ich mir jetzt mal angucken.

Grüße,
anogayales
Antworten