I have a PySide (Qt) GUI which spawns multiple threads. The threads sometimes need to update the GUI. I have solved this in the following way:
class Signaller(QtCore.QObject) :
my_signal = QtCore.Signal(QListWidgetItem, QIcon)
signaller = Signaller()
class MyThread(threading.Thread):
def __init__(self):
super(IconThread, self).__init__()
# ...
def run(self) :
# ...
# Need to update the GUI
signaller.my_signal.emit(self.item, icon)
#
# MAIN WINDOW
#
class Main(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
# ...
# Connect signals
signaller.my_signal.connect(self.my_handler)
#QtCore.Slot(QListWidgetItem, QIcon)
def my_handler(self, item, icon):
item.setIcon(icon)
def do_something(self, address):
# ...
# Start new thread
my_thread = MyThread(newItem)
my_thread.start()
# ...
Is there an easier way? Creating the signals, handlers and connect them requires a few lines of code.
I started coding with PySide recently and I needed a equivalent of PyGObject's GLib.idle_add behaviour. I based the code off of your answer ( https://stackoverflow.com/a/11005204/1524507 ) but this one uses events instead of using a queue ourselves.
from PySide import QtCore
class InvokeEvent(QtCore.QEvent):
EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
def __init__(self, fn, *args, **kwargs):
QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE)
self.fn = fn
self.args = args
self.kwargs = kwargs
class Invoker(QtCore.QObject):
def event(self, event):
event.fn(*event.args, **event.kwargs)
return True
_invoker = Invoker()
def invoke_in_main_thread(fn, *args, **kwargs):
QtCore.QCoreApplication.postEvent(_invoker,
InvokeEvent(fn, *args, **kwargs))
Which is used the same way in the above answer link.
This is what I have so far. I wrote the following code somewhere in a helper module:
from Queue import Queue
class Invoker(QObject):
def __init__(self):
super(Invoker, self).__init__()
self.queue = Queue()
def invoke(self, func, *args):
f = lambda: func(*args)
self.queue.put(f)
QMetaObject.invokeMethod(self, "handler", QtCore.Qt.QueuedConnection)
#Slot()
def handler(self):
f = self.queue.get()
f()
invoker = Invoker()
def invoke_in_main_thread(func, *args):
invoker.invoke(func,*args)
Then my threads can very easily run code to update the GUI in the main thread. There is no need to create and connect signals for every operation.
class MyThread(threading.Thread):
def __init__(self):
super(IconThread, self).__init__()
# ...
def run(self) :
# ...
# Need to update the GUI
invoke_in_main_thread(self.item.setIcon, icon)
I think something like this is quite nice.
Related
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
I am wanting to have a list of commands being processed through a QProcess and have its output be appended to a textfield I have. I've found a these two pages that seems to do each of the things i need (updating the UI, and not freezing the UI via QThread):
Printing QProcess Stdout only if it contains a Substring
https://nikolak.com/pyqt-threading-tutorial/
So i tried to combine these two....
import sys
from PySide import QtGui, QtCore
class commandThread(QtCore.QThread):
def __init__(self):
QtCore.QThread.__init__(self)
self.cmdList = None
self.process = QtCore.QProcess()
def __del__(self):
self.wait()
def command(self):
# print 'something'
self.process.start('ping', ['127.0.0.1'])
processStdout = str(self.process.readAll())
return processStdout
def run(self):
for i in range(3):
messages = self.command()
self.emit(QtCore.SIGNAL('dataReady(QString)'), messages)
# self.sleep(1)
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def dataReady(self,outputMessage):
cursorOutput = self.output.textCursor()
cursorSummary = self.summary.textCursor()
cursorOutput.movePosition(cursorOutput.End)
cursorSummary.movePosition(cursorSummary.End)
# Update self.output
cursorOutput.insertText(outputMessage)
# Update self.summary
for line in outputMessage.split("\n"):
if 'TTL' in line:
cursorSummary.insertText(line)
self.output.ensureCursorVisible()
self.summary.ensureCursorVisible()
def initUI(self):
layout = QtGui.QHBoxLayout()
self.runBtn = QtGui.QPushButton('Run')
self.runBtn.clicked.connect(self.callThread)
self.output = QtGui.QTextEdit()
self.summary = QtGui.QTextEdit()
layout.addWidget(self.runBtn)
layout.addWidget(self.output)
layout.addWidget(self.summary)
centralWidget = QtGui.QWidget()
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
# self.process.started.connect(lambda: self.runBtn.setEnabled(False))
# self.process.finished.connect(lambda: self.runBtn.setEnabled(True))
def callThread(self):
self.runBtn.setEnabled(False)
self.get_thread = commandThread()
# print 'this this running?'
self.connect(self.get_thread, QtCore.SIGNAL("dataReady(QString)"), self.dataReady)
self.connect(self.get_thread, QtCore.SIGNAL("finished()"), self.done)
def done(self):
self.runBtn.setEnabled(True)
def main():
app = QtGui.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The problem is that once I click the "Run" button the textfield on the right doesn't seem to populate, and i am no longer getting any errors so I am not sure what is happening.
I tried referring to this page as well but I think i am already emulating what it is describing...?
https://www.qtcentre.org/threads/46056-QProcess-in-a-loop-works-but
Ultimately what I want to build is for a main window to submit a series of commands via subprocess/QProcess, and open up a little log window that constantly updates it on the progress via displaying the console output. Similar to what you kind of see in like Installer packages...
I feel like i am so close to an answer, yet so far away. Is anyone able to chime in on this?
EDIT: so to answer eyllanesc's question, the list of commands has to be run one after the previous one has completed, as the command i plan to use will be very CPU intensive, and i cannot have more than one process of it running. also the time of each command completing will completely vary so I can't just have a arbitrary hold like with time.sleep() as some may complete quicker/slower than others. so ideally figuring out when the process has finished should kickstart another command (which is why i have a for loop in this example to represent that).
i also decided to use threads because apparently that was one way of preventing the UI to freeze while the process was running,so i assumed i needed to utilize this to have a sort of live feed/update in the text field.
the other thing is in the UI i would ideally in addition to updating a text field with console logs, i would want it to have some sort of label that gets updated that says something like "2 of 10 jobs completed". so something like this:
It would be nice too when before a new command is being processed a custom message can be appended to the text field indicating what command is being run...
UPDATE: apologies for taking so long to post an update on this, but based on eyllanesc's answer, I was able to figure out how to make this open a separate window and run the "ping" commands. here is the example code I have made to achieve my results in my main application:
from PySide import QtCore, QtGui
class Task:
def __init__(self, program, args=None):
self._program = program
self._args = args or []
#property
def program(self):
return self._program
#property
def args(self):
return self._args
class SequentialManager(QtCore.QObject):
started = QtCore.Signal()
finished = QtCore.Signal()
progressChanged = QtCore.Signal(int)
dataChanged = QtCore.Signal(str)
#^ this is how we can send a signal and can declare what type
# of information we want to pass with this signal
def __init__(self, parent=None):
super(SequentialManager, self).__init__(parent)
self._progress = 0
self._tasks = []
self._process = QtCore.QProcess(self)
self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
self._process.finished.connect(self._on_finished)
self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
def execute(self, tasks):
self._tasks = iter(tasks)
#this 'iter()' method creates an iterator object
self.started.emit()
self._progress = 0
self.progressChanged.emit(self._progress)
self._execute_next()
def _execute_next(self):
try:
task = next(self._tasks)
except StopIteration:
return False
else:
self._process.start(task.program, task.args)
return True
# QtCore.Slot()
#^ we don't need this line here
def _on_finished(self):
self._process_task()
if not self._execute_next():
self.finished.emit()
# #QtCore.Slot()
def _on_readyReadStandardOutput(self):
output = self._process.readAllStandardOutput()
result = output.data().decode()
self.dataChanged.emit(result)
def _process_task(self):
self._progress += 1
self.progressChanged.emit(self._progress)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.outputWindow = outputLog(parentWindow=self)
self._button = QtGui.QPushButton("Start")
central_widget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(self._button)
self.setCentralWidget(central_widget)
self._button.clicked.connect(self.showOutput)
def showOutput(self):
self.outputWindow.show()
self.outputWindow.startProcess()
#property
def startButton(self):
return self._button
class outputLog(QtGui.QWidget):
def __init__(self, parent=None, parentWindow=None):
QtGui.QWidget.__init__(self,parent)
self.parentWindow = parentWindow
self.setWindowTitle('Render Log')
self.setMinimumSize(225, 150)
self.renderLogWidget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(self.renderLogWidget)
self._textedit = QtGui.QTextEdit(readOnly=True)
self._progressbar = QtGui.QProgressBar()
self._button = QtGui.QPushButton("Close")
self._button.clicked.connect(self.windowClose)
lay.addWidget(self._textedit)
lay.addWidget(self._progressbar)
lay.addWidget(self._button)
self._manager = SequentialManager(self)
self.setLayout(lay)
def startProcess(self):
self._manager.progressChanged.connect(self._progressbar.setValue)
self._manager.dataChanged.connect(self.on_dataChanged)
self._manager.started.connect(self.on_started)
self._manager.finished.connect(self.on_finished)
self._progressbar.setFormat("%v/%m")
self._progressbar.setValue(0)
tasks = [
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
]
self._progressbar.setMaximum(len(tasks))
self._manager.execute(tasks)
#QtCore.Slot()
def on_started(self):
self._button.setEnabled(False)
self.parentWindow.startButton.setEnabled(False)
#QtCore.Slot()
def on_finished(self):
self._button.setEnabled(True)
#QtCore.Slot(str)
def on_dataChanged(self, message):
if message:
cursor = self._textedit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(message)
self._textedit.ensureCursorVisible()
def windowClose(self):
self.parentWindow.startButton.setEnabled(True)
self.close()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
i still don't really understand the use of the QtCore.Slot() decorators as when I commented them out it didn't really seem to change the result. But i kept them in just to be safe.
It is not necessary to use threads in this case since QProcess is executed using the event loop. The procedure is to launch a task, wait for the finishes signal, get the result, send the result, and execute the next task until all the tasks are finished. The key to the solution is to use the signals and distribute the tasks with an iterator.
Considering the above, the solution is:
from PySide import QtCore, QtGui
class Task:
def __init__(self, program, args=None):
self._program = program
self._args = args or []
#property
def program(self):
return self._program
#property
def args(self):
return self._args
class SequentialManager(QtCore.QObject):
started = QtCore.Signal()
finished = QtCore.Signal()
progressChanged = QtCore.Signal(int)
dataChanged = QtCore.Signal(str)
def __init__(self, parent=None):
super(SequentialManager, self).__init__(parent)
self._progress = 0
self._tasks = []
self._process = QtCore.QProcess(self)
self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
self._process.finished.connect(self._on_finished)
self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
def execute(self, tasks):
self._tasks = iter(tasks)
self.started.emit()
self._progress = 0
self.progressChanged.emit(self._progress)
self._execute_next()
def _execute_next(self):
try:
task = next(self._tasks)
except StopIteration:
return False
else:
self._process.start(task.program, task.args)
return True
QtCore.Slot()
def _on_finished(self):
self._process_task()
if not self._execute_next():
self.finished.emit()
#QtCore.Slot()
def _on_readyReadStandardOutput(self):
output = self._process.readAllStandardOutput()
result = output.data().decode()
self.dataChanged.emit(result)
def _process_task(self):
self._progress += 1
self.progressChanged.emit(self._progress)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self._button = QtGui.QPushButton("Start")
self._textedit = QtGui.QTextEdit(readOnly=True)
self._progressbar = QtGui.QProgressBar()
central_widget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(self._button)
lay.addWidget(self._textedit)
lay.addWidget(self._progressbar)
self.setCentralWidget(central_widget)
self._manager = SequentialManager(self)
self._manager.progressChanged.connect(self._progressbar.setValue)
self._manager.dataChanged.connect(self.on_dataChanged)
self._manager.started.connect(self.on_started)
self._manager.finished.connect(self.on_finished)
self._button.clicked.connect(self.on_clicked)
#QtCore.Slot()
def on_clicked(self):
self._progressbar.setFormat("%v/%m")
self._progressbar.setValue(0)
tasks = [
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
]
self._progressbar.setMaximum(len(tasks))
self._manager.execute(tasks)
#QtCore.Slot()
def on_started(self):
self._button.setEnabled(False)
#QtCore.Slot()
def on_finished(self):
self._button.setEnabled(True)
#QtCore.Slot(str)
def on_dataChanged(self, message):
if message:
cursor = self._textedit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(message)
self._textedit.ensureCursorVisible()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
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()
This question already has answers here:
PyQt: Connecting a signal to a slot to start a background operation
(3 answers)
Closed 5 years ago.
So having followed my best understanding of how to correctly use threads in Qt, I've written a small toy example in which a QObject is moved to a running thread and a signal is called on that object when the window is clicked.
The problem I'm having is the slot is never called in the correct thread - it is always called in the main thread.
This is implemented with the following code:
from PyQt5 import QtWidgets, QtCore
class WidgetWithInput(QtWidgets.QWidget):
widgetClicked = QtCore.pyqtSignal()
def mousePressEvent(self, event):
self.widgetClicked.emit()
class MyObject(QtCore.QObject):
aSignal = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(MyObject, self).__init__(*args, **kwargs)
self.aSignal.connect(self.onASignal)
def onASignal(self):
print('MyObject signal thread: %s'
% str(int(QtCore.QThread.currentThreadId())))
class SimpleDisplay(QtWidgets.QMainWindow):
''' Class presenting the display subsytem
'''
def __init__(self, my_object, *args, **kwargs):
super(SimpleDisplay, self).__init__(*args, **kwargs)
self._my_object = my_object
self._widget = WidgetWithInput()
self.setCentralWidget(self._widget)
self._widget.widgetClicked.connect(self._do_something)
def _do_something(self):
self._my_object.aSignal.emit()
def main(run_until_time=None):
import sys
app = QtWidgets.QApplication(sys.argv)
print ('main thread: %s' % str(int(QtCore.QThread.currentThreadId())))
concurrent_object = MyObject()
display = SimpleDisplay(concurrent_object)
myobject_thread = QtCore.QThread()
myobject_thread.start()
concurrent_object.moveToThread(myobject_thread)
display.show()
exit_status = app.exec_()
myobject_thread.quit()
myobject_thread.wait()
sys.exit(exit_status)
if __name__ == '__main__':
main()
Which outputs something like:
main thread: 139939383113472
MyObject signal thread: 139939383113472
I would expect those two printed thread ids to be different.
Am I missing something here? Explicitly setting the signal to be queued doesn't change anything.
With thanks to #three_pinapples, his suggestion in the comments solves the problem: The slot needs to be decorated with QtCore.pyqtSlot:
class MyObject(QtCore.QObject):
aSignal = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(MyObject, self).__init__(*args, **kwargs)
self.aSignal.connect(self.onASignal)
#QtCore.pyqtSlot()
def onASignal(self):
print('MyObject signal thread:\t %s'
% str(int(QtCore.QThread.currentThreadId())))
I would like to transfer data to threading class, but I can't get what is wrong. The code below is from this question and I changed it a little.
This is a code:
import gtk, gobject, threading, time
gobject.threads_init()
class T(threading.Thread):
pause = threading.Event()
stop = False
def start(self, data, *args):
super(T, self).start()
def run(self, data):
while not self.stop:
self.pause.wait()
gobject.idle_add(lambda *a : self.rungui(data))
time.sleep(0.1)
def rungui(self, data):
print "printed"
print data
thread = T()
class Start:
def toggle_thread(self, data=None, *args):
if not thread.is_alive():
thread.start(data)
thread.pause.set()
self.button.set_label('Pause Thread')
return
if thread.pause.is_set():
thread.pause.clear()
self.button.set_label('Resume Thread')
else:
thread.pause.set()
self.button.set_label('Pause Thread')
def __init__(self):
thread = T()
window = gtk.Window()
self.button = gtk.ToggleButton('Start Thread')
data = 3
self.button.connect('toggled', lambda *a : self.start(data), None)
window.add(self.button)
self.button.show()
window.show()
def start(self, data=None):
self.toggle_thread(data)
def main(self):
gtk.main()
if __name__ == "__main__":
start = Start()
start.main()
What do I have to correct to get threading fully working?
Don`t work with gtk out of gui thread. That about:
gobject.idle_add(self.rungui)
Example at your link work fine, but need system kill command for termination.
And super() can`t bring arguments to run() function.
My threads initialization looks like this:
class VirtService(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def thread_loop(self):
while self.queue.qsize():
data_command = self.queue_get()
...
queue = Queue()
if __name__ == '__main__':
gobject.threads_init()
vs = VirtService(queue)
And you may use Queue for data translation to both directions. You may use also queue for command. In non-graphical thread create c++ poll() analog through Queue.qet(), and in gui thread queue.put()