I am trying to start a Dialog including QListWidget. In this list every time a status is changed, the list should get a new line. At the end, if the backup process has finished, the dialog should terminate itself. Question: how I can update the status in the QListWidget during the process and how to close the dialog at the end without pushing buttons.
import random
import sys
import time
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QListWidgetItem, QMessageBox
class Backup_GUI(QDialog):
final_signal = QtCore.pyqtSignal(int)
def __init__(self):
super().__init__(parent=None)
self.setWindowTitle("Daily Backup ... ")
self.layout = QVBoxLayout()
self.info = QListWidget()
self.info.addItem(QListWidgetItem("Please insert USB drive ... "))
self.info.addItem(QListWidgetItem("Looking for new USB drive ... "))
self.info.setAutoScroll(True)
self.layout.addWidget(self.info)
self.setLayout(self.layout)
self.final_signal.connect(self.own_func)
self.show()
def own_func(self, backupcode):
if backupcode == 1:
self.info.addItem(QListWidgetItem(f"own_func() called; BackupCode {backupcode} ==> self.accept()"))
self.accept()
else:
self.info.addItem(QListWidgetItem(f"own_func() called; BackupCode {backupcode} ==> self.reject()"))
self.reject()
def getting_started(self):
self.info.addItem(QListWidgetItem(f"getting_started() called"))
self.check_usb_drive()
def check_usb_drive(self):
# code checking if the correct usb drive is connectected
# starting USB Monitor if no USB Drive is found
self.info.addItem(QListWidgetItem(f"check_usb_drive() called"))
self.info.addItem(QListWidgetItem(f"randomly time.sleep(random.randint(1,5))"))
time.sleep(random.randint(1, 5))
self.backup_routine()
def backup_routine(self):
# building backup-command for mariadbbackup
# copying files
# process of backing up
self.info.addItem(QListWidgetItem(f"backup_routine() called"))
self.info.addItem(QListWidgetItem(f"randomly time.sleep(random.randint(1,5))"))
time.sleep(random.randint(1, 5))
backup_ok = random.choice([True, False])
if backup_ok:
self.info.addItem(QListWidgetItem(f"Signal 1 emitted, backup ok"))
self.final_signal.emit(1)
else:
self.info.addItem(QListWidgetItem(f"Signal 0 emitted, backup not ok"))
self.final_signal.emit(0)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
backup_window = Backup_GUI()
backup_window.getting_started()
answ = backup_window.exec()
if answ == QMessageBox.Accepted:
print("all good")
else:
print("not so good")
print("finished and closed ... or not")
So I think I know the problem: I call getting_started() before doing the .exec() command. So technically the .accept/.reject signal emits in a moment, the dialog has not been shown/initialized/... - therefore I call self.show() in init.
Whatsoever - the dialog never closes.
Here I already work with the signal idea. When I added the random sleep lines I recognized, that the dialog is show (due to .show() in init) but nothing is prompted. Only after finishing all code, the lines are prompted. I am slowly getting the idea, why threading is proposed... I never did that before.
1 Answer 1
I don't know how you run backup code but I would run it in QThread and use signal to execute dlg.accept at the end of thread.
This way it doesn't block GUI and it can use exec() to display dialog.
It can also react on clicked buttons in this dialog.
class BackUpThread(QThread):
finished = pyqtSignal()
def run(self):
# code
self.finished.emit()
# ---
dialog = MyDialog()
thread = BackUpThread()
thread.finished.connect(dialog.accept)
thread.start()
dialog.exec()
I created minimal working code with dialog without buttons.
from PyQt5.QtWidgets import QApplication, QDialog, QLabel, QVBoxLayout
from PyQt5.QtCore import QThread, pyqtSignal
class BackUpThread(QThread):
finished = pyqtSignal()
def run(self):
import time
for i in range(5):
print(f"Long running code ... {i}")
time.sleep(1)
self.finished.emit()
class MyDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Working...")
layout = QVBoxLayout()
layout.addWidget(QLabel("Please wait ..."))
self.setLayout(layout)
if __name__ == "__main__":
app = QApplication([])
dialog = MyDialog()
thread = BackUpThread()
thread.finished.connect(dialog.accept)
thread.start()
dialog.exec()
print("Task finished and dialog closed")
And if you need to keep code inside dialog then
from PyQt5.QtWidgets import QApplication, QDialog, QLabel, QVBoxLayout
from PyQt5.QtCore import QThread, pyqtSignal
class BackUpThread(QThread):
finished = pyqtSignal()
def run(self):
import time
for i in range(5):
print(f"Long running code ... {i}")
time.sleep(1)
self.finished.emit()
class MyDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Working...")
layout = QVBoxLayout()
layout.addWidget(QLabel("Please wait ..."))
self.setLayout(layout)
def start_backup(self):
# thread has to be assigned to self.
self.thread = BackUpThread()
self.thread.finished.connect(self.accept)
self.thread.start()
if __name__ == "__main__":
app = QApplication([])
dialog = MyDialog()
dialog.start_backup()
dialog.exec()
print("Task finished and dialog closed")
EDIT:
I created version for new code from question.
It uses QThread to execute long-running code - so this doesn't block exec() and it can display QDialog correctly.
But QThread can't directly change GUI (it can't add QLabel) - so this needs another signal to send message to QDialog which add QLabel.
import random
import sys
import time
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import (
QDialog,
QVBoxLayout,
QListWidget,
QListWidgetItem,
QMessageBox,
)
class BackupThread(QtCore.QThread):
message = QtCore.pyqtSignal(str)
final_signal = QtCore.pyqtSignal(int)
def run(self):
self.getting_started()
def getting_started(self):
# self.info.addItem(QListWidgetItem(f"getting_started() called"))
self.message.emit("getting_started() called")
self.check_usb_drive()
def check_usb_drive(self):
# code checking if the correct usb drive is connectected
# starting USB Monitor if no USB Drive is found
# self.info.addItem(QListWidgetItem(f"check_usb_drive() called"))
self.message.emit("check_usb_drive() called")
value = random.randint(1, 5)
# self.info.addItem(QListWidgetItem(f"randomly time.sleep(random.randint(1,5))"))
self.message.emit(f"randomly time.sleep(random.randint(1,5)): {value}")
# time.sleep(value)
for i in range(value):
self.message.emit(f"sleeping: {i}")
time.sleep(1)
self.backup_routine()
def backup_routine(self):
# building backup-command for mariadbbackup
# copying files
# process of backing up
# self.info.addItem(QListWidgetItem(f"backup_routine() called"))
self.message.emit("backup_routine() called")
value = random.randint(1, 5)
# self.info.addItem(QListWidgetItem(f"randomly time.sleep(random.randint(1,5))"))
self.message.emit(f"randomly time.sleep(random.randint(1,5)): {value}")
# time.sleep(value)
for i in range(value):
self.message.emit(f"sleeping: {i}")
time.sleep(1)
backup_ok = random.choice([True, False])
if backup_ok:
# self.info.addItem(QListWidgetItem(f"Signal 1 emitted, backup ok"))
self.message.emit("Signal 1 emitted, backup ok")
self.final_signal.emit(1)
else:
# self.info.addItem(QListWidgetItem(f"Signal 0 emitted, backup not ok"))
self.message.emit("Signal 0 emitted, backup not ok")
self.final_signal.emit(0)
class BackupGUI(QDialog):
def __init__(self):
super().__init__(parent=None)
self.setWindowTitle("Daily Backup ... ")
self.layout = QVBoxLayout()
self.info = QListWidget()
self.info.addItem(QListWidgetItem("Please insert USB drive ... "))
self.info.addItem(QListWidgetItem("Looking for new USB drive ... "))
self.info.setAutoScroll(True)
self.layout.addWidget(self.info)
self.setLayout(self.layout)
# self.show()
def add_message(self, text):
self.info.addItem(QListWidgetItem(text))
def own_func(self, backupcode):
print("backupcode:", backupcode)
if backupcode == 1:
self.info.addItem(
QListWidgetItem(
f"own_func() called; BackupCode {backupcode} ==> self.accept()"
)
)
self.accept()
else:
self.info.addItem(
QListWidgetItem(
f"own_func() called; BackupCode {backupcode} ==> self.reject()"
)
)
self.reject()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
backup_window = BackupGUI()
backup_worker = BackupThread()
backup_worker.message.connect(backup_window.add_message)
backup_worker.final_signal.connect(backup_window.own_func)
backup_worker.start()
# backup_window.getting_started()
answ = backup_window.exec()
if answ == QMessageBox.Accepted:
print("all good")
else:
print("not so good")
print("finished and closed ... or not")
8 Comments
QThread so code doesn't block GUI. Even if you would run exec() then still your backup code may block GUI and it will freeze. But thread can make other problem: some GUIs don't like to work in thread (if GUI was created in one thread then other thread may not have access to GUI) and it may need to use signals to send message from other thread to main thread, an main thread may need to update widgets.QThread and it uses signal to send message to QDialog which add QLabel. This way backuping doesn't block GUI code.self.? You don't have to define them in __init__ but you can still use __init__(*args, **kwargs) in Thread, but you have to remeber to use super().__init__(*args, **kwargs). But maybe you shuld send values as parameters func_b(param1, param2) and send back with return param1, param2. Signals are needed to communicate betweeen main thread and backup thread.Explore related questions
See similar questions with these tags.
exec()function (which is also used in static functions of QDialog subclasses such as QMessageBox), the documentation for that very function also warns against its usage. Based on the code you provided, your attempts are inappropriate to begin with: callingshow()in the__init__or eventually callingaccept()orreject()ingetting_started()won't change anything as soon as »exec()right afterwards: since all the above calls are synchronous (done as soon as the previous call has returned), it doesn't really matter what you do beforeexec(), as that very call would simply show again the dialog anyway while simply ignoring anything done before. It would be like trying to show again the dialog after it's been closed. There are many ways to work around your issues (but, as said, without knowing more it's difficult to really tell you what would work better). For starters, you shouldn't try to implicitlyshow()the dialog to begin with. »getting_started()is blocking, then you should consider moving its implementation in a separate thread (assuming it's IO bound, not CPU bound). Then, what you could do is to internally callopen()using a properly parented single-shot QTimer object only in case the timer interval has elapsed, or simply delete the dialog if it not needed. The concept is similar to QProgressDialog, which is shown only after a certain amount of time after its creation. In any case, callingexec()after doing a blocking computation that could potentially ignore the dialog is simply inappropriate.