PyQt5 multithreading: How to combine output of multiple threads - multithreading

I am trying to build a Python GUI using PyQt5 which incorporates multi-threading. Using various snippets of code (acquired mainly from Stack Overflow), I have come up with the code below which reads data from a csv file into a pandas dataframe and updates the GUI (triggered from a QTimer).
I would like to implement a 2nd thread which reads from a different csv file and a function which accepts both dataframes and does some computation on them before updating the GUI. I'm not sure how I can do this within the signals/slots framework of PyQt5. Any help is much appreciated!
import time
import traceback
import sys
import pandas as pd
import numpy as np
from PyQt5.QtWidgets import (QWidget, QTabWidget, QGridLayout, QApplication, QMainWindow, QStatusBar, QTableView)
from PyQt5.QtCore import (Qt, QTimer, QAbstractTableModel, QThread, QVariant, QObject, QRect, pyqtSlot, pyqtSignal)
class PandasModel(QAbstractTableModel):
"""
Class to populate a table view with a pandas dataframe
"""
def __init__(self, data, parent=None):
QAbstractTableModel.__init__(self, parent)
self._data = np.array(data.values)
self._cols = data.columns
self.r, self.c = np.shape(self._data)
def rowCount(self, parent=None):
return self.r
def columnCount(self, parent=None):
return self.c
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
if role == Qt.DisplayRole:
return self._data[index.row(), index.column()]
return None
def headerData(self, p_int, orientation, role):
if role == Qt.DisplayRole:
try:
if orientation == Qt.Horizontal:
return self._cols[p_int]
elif orientation == Qt.Vertical:
return p_int
except(IndexError, ):
return QVariant()
else:
return QVariant()
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
Supported signals are:
finished: No data
error: `tuple` (exctype, value, traceback.format_exc() )
result: `object` data returned from processing, anything
progress: `int` indicating % progress
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QObject):
"""
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
"""
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# Add the callback to our kwargs
# kwargs['progress_callback'] = self.signals.progress
#pyqtSlot()
def run(self):
"""
Initialise the runner function with passed args, kwargs.
"""
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class MyTabWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QGridLayout(self)
# Initialize tab screen
self.tabs = QTabWidget()
self.tab1 = QWidget()
self.tabs.resize(300, 200)
# Add tabs
self.tabs.addTab(self.tab1, "Tab 1")
# Create tabs
self.tab1.layout = QGridLayout(self)
self.tab1.setLayout(self.tab1.layout)
# Create table widgets
self.table_widget1 = MyTableWidget(self)
# Add tables to tabs
self.tab1.layout.addWidget(self.table_widget1)
# Add tabs to widget
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
class MyTableWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QGridLayout(self)
# Initialize tables
self.table = QTableView()
# Add tabs to widget
self.layout.addWidget(self.table)
self.setLayout(self.layout)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
# Create a central Widgets
self.central_widget = QWidget()
# Create tab Widgets
self.tab_widget = MyTabWidget(self)
# Create a Layout for the central Widget
self.central_layout = QGridLayout()
# Set the Layout
self.central_widget.setLayout(self.central_layout)
# Set the Widget
self.setCentralWidget(self.central_widget)
self.central_layout.addWidget(self.tab_widget)
self.setGeometry(QRect(0, 100, 2000, 1500))
self.setWindowTitle('CSV viewer')
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.proxy = None
self.model = None
self.show()
# Setup worker thread
self.worker_thread = QThread(self)
self.worker = Worker(self.read_csv)
self.worker.moveToThread(self.worker_thread)
self.worker_thread.started.connect(self.worker.run)
# Connect worker signals to appropriate ports
self.worker.signals.result.connect(self.process_csv)
self.worker.signals.finished.connect(self.worker_thread.quit)
self.worker.signals.finished.connect(self.print_refresh_time)
# Setup timer to repetitively trigger start of worker thread e.g. every 5000ms
self.timer = QTimer()
self.timer.timeout.connect(self.worker_thread.start)
self.timer.start(5000)
def process_csv(self, df):
self.model = PandasModel(df)
self.tab_widget.table_widget1.table.setModel(self.model)
self.tab_widget.table_widget1.table.resizeColumnsToContents()
def print_refresh_time(self):
self.status_bar.showMessage('Last updated: ' + time.strftime("%d/%m/%Y %H:%M:%S", time.localtime()))
#staticmethod
def read_csv():
df = pd.read_csv("C:\\Users\\Brian\\Desktop\\E0.csv")
return df
def main():
app = QApplication(sys.argv)
window = MainWindow()
app.exec_()
if __name__ == '__main__':
main()

Related

I wonder why pyqtSignal() is used. (pyqt, QThread, signal, slot)

This is the test code about QThread and Signal.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import sys
class Thread1(QThread):
set_signal = pyqtSignal(int) # (1) ##
def __init__(self, parent):
super().__init__(parent)
def run(self):
for i in range(10):
time.sleep(1)
self.set_signal.emit(i) # (3) ##
class MainWidget(QWidget):
def __init__(self):
super().__init__()
thread_start = QPushButton("시 작!")
thread_start.clicked.connect(self.increaseNumber)
vbox = QVBoxLayout()
vbox.addWidget(thread_start)
self.resize(200,200)
self.setLayout(vbox)
def increaseNumber(self):
x = Thread1(self)
x.set_signal.connect(self.print) # (2) ##
x.start()
def print(self, number):
print(number)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
sys.exit(app.exec_())
In the example of QThread I searched for, a pyqtSignal()(step 1) object was created, the desired slot function was connected(step 2) by connect, and then called by emit()(step 3).
I don't know the difference from calling the desired method immediately without connecting the connect().
So, the goal of codes is triggering a desired function every second. You are right about it.
But if you create multiple objects, you can connect it to different desired functions. Or connect multiple function to one signal.
x = Thread1(self)
x.set_signal.connect(self.print) # (2) ##
x.start()
y = Thread1(self)
y.set_signal.connect(self.print2)
time.sleep(0.5)
y.start()

Connecting external threaded log to PyQt5 QPlainTextEdit

I'm new to PyQt and handlers in general, I tried to read from every repo I found but I can't figure out how to fix my issue, as you can see in my code, I'm executing logging thread in the background and I'm trying to show the logs in my QPlainTextEdit console - for some reason I can see the logs in my terminal and the text_box doesn't get the logs at all,
I will appreciate a lot your smart help.
import pandas as pd
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import time
import os
import threading
import json
class ConsolePanelHandler(logging.Handler):
def __init__(self, stream):
# super().__init__()
logging.Handler.__init__(self)
# logging.StreamHandler.__init__(self, stream)
self.stream = stream
def handle(self, record):
rv = self.filter(record)
if rv:
self.acquire()
try:
self.emit(record)
finally:
self.release()
return rv
def emit(self, record):
try:
stream = self.stream
stream(self.format(record))
except RecursionError:
raise
except Exception:
self.handleError(self.format(record))
def thread():
for index in range(20):
logging.warning('scheiBe '+str(index))
time.sleep(1)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.continue_ = QPushButton("Continue")
self.continue_.setStyleSheet("background-color: green")
self.continue_.setFont(QFont('SansSerif', 10))
self.continue_.setFixedSize(QSize(300, 22))
self.pause = QPushButton("Pause")
self.pause.setStyleSheet("background-color: orange")
self.pause.setFont(QFont('SansSerif', 10))
self.pause.setFixedSize(QSize(300, 22))
self.stop = QPushButton("Stop")
self.stop.setStyleSheet("background-color: #FD4B4B")
self.stop.setFont(QFont('SansSerif', 10))
self.stop.setFixedSize(QSize(300, 22))
self.text_box = QPlainTextEdit()
self.text_box.setPlaceholderText("Bugs will be printed here")
self.text_box.setReadOnly(True)
logging.getLogger().addHandler(self.text_box)
logging.getLogger().setLevel(logging.DEBUG)
ConsolePanelHandler(self.appendDebug , logging.DEBUG)
self.text_box.moveCursor(QTextCursor.End)
layout.addWidget(self.continue_)
layout.addWidget(self.pause)
layout.addWidget(self.stop)
layout.addWidget(self.text_box)
self.w = QWidget()
self.w.setLayout(layout)
self.setCentralWidget(self.w)
thread1 = threading.Thread(target=thread, args=(), daemon=True)
thread1.start()
self.show()
def closeEvent(self, event):
close = QMessageBox()
close.setText("Are you sure want to stop and exit?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
sys.exit()
else:
event.ignore()
def appendDebug(self, string):
self.text_box.appendPlainText(string +'\n')
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
sys.exit(app.exec())
First, you should not pass the function that updates the text_area instead you should create a pyqtSignal which updates the text_area and pass it to ConsolePanelHandler.
Add ConsolePanelHandler as handler not text_area to logging.getLogger().addHandler()
and it is recommended to use QThread instead of threading.Thread
here is the complete updated code.
# import pandas as pd
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import time
import os
import sys
import threading
import json
import logging
class ConsolePanelHandler(logging.Handler):
def __init__(self, sig):
# super().__init__()
logging.Handler.__init__(self)
# logging.StreamHandler.__init__(self, stream)
self.stream = sig
def handle(self, record):
rv = self.filter(record)
if rv:
self.acquire()
try:
self.emit(record)
finally:
self.release()
return rv
def emit(self, record):
try:
self.stream.emit(self.format(record))
except RecursionError:
raise
except Exception:
self.handleError(record)
class thread(QThread):
def run(self) -> None:
for index in range(20):
logging.warning('scheiBe ' + str(index))
self.sleep(1)
class MainWindow(QMainWindow):
sig = pyqtSignal(str)
def __init__(self):
super().__init__()
self.layout = QVBoxLayout()
self.continue_ = QPushButton("Continue")
self.continue_.setStyleSheet("background-color: green")
self.continue_.setFont(QFont('SansSerif', 10))
self.continue_.setFixedSize(QSize(300, 22))
self.pause = QPushButton("Pause")
self.pause.setStyleSheet("background-color: orange")
self.pause.setFont(QFont('SansSerif', 10))
self.pause.setFixedSize(QSize(300, 22))
self.stop = QPushButton("Stop")
self.stop.setStyleSheet("background-color: #FD4B4B")
self.stop.setFont(QFont('SansSerif', 10))
self.stop.setFixedSize(QSize(300, 22))
self.c = ConsolePanelHandler(self.sig)
self.text_box = QPlainTextEdit()
self.text_box.setPlaceholderText("Bugs will be printed here")
self.text_box.setReadOnly(True)
logging.getLogger().addHandler(self.c)
logging.getLogger().setLevel(logging.DEBUG)
self.sig.connect(self.appendDebug)
self.text_box.moveCursor(QTextCursor.End)
self.layout.addWidget(self.continue_)
self.layout.addWidget(self.pause)
self.layout.addWidget(self.stop)
self.layout.addWidget(self.text_box)
self.w = QWidget()
self.w.setLayout(self.layout)
self.setCentralWidget(self.w)
self.thread1 = thread(self) # self is parent for Qthread so Qthread will be destroyed when it's parent no longer exist
self.thread1.start()
self.show()
def closeEvent(self, event):
close = QMessageBox()
close.setText("Are you sure want to stop and exit?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
self.thread1.terminate()
sys.exit()
else:
event.ignore()
#pyqtSlot(str)
def appendDebug(self, string):
self.text_box.appendPlainText(string + '\n')
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
sys.exit(app.exec())

PyQt Thread Completes and outputs same message twice

I'm trying to create a play/pause/resume/ button with a progress bar. User presses 'Start' and the progress bar initializes and a pause/resume button populates. Once the progress completes, a message is printed and the pause/resume buttons are hidden while the 'Start' button is shown again. However, when I try to run 'Start' again, it prints the same completion message an additional time.
If I run it twice, it prints out two completion messages. Run it three times, prints out three completion messages and etc.
from PyQt5.QtWidgets import (
QWidget, QApplication, QProgressBar, QMainWindow,
QHBoxLayout, QPushButton
)
from PyQt5.QtCore import (
Qt, QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool
)
import time
class WorkerSignals(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal()
class JobRunner(QRunnable):
signals = WorkerSignals()
def __init__(self):
super().__init__()
self.is_paused = False
self.is_killed = False
#pyqtSlot()
def run(self):
for n in range(100):
self.signals.progress.emit(n + 1)
time.sleep(0.001)
while self.is_paused:
time.sleep(0)
if self.is_killed:
break
self.signals.finished.emit()
def finished(self):
print('Finished')
def pause(self):
self.is_paused = True
def resume(self):
self.is_paused = False
# def kill(self):
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Some buttons
self.w = QWidget()
self.l = QHBoxLayout()
self.w.setLayout(self.l)
self.btn_start = QPushButton("Start")
self.l.addWidget(self.btn_start)
self.setCentralWidget(self.w)
# Create a statusbar.
self.status = self.statusBar()
self.progress = QProgressBar()
self.status.addPermanentWidget(self.progress)
# Thread runner
# self.threadpool = QThreadPool()
# # Create a runner
# self.runner = JobRunner()
# self.runner.signals.progress.connect(self.update_progress)
# self.threadpool.start(self.runner)
self.btn_start.pressed.connect(self.start_runner)
# btn_pause.pressed.connect(self.runner.pause)
# btn_resume.pressed.connect(self.runner.resume)
# self.startTestSignal.connect(self.update_progress)
self.show()
def update_progress(self, n):
self.progress.setValue(n)
def start_runner(self):
# Create ThreadPool
self.threadpool = QThreadPool()
self.threadpool.clear()
# Create a runner
self.runner = JobRunner()
self.runner.signals.progress.connect(self.update_progress)
self.threadpool.start(self.runner)
self.progress.setValue(0)
# Change Start Button to Pause
self.btn_start.hide()
# self.btn_start.setEnabled(False)
self.btn_pause = QPushButton("Pause")
self.btn_resume = QPushButton("Resume")
self.l.addWidget(self.btn_pause)
self.l.addWidget(self.btn_resume)
self.btn_pause.pressed.connect(self.runner.pause)
self.btn_resume.pressed.connect(self.runner.resume)
self.runner.signals.finished.connect(self.check)
def check(self):
print('Thread Done')
self.btn_start.show()
self.progress.setValue(0)
# self.runner.terminate()
self.btn_pause.hide()
self.btn_resume.hide()
app = QApplication([])
w = MainWindow()
app.exec_()
You are creating a WorkerSignals as a class attribute, making it a persistent for the class; so when you do this:
self.runner.signals.finished.connect(self.check)
you are actually connecting again to signals.finished, and when you emit the signal the function self.check is called each time the signal has been connected.
The solution is to create the WorkerSignals as an instance attribute:
class JobRunner(QRunnable):
def __init__(self):
super().__init__()
self.signals = WorkerSignals()
self.is_paused = False
self.is_killed = False

wxpyton - threads in many modules

I'm trying to create simple frame that will run longrun procedure, but will show counting in different window (dialog), but it don't work (staticText1 is not updating)... Here's my code:
Main frame (simple frame with one button)
import wx
import time
from threading import Thread
import Dialog1
def create(parent):
return Frame1(parent)
[wxID_FRAME1, wxID_FRAME1BUTTON1, ] = [wx.NewId() for _init_ctrls in range(2)]
class Frame1(wx.Frame):
def test1(self):
for i in range(self.delays):
time.sleep(1)
def _init_ctrls(self, prnt):
# generated method, don't edit
wx.Frame.__init__(self, id=wxID_FRAME1, name='', parent=prnt,
pos=wx.Point(589, 259), size=wx.Size(178, 128),
style=wx.DEFAULT_FRAME_STYLE, title='Frame1')
self.button1 = wx.Button(id=wxID_FRAME1BUTTON1, label='button1',
name='button1', parent=self, pos=wx.Point(0, 0), size=wx.Size(178,
128), style=0)
self.button1.Bind(wx.EVT_BUTTON, self.OnButton1Button,
id=wxID_FRAME1BUTTON1)
def __init__(self, parent):
self._init_ctrls(parent)
self.delays = 5
self.timer = wx.Timer(self)
def OnButton1Button(self, event):
self.testThread = Thread(target=self.test1)
self.testThread.start()
#self.Show(False)
self.dlg = Dialog1.Dialog1(self)
self.dlg.ShowModal()
self.button1.Disable()
self.Bind(wx.EVT_TIMER, self.PollThread)
self.timer.Start(20, oneShot=True)
event.Skip()
def PollThread(self, event):
if self.testThread.isAlive():
self.Bind(wx.EVT_TIMER, self.PollThread)
self.timer.Start(200, oneShot=True)
self.dlg.staticText1.SetLabel(self.dlg.staticText1.GetLabel()+".")
else:
self.button1.Enable()
self.dlg.Destroy()
Dialog (simple dialog with one statictext):
import wx
def create(parent):
return Dialog1(parent)
[wxID_DIALOG1, wxID_DIALOG1STATICTEXT1, ] = [wx.NewId() for _init_ctrls in range(2)]
class Dialog1(wx.Dialog):
def _init_ctrls(self, prnt):
# generated method, don't edit
wx.Dialog.__init__(self, id=wxID_DIALOG1, name='', parent=prnt,
pos=wx.Point(451, 295), size=wx.Size(544, 59),
style=wx.DEFAULT_DIALOG_STYLE, title='Dialog1')
self.staticText1 = wx.StaticText(id=wxID_DIALOG1STATICTEXT1,
label=u'Please wait', name='staticText1', parent=self,
pos=wx.Point(56, 24), size=wx.Size(95, 17), style=0)
def __init__(self, parent):
self._init_ctrls(parent)

How to debug pyqt signal/slot connections, probably threading issue

I have the following piece of example code of my problem. Running this, I would expect that (if you type something in the lineedit) the A.updateValue slot would be called twice and thus show 'a.updatevalue called' and 'a2.updatevalue called'
However, it is only called once, namely for the self.a2 object and not for the self.a object, the latter which is sent from a worker thread to the GUI thread. How can I fix this so that this piece of code also triggers the slot for the self.a object?
Thank you,
David
import os, sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class A(QObject):
def __init__(self, name):
QObject.__init__(self)
self.name = name
def updateValue(self, value):
print(self.name + ".updatevalue called")
class workerthread(QThread):
def __init__(self, parent=None):
QThread.__init__(self, parent)
def run(self):
a = A('a')
QObject.emit(self, SIGNAL("mySignal"), a)
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.centralwidget = QWidget(self)
self.hbox = QHBoxLayout()
self.centralwidget.setLayout(self.hbox)
def update(self, a):
self.a = a
edit = QLineEdit("", self)
self.hbox.addWidget(edit)
edit.textChanged.connect(self.a.updateValue)
self.a2 = A('a2')
edit.textChanged.connect(self.a2.updateValue)
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = Main()
worker = workerthread()
worker.connect(worker, SIGNAL('mySignal'), gui.update)
worker.start()
gui.show()
sys.exit(app.exec_())
Define a when you initialize workerThread
class workerthread(QThread):
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.a = A('a')
def run(self):
QObject.emit(self, SIGNAL("mySignal"), self.a)

Resources