QWidget not loading child elements when QRunnable is also called - python

I'm working in a data processing desktop application with Python 3.7 and PySide2 on which requires me to load data from several large (approx 250k rows) excel files into the program's processing library. For this I've set up in my application a simple popup (called LoadingPopup) which includes a rotating gif and a simple caption, and also some code that loads the data from the excel files into a global object using pandas. Both of these things work as intended when run on their own, but if I happen to create a loading dialog and a QRunnable worker in the same scope of my codebase, the widgets contained in loading widget (a gif and a simple caption) will simply not show.
I've tried changing the parent type for my widget from QDialog to QWidget, or initializing the popup (the start() function) both outside and inside the widget. I'm not very experienced with Qt5 so I don't know what else to do.
import sys, time, traceback
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *
from TsrUtils import PathUtils
class WorkerSignals(QObject):
finished = Signal()
error = Signal(tuple)
result = Signal(object)
class TsrWorker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(TsrWorker, self).__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
#Slot()
def run(self):
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)
finally:
self.signals.finished.emit()
class LoadingPopup(QWidget):
def __init__(self):
super().__init__()
self.message = "Cargando"
self.setMinimumSize(350,300)
self.setWindowIcon(\
QIcon(PathUtils.ruta_absoluta('resources/icons/tsr.png')))
self.setWindowTitle(self.message)
self.layout = QVBoxLayout(self)
self.setLayout(self.layout)
self.movie = QMovie(self)
self.movie.setFileName("./resources/img/spinner.gif")
self.movie.setCacheMode(QMovie.CacheAll)
self.movie.start()
self.loading = QLabel(self)
self.loading.setMovie(self.movie)
self.loading.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.loading)
self.lbl = QLabel(self.message, self)
self.lbl.setAlignment(Qt.AlignCenter)
self.lbl.setStyleSheet("font: 15pt")
self.layout.addWidget(self.lbl)
class MyMainApp(QApplication):
def __init__(self, args):
super().__init__()
self.l = LoadingPopup()
self.l.show()
w = TsrWorker(time.sleep, 5)
w.signals.finished.connect(self.terminado)
w.run()
def terminado(self):
print('timer finished')
self.l.hide()
if __name__ == '__main__':
app = MyMainApp(sys.argv)
sys.exit(app.exec_())
I've changed the actual data loading part of the application in the example with a time.sleep function. MY expected results are that I should be able to have the LoadingPopup show up with a gif moving, and then it should close once the QRunnable finishes.

You should not call the run method directly since you will have the heavy task run on the GUI thread freezing it. You must launch it using QThreadPool:
class MyMainApp(QApplication):
def __init__(self, args):
super().__init__()
self.l = LoadingPopup()
self.l.show()
w = TsrWorker(time.sleep, 5)
w.signals.finished.connect(self.terminado)
# w.run()
QThreadPool.globalInstance().start(w) # <---
def terminado(self):
print('timer finished')
self.l.hide()

Related

How to make a QCompleter object run on a worker thread

I wish to implement several autocompleters where the data passed to QCompleter is huge (63000 items), and therefore freezes the main thread. I read the QCompleter class docs but didn't find any reference to the question title, and setMaxVisibleItems isn't really what I look for, since the whole search is carried out anyway. Alternatively, limiting the search in some way to a fixed number of items as a workaround could be a valid option (also no reference to that in the docs). Maybe there is a simpler way I haven't noticed?
Update
main.py
import sys
from PyQt5 import QtCore, QtWidgets as qtw
from itertools import chain
import csv
import traceback
class WorkerSignals(QtCore.QObject):
finished = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(tuple)
result = QtCore.pyqtSignal(object)
progress = QtCore.pyqtSignal(int)
class Worker(QtCore.QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
#QtCore.pyqtSlot()
def run(self):
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)
finally:
self.signals.finished.emit()
class MainWindow(qtw.QWidget):
def __init__(self):
super().__init__()
self.setLayout(qtw.QVBoxLayout())
self.searchBox = qtw.QLineEdit()
self.layout().addWidget(self.searchBox)
path = r"components.csv"
with open(path, "r") as f:
self.data = list(chain.from_iterable(csv.reader(f)))
self.data.sort()
self.threadpool = QtCore.QThreadPool()
# can't have completer on another thread
# self.worker_fn(self.create_completer)
self.create_completer()
self.show()
def worker_fn(self, fn):
worker = Worker(fn)
self.threadpool.start(worker)
def create_completer(self):
completer = qtw.QCompleter(self.data)
# case sensitive by default
completer.setModelSorting(qtw.QCompleter.CaseSensitivelySortedModel)
self.searchBox.setCompleter(completer)
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec())
components.csv
components.csv

PyQt5 threading GUI does not work

I am trying to load some data which takes 30+ seconds. During this time I wish the user to see a small GUI which says "Loading .", then "Loading ..", then "Loading ...", then "Loading ." etc. I have done some reading and I think I have to put this in a separate thread. I found someone who had a similar problem suggesting the solution was this in the right spot:
t = threading.Thread(target=self.test)
t.daemon = True
t.start()
In a lower part of the file I have the test function
def test(self):
tmp = InfoMessage()
while True:
print(1)
and the InfoMessage function
from PyQt5 import uic, QtCore, QtGui, QtWidgets
import sys
class InfoMessage(QtWidgets.QDialog):
def __init__(self, msg='Loading ', parent=None):
try:
super(InfoMessage, self).__init__(parent)
uic.loadUi('ui files/InfoMessage.ui',self)
self.setWindowTitle(' ')
self.o_msg = msg
self.msg = msg
self.info_label.setText(msg)
self.val = 0
self.timer = QtCore.QTimer()
self.timer.setInterval(500)
self.timer.timeout.connect(self.update_message)
self.timer.start()
self.show()
except BaseException as e:
print(str(e))
def update_message(self):
self.val += 1
self.msg += '.'
if self.val < 20:
self.info_label.setText(self.msg)
else:
self.val = 0
self.msg = self.o_msg
QtWidgets.QApplication.processEvents()
def main():
app = QtWidgets.QApplication(sys.argv) # A new instance of QApplication
form = InfoMessage('Loading ') # We set the form to be our MainWindow (design)
app.exec_() # and execute the app
if __name__ == '__main__': # if we're running file directly and not importing it
main() # run the main function
When I run the InfoMessage function alone it works fine and it updates every 0.5 seconds etc. However, when I fun this as part of the loading file the GUI is blank and incorrectly displayed. I know it is staying in the test function because of the print statement in there.
Can someone point me in the right direction? I think I am missing a couple of steps.
First, there are two ways of doing this. One way is to use the Python builtin threading module. The other way is to use the QThread library which is much more integrated with PyQT. Normally, I would recommend using QThread to do threading in PyQt. But QThread is only needed when there is any interaction with PyQt.
Second, I've removed processEvents() from InfoMessage because it does not serve any purpose in your particular case.
Finally, setting your thread as daemon implies your thread will never stop. This is not the case for most functions.
import sys
import threading
import time
from PyQt5 import uic, QtCore, QtWidgets
from PyQt5.QtCore import QThread
def long_task(limit=None, callback=None):
"""
Any long running task that does not interact with the GUI.
For instance, external libraries, opening files etc..
"""
for i in range(limit):
time.sleep(1)
print(i)
if callback is not None:
callback.loading_stop()
class LongRunning(QThread):
"""
This class is not required if you're using the builtin
version of threading.
"""
def __init__(self, limit):
super().__init__()
self.limit = limit
def run(self):
"""This overrides a default run function."""
long_task(self.limit)
class InfoMessage(QtWidgets.QDialog):
def __init__(self, msg='Loading ', parent=None):
super(InfoMessage, self).__init__(parent)
uic.loadUi('loading.ui', self)
# Initialize Values
self.o_msg = msg
self.msg = msg
self.val = 0
self.info_label.setText(msg)
self.show()
self.timer = QtCore.QTimer()
self.timer.setInterval(500)
self.timer.timeout.connect(self.update_message)
self.timer.start()
def update_message(self):
self.val += 1
self.msg += '.'
if self.val < 20:
self.info_label.setText(self.msg)
else:
self.val = 0
self.msg = self.o_msg
def loading_stop(self):
self.timer.stop()
self.info_label.setText("Done")
class MainDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MainDialog, self).__init__(parent)
# QThread Version - Safe to use
self.my_thread = LongRunning(limit=10)
self.my_thread.start()
self.my_loader = InfoMessage('Loading ')
self.my_thread.finished.connect(self.my_loader.loading_stop)
# Builtin Threading - Blocking - Do not use
# self.my_thread = threading.Thread(
# target=long_task,
# kwargs={'limit': 10}
# )
# self.my_thread.start()
# self.my_loader = InfoMessage('Loading ')
# self.my_thread.join() # Code blocks here
# self.my_loader.loading_stop()
# Builtin Threading - Callback - Use with caution
# self.my_loader = InfoMessage('Loading ')
# self.my_thread = threading.Thread(
# target=long_task,
# kwargs={'limit': 10,
# 'callback': self.my_loader}
# )
# self.my_thread.start()
def main():
app = QtWidgets.QApplication(sys.argv)
dialog = MainDialog()
app.exec_()
if __name__ == '__main__':
main()
Feel free to ask any follow up questions regarding this code.
Good Luck.
Edit:
Updated to show how to run code on thread completion. Notice the new parameter added to long_task function.

Multithreading with PySide : is my structure viable?

I'm running into a very strange error while using QThread in PySide. As I run my code, when I click on the button that is supposed to launch the thread and send Signal, I get :
AttributeError: 'builtin_function_or_method' object has no attribute 'displayText'
at line :
self.displayMessage.connect(self.window.displayText)
NB : displayText is a method that I defined in my MainWindow(QWidget) class, while displayMessage is the signal to be emitted.
This error seems, according to the Internet, to occur in various situations, and I couldn't find one that suits my case yet. Therefore I have 2 questions :
Have you guys ever met this error before in a similar situation, and could you (if yes) give me some tips ?
Am I doing it right ? I can't post my code directly down here, so I created a short verifiable example in which I used the same process. Unfortunately, it seems to work perfectly. Please tell me if at least my construction is correct.
Thank you very much.
EDIT : I eventually figured out that I have forgotten a self somewhere in my big code, but the actual error was hidden to me behind this one that I didn't know. I'm still interested in whether or not this code is reliable, and am fully open to suggestions of improvement.
Without thread
When you click "GO !" and try to move the window, you can see that it freezes for a second.
#!/usr/bin/env python
# -*- encoding : utf-8 -*-
import sys
import time
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QWidget):
def __init__(self, qt_app):
QWidget.__init__(self)
worker = Worker(self)
self.qt_app = qt_app
self.setGeometry(100, 100, 220, 40)
self.infoPanel = QTextEdit(self)
self.infoPanel.setReadOnly(True)
self.goButton = QPushButton('GO !', self)
self.goButton.clicked.connect(lambda: worker.displayOnWindow())
self.layout = QVBoxLayout()
self.layout.addWidget(self.infoPanel)
self.layout.addWidget(self.goButton)
self.setLayout(self.layout)
def launch(self):
self.show()
self.qt_app.exec_()
class Worker():
def __init__(self, window):
self.counter = 0
self.window = window
def displayOnWindow(self):
time.sleep(1)
self.window.infoPanel.append(str(self.counter))
self.counter += 1
if __name__=='__main__':
qt_app = QApplication(sys.argv)
mw = MainWindow(qt_app)
mw.launch()
With thread
Now you can move the window without trouble, since the sleeping thread is not the one that displays the window. I've written a sub-class for my thread because in the original code there are some functions that will be called by several buttons.
#!/usr/bin/env python
# -*- encoding : utf-8 -*-
import sys
import time
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QWidget):
def __init__(self, qt_app):
QWidget.__init__(self)
worker = SubWorker(self)
self.qt_app = qt_app
self.setGeometry(100, 100, 220, 40)
self.infoPanel = QTextEdit(self)
self.infoPanel.setReadOnly(True)
self.goButton = QPushButton('GO !', self)
self.goButton.clicked.connect(lambda: worker.start())
self.layout = QVBoxLayout()
self.layout.addWidget(self.infoPanel)
self.layout.addWidget(self.goButton)
self.setLayout(self.layout)
def launch(self):
self.show()
self.qt_app.exec_()
#Slot(int)
def displayOnWindow(self, i):
self.infoPanel.append(str(i))
class Worker(QThread):
displayMessage = Signal(int)
def __init__(self, window):
QThread.__init__(self)
self.counter = 0
self.window = window
self.displayMessage.connect(self.window.displayOnWindow)
def run(self, *args, **kwargs):
return QThread.run(self, *args, **kwargs)
class SubWorker(Worker):
def __init__(self, window):
Worker.__init__(self, window)
def run(self):
time.sleep(1)
self.displayMessage.emit(self.counter)
self.counter += 1
if __name__=='__main__':
qt_app = QApplication(sys.argv)
mw = MainWindow(qt_app)
mw.launch()

PyQt: moveToThread does not work when using partial() for slot

I am building a small GUI application which runs a producer (worker) and the GUI consumes the output on demand and plots it (using pyqtgraph).
Since the producer is a blocking function (takes a while to run), I (supposedly) moved it to its own thread.
When calling QThread.currentThreadId() from the producer it outputs the same number as the main GUI thread. So, the worker is executed first, and then all the plotting function calls are executed (because they are being queued on the same thread's event queue). How can I fix this?
Example run with partial:
gui thread id 140665453623104
worker thread id: 140665453623104
Here is my full code:
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal
import pyqtgraph as pg
import numpy as np
from functools import partial
from Queue import Queue
import math
import sys
import time
class Worker(QtCore.QObject):
termino = pyqtSignal()
def __init__(self, q=None, parent=None):
super(Worker, self).__init__(parent)
self.q = q
def run(self, m=30000):
print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
for x in xrange(m):
#y = math.sin(x)
y = x**2
time.sleep(0.001) # Weird, plotting stops if this is not present...
self.q.put((x,y,y))
print('Worker finished')
self.termino.emit()
class MainWindow(QtGui.QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.q = Queue()
self.termino = False
self.worker = Worker(self.q)
self.workerThread = None
self.btn = QtGui.QPushButton('Start worker')
self.pw = pg.PlotWidget(self)
pi = self.pw.getPlotItem()
pi.enableAutoRange('x', True)
pi.enableAutoRange('y', True)
self.ge1 = pi.plot(pen='y')
self.xs = []
self.ys = []
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.pw)
layout.addWidget(self.btn)
self.resize(400, 400)
def run(self):
self.workerThread = QtCore.QThread()
self.worker.moveToThread(self.workerThread)
self.worker.termino.connect(self.setTermino)
# moveToThread doesn't work here
self.btn.clicked.connect(partial(self.worker.run, 30000))
# moveToThread will work here
# assume def worker.run(self): instead of def worker.run(self, m=30000)
# self.btn.clicked.connect(self.worker.run)
self.btn.clicked.connect(self.graficar)
self.workerThread.start()
self.show()
def setTermino(self):
self.termino = True
def graficar(self):
if not self.q.empty():
e1,e2,ciclos = self.q.get()
self.xs.append(ciclos)
self.ys.append(e1)
self.ge1.setData(y=self.ys, x=self.xs)
if not self.termino:
QtCore.QTimer.singleShot(1, self.graficar)
if __name__ == '__main__':
app = QtGui.QApplication([])
window = MainWindow()
QtCore.QTimer.singleShot(0, window.run);
sys.exit(app.exec_())
The problem is that Qt attempts to choose the connection type (when you call signal.connect(slot)) based on the what thread the slot exists in. Because you have wrapped the slot in the QThread with partial, the slot you are connecting to resides in the MainThread (the GUI thread). You can override the connection type (as the second argument to connect() but that doesn't help because the method created by partial will always exist in the MainThread, and so setting the connection type to by Qt.QueuedConnection doesn't help.
The only way around this that I can see is to set up a relay signal, the sole purpose of which is to effectively change an emitted signal with no arguments (eg the clicked signal from a button) to a signal with one argument (your m parameter). This way you don't need to wrap the slot in the QThread with partial().
The code is below. I've created a signal with one argument (an int) called 'relay' in the main windows class. The button clicked signal is connected to a method within the main window class, and this method has a line of code which emits the custom signal I created. You can extend this method (relay_signal()) to get the integer to pass to the QThread as m (500 in this case), from where ever you like!
So here is the code:
from functools import partial
from Queue import Queue
import math
import sys
import time
class Worker(QtCore.QObject):
termino = pyqtSignal()
def __init__(self, q=None, parent=None):
super(Worker, self).__init__(parent)
self.q = q
def run(self, m=30000):
print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
for x in xrange(m):
#y = math.sin(x)
y = x**2
#time.sleep(0.001) # Weird, plotting stops if this is not present...
self.q.put((x,y,y))
print('Worker finished')
self.termino.emit()
class MainWindow(QtGui.QWidget):
relay = pyqtSignal(int)
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.q = Queue()
self.termino = False
self.worker = Worker(self.q)
self.workerThread = None
self.btn = QtGui.QPushButton('Start worker')
self.pw = pg.PlotWidget(self)
pi = self.pw.getPlotItem()
pi.enableAutoRange('x', True)
pi.enableAutoRange('y', True)
self.ge1 = pi.plot(pen='y')
self.xs = []
self.ys = []
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.pw)
layout.addWidget(self.btn)
self.resize(400, 400)
def run(self):
self.workerThread = QtCore.QThread()
self.worker.termino.connect(self.setTermino)
self.worker.moveToThread(self.workerThread)
# moveToThread doesn't work here
# self.btn.clicked.connect(partial(self.worker.run, 30000))
# moveToThread will work here
# assume def worker.run(self): instead of def worker.run(self, m=30000)
#self.btn.clicked.connect(self.worker.run)
self.relay.connect(self.worker.run)
self.btn.clicked.connect(self.relay_signal)
self.btn.clicked.connect(self.graficar)
self.workerThread.start()
self.show()
def relay_signal(self):
self.relay.emit(500)
def setTermino(self):
self.termino = True
def graficar(self):
if not self.q.empty():
e1,e2,ciclos = self.q.get()
self.xs.append(ciclos)
self.ys.append(e1)
self.ge1.setData(y=self.ys, x=self.xs)
if not self.termino or not self.q.empty():
QtCore.QTimer.singleShot(1, self.graficar)
if __name__ == '__main__':
app = QtGui.QApplication([])
window = MainWindow()
QtCore.QTimer.singleShot(0, window.run);
sys.exit(app.exec_())
I also modified the graficar method to continue plotting (even after the thread is terminated) if there is still data in the queue. I think this might be why you needed the time.sleep in the QThread, which is now also removed.
Also regarding your comments in the code on where to place moveToThread, where it is now is correct. It should be before the call that connects the QThread slot to a signal, and the reason for this is discussed in this stack-overflow post: PyQt: Connecting a signal to a slot to start a background operation

using threading for a qt application

I have the following two files:
import sys
import time
from PyQt4 import QtGui, QtCore
import btnModule
class WindowClass(QtGui.QWidget):
def __init__(self):
super(WindowClass, self).__init__()
self.dataLoaded = None
# Widgets
# Buttons
thread = WorkerForLoop(self.runLoop)
# thread.start()
self.playBtn = btnModule.playpauselBtnClass \
('Play', thread.start)
# Layout
layout = QtGui.QHBoxLayout()
layout.addWidget(self.playBtn)
self.setLayout(layout)
# Window Geometry
self.setGeometry(100, 100, 100, 100)
def waitToContinue(self):
print self.playBtn.text()
while (self.playBtn.text() != 'Pause'):
pass
def runLoop(self):
for ii in range(100):
self.waitToContinue()
print 'num so far: ', ii
time.sleep(0.5)
class WorkerForLoop(QtCore.QThread):
def __init__(self, function, *args, **kwargs):
super(WorkerForLoop, self).__init__()
self.function = function
self.args = args
self.kwargs = kwargs
def __del__(self):
self.wait()
def run(self):
print 'let"s run it'
self.function(*self.args, **self.kwargs)
return
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
wmain = WindowClass()
wmain.show()
sys.exit(app.exec_())
and the second file btnModule.py:
from PyQt4 import QtGui, QtCore
class playpauselBtnClass(QtGui.QPushButton):
btnSgn = QtCore.pyqtSignal()
def __init__(self, btnName, onClickFunction):
super(playpauselBtnClass, self).__init__(btnName)
self.clicked.connect(self.btnPressed)
self.btnSgn.connect(onClickFunction)
def btnPressed(self):
if self.text() == 'Play':
self.setText('Pause')
self.btnSgn.emit()
print 'Changed to pause and emited signal'
elif self.text() == 'Pause':
self.setText('Continue')
print 'Changed to Continue'
elif self.text() == 'Continue':
self.setText('Pause')
print 'Changed to Pause'
If in the first file I remove the comment at thread.start() it works as expected, it starts the thread and then hangs until I click Play on the UI. However, I thought it should work even if I didn't start it there as the signal is btnSgn is connected to onClickFunction which in this case takes the value thread.start.
Used this as a reference
http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/
I think the reason it doesn't work when you try to call thread.start() in your second file (via self.btnSgn.emit()) is that the thread object goes out of scope outside of the init function where you created it. So you are calling start() on an already deleted thread.
Just changing thread -> self.thread (i.e. making the thread object a member of the WindowClass object) works fine when I tried it, since thread is then kept alive till the end of the program.

Categories