PyQT5: how to add custom signal and slot? - python

i want to change slider position, when the self.factor is changed during set_parameter(). More precisely, how to update slider position as the value changes during computation?
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QSlider, QShortcut
from PyQt5.QtGui import QKeySequence
class Params():
def __init__(self):
self.factor = 0
def get_parameter(self):
return self.factor
def set_parameter(self, val):
self.factor =val
class SliderWidget(QWidget):
def __init__(self, param: Params):
QWidget.__init__(self)
self.params = param
layout = QVBoxLayout()
self.setLayout(layout)
self.slider = QSlider()
self.slider.valueChanged.connect(self.on_param_change)
layout.addWidget(self.slider)
self.slider.setMinimum(4)
self.slider.setMaximum(8)
def on_param_change(self):
value = self.params.get_parameter()
self.slider.setValue(value)
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.box = QVBoxLayout()
self.params = Params()
self.scene = SliderWidget(self.params)
self.setCentralWidget(self.scene)
self.shortcutUp = QShortcut(QKeySequence('Ctrl+W'), self)
self.shortcutDw = QShortcut(QKeySequence('Ctrl+S'), self)
self.shortcutUp.activated.connect(self.up)
self.shortcutDw.activated.connect(self.down)
def up(self):
print('up')
value = (self.params.get_parameter() + 1)
self.params.set_parameter(value)
def down(self):
print('down')
value = (self.params.get_parameter() -1)
self.params.set_parameter(value)
import sys
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
app.exec_()

You need to go through threading.
There is an exact procedure to achieve threading using PyQt5.
If you are not following it correctly, the application will crash without any error messages.
You will need the QThread lib and create workers to overwrite run method of QThread.
Here some link describing this:
https://www.youtube.com/watch?v=G7ffF0U36b0
https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
Here is the code with buttons:
from PyQt5.QtCore import QObject, QThread, pyqtSignal
class Worker(QObject): # Step 1: Create a worker class
finished = pyqtSignal()
progress = pyqtSignal(int)
def run(self):
"""Long-running task."""
for i in range(5):
sleep(1)
self.progress.emit(i + 1)
self.finished.emit()
class Window(QMainWindow):
# Snip...
def runLongTask(self):
# Step 2: Create a QThread object
self.thread = QThread()
# Step 3: Create a worker object
self.worker = Worker()
# Step 4: Move worker to the thread
self.worker.moveToThread(self.thread)
# Step 5: Connect signals and slots
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.reportProgress)
# Step 6: Start the thread
self.thread.start()
# Final resets
self.longRunningBtn.setEnabled(False)
self.thread.finished.connect(
lambda: self.longRunningBtn.setEnabled(True)
)
self.thread.finished.connect(
lambda: self.stepLabel.setText("Long-Running Step: 0")
)

Using PySide2 (which is basically the same as PyQt5), I created this minor example. It displays a single QSlider. If you press Ctrl+W or Ctrl+S, the QShortcut is activated and it respectively increases or decreases the QSlider by +1 or -1.
Here's the script:
from PySide2.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QSlider, QShortcut
from PySide2.QtGui import QKeySequence
class Scene(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout()
self.setLayout(layout)
self.slider = QSlider()
layout.addWidget(self.slider)
self.slider.setMinimum(4)
self.slider.setMaximum(8)
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.scene = Scene()
self.setCentralWidget(self.scene)
self.shortcutUp = QShortcut(QKeySequence('Ctrl+W'), self)
self.shortcutDw = QShortcut(QKeySequence('Ctrl+S'), self)
self.shortcutUp.activated.connect(self.up)
self.shortcutDw.activated.connect(self.down)
def up(self):
print('up')
self.scene.slider.setValue(self.scene.slider.value()+1)
def down(self):
print('down')
self.scene.slider.setValue(self.scene.slider.value()-1)
if __name__ == '__main__':
app = QApplication()
win = Window()
win.show()
app.exec_()
I used a QShortcut so I didn't have to create a menubar, and set a shortcut there. It makes the exmaple smaller but with the same behaviour.

Related

PyQT5: How to run an arbitrary Callable in a separate QThread, using the same worker definition?

How can I define a worker so that I can run an arbitrary Callable in a separate QThread?
I tried using lambda expression to pass arguments at run-time, but this does not work (worker still runs in the main thread), see code below, method run_in_separate_thread:
import sys
from time import sleep
from typing import Callable
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget, QPlainTextEdit,
)
from PyQt5.QtCore import QObject, QThread, pyqtSignal
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi()
def setupUi(self):
self.resize(300, 150)
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.text_edit = QPlainTextEdit()
self.bt = QPushButton("Run in separate thread", self)
layout = QVBoxLayout()
layout.addWidget(self.text_edit)
layout.addWidget(self.bt)
self.centralWidget.setLayout(layout)
self.bt.clicked.connect(lambda: self.run_in_separate_thread(my_long_function))
def report_progress(self, text: str) -> None:
""" Report progress of function in separate thread """
self.text_edit.appendPlainText(str(text))
def run_in_separate_thread(self, function: Callable) -> None:
""" Wrapper to run a function in a separate thread """
self.thread = QThread(objectName="workerThread")
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.report_progress)
self.thread.started.connect(lambda: self.worker.run(function))
self.thread.start()
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(str)
def run(self, function: Callable):
function(self.progress)
self.finished.emit()
def my_long_function(signal):
print(f"mylongfunction thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
for _ in range(5):
sleep(1)
signal.emit("hello")
app = QApplication(sys.argv)
QThread.currentThread().setObjectName('main')
print(f"Main thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
win = Window()
win.show()
sys.exit(app.exec())
This gives me the following output, and no "live" output is shown in the QTextEdit.
Main thread: ('main', 139754959259456)
mylongfunction thread: ('main', 139754959259456)
Is it because lambda expression "live" in the Main thread?
If I hard code my_long_function in the worker run() method and remove all lambdas expressions, it works as intended, see code below:
import sys
from time import sleep
from typing import Callable
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget, QPlainTextEdit,
)
from PyQt5.QtCore import QObject, QThread, pyqtSignal
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi()
def setupUi(self):
self.resize(300, 150)
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.text_edit = QPlainTextEdit()
self.bt = QPushButton("Run in separate thread", self)
layout = QVBoxLayout()
layout.addWidget(self.text_edit)
layout.addWidget(self.bt)
self.centralWidget.setLayout(layout)
self.bt.clicked.connect(self.run_in_separate_thread)
def report_progress(self, text: str) -> None:
""" Report progress of function in separate thread """
self.text_edit.appendPlainText(str(text))
def run_in_separate_thread(self) -> None:
""" Wrapper to run a function in a separate thread """
self.thread = QThread(objectName="workerThread")
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.report_progress)
self.thread.started.connect(self.worker.run)
self.thread.start()
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(str)
def run(self):
self.my_long_function()
self.finished.emit()
def my_long_function(self):
print(f"mylongfunction thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
for _ in range(5):
sleep(1)
self.progress.emit("hello")
def my_long_function(signal):
print(f"mylongfunction thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
for _ in range(5):
sleep(1)
signal.emit("hello")
app = QApplication(sys.argv)
QThread.currentThread().setObjectName('main')
print(f"Main thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
win = Window()
win.show()
sys.exit(app.exec())
This gives following desired output:
Main thread: ('main', 139687209740096)
mylongfunction thread: ('workerThread', 139686815192832)
OK here is a solution, by defining the Callable as an instance variable of the worker:
With this the worker is properly running in a separate thread.
full code:
import sys
from time import sleep
from typing import Callable
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget, QPlainTextEdit,
)
from PyQt5.QtCore import QObject, QThread, pyqtSignal
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi()
def setupUi(self):
self.resize(300, 150)
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.text_edit = QPlainTextEdit()
self.bt = QPushButton("Run in separate thread", self)
layout = QVBoxLayout()
layout.addWidget(self.text_edit)
layout.addWidget(self.bt)
self.centralWidget.setLayout(layout)
self.bt.clicked.connect(lambda: self.run_in_separate_thread(my_long_function))
def report_progress(self, text: str) -> None:
""" Report progress of function in separate thread """
self.text_edit.appendPlainText(str(text))
def run_in_separate_thread(self, f: Callable) -> None:
""" Wrapper to run a function in a separate thread """
self.thread = QThread(objectName="workerThread")
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.report_progress)
self.thread.started.connect(self.worker.run)
self.worker.task = f
self.thread.start()
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(str)
task = None
def run(self):
self.task(self.progress)
self.finished.emit()
def my_long_function(signal):
print(f"mylongfunction thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
for _ in range(5):
sleep(1)
signal.emit("hello")
app = QApplication(sys.argv)
QThread.currentThread().setObjectName('main')
print(f"Main thread: {QThread.currentThread().objectName(), int(QThread.currentThreadId())}")
win = Window()
win.show()
sys.exit(app.exec())

What is the proper way of opening a child dialog in a second thread in PyQt when it is previously onknown at what point the dialog will be opened?

I have an application where I run some process in a second thread. At some points during this process, given a certain condition is met, another dialog window opens, which halts the process until you confirm something. This causes the following error message:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x1f9c82383d0), parent's thread is QThread(0x1f9c7ade2a0), current thread is QThread(0x1f9c8358800)
Interestingly, if you also move your cursor over the MainWindow while the process is running, and before the new dialog pops up, it also produces this error message a couple of times:
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
Very strange. Because it only occurs if you move your cursor over the MainWindow.
Now, in my application, I actually load an interface for the new dialog that pops up using PyQt5.uic.loadUi, and this hasn't caused any problems. However, when I was creating the example for this post, another issue occurred, due to the fact that I was setting the layout of the new dialog during its initialization:
QObject::setParent: Cannot set parent, new parent is in a different thread
Which results in the application crashing:
Process finished with exit code -1073741819 (0xC0000005)
I'm obviously doing something wrong here regarding the threading I would guess, but I don't know what. I am especially baffled by the fact that I cannot set the layout of the new dialog during its initialization, while using loadUi is totally fine. Here is my example code:
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import (
QDialog, QApplication, QPushButton, QGridLayout, QProgressBar, QLabel
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('pass variable')
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
def run(self):
result = None
i = 0
while i < 100 and self._isRunning:
if i == np.random.randint(0, 100):
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('test')
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel('Result:')
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.run)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f'Result: {result}')
self.pbar.setValue(0)
self.handler.stop()
self.handler.progress.disconnect(self.progress)
self.handler.finished.disconnect(self.finisher)
self.handler_thread.started.disconnect(self.handler.run)
self.handler_thread.terminate()
self.handler = None
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
EDIT
I forgot to mention that I already found this post, which may be related to my problem, however, I don't undestand the reasoning of the solution in the top answer, and more importantly, I don't speak what I believe is C++.
If we analyze the case before equality happens it is the same case after that equality happens unless the initial progress is different, with this it indicates that when equality happens a signal must be sent to the GUI to ask for the information, and when you have that information, launch the same task but with an initial progress equal to what you had before the signal was emitted.
from functools import partial
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import (
QDialog,
QApplication,
QPushButton,
QGridLayout,
QProgressBar,
QLabel,
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("pass variable")
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal()
resultChanged = pyqtSignal(int)
requestSignal = pyqtSignal()
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
self.iter = 0
self.result = None
#pyqtSlot()
def start(self):
self._isRunning = True
self._success = False
self.iter = 0
self.result = None
self.task()
#pyqtSlot()
#pyqtSlot(int)
def task(self, value=None):
if value is not None:
self.result = value
while self.iter < 100 and self._isRunning:
if self.iter == np.random.randint(0, 100):
self.requestSignal.emit()
return
time.sleep(0.01)
self.iter += 1
self.progress.emit(self.iter)
if self.iter == 100:
self._success = True
if self.result:
self.resultChanged.emit(self.result)
self.finished.emit()
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("test")
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel("Result:")
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.resultChanged.connect(self.on_result_changed)
self.handler.finished.connect(self.on_finished)
self.handler.requestSignal.connect(self.onRequestSignal)
self.handler_thread.started.connect(self.handler.start)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def onRequestSignal(self):
dialog = SpecialDialog()
dialog.exec_()
wrapper = partial(self.handler.task, dialog.variable)
QTimer.singleShot(0, wrapper)
#pyqtSlot(int)
def on_result_changed(self, result):
self.result = result
self.resultLabel.setText(f"Result: {result}")
#pyqtSlot()
def on_finished(self):
self.pbar.setValue(0)
self.handler.stop()
self.handler_thread.quit()
self.handler_thread.wait()
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())

What is the proper way of opening a child dialog in a second thread in PyQt?

I have an application where I run some process in a second thread and at some point, given a certain condition, another dialog window opens, which halts the process until you confirm something. This causes the following error message:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x1f9c82383d0), parent's thread is QThread(0x1f9c7ade2a0), current thread is QThread(0x1f9c8358800)
Interestingly, if you also move your cursor over the MainWindow while the process is running, and before the new dialog pops up, it also produces this error message a couple of times:
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
Very strange. Because it only occurs if you move your cursor over the MainWindow.
Now, in my application, I actually load an interface for the new dialog that pops up using PyQt5.uic.loadUi, and this hasn't caused any problems. However, when I was creating the example for this post, another issue occurred, due to the fact that I was setting the layout of the new dialog during its initialization:
QObject::setParent: Cannot set parent, new parent is in a different thread
Which results in the application crashing:
Process finished with exit code -1073741819 (0xC0000005)
I'm obviously doing something wrong here regarding the threading I would guess, but I don't know what. I am especially baffled by the fact that I cannot set the layout of the new dialog during its initialization, while using loadUi is totally fine. Here is my example code:
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import (
QDialog, QApplication, QPushButton, QGridLayout, QProgressBar, QLabel
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('pass variable')
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
def run(self):
result = None
i = 0
while i < 100 and self._isRunning:
if i == 50:
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('test')
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel('Result:')
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.run)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f'Result: {result}')
self.pbar.setValue(0)
self.handler.stop()
self.handler.progress.disconnect(self.progress)
self.handler.finished.disconnect(self.finisher)
self.handler_thread.started.disconnect(self.handler.run)
self.handler_thread.terminate()
self.handler = None
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
EDIT
I forgot to mention that I already found this post, which may be related to my problem, however, I don't undestand the reasoning of the solution in the top answer, and more importantly, I don't speak what I believe is C++.
You cannot create or modify a GUI element from a secondary thread and this is signaled by the error message.
You have to redesign the Handler class, with your requirement you must divide run into 2 methods, the first method will generate progress up to 50% where the GUI will open the dialogue, obtain the result and launch the second method.
import sys
import time
import numpy as np
from functools import partial
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import (
QDialog,
QApplication,
QPushButton,
QGridLayout,
QProgressBar,
QLabel,
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("pass variable")
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
#pyqtSlot()
def task1(self):
i = 0
while i <= 50 and self._isRunning:
time.sleep(0.01)
i += 1
self.progress.emit(i)
#pyqtSlot(int)
def task2(self, result):
i = 50
while i < 100 and self._isRunning:
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("test")
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel("Result:")
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.task1)
self.handler_thread.start()
#pyqtSlot(int)
def progress(self, val):
self.pbar.setValue(val)
if val == 50:
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
wrapper = partial(self.handler.task2, result)
QTimer.singleShot(0, wrapper)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f"Result: {result}")
self.pbar.setValue(0)
self.handler.stop()
self.handler_thread.quit()
self.handler_thread.wait()
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())

Implement QThread with QProgressBar in PySide (or PyQt) during calculation

I would like to know how to implement QProgressBar, which shows the progress of calculation in main thread.
Please refer to below codes.
import sys
from PySide2.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QProgressBar
from PySide2.QtCore import QThread
class BarThread(QThread):
# Progress Bar UI Definition
def __init__(self):
QThread.__init__(self)
self.window = QWidget()
self.pgsb = QProgressBar()
self.lay = QVBoxLayout()
self.lay.addWidget(self.pgsb)
self.window.setLayout(self.lay)
self.isRun = False
# Thread Function Definition
def run(self):
self.window.show()
while self.isRun:
self.pgsb.setValue(self.percent)
print(self.percent)
if self.percent == 100:
self.isRun = False
class Tool(QWidget):
# Main UI Definition
def __init__(self):
windowWidth = 300
windowHeight = 300
QWidget.__init__(self)
self.setWindowTitle("Example")
self.resize(windowWidth, windowHeight)
self.bt = QPushButton('Numbering')
self.layout = QVBoxLayout()
self.layout.addWidget(self.bt)
self.setLayout(self.layout)
# Main Function Link Definition
self.bt.clicked.connect(self.numbering)
# Main Function Definition
def numbering(self):
bth = BarThread()
bth.start()
bth.isRun = True
for x in range(0,100000):
bth.percent = x/1000
print(x)
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = Tool()
widget.show()
sys.exit(app.exec_())
You can copy and paste directly onto your python IDE.
(it needs PySide2. It can be installed with 'pip install pyside2' in your prompt).
This code executes simple numbering, however, this doesn't show numbering progress.
How can I solve this problem? Thank you in advance.
P.S. I'm using Windows 10 with PyCharm.
You have at least the following errors:
You must not modify the GUI from another thread, in your case the run method is executed in another thread but you try to modify the value of the QProgressBar, in addition to displaying a widget which is not allowed. If you want to modify the GUI with the information provided in the execution in the secondary thread you must do it through signals since they are thread-safe
The for loop is the blocking task so it must be executed in another thread.
Considering the above, the solution is:
import sys
from PySide2.QtWidgets import (
QApplication,
QWidget,
QPushButton,
QVBoxLayout,
QProgressBar,
)
from PySide2.QtCore import QThread, Signal
class ProgressWidget(QWidget):
def __init__(self, parent=None):
super(ProgressWidget, self).__init__(parent)
self.pgsb = QProgressBar()
lay = QVBoxLayout(self)
lay.addWidget(self.pgsb)
class BarThread(QThread):
progressChanged = Signal(int)
def run(self):
percent = 0
for x in range(0, 100000):
percent = x / 100
self.progressChanged.emit(percent)
class Tool(QWidget):
"""Main UI Definition"""
def __init__(self, parent=None):
super(Tool, self).__init__(parent)
self.setWindowTitle("Example")
self.resize(300, 300)
self.bt = QPushButton("Numbering")
layout = QVBoxLayout(self)
layout.addWidget(self.bt)
# Main Function Link Definition
self.bt.clicked.connect(self.numbering)
self.bar_thread = BarThread(self)
self.progress_widget = ProgressWidget()
self.bar_thread.progressChanged.connect(self.progress_widget.pgsb.setValue)
# Main Function Definition
def numbering(self):
self.bar_thread.start()
self.progress_widget.show()
def closeEvent(self, event):
super(Tool, self).closeEvent(event)
self.bar_thread.quit()
self.bar_thread.wait()
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = Tool()
widget.show()
sys.exit(app.exec_())

How to show new QMainWindow in every loop in PyQT5?

I'm trying to write a Python program using PyQt5 that will display a window in each iteration of the for loop. I would like to close after incrementing and displaying the next window. However, I do not know how to stop the loop every iteration and at the moment I am getting 6 windows at once.
main.py
import sys
from PyQt5.QtWidgets import (QLineEdit, QVBoxLayout, QMainWindow,
QWidget, QDesktopWidget, QApplication, QPushButton, QLabel,
QComboBox, QFileDialog, QRadioButton)
from PyQt5.QtCore import pyqtSlot, QByteArray
from alert import Window2
from test import test
class SG(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(300, 150)
self.setWindowTitle('TEST')
self.resultsGen = QPushButton('TEST', self)
self.resultsGen.clicked.connect(lambda: self.on_click())
self.show()
#pyqtSlot()
def on_click(self):
test(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
sg = SG()
sys.exit(app.exec_())
alert.py
from PyQt5.QtWidgets import (QLineEdit, QVBoxLayout, QMainWindow,
QWidget, QDesktopWidget, QApplication, QPushButton, QLabel,
QComboBox, QFileDialog, QRadioButton)
from PyQt5.QtCore import pyqtSlot, QByteArray
from PyQt5.QtGui import QPixmap
from PyQt5 import QtGui, QtCore
class Window2(QMainWindow):
def __init__(self):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
self.setWindowTitle("Window22222")
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
label = QLabel(self)
pixmap = QPixmap('cropped/8.png')
label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
lay.addWidget(label)
self.textbox = QLineEdit(self)
self.textbox.move(20, 20)
self.textbox.resize(280, 40)
# Create a button in the window
self.button = QPushButton('Show text', self)
self.button.move(20, 80)
# connect button to function on_click
self.button.clicked.connect(lambda: self.on_clickX())
self.show()
#pyqtSlot()
def on_clickX(self):
textboxValue = self.textbox.text()
print(textboxValue)
self.textbox.setText("")
self.hide()
test.py
from alert import Window2
def test(self):
for x in range(6):
w = Window2()
As soon as you run the for cycle, all the code of the initialization will be executed, which includes the show() call you used at the end of initPopup().
A simple solution is to create a new signal that is emitted whenever you hide a window, and connect that signal to a function that creates a new one until it reaches the maximum number.
main.py:
import sys
from PyQt5.QtWidgets import QWidget, QApplication, QPushButton
from alert import Window2
class SG(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.alerts = []
def initUI(self):
self.resize(300, 150)
self.setWindowTitle('TEST')
self.resultsGen = QPushButton('TEST', self)
self.resultsGen.clicked.connect(self.nextAlert)
self.show()
def nextAlert(self):
if len(self.alerts) >= 6:
return
alert = Window2()
self.alerts.append(alert)
alert.setWindowTitle('Window {}'.format(len(self.alerts)))
alert.closed.connect(self.nextAlert)
alert.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
sg = SG()
sys.exit(app.exec_())
alert.py:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Window2(QMainWindow):
closed = pyqtSignal()
def __init__(self):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
label = QLabel(self)
pixmap = QPixmap('cropped/8.png')
label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
lay.addWidget(label)
self.textbox = QLineEdit(self)
lay.addWidget(self.textbox)
# Create a button in the window
self.button = QPushButton('Show text', self)
lay.addWidget(self.button)
# connect button to function on_click
self.button.clicked.connect(lambda: self.on_clickX())
self.show()
#pyqtSlot()
def on_clickX(self):
textboxValue = self.textbox.text()
print(textboxValue)
self.textbox.setText("")
self.hide()
self.closed.emit()
Just note that with this very simplified example the user might click on the button of the "SG" widget even if an "alert" window is visibile. You might prefer to use a QDialog instead of a QMainWindow and make the main widget a parent of that dialog.
main.py:
class SG(QWidget):
# ...
def nextAlert(self):
if len(self.alerts) >= 6:
return
alert = Window2(self)
# ...
alert.py:
class Window2(QDialog):
closed = pyqtSignal()
def __init__(self, parent):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
# a QDialog doesn't need a central widget
lay = QVBoxLayout(self)
# ...
Also, if an alert window is closed using the "X" button the new one will not be shown automatically. To avoid that, you can implement the "closeEvent" and ignore the event, so that the user will not be able to close the window until the button is clicked. As QDialogs can close themself when pressing the escape key, I'm also ignoring that situation.
alert.py:
class Window2(QMainWindow):
# ...
def closeEvent(self, event):
event.ignore()
def keyPressEvent(self, event):
if event.key() != Qt.Key_Escape:
super().keyPressEvent(event)

Categories