Seite 1 von 4
Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 12:58
von Sophus
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?
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 13:14
von 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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 13:25
von Sophus
@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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 13:44
von 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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 14:41
von Hyperion
@Sophus: Hier mal das beste Beispiel der Welt:
https://github.com/lunaryorn/snippets/b ... rogress.py
(Ok, für Qt4 - aber afaik hat sich da grundsätzlich nichts geändert bei Qt 5)
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 15:26
von Sophus
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?
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 15:59
von Sirius3
@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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 16:02
von Sophus
@Sirius3: Meinst du, dass es an den Pfad liegt?
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 16:08
von 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…
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 16:18
von Sophus
@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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 16:47
von 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?
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 16:59
von Sophus
@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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 17:08
von 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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 17:28
von Sophus
@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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 17:51
von 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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 17:54
von Sophus
@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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 18:16
von Sophus
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.
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 19:29
von 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”).
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 19:52
von Sophus
@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?
Re: Threading für Anfänger
Verfasst: Donnerstag 23. April 2015, 21:50
von 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.