Passing multiple parameters back from PyQt thread - python

Is there a way to pass multiple parameters from a thread back to the main thread at the same time?
I have started a thread using PyQt5 in which two real time variables are calculated. I want to pass both parameters back to the main thread to be combined and calculated with parameters from another thread.
As attached in the code below, I am able to return each parameter individually and print to the screen. How do I return all parameters into one function so I can proceed with calculations from another thread?
Thank you!
import sys
import time
from PyQt5.QtWidgets import QMainWindow, QPushButton, QVBoxLayout, QFrame, QApplication
from PyQt5.QtCore import pyqtSignal, QObject, QThread
class Counter(QObject):
'''
Class intended to be used in a separate thread to generate numbers and send
them to another thread.
'''
param1 = pyqtSignal(str)
param2 = pyqtSignal(str)
stopped = pyqtSignal()
def __init__(self):
QObject.__init__(self)
def start(self):
'''
Count from 0 to 99 and emit each value to the GUI thread to display.
'''
for x in range(4):
self.param1.emit(str(x))
self.param2.emit(str(x)+'2')
time.sleep(0.7)
self.stopped.emit()
class Application(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
# Configuring widgets
self.button = QPushButton()
self.button.setText('99')
self.layout = QVBoxLayout()
self.layout.addWidget(self.button)
self.frame = QFrame()
self.frame.setLayout(self.layout)
self.setCentralWidget(self.frame)
# Configuring separate thread
self.counterThread = QThread()
self.counter = Counter()
self.counter.moveToThread(self.counterThread)
# Connecting signals
self.button.clicked.connect(self.startCounting)
self.counter.param1.connect(self.button.setText)
self.counter.param1.connect(self.someFunction1)
self.counter.param2.connect(self.someFunction2)
self.counter.stopped.connect(self.counterThread.quit)
self.counterThread.started.connect(self.counter.start)
# print data from parameter 1
def someFunction1(self, data):
print(data + ' in main')
# print data from parameter 2
def someFunction2(self, data):
print(data + ' in main')
def startCounting(self):
if not self.counterThread.isRunning():
self.counterThread.start()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Application()
window.show()
sys.exit(app.exec_())

The signals also support the transmission of lists so you can use it to transport several variables:
class Counter(QObject):
"""
Class intended to be used in a separate thread to generate numbers and send
them to another thread.
"""
params = pyqtSignal(list)
stopped = pyqtSignal()
def start(self):
"""
Count from 0 to 99 and emit each value to the GUI thread to display.
"""
for x in range(4):
values = [str(x), str(x) + "2"]
self.params.emit(values)
time.sleep(0.7)
self.stopped.emit()
class Application(QMainWindow):
def __init__(self):
super(Application, self).__init__()
# Configuring widgets
self.frame = QFrame()
self.button = QPushButton("99")
lay = QVBoxLayout(self.frame)
lay.addWidget(self.button)
self.setCentralWidget(self.frame)
# Configuring separate thread
self.counterThread = QThread()
self.counter = Counter()
self.counter.moveToThread(self.counterThread)
# Connecting signals
self.button.clicked.connect(self.startCounting)
self.counter.params.connect(self.someFunction)
self.counter.stopped.connect(self.counterThread.quit)
self.counterThread.started.connect(self.counter.start)
#pyqtSlot(list)
def someFunction(self, params):
print(params)
if params:
self.button.setText(params[0])
def startCounting(self):
if not self.counterThread.isRunning():
self.counterThread.start()

Related

PyQt delaying function's computation without blocking main thread

I've inhereted a GUI code which is structured something like this:
any button signal triggers a slot, those slots then call an external process to receive information and wait until that process finishes, then the slot proceeds. the issue is, this external process takes between 0.5 to 60 seconds, and in that time the GUI freezes. i'm struggling to find a good way to seperate this process call to a different thread or QProcess (that way i will not block the main event loop) and then return and continue the relevent slot (or function) from that same point with the information received from that external slow process. generators seem like something that should go here, but im struggling to figure how to restructure the code so this will work.
any suggestions or ideas? is there a Qt way to "yield" a function until that process completes and then continue that function?
Psudo code of the current structure:
button1.clicked.connect(slot1)
button2.clicked.connect(slot2)
def slot1():
status = subprocess.run("external proc") # this is blocking
...
...
return
def slot2():
status = subprocess.run("external proc") # this is blocking
...
...
return
Here is the code with the example I was mentioning in the comments:
class MainWindow(QMainWindow, ui_MainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
ui_MainWindow.__init__(self)
self.setupUi(self)
self.button_1.clicked.connect(lambda: self.threaded_wait(1))
self.button_5.clicked.connect(lambda: self.threaded_wait(5))
self.button_10.clicked.connect(lambda: self.threaded_wait(10))
#Start a timer that executes every 0.5 seconds
self.timer = QtCore.QBasicTimer()
self.timer.start(500, self)
#INIT variables
self.results = {}
self.done = False
def timerEvent(self, event):
#Executes every 500msec.
if self.done:
print(self.results)
self.done = False
def threaded_wait(self, time_to_wait):
self.done = False
new_thread = threading.Thread(target=self.actual_wait, args=(time_to_wait,self.sender().objectName()))
new_thread.start()
def actual_wait(self, time_to_wait: int, button_name):
print(f"Button {button_name} Pressed:\nSleeping for {int(time_to_wait)} seconds")
time_passed = 0
for i in range(0, time_to_wait):
print(int( time_to_wait - time_passed))
time.sleep(1)
time_passed = time_passed + 1
self.results[button_name] = [1,2,3,4,5]
self.done = True
print("Done!")
You can use QThread. With Qthread you can pass arguments to a function in mainWindow with signal mechanism.
Here is a source that explains how to use Qthread:
https://realpython.com/python-pyqt-qthread/
if you read the soruce it will be helpfull to you, i think. And there is a sample gui in the page, i write it down to you(you can run it):
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import QMainWindow
import time
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
import sys
# Snip...
# Step 1: Create a worker class
#
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(int)
def run(self):
"""Long-running task."""
for i in range(5):
time.sleep(1)
self.progress.emit(i + 1)
self.finished.emit()
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.clicksCount = 0
self.setupUi()
def setupUi(self):
self.setWindowTitle("Freezing GUI")
self.resize(300, 150)
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
# Create and connect widgets
self.clicksLabel = QLabel("Counting: 0 clicks", self)
self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.stepLabel = QLabel("Long-Running Step: 0")
self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.countBtn = QPushButton("Click me!", self)
self.countBtn.clicked.connect(self.countClicks)
self.longRunningBtn = QPushButton("Long-Running Task!", self)
self.longRunningBtn.clicked.connect(self.runLongTask)
# Set the layout
layout = QVBoxLayout()
layout.addWidget(self.clicksLabel)
layout.addWidget(self.countBtn)
layout.addStretch()
layout.addWidget(self.stepLabel)
layout.addWidget(self.longRunningBtn)
self.centralWidget.setLayout(layout)
def countClicks(self):
self.clicksCount += 1
self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")
def reportProgress(self, n):
self.stepLabel.setText(f"Long-Running Step: {n}")
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")
)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
Usually what I do is have the button press run a function that launches a thread to do the work for me.
In my example I have 3 buttons. One that waits for one second, another that waits for 5, and another that waits for 10.
I connect the button slots when they are clicked to threaded_wait() and I use lambda because I want to pass that method an integer argument on how long to wait for (Waiting in this example is just fake processing time).
Then I have the method actual_wait() which is the code that is actually waiting, which is being executed by the thread. Since there is a thread running that code, the main GUI event loop exits the threaded_wait() method right after starting the thread and it is allowed to continue it's event loop
class MainWindow(QMainWindow, ui_MainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
ui_MainWindow.__init__(self)
self.setupUi(self)
self.button_1.clicked.connect(lambda: self.threaded_wait(1))
self.button_5.clicked.connect(lambda: self.threaded_wait(5))
self.button_10.clicked.connect(lambda: self.threaded_wait(10))
def threaded_wait(self, time_to_wait):
new_thread = threading.Thread(target=self.actual_wait, args=(time_to_wait,))
new_thread.start()
def actual_wait(self, time_to_wait: int):
print(f"Sleeping for {int(time_to_wait)} seconds")
time_passed = 0
for i in range(0, time_to_wait):
print(int( time_to_wait - time_passed))
time.sleep(1)
time_passed = time_passed + 1
print("Done!")
This prevents my GUI from freezing up.
EDIT:
Sorry as for the second part of your question, if you want to wait for the thread to finish before doing something else, you can use a flag like this:
def actual_wait(self, time_to_wait: int):
print(f"Sleeping for {int(time_to_wait)} seconds")
....
self.DONE = True
And check that self.DONE flag wherever you need it.
It kind of depends what you mean by wait for it to complete.
I think if you use QThread you can also emit a signal when the thread is done and connect that signal to whatever slot after that, but I haven't used QThread.

How to update PyQt progressbar from an independent function with arguments?

I want to use multiple imported function with arguments that takes some while to run. I want a 'working' progress bar that track the processes of that function. I have followed 2 questions already here.
Connect an imported function to Qt5 progress bar without dependencies
Report progress to QProgressBar using variable from an imported module
The difference is that the thread can take any function which can have arguments. The function also not needs to yield the percent to return to the progressbar. The progressbar always start at 0%.
I copied a snippet from first link and modified it for example purpose.
from external_script import long_running_function
class Actions(QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Progress Bar')
self.progress = QProgressBar(self)
self.button = QPushButton('Start', self)
self.show()
self.button.clicked.connect(self.onButtonClick)
def onButtonClick(self):
long_running_function(**kwargs) # This can be any function that takes argument/s
self.progress.setValue(value)
Do not get too complicated with the answers as they are limited to a very particular context. In general the logic is to pass a QObject to it that updates the percentage value and then emits a signal with that value. For example a simple solution is to use the threading module:
import sys
import threading
from PyQt5 import QtCore, QtWidgets
class PercentageWorker(QtCore.QObject):
started = QtCore.pyqtSignal()
finished = QtCore.pyqtSignal()
percentageChanged = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self._percentage = 0
#property
def percentage(self):
return self._percentage
#percentage.setter
def percentage(self, value):
if self._percentage == value:
return
self._percentage = value
self.percentageChanged.emit(self.percentage)
def start(self):
self.started.emit()
def finish(self):
self.finished.emit()
class FakeWorker:
def start(self):
pass
def finish(self):
pass
#property
def percentage(self):
return 0
#percentage.setter
def percentage(self, value):
pass
import time
def long_running_function(foo, baz="1", worker=None):
if worker is None:
worker = FakeWorker()
worker.start()
while worker.percentage < 100:
worker.percentage += 1
print(foo, baz)
time.sleep(1)
worker.finish()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.progress = QtWidgets.QProgressBar()
self.button = QtWidgets.QPushButton("Start")
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.button)
lay.addWidget(self.progress)
self.button.clicked.connect(self.launch)
def launch(self):
worker = PercentageWorker()
worker.percentageChanged.connect(self.progress.setValue)
threading.Thread(
target=long_running_function,
args=("foo",),
kwargs=dict(baz="baz", worker=worker),
daemon=True,
).start()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

Sending information from model to view, MVC architecture, PyQT5

I'm trying to create an application with PyQT5 using the MVC architecture and I'm having trouble understanding how to send some of the information from the model to the view. In the sample application below, I have a countdown timer running in the model and I'd like the progress bar in the View to track it's progress. But I'm unsure how to send information to the view from the model when the method in the model is being executed. In my case, the progress_bar gets updated only after the execution of the model.counter method has completed executed. What would be a graceful way of handling this ? I need the progress_bar to update as model.i gets updated.
import sys
import time
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWidgets import QGridLayout, QLineEdit, QPushButton, QVBoxLayout, QProgressBar
from PyQt5.QtCore import Qt
class ExampleGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Sample Application')
# Set some main window's properties
self.setFixedSize(235, 235)
# Set the central widget and the general layout
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Create the display and the buttons
self._createDisplay()
self._createButtons()
def _createDisplay(self):
"""Create the display."""
# Create the display widget
self.display = QLineEdit()
self.progress_bar = QProgressBar()
# Set some display's properties
self.display.setFixedHeight(35)
self.display.setAlignment(Qt.AlignRight)
self.display.setReadOnly(True)
# Add the display to the general layout
self.generalLayout.addWidget(self.display)
self.generalLayout.addWidget(self.progress_bar)
def _createButtons(self):
"""Create the buttons."""
buttonsLayout = QGridLayout()
self.button1 = QPushButton("Start")
self.button1.setFixedSize(80, 40)
self.button2 = QPushButton("Clear")
self.button2.setFixedSize(80, 40)
buttonsLayout.addWidget(self.button1)
buttonsLayout.addWidget(self.button2)
self.generalLayout.addLayout(buttonsLayout)
def setDisplayText(self, text):
"""Set display's text."""
self.display.setText(text)
self.display.setFocus()
def clearDisplay(self):
"""Clear the display."""
self.setDisplayText("")
class Model:
def __init__(self):
self.counter = ''
self.i = ''
def countdown(self, counter):
self.i = 0
self.counter = counter
while self.i < self.counter:
self.i+=1
time.sleep(1)
return True
class Controller:
def __init__(self, view, model):
self._view = view
self._model = model
self._connect_settings_signals()
def _set_message(self):
self._view.progress_bar.setMaximum(10)
reply = self._model.countdown(10)
self._view.progress_bar.setValue(self._model.i)
if reply:
self._view.setDisplayText("Countdown complete!")
def _clear_message(self):
self._view.clearDisplay()
def _connect_settings_signals(self):
self._view.button1.clicked.connect(self._set_message)
self._view.button2.clicked.connect(self._clear_message)
def main():
"""Main function."""
# Create an instance of `QApplication`
pycalc = QApplication(sys.argv)
# Show the calculator's GUI
view = ExampleGUI()
view.show()
model = Model()
# Create instances of the model and the controller
ctrl = Controller(view=view, model=model)
# Execute calculator's main loop
sys.exit(pycalc.exec_())
if __name__ == "__main__":
main()
That sleep(1) is blocking operation and event loop returns to '_set_message' only after 'countdown' is finished.
I would move update of model and loop out of model, back to controller. (As its kind of controlling, telling model update its value, wait a bit...)
I would update to something similar as:
def _set_message(self):
self._view.progress_bar.setMaximum(10)
self.i = 0
while self.i < 10:
self.i+=1
self._model.i += 1
self._view.progress_bar.setValue(self._model.i)
time.sleep(1)
self._view.setDisplayText("Countdown complete!")
If you would insist on looping and sleeping in model (I don't think thats a good idea), you would have to dive into threading, create separate thread for that operation, and keep main event loop unblocked for redrawing of progress bar. (imho)

Pyqt5 GUI Still Hangs When Using Thread

I'm new to python and pyqt.
I'm learning how to use threading with GUI.
I followed this tutorial
http://www.xyzlang.com/python/PyQT5/pyqt_multithreading.html
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import threading
from _ast import While
class Communicate(QObject):
signal = pyqtSignal(int, str)
class My_Gui(QWidget):
def __init__(self):
super().__init__()
self.comm = Communicate()
self.comm.signal.connect(self.append_data)
self.initUI()
def initUI(self):
btn_count = QPushButton('Count')
btn_count.clicked.connect(self.start_counting)
self.te = QTextEdit()
vbox = QVBoxLayout()
vbox.addWidget(btn_count)
vbox.addWidget(self.te)
self.setLayout(vbox)
self.setWindowTitle('MultiThreading in PyQT5')
self.setGeometry(400, 400, 400, 400)
self.show()
def count(self, comm):
'''
for i in range(10):
data = "Data "+str(i)
comm.signal.emit(i, data)
'''
i = 0
while True:
data = "Data "+str(i)
comm.signal.emit(i, data)
i+=1
def start_counting(self):
my_Thread = threading.Thread(target=self.count, args=(self.comm,))
my_Thread.start()
def append_data(self, num, data):
self.te.append(str(num) + " " + data)
if __name__ == '__main__':
app = QApplication(sys.argv)
my_gui = My_Gui()
sys.exit(app.exec_())
I changed the for loop to infinite while loop(incrementing the 'i').
If I execute the program, the GUI still hangs but if I remove the emit signal inside the loop, it no longer hangs.
Are there some tricks to make it not hangs?
while True makes an endless loop in the background
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import threading
from _ast import While
class Communicate(QObject):
signal = pyqtSignal(int, str)
class My_Gui(QWidget):
def __init__(self):
super().__init__()
self.comm = Communicate()
self.comm.signal.connect(self.append_data)
self.initUI()
def initUI(self):
btn_count = QPushButton('Count')
btn_count.clicked.connect(self.start_counting)
self.te = QTextEdit()
vbox = QVBoxLayout()
vbox.addWidget(btn_count)
vbox.addWidget(self.te)
self.setLayout(vbox)
self.setWindowTitle('MultiThreading in PyQT5')
self.setGeometry(400, 400, 400, 400)
self.show()
def count(self, comm):
for i in range(10):
data = "Data "+str(i)
comm.signal.emit(i, data)
# While True below will never stop and cause your program to stuck
'''
i = 0
while True:
data = "Data "+str(i)
comm.signal.emit(i, data)
i+=1
'''
def start_counting(self):
my_Thread = threading.Thread(target=self.count, args=(self.comm,))
my_Thread.start()
def append_data(self, num, data):
self.te.append(str(num) + " " + data)
if __name__ == '__main__':
app = QApplication(sys.argv)
my_gui = My_Gui()
sys.exit(app.exec_())
I think you are getting downvoted for two things:
you did only copy and paste the code from the tutorial
you didn't read the tutorial
In the tutorial, the author states:
In the above example, we have created a QPushbutton and QTextEdit. When the button is clicked it creates a new Thread that counts from 0 to 9 and emits the signal in order to append the number and data in the QTextEdit. In class Communicate signal is initialized as pyqtSignal(int, str). This int and str means when a signal will be emitted it will also pass two arguments the first one will be of Integer type and second one will be of String type.
By changing the loop to while true you continuosly emit signals and append the text in the QTextEdit. Probably not what you want.
Also commenting the emit statement internally continues to run the while loop.

PyQt update gui

I'm trying to update the text in a Qt GUI object via a QThread in PyQt but I just get the error QPixmap: It is not safe to use pixmaps outside the GUI thread, then it crashes. I would really appreciate any help, thanks.
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent = None):
QMainWindow.__init__(self, parent)
self.setupUi(self)
self.output = Output()
def __del__ (self):
self.ui = None
#pyqtSignature("")
def on_goBtn_released(self):
threadnum = 1
#start threads
for x in xrange(threadnum):
thread = TheThread()
thread.start()
class Output(QWidget, Ui_Output):
def __init__(self, parent = None):
QWidget.__init__(self, parent)
self.setupUi(self)
self.ui = Ui_Output
self.show()
def main(self):
self.textBrowser.append("sdgsdgsgsg dsgdsg dsgds gsdf")
class TheThread(QtCore.QThread):
trigger = pyqtSignal()
def __init__(self):
QtCore.QThread.__init__(self)
def __del__(self):
self.wait()
def run(self):
self.trigger.connect(Output().main())
self.trigger.emit()
self.trigger.connect(Output().main())
This line is problematic. You are instantiating a class in the thread which looks like a widget. This is wrong. You shouldn't use GUI elements in a different thread. All GUI related code should run in the same thread with the event loop.
The above line is also wrong in terms of design. You emit a custom signal from your thread and this is a good way. But the object to process this signal should be the one that owns/creates the thread, namely your MainWindow
You also don't keep a reference to your thread instance. You create it in a method, but it is local. So it'll be garbage collected, you probably would see a warning that it is deleted before it is finished.
Here is a minimal working example:
import sys
from PyQt4 import QtGui, QtCore
import time
import random
class MyThread(QtCore.QThread):
trigger = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super(MyThread, self).__init__(parent)
def setup(self, thread_no):
self.thread_no = thread_no
def run(self):
time.sleep(random.random()*5) # random sleep to imitate working
self.trigger.emit(self.thread_no)
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.text_area = QtGui.QTextBrowser()
self.thread_button = QtGui.QPushButton('Start threads')
self.thread_button.clicked.connect(self.start_threads)
central_widget = QtGui.QWidget()
central_layout = QtGui.QHBoxLayout()
central_layout.addWidget(self.text_area)
central_layout.addWidget(self.thread_button)
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
def start_threads(self):
self.threads = [] # this will keep a reference to threads
for i in range(10):
thread = MyThread(self) # create a thread
thread.trigger.connect(self.update_text) # connect to it's signal
thread.setup(i) # just setting up a parameter
thread.start() # start the thread
self.threads.append(thread) # keep a reference
def update_text(self, thread_no):
self.text_area.append('thread # %d finished' % thread_no)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
mainwindow = Main()
mainwindow.show()
sys.exit(app.exec_())

Categories

Resources