Threading für Anfänger

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute, ich versuche mich gerade an das Thema Thread heranzutasten. Ich möchte eine Datei vom Webserver herunterladen, und diesen Prozess über eine QProgressbar anzeigen lassen. Dies habe ich versucht wie folgt zu realisieren:

Code: Alles auswählen


import os
import sys
import requests
import threading


from PyQt4.QtGui import QDialog, QIcon
from PyQt4.uic import loadUi

class DownloadThread(threading.Thread):
    def __init__(self, url, update_window):
        threading.Thread.__init__(self)
        self.url = url
        self.update_window = update_window

    def run(self):
        location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
        file = requests.get(self.url, stream=True)

        file_size = int(requests.head(self.url).headers.get('content-length', None))

        chunk_size = (10000)
        downloaded_bytes = 0
        block_size = 1024*8

        with open(location, 'wb') as fd:
            for chunk in file.iter_content(chunk_size):
                fd.write(chunk)
                downloaded_bytes += block_size
                self.update_window.setProgress(float(downloaded_bytes)/file_size*100)
        print "Finish"
        return

class Update_Window(QDialog):
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)

        self.getPath_update = os.path.abspath(os.path.join('files', "qt_ui", 'pp_update.ui'))

        self.ui_pp_update = loadUi(self.getPath_update, self)

        self.ui_pp_update.setWindowModality(Qt.ApplicationModal)

        self.set_caption_buttons()
        self.create_actions_buttons()

    def on_update(self):
        DownloadThread('http://sophus.bplaced.net/download/example-app-0.3.win32.zip', self.ui_pp_update).start()

    def set_caption_buttons(self):
        self.ui_pp_update.pushButtonUpdate.setText("Update")
        self.ui_pp_update.pushButtonClose.setText("Cancel")

    def create_actions_buttons(self):
        self.ui_pp_update.pushButtonUpdate.clicked.connect(self.on_update)

    def set_ui_pp_update(self):
        self.progressBarUpdate.setAlignment(Qt.AlignCenter)
        self.progressBarUpdate.setRange(0, 1)

    def setProgress(self, value):
        if value > 100:
            value = 100
        self.progressBarUpdate.setValue(value)
Ich bekomme zwar keine Fehlermeldung, aber es tut sich auch nichts. Was habe ich hierbei übersehen?
BlackJack

@Sophus: Man darf die GUI nicht aus anderen Threads heraus verändern. Der Signal/Slot-Mechanismus von Qt ist aber thread-sicher, man kann also von einem anderen Thread aus Signale an den GUI-Thread schicken.

Extra noch eine HEAD-Abfrage abzusetzen obwohl die gleichen Informationen auch schon in der Antwort zur GET-Abfrage stehen ist etwas umständlich. Und `None` ist ein ungünstiger Defaultwert für etwas das man in eine ganze Zahl umwandeln möchte.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Wieso darf man eine GUI aus anderen Threads verändern?

Zum Studieren dieser Thematik habe ich mich an diese Information gerichtet: PySide downloading file with progress bar. Ich weiß, dass es sich hierbei um PySide handelt, jedoch ging ich davon aus, dass zwischen PySide und QT sehr marginale Unterschiede vorhanden sind, so dass ich mich problemlos an dieses "Tutorial" halten kann.
BlackJack

@Sophus: Man darf die GUI *nicht* aus anderen Threads verändern. Das ist nicht thread-sicher. Das kann ”funktionieren”, das kann gar nichts machen, oder Dein Programm kann abstürzen. Und zwar richtig, also nicht kontrolliert über eine Ausnahme sondern hart vom Betriebssystem aus beendet werden.

Das Problem mit dem verlinkten Tutorial ist nicht der tatsächlich kaum vorhandene Unterschied zwischen den beiden Qt-Anbindungen PyQt und PySide sondern das der Autor anscheinend keine Ahnung hat was er da tut. Das `os._exit()` weil er nicht weiss wie man Threads so konfiguriert das sie aufhören wenn der Hauptthread zuende ist, spricht auch nicht gerade für dieses Tutorial.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

@Sophus: Hier mal das beste Beispiel der Welt: https://github.com/lunaryorn/snippets/b ... rogress.py :mrgreen:

(Ok, für Qt4 - aber afaik hat sich da grundsätzlich nichts geändert bei Qt 5)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Hyperion und BlackJack, danke für die Hinweise, und für das beste Beispiel der Welt, Hyperion. Aber ich habe zu spät bemerkt, dass du mir ein Beispiel präsentierst, und währenddessen habe ich weiter an meinem Beispiel gebastelt, jedoch ohne Erfolg. Deswegen möchte ich hier meinen Schnipsel zeigen.

Code: Alles auswählen

FILE_NAME = "ui_pp_update.py"

import os
import sys
import requests
import time


from PyQt4.QtCore import QThread, Qt, pyqtSignal, QThread
from PyQt4.QtGui import QDialog
from PyQt4.uic import loadUi

class Update_Window(QDialog):
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)

        self.getPath_update = os.path.abspath(os.path.join('files', "qt_ui", 'pp_update.ui'))

        self.ui_pp_update = loadUi(self.getPath_update, self)

        self.ui_pp_update.setWindowModality(Qt.ApplicationModal)

        self.set_language_pp_ui_about()
        self.set_ui_pp_update()
        self.create_actions_buttons()

        self.download_task = Download_Thread()
        self.download_task.notify_progress.connect(self.on_download)

    def on_download(self, i):
        self.progressBarUpdate.setValue(i)
        self.download_task.start()

    def set_language_pp_ui_about(self):
        self.ui_pp_update.setWindowTitle("Update...")
        self.ui_pp_update.pushButtonUpdate.setText("Update")
        self.ui_pp_update.pushButtonClose.setText("Close")

    def create_actions_buttons(self):
        self.ui_pp_update.pushButtonUpdate.clicked.connect(self.on_download)
        self.ui_pp_update.pushButtonClose.clicked.connect(self.on_finished)

    def set_ui_pp_update(self):
        self.progressBarUpdate.setAlignment(Qt.AlignCenter)
        self.progressBarUpdate.setValue(0)

    def on_finished(self):
        self.progressBarUpdate.setValue(1)
        self.close()


class Download_Thread(QThread):
    def __init__(self):
        QThread.__init__(self)

    finished_thread = pyqtSignal()
    notify_progress = pyqtSignal(int)

    def run(self):

        location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
        url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'
        file = requests.get(url, stream=True)

        file_size = int(requests.head(self.url).headers.get('content-length', [0]))

        result = float(int(file_size)/(1024.0*1024.0))

        chunk_size = (10000)
        downloaded_bytes = 0
        block_size = 1024*8

        with open(location, 'wb') as fd:
            for chunk in file.iter_content(chunk_size):
                fd.write(chunk)
                downloaded_bytes += block_size
                self.notify_progress.emit(float(downloaded_bytes)/file_size*100)

        self.finished_thread.emit()
Um zu überprüfen, ob ich wirklich verstehe was ich da tue:
Zeile 27 und 28: Damit zwischen der GUI-Klasse und der Thread-Klasse eine Kommunikation stattfindet, wird die Thread-Klasse in der GUI-Klasse integriert, indem die Thread-Klasse dort zunächst instanziiert und anschließend mit der on_download()-Funktion verknüpft.

Zeile 56 und 57: In der Thread-Klasse werden die Signale definiert bzw. instanziiert. Denn wie BlackJacks mir den Hinweis gab, läuft alles über Signale und Slots ab.

Zeile 55-77: Hier wird der Download in der run()-Funktion abgewickelt.

Zeile 77: Hier kommuniziert der Thread mit der GUI, indem die errechneten Bytes, die bisher schon heruntergeladen wurden, prozentual in der Progressbar angezeigt werden kann.

Aber hier tut sich nichts. Keine Fehlermeldung, und auch keine (scheinbare) Reaktion. Wo liegt nun der Köder begraben?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: die Pfadbehandlung in der Zeile "self.getPath_update = os.path.abspath(os.path.join('files', "qt_ui", 'pp_update.ui'))" hatten wir doch schon an anderer Stelle mit Bildern, scheint aber nicht bis in dieses Skript vorgedrungen zu sein.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Meinst du, dass es an den Pfad liegt?
BlackJack

@Sophus: Also ich würde ja erwarten das man wenn man das von einem Terminal aus startet eine Ausnahme sehen wird wenn man auf die Schaltfläche klickt…
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ich habe mal eine Print-Anweisung angesetzt. Ergebnis:

Ausgabe:
Los: D:\Dan\Python\project_xarphus\temp\example-app-0.3.win32.zip
Der Pfad ist vollkommen richtig. Die zip-Datei kann jedoch noch nicht existieren, denn die Datei soll ja heruntergeladen werden.
BlackJack

@Sophus: Wie gesagt ich bin recht sicher das auf den klick auf die Schaltfläche eine Ausnahme kommen müsste. Entweder schon beim Aufruf der verbundenen Methode direkt oder gleich durch die erste Zeile in der Methode. Was wird denn da mit welchen Werten Deiner Meinung nach aufgerufen?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Keine Fehlermeldung vorhanden. Was genau möchtest du von mir abfragen?

Ich habe mal den Code etwas abgeändert. Nichts großartiges:

Code: Alles auswählen

 
import os
import sys
import requests
import time
 
 
from PyQt4.QtCore import QThread, Qt, pyqtSignal
from PyQt4.QtGui import QDialog
from PyQt4.uic import loadUi
 
class Update_Window(QDialog):
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
 
        self.getPath_update = os.path.abspath(os.path.join('files', "qt_ui", 'pp_update.ui'))
 
        self.ui_pp_update = loadUi(self.getPath_update, self)
 
        self.ui_pp_update.setWindowModality(Qt.ApplicationModal)
 
        self.set_language_pp_ui_about()
        self.set_ui_pp_update()
        self.create_actions_buttons()

        self.location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
        self.url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'
 
        self.download_task = Download_Thread(self.location, self.url)
        self.download_task.notify_progress.connect(self.on_download)
 
    def on_download(self, i):
        self.progressBarUpdate.setValue(i)
        self.download_task.start()
 
    def set_language_pp_ui_about(self):
        self.ui_pp_update.setWindowTitle("Update...")
        self.ui_pp_update.pushButtonUpdate.setText("Update")
        self.ui_pp_update.pushButtonClose.setText("Close")
 
    def create_actions_buttons(self):
        self.ui_pp_update.pushButtonUpdate.clicked.connect(self.on_download)
        self.ui_pp_update.pushButtonClose.clicked.connect(self.on_finished)
 
    def set_ui_pp_update(self):
        self.progressBarUpdate.setAlignment(Qt.AlignCenter)
        self.progressBarUpdate.setValue(0)
 
    def on_finished(self):
        self.progressBarUpdate.setValue(1)
        self.close()
 
 
class Download_Thread(QThread):
    finished_thread = pyqtSignal()
    notify_progress = pyqtSignal(int)

    def __init__(self, loc, link):
        QThread.__init__(self)

        self.url = link
        self.location = loc
 
    def run(self):

        file = requests.get(url, stream=True)
 
        file_size = int(requests.head(self.url).headers.get('content-length', [0]))
 
        result = float(int(file_size)/(1024.0*1024.0))
 
        chunk_size = (10000)
        downloaded_bytes = 0
        block_size = 1024*8
 
        with open(location, 'wb') as fd:
            for chunk in file.iter_content(chunk_size):
                fd.write(chunk)
                downloaded_bytes += block_size
                self.notify_progress.emit(float(downloaded_bytes)/file_size*100)
 
        self.finished_thread.emit()
Was hat sich geändert? Ich habe in Zeile 26 und 27 die Daten wie Pfad und URL dorthin platziert, so dass ich diese Daten in Form eines Parameters an die Thread-Klasse übergebe. Und weil Python irgendwie Probleme hat bei den Signalen das Attribut connect zu erkennen, so habe ich in Zeile 55 und 56 die Instanziierung der Signale vorgenommen, also noch vor dem Konstrukteur. Ansonsten alles wie beim Alten. Keine Fehlermeldung - sowohl über PyCharm als auch über den hauseigenen IDLE.
BlackJack

@Sophus: Was ich abfragen möchte habe ich bereits gefragt, bekomme anscheinend nur keine Antwort. Was passiert wenn man auf die Schaltfläche zum aktualisieren drückt? Welche Methode wird dann aufgerufen und mit welchen Werten als Argumenten. Dann sollte Dir hoffentlich auffallen dass das nicht funktionieren kann was da steht und das müsste eigentlich eine Ausnahme geben, die auf der Konsole landet. Dazu muss man das Programm natürlich auch dort starten.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Kommunikation geschieht auf zwei Ebenen, einmal das was du sagst, und einmal was ich verstehe. Sender-Empfänger-Prinzip. Und wenn ich dich nicht verstehe, dann stimmt was an der Kommunikation nicht. Einfaches und gängiges Prinzip in der Psychologie :-) Irgendwie logisch, mhmh? :-)

Ich habe jetzt mal meine Anwendung nun als ausführbar umgebaut, damit ihr selbst testen könnt, dass dort keine Fehlermeldungen auftauchen:

Code: Alles auswählen

import os
import requests
import sys

from PyQt4.QtCore import QThread, pyqtSignal, Qt
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication

class MyCustomDialogt(QDialog):

    def __init__(self, parent=None):
        super(MyCustomDialogt, self).__init__(parent)
        layout = QVBoxLayout(self)

        # Create a progress bar and a button and add them to the main layout
        self.progressBar = QProgressBar(self)
        self.progressBar.setAlignment(Qt.AlignCenter)
        self.progressBar.setValue(0)
        self.progressBar.setRange(0, 1)
        layout.addWidget(self.progressBar)

        button = QPushButton("Start", self)
        layout.addWidget(button)      

        button.clicked.connect(self.check_folder_exists)

        # Set data for download and saving in path
        self.location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
        self.url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'

        self.download_task = Download_Thread(self.location, self.url)
        self.download_task.notify_progress.connect(self.on_progress)

    def on_progress(self, i):
        self.progressBar.setValue(i)

    def on_start(self):
        self.download_task.start()

    def check_folder_exists(self):
        location = os.path.abspath(os.path.join('temp'))
        if not os.path.exists(location):
            os.makedirs(location)
            print "Folder was created"
            self.on_start()
        else:
            print "Folder already exists"
            self.on_start()

    def onFinished(self):
        # Stop the pulsation
        self.progressBar.setRange(0, 1)


class Download_Thread(QThread):
    finished_thread = pyqtSignal()
    notify_progress = pyqtSignal(int)

    def __init__(self, loc, link):
        QThread.__init__(self)

        self.url = link
        self.location = loc

    def run(self):
        file = requests.get(self.url, stream=True)
        file_size = int(requests.head(self.url).headers.get('content-length', None))


        chunk_size = (10000)
        downloaded_bytes = 0
        block_size = 1024*8

        with open(self.location, 'wb') as fd:
            for chunk in file.iter_content(chunk_size):
                fd.write(chunk)
                downloaded_bytes += block_size
                self.notify_progress.emit(float(downloaded_bytes)/file_size*100)

        print "Finish"
        self.finished_thread.emit()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyCustomDialogt()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
Beim Klick auf button wird die check_folder_exists()-Funktion aufgerufen. Hier wird geschaut, ob es den Ordner gibt, wenn nicht, dann erstellen, ansonsten brauchen wir nichts erstellen. In beiden Fällen wird dann die on_start()-Funktion aufgerufen. Dabei wird dann der Thread (download_task) mit der start()-Methode gestartet. Dabei wird dem Thread einige Argumente (self.location und self.url) in Form eines Parameters übergeben. Damit der Fortschritt im Progressbar angezeigt werden kann, ist das download_task-Attribut mit der on_progress()-Funktion verbunden. Und über das Argument i soll der Wert in der Progressbar entsprechend ständig übergeben werden.
BlackJack

@Sophus: Sorry aber da bin ich mir jetzt 100% sicher das ein Fehler kommen *muss*. Den ``self.on_start(i)`` *muss* daran scheitern das der Name `i` nicht definiert ist und damit wird diese Methode nicht aufgerufen. Das ist jetzt echt total offensichtlich. Beim Quelltext davor wäre das Problem übrigens gewesen das für das `i` bei der Methode nichts übergeben wurde und damit der Aufruf scheitern muss. Wenn Du solche Fehler nicht selber findest dann weiss ich nicht wie man Dir sinnvoll helfen soll, denn das sind Sachen die beim Ausführen auffallen. Da muss man nicht wirklich nach suchen, da werden Tracebacks für ausgegeben.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ein Blick in meinem Beitrag, und du siehst, dass alles behoben wurde. Das mit dem i habe ich bereits selbst gefunden. Und ich habe die Funktion on_progress hinzugefügt. Also on_start startet den Thread und in on_progress erwarte ich, dass der Prozessbalken sich entsprechend aktualisiert. Tut er aber nicht.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich weiß zwar nicht ob ich die Lösung korrekt gefunden habe, aber der Prozessbalken bewegt sich jetzt:

Code: Alles auswählen

import os
import requests
import sys

from PyQt4.QtCore import QThread, pyqtSignal, Qt
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication

class MyCustomDialog(QDialog):

    def __init__(self, parent=None):
        super(MyCustomDialog, self).__init__(parent)
        layout = QVBoxLayout(self)

        # Create a progress bar and a button and add them to the main layout
        self.progressBar = QProgressBar(self)
        self.progressBar.setAlignment(Qt.AlignCenter)
        #self.progressBar.setValue(0)
        #self.progressBar.setRange(0, 1)
        layout.addWidget(self.progressBar)

        button = QPushButton("Start", self)
        layout.addWidget(button)

        button.clicked.connect(self.check_folder_exists)

        # Set data for download and saving in path
        self.location = os.path.abspath(os.path.join('temp', 'example-app-0.3.win32.zip'))
        self.url = 'http://sophus.bplaced.net/download/example-app-0.3.win32.zip'

        self.download_task = Download_Thread(self.location, self.url)
        self.download_task.notify_progress.connect(self.on_progress)

    def on_progress(self, i):
        self.progressBar.setValue(i)

    def on_start(self):
        self.download_task.start()

    def check_folder_exists(self):
        location = os.path.abspath(os.path.join('temp'))
        if not os.path.exists(location):
            os.makedirs(location)
            print "Folder was created"
            self.on_start()
        else:
            print "Folder already exists"
            self.on_start()

    def onFinished(self):
        # Stop the pulsation
        self.progressBar.setRange(0, 1)


class Download_Thread(QThread):
    finished_thread = pyqtSignal()
    notify_progress = pyqtSignal(int)

    def __init__(self, loc, link):
        QThread.__init__(self)

        self.url = link
        self.location = loc

    def run(self):
        print self.url
        print self.location
        file = requests.get(self.url, stream=True)
        file_size = int(requests.head(self.url).headers.get('content-length', [0]))


        chunk_size = (10000)
        downloaded_bytes = 0
        block_size = 1024*8

        with open(self.location, 'wb') as fd:
            for chunk in file.iter_content(chunk_size):
                fd.write(chunk)
                downloaded_bytes += block_size
                print (float(downloaded_bytes)/file_size*100)
                self.notify_progress.emit(float(downloaded_bytes)/file_size*100)

        print "Finish"
        self.finished_thread.emit()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
Ich habe hier Zeile 17 und 18 auskommentiert, und schon macht der Balken was er soll. Aber wie gesagt, ob es nun korrekt ist? Ich bin mir deswegen nicht sicher, weil BlackJack und Sirius3 die ganze Zeit von anderen Problemen sprachen.
BlackJack

@Sophus: Was soll sich denn bei den Werten 0 und 1 auch grossartig bewegen‽ Gibt ja nur zwei mögliche Werte, 0 (der Balken ist ”leer”) und 1 (der Balken ist 100% ”voll”).
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ach, das meintest du vorhin mit Werte? Aber ich habe ein weiteres Problem. Eher ein mathematisches Problem. Wie kriege ich das korrekt auf dem Balken angezeigt, sprich, die herunterzuladende Datei ist sagen wir mal 65 MB groß. Bei meinem Beispiel hält mein Programm immer bei 81 Prozent an. Das heißt, die Datei ist komplett heruntergeladen, und der Prozessbalken müssten dann auch 100 Prozent anzeigen und nicht 81 Prozent?Hättest du da eine Idee?
BlackJack

@Sophus: Ich würde ”mathematisch” gar nichts (selber) machen. Man kann den Wertebereich für den Balken ja beliebig setzen. Zum Beispiel auch auf 0 und die Dateigrösse in Bytes. Und dann als aktuellen Wert einfach die aktuelle Anzahl von Bytes setzen.
Antworten