PyQt6 Multithreading thousands of tasks with progress bar - python-3.x

Please bear with me I am just getting into PyQt6 multithreading so this question might seem too simple and very well might have been answered previously, however I am at an impass and none of the answers or posts about the topic really make anything clear to me. I am looking to be able to click a button it then starts a thread for each task and as each one finishes it updates the progress bar. Here is what I have so far:
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton,QVBoxLayout, QWidget, QProgressBar
import sys
from time import sleep
from random import random
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.fi = [1,2,3,4,5,6,7,8,9,10] * 100
self.p = None
# add the widgets
self.btn = QPushButton("Execute")
self.btn.pressed.connect(self.start_threads)
self.progress = QProgressBar()
self.progress.setRange(0, len(self.fi))
l = QVBoxLayout()
l.addWidget(self.btn)
l.addWidget(self.progress)
w = QWidget()
w.setLayout(l)
self.setCentralWidget(w)
def task(self,num):
sleep(random())
# where i will run the task using what is passed through
def start_threads(self):
with ThreadPoolExecutor(len(self.fi)) as executor:
futures = []
for i in self.fi:
futures.append(executor.submit(lambda:self.task(i)))
for future in as_completed(futures):
value = self.progress.value()
self.progress.setValue(value + 1)
print(value)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Whilst this does nearly all my criteria the it only runs 998 of the 1000 tasks it is supposed to. It also makes the GUI freeze which is exactly what I'm trying to avoid by multithreading. I am aware that Qt workers exist however my test for them did not result in any success. I am also aware that this is definetly not how this should be done and thats why I am asking for a more acceptable solution.

Related

How to interact with PyQt5 main thread from another thread in another file?

I'm trying to figure out how to interact with the main thread in PyQt5 from another thread running in another file.
In the example below I'm trying to make a button green from the followup.py file's initialize function which is a thread started in the workbot_main.py file.
From the followup.py file I can't call 'workbot_main.widget' because it doesn't recognize it and I can't call workbot_main.MainWindow because it doesn't recognize it as an instantiated class so 'self' doesn't work and therefore most things within the class.
How am I supposed to interact with the MainWindow thread from another file?
I tried using slots and signals but I can't get that to work either.
Help would be massively appreciated.
#workbot_main.py
import threading
from workbot import *
import followup
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMainWindow, QApplication
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
#UI_MainWindow is from the workbot.py file which is generated from QTDesigner
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
followup_start_button = self.ui.followup_start_button
followup_start_button.clicked.connect(threads.thread_launch_followup_initialize)
p1_followup_button = self.ui.p1_followup_button
#QtCore.pyqtSlot()
def p1_followup_button_color_green():
self.p1_followup_button.setStyleSheet("background-color : green")
class Threads:
def __init__(self):
pass
def thread_launch_followup_initialize(self):
t1 = threading.Thread(target = followup.initialize, args = ())
t1.start()
threads = Threads()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
widget = MainWindow()
widget.show()
app.exec_()
#followup.py file
def make_green():
workbot_main.MainWindow.p1_followup_button_color_green()
initialize():
make_green()
do other stuff
Edit:
I've tried doing a great many things, the only thing I could get to work is this
#workbot_main.py
import threading
from workbot import *
import followup
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import QThread, pyqtSignal, QObject, pyqtSlot, Qt
class Change_green(QObject):
setgreen = pyqtSignal()
#pyqtSlot()
def green(self):
self.setgreen.emit()
print("clicked")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
#UI_MainWindow is from the workbot.py file which is generated from QTDesigner
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.Change_green = Change_green()
self.Change_green.setgreen.connect(lambda: print("connected"))
self.Change_green.setgreen.connect(self.p1_followup_button_color_green)
followup_start_button = self.ui.followup_start_button
followup_start_button.clicked.connect(self.Change_green.green) # <--- this works
p1_followup_button = self.ui.p1_followup_button
#pyqtSlot()
def p1_followup_button_color_green():
self.p1_followup_button.setStyleSheet("background-color : green")
if __name__ == '__main__':
app = QtWidgets.QApplication([])
widget = MainWindow()
widget.show()
app.exec_()
But that is useless to me since the signal is sent from inside the main thread.
What I need to work is this
#workbot_main.py
import threading
from workbot import *
import followup
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import QThread, pyqtSignal, QObject, pyqtSlot, Qt
class Change_green(QObject):
setgreen = pyqtSignal()
#pyqtSlot()
def green(self):
self.setgreen.emit()
print("clicked")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
#UI_MainWindow is from the workbot.py file which is generated from QTDesigner
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.Change_green = Change_green()
self.Change_green.setgreen.connect(lambda: print("connected"))
self.Change_green.setgreen.connect(self.p1_followup_button_color_green)
followup_start_button = self.ui.followup_start_button
#starts the initialize function in followup.py which calls the Change_green class above
followup_start_button.clicked.connect(threads.thread_launch_followup_initialize)
p1_followup_button = self.ui.p1_followup_button
#pyqtSlot()
def p1_followup_button_color_green():
self.p1_followup_button.setStyleSheet("background-color : green")
class Threads:
def __init__(self):
pass
def thread_launch_followup_initialize(self):
t1 = threading.Thread(target = followup.initialize, args = ())
t1.start()
threads = Threads()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
widget = MainWindow()
widget.show()
app.exec_()
# followup.py
import workbot_main
color = workbot_main.Change_green()
def initialize():
print("initializing")
color.green()
But if I call the Change_green() function from followup.py, the Change_green() function gets called properly because "clicked" gets printed but there is no following "connected" being printed from the main thread.
It's as if self.setgreen.emit() only works when called from within the main thread.
I couldn't execute your script since you used a QTDesigner project to create the GUI. Also, I don't know which modules are workbot or followup, so I didn't tried to install them as they might be private modules.
I can see that you used the threading module from python. In many tutorials available on the internet, they tell you to not use this module when dealing with Qt. If need multithreading, use the QThread class instead.
The reason is that Qt schedules its own events. When dealing with raw data, that might be ok. But when dealing with QWidgets, their methods are not thread safe, so executing them from another thread might lead you to many undesired problems.
As I recommend using QThreads, I provided a minor example below explaining how to use it (it's not so easy to start using QThreads, I had and still have a hard time when dealing with them).
If you need, you can place the QThread and QObject classes in another file, then import them from main file. There's no problem about that, just be careful to not let their instances' references be collected by the python's garbage collector.
I keep them out of reach by using self.thread = ChangeColorThread(...) inside the Scene's constructor. In this way, the thread's instance is bound to the Scene, and it's reference will not go out of scope when the Scene's constructor block ends.
from PySide2.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PySide2.QtCore import QObject, QThread, Signal, QMutex
import random
# I use PySide2 which is the same thing as PyQt5 with the exception of a few variable names
# that might change from one module to another. One of them might be `Signal` (PySide2) to
# `pyqtSignal` (PyQt5) for example.
#
# The imports should also change:
# from PySide2.QtWidgets import ...
# to
# from PyQt5.QtWidgets import ...
#
# But the idea is the same for both modules.
# -------------------------------------------------------------------------------------
# After a few researches on the Multi-Threading topic, I liked the option of using the
# `QObject.moveToThread(thread)` method to create threads based on QObjects.
#
# There are others:
# - Using QThreadPool and QRunnable
# - Using QThread only (by overwriting run() method. Not recommended.)
#
# For me, I got everything working so far using the `moveToThread` method, so that's
# the one I will be using on this example.
# -------------------------------------------------------------------------------------
# Here we create the Worker Object. Everything inside `Worker.main(self)` will be
# executed in another thread: ChangeColorThread.
#
# As we might inspect / change the value of `Worker.running` on both threads at the same time
# (Main Thread and ChangeColorThread), I recommend using a QMutex to restrict access
# to this variable.
class Worker(QObject):
# Note the signal possesses a `object` as a parameter. It signalizes the QObject
# that you want to pass an object type to the signal's receiver.
#
# In this case, it's a tuple (red, green, blue) as `Scene.setRandomColor` has the
# `color` parameter.
produce = Signal(object)
# Called from main thread only to construct the Worker instance.
def __init__(self, delay):
QObject.__init__(self)
self.running = True
self.delay = delay
self.runningLock = QMutex()
# Might be called from both threads.
def stop(self):
self.runningLock.lock()
self.running = False
self.runningLock.unlock()
# Might be called from both threads.
def stillRunning(self):
self.runningLock.lock()
value = self.running
self.runningLock.unlock()
return value
# Executes on ChangeColorThread only.
def main(self):
while (self.stillRunning()):
# Generate the random colors from the ChangeColorThread.
red = random.randint(0, 255)
green = random.randint(0, 255)
blue = random.randint(0, 255)
# Emit the `produce` signal to safely call the `Scene.setRandomColor()`
# on the Main Thread.
#
# In order to update any GUI or call any QWidget method, you must emit
# a connected signal to the main thread, so you don't raise any exceptions
# or segmentation faults (or worse, silent crashes).
self.produce.emit((red, green, blue))
# Tell the ChangeColorThread to sleep for delay microseconds
# (aka a value of 1000 == 1 second)
self.thread().msleep(self.delay)
print('Quit from ChangeColorThread Worker.main()')
self.thread().quit()
# Here we have the other thread we will instantiate. The thread by itself
# is nothing special. It acts like a QObject until `QThread.start()` is
# called. It also has a few important properties you might want to take a look:
# - started (calls one or more connected functions once the thread starts executing).
# - finished (calls one or more connected functions once the thread properly finishes).
#
# When `start()` is called, as we connected `self.started` to `Worker.main`,
# `Worker.main` will start executing on the other thread until a `stop` call
# is requested.
class ChangeColorThread(QThread):
def __init__(self, produce_callback, delay=100):
QThread.__init__(self)
self.worker = Worker(delay)
self.worker.moveToThread(self)
self.started.connect(self.worker.main)
self.worker.produce.connect(produce_callback)
# Stops the worker object's main loop from the main thread.
def stop(self):
self.worker.stop()
# Here we will create the GUI. I kept it simple, it contains a single QLabel.
#
# It also starts the ChangeColorThread thread.
class Scene(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout()
self.setLayout(layout)
self.label = QLabel("<h3>See the Color Change?</h3>")
layout.addWidget(self.label)
self.thread = ChangeColorThread(self.setRandomColor, 500)
self.thread.start()
# This function will execute on the Main Thread only.
#
# However, this function is not called by the user. It is scheduled to execute at some point
# by PyQt5 once `Worker.produce` is emitted.
def setRandomColor(self, color):
self.label.setStyleSheet("background-color:rgb(%d,%d,%d)" % (color[0], color[1], color[2]))
# As we don't know wether ChangeColorThread has stopped or not, we signalize
# it to stop before closing the application.
#
# This step is important, because even if PyQt5 / PySide2 tries its best to close the
# remaining thread, once the window is closed, this process can fail. The result is a
# hidden process still running even after the program is closed.
def sceneInterceptCloseEvent(self, evt):
self.thread.stop()
self.thread.wait()
evt.accept()
# Here we create the Main Window. It contains a single scene with the QLabel inside it.
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.scene = Scene()
self.setCentralWidget(self.scene)
# We call Scene.sceneInterceptCloseEvent in order to signalize the running thread
# to stop its main loop before the application is closed.
def closeEvent(self, evt):
self.scene.sceneInterceptCloseEvent(evt)
if __name__ == '__main__':
app = QApplication()
win = Window()
win.show()
app.exec_()
So the idea of this example is simple. We have 2 threads:
The Main Thread: which handles the GUI, scene creation, user input, and thread management. Whenever the user tries to exit the application, it's on this thread that the other ones will be signalized to be stopped.
ChangeColorThread: which is an infinite loop that executes on the background, to change the QLabel's color at every few seconds, set by the delay variable. It can be signalized to stop by the Main Thread.
Once the application starts executing, the label's background color should change based on the emission of Worker.produce signal, which sets which random color should be used for the label.
You can also move the window or resize it. By doing these manual operations, it proves that the ChangeColorThread is really executing at the background of the application. Otherwise the application would become unresponsive (frozen).
As stated on the script, I used PySide2 instead of PyQt5. But its almost the same thing, as just a few variable names should change from one to another. If you find any problems because of this, just let me know on the comments.
Edit: How to make your script work with both ways
What confused me about your post was that you rewrote your workbot_main.py script 3x, then wrote followup.py 2x, one at the middle and one at the end. I was not understanding your examples. It's hard to keep track of a lot of code, specially if you provide a whole script when you just change one line or another.
After checking them with more paciency, I was able to understand what you wanted to do. I think...
You're trying to this right?
You click on self.ui.followup_start_button
This button is connected to threads.thread_launch_followup_initialize
Which calls Change_green.green()
Which emits the signal setgreen
setgreen is connected to MainWindow.p1_followup_button_color_green
This function on MainThread changes the background color of self.ui.p1_followup_button to the green color.
The main problem I found with your script is that you're creating two different instances of Change_green:
First one: in your followup.py file.
Second one: inside your MainWindow.__init__(self).
So everytime that you tried to access the object created on MainWindow from your thread, you were in fact accessing a different instance of the same class. After solving this problem and making a few tweaks on your script, it worked without any errors so far. (Yes, the button's background color did changed to green.)
Also, you say that you can't solve your problem using QThreads. In the final example below, I also use your own classes (with one additional method and Signal) and a custom QThread I created, to reproduce the same type of idea, but now changing the button color to red.
I put all Threading Classes into followup.py, and left only Scene and Logic Classes on workbot_main.py.
Here is your new workbot_main.py:
# from workbot import * # Can't import from workbot as I don't have this module on my side
import followup
from PySide2 import QtWidgets
from PySide2 import QtCore
from PySide2.QtWidgets import QMainWindow, QApplication, QWidget, QPushButton, QVBoxLayout
from PySide2.QtCore import QThread, Signal, QObject, Slot, Qt
class Ui(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout()
self.setLayout(layout)
self.followup_start_button = QPushButton("Start FollowUp (Green)")
self.qthread_button = QPushButton("Start QThread (Red)")
self.p1_followup_button = QPushButton("Check me Turn Out Green or Red")
layout.addWidget(self.followup_start_button)
layout.addWidget(self.qthread_button)
layout.addWidget(self.p1_followup_button)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# --------------------------------------------
# # As I don't have your QTDesigner file, I cannot
# # load it.
# # UI_MainWindow is from the workbot.py file which is generated from QTDesigner:
# self.ui = Ui_MainWindow()
# self.ui.setupUi(self)
# --------------------------------------------
# So I created my own "UI" to load both buttons on the screen
# and execute your script.
self.ui = Ui()
self.setCentralWidget(self.ui)
# --------------------------------------------
# threading module (green)
# --------------------------------------------
self.Change_green = followup.Change_green()
self.Change_green.setgreen.connect(lambda: print("connected green"))
self.Change_green.setgreen.connect(self.p1_followup_button_color_green)
# Starts the initialize function in followup.py which calls the Change_green class above
#
# After creating the Change_green instance, we initialize the thread
# from another file here, and pass the instance to the thread, so it
# can access it from there.
self.threads = followup.Threads(self.Change_green)
self.followup_start_button = self.ui.followup_start_button
self.followup_start_button.clicked.connect(self.threads.thread_launch_followup_initialize)
# This line was wrong. But you probably meant to assign this variable to
# your class. After fixing it your script does not raises any error.
self.p1_followup_button = self.ui.p1_followup_button
# --------------------------------------------
# QThread class (red)
# --------------------------------------------
# With QThread you solve this in one line given that everying is configured
# on MyThread(QThread) class.
self.qthread = followup.MyThread(self.Change_green, self.qthread_button_color_red)
self.ui.qthread_button.clicked.connect(self.qthread.start)
# I Changed pyqtSlot -> Slot (As I use PySide2 instead of PyQt5)
#Slot()
def p1_followup_button_color_green(self):
self.p1_followup_button.setStyleSheet("background-color: green")
# I Changed pyqtSlot -> Slot (As I use PySide2 instead of PyQt5)
#Slot()
def qthread_button_color_red(self):
self.p1_followup_button.setStyleSheet("background-color: red")
if __name__ == '__main__':
app = QtWidgets.QApplication([])
widget = MainWindow()
widget.show()
app.exec_()
Here is your new followup.py:
from PySide2.QtCore import QObject, QThread, Signal, Slot
import threading
# ------------------------------------------------------------
# - Fixing your script to use threading module
# ------------------------------------------------------------
# We don't need to import workbot_main anymore.
#
# `color` is the same object being referenced from `MainWindow.Change_green`,
# which was passed by parameter when the thread was initialized on
# `MainWindow.__init__(self)`.
#
# Now the code should work as expected.
def initialize(color):
print("initializing: ", color)
color.green()
# To make the example simpler, I've put all threading scripts here on followup.py
class Threads:
def __init__(self, color):
self.color = color
def thread_launch_followup_initialize(self):
t1 = threading.Thread(target=initialize, args=[self.color])
t1.start()
# Including this object here which will run in another thread.
#
# As I use PySide2 I changed
# "pyqtSignal" -> "Signal"
# "pyqtSlot" -> "Slot"
class Change_green(QObject):
setgreen = Signal()
setred = Signal()
#Slot()
def green(self):
self.setgreen.emit()
print("clicked on green")
#Slot()
def red(self):
self.setred.emit()
print("clicked on red")
self.thread().quit()
# ------------------------------------------------------------
# - Using QThreads instead of python threading module
# ------------------------------------------------------------
class MyThread(QThread):
def __init__(self, change_green, qthread_button_color_red):
QThread.__init__(self)
self.Change_green = change_green
self.Change_green.setred.connect(lambda: print("connected red"))
self.Change_green.setred.connect(qthread_button_color_red)
# This is important. It sets the QObject to run on another thread.
self.Change_green.moveToThread(self)
# Set Change_green.red method to be runned on the other thread
self.started.connect(self.Change_green.red)
# Set the main thread to wait for the other thread, once this
# even is fired.
self.Change_green.setred.connect(self.wait)
Again, I still use PySide2 instead of PyQt5, so instead of using pyqtSignal and pyqtSlot, I used Signal and Slot. This change also happened on the module imports.
I left the QThread example here (Original Answer before the edit) in case you want to compare both of them.

QThread closes whenever QFileDialog Called After Migrating from PyQt5 to Pyside2

First of all, I'm currently migrating my source code from PyQt5 to PySide2 which requires me to change some of the syntaxes. As this site said that it only needs 3 things to do migrate from PyQt to Pyside2.
1.app.exec_. exec_ was used as exec is a Python2 keyword. Under Python3, PyQt5 allows the use of exec but not PySide2.
2.Under PyQt5 it’s QtCore.pyqtSignal and QtCore.pyqtSlot and under PySide2 it’s QtCore.Signal and QtCore.Slot .
3.loading Ui files.
But anyway later on when I tried to run my code it gave me this following error:
QThread: Destroyed while thread is still running
I had more than 2000 lines of code and I cannot determine which is the cause of this other than my last action which is trying to call QFileDialog which shouldn't be a problem (I've tested this with PyQt import and there's no problem and no warning at all). But in PySide2 it definitely might be the cause of it. I look up into this, he doesn't have the same problem as mine exactly. I'm not trying to call QFileDialog from different thread.
this is the minimal reproducible example of my working code in PyQt5:
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QWidget, QDialog
import random
class MyWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.path = os.path.abspath(os.path.dirname(sys.argv[0]))
self.button = QtWidgets.QPushButton("Open File")
self.labelFile = QtWidgets.QLabel("empty")
self.labelData = QtWidgets.QLabel("None")
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.button)
self.layout.addWidget(self.labelFile)
self.layout.addWidget(self.labelData)
self.setLayout(self.layout)
self.button.clicked.connect(self.open_file)
timer = QtCore.QTimer(self)
timer.timeout.connect(self.update_data_value)
timer.start(1000)
def open_file(self):
x = QFileDialog.getOpenFileName(self,"Pilih File CSV yang Ingin Diproses",self.path,"CSV Files (*.csv)")
self.labelFile.setText(x[0])
def update_data_value(self):
self.DataProcess = DataProcess()
self.DataProcess.progress.connect(self.update_data_label)
self.DataProcess.start()
def update_data_label(self,x):
self.labelData.setText(str(x[0]))
class DataProcess(QtCore.QThread):
progress = QtCore.pyqtSignal(object)
def __init__(self):
QtCore.QThread.__init__(self)
def run(self):
x = random.randint(1,100)
self.progress.emit([str(x)+ " from thread"])
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
and this is the non-working one in PySide2 after renaming import accordingly to PySide2 also renaming 'pyqtsignal' to 'Signal'
import sys
import os
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QWidget, QDialog
import random
class MyWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.path = os.path.abspath(os.path.dirname(sys.argv[0]))
self.button = QtWidgets.QPushButton("Open File")
self.labelFile = QtWidgets.QLabel("empty")
self.labelData = QtWidgets.QLabel("None")
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.button)
self.layout.addWidget(self.labelFile)
self.layout.addWidget(self.labelData)
self.setLayout(self.layout)
self.button.clicked.connect(self.open_file)
timer = QtCore.QTimer(self)
timer.timeout.connect(self.update_data_value)
timer.start(1000)
def open_file(self):
x = QFileDialog.getOpenFileName(self,"Pilih File CSV yang Ingin Diproses",self.path,"CSV Files (*.csv)")
self.labelFile.setText(x[0])
def update_data_value(self):
self.DataProcess = DataProcess()
self.DataProcess.progress.connect(self.update_data_label)
self.DataProcess.start()
def update_data_label(self,x):
self.labelData.setText(str(x[0]))
class DataProcess(QtCore.QThread):
progress = QtCore.Signal(object)
def __init__(self):
QtCore.QThread.__init__(self)
def run(self):
x = random.randint(1,100)
self.progress.emit([str(x)+ " from thread"])
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
so after creating this minimal example, I realized that PySide QFileDialog makes the QThread stop while PyQt QFileDialog doesn't freeze the main thread. Is there anything I could do to handle this in similar syntax architecture? (e.g not using "movetothread" or "QObject")
The problem is that you're overwriting self.DataProcess every time a new thread is created, which may cause the previous object to be garbage-collected by Python before Qt has a chance to delete it. This can result in a core-dump if Qt tries to delete an object which is no longer there. Problems of this kind are quite common in both PyQt and PySide, and are usually caused by not keeping proper references to dependant objects. The normal solution is to ensure that the affected objects are given a parent and, if necessary, explicitly delete them at an appropriate time.
Here is one way to fix your example:
class MyWidget(QtWidgets.QWidget):
...
def update_data_value(self):
# ensure the thread object has a parent
process = DataProcess(self)
process.progress.connect(self.update_data_label)
process.start()
def update_data_label(self,x):
self.labelData.setText(str(x[0]))
class DataProcess(QtCore.QThread):
progress = QtCore.Signal(object)
def __init__(self, parent):
# ensure the thread object has a parent
QtCore.QThread.__init__(self, parent)
def run(self):
x = random.randint(1,100)
self.progress.emit([str(x)+ " from thread"])
# explicitly schedule for deletion
self.deleteLater()
It's hard to say exactly why PySide behaves differently to PyQt in this particular case. It usually just comes down to low-level differences between the two implementations. Probably there are equivalent cases that affect PyQt but not PySide. However, if you manage object references and cleanup carefully, such differences can usually be eliminated.

"RuntimeError: Calling Tcl from different appartment" tkinter and threading

I want to implement GUI using threading and tkinter (python 3.6).
When I run GUIExecution.py, the following error occurs.
"RuntimeError: Calling Tcl from different appartment" on self.root.mainloop() in base_gui_class.py
I am implementing it on a class basis, and the three code files are as follows.
The executable file is GUIExecution.py.
I spent a lot of time trying to fix the error, but I have not been able to fix it yet.
Please give a lot of advice.
Additionally, if I run the following code in a python2 environment, it works fine without error.
GUIExecution.py
from base_gui_class import *
from base_class import *
speed = 1000
height = 500
width = 700
base_model = base_class()
gui = base_gui_class(base_model, speed, height, width)
base_model.visualize()
base_class.py
class base_class():
genes = []
dicLocations = {}
gui = ''
best = ''
time = 0
def __init__(self):
pass
def visualize(self):
if self.gui != '':
self.gui.start()
def registerGUI(self, gui):
self.gui = gui
base_gui_class.py
import threading
import tkinter as tk
import math
import threading
import time
class base_gui_class(threading.Thread):
root = ''
canvas = ''
speed = 0
base_model = ''
def __init__(self, base_model, speed, h, w):
threading.Thread.__init__(self)
self.base_model = base_model
base_model.registerGUI(self)
self.root = tk.Tk()
self.canvas = tk.Canvas(self.root, height=h, width=w)
self.canvas.pack()
self.root.title("Test")
self.speed = 1 / speed
def run(self):
self.root.mainloop()
def update(self):
time.sleep(self.speed)
width = int(self.canvas.cget("width"))
height = int(self.canvas.cget("height"))
self.canvas.create_rectangle(0, 0, width, height, fill='white')
def stop(self):
self.root.quit()
To a very good first and second approximation, the core of Tk is single threaded. It can be used from multiple threads, but only by initialising it separately in each of those threads. Internally, it uses thread-specific variables extensively to avoid the need for major locking (that is, it has nothing like a big Global Interpreter Lock) but that means you must not cheat. Whatever thread initialises a Tk context must be the only thread that interacts with that Tk context. This includes loading the Tkinter module so you are effectively restricted to using Tkinter from your main thread only; working around this is serious expert's-only stuff.
I recommend that you make your worker threads make changes to your GUI by posting events to it using a queue (or otherwise interlock with critical sections and condition variables, though I find queues easier in practice).
pip install tkthread
#call the function which shows error like this:
tkthread.call_nosync(yourfunction)
this tkthread library handles all the threading internally by itself.
I recommend you reading the documentation of this library:https://pypi.org/project/tkthread/ .

PyQt: How to use a Qthread to make process stoppable if it´s running too long

I have read a lot on threads, but I really need help with this one:
I have a PyQt Main GUI that runs an optimization with scipy.minimize...
As I cannot not make an example of this I use a "placeholder" process
to show what my problem is.
I want to let the Main GUI remain stoppable by the User, if the Process takes too long to give
a result.
My working example is this one, where I use an integration with sympy
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
import time, sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class IntegrationRunner(QObject):
'Object managing the integration'
def __init__(self):
super(IntegrationRunner, self).__init__()
self._isRunning = True
def longRunning(self):
# reset
if not self._isRunning:
self._isRunning = True
print("preparing Integration")
#this is known to be time consuming and returning not the right integral
#this is a placeholder for a "time consuming" operation that sould
#be handled by a thread outside the main GUI
#imagine an optimization process or any other time consuming operation
#that would freeze the Main GUI
t=sp.symbols('t')
exp=sp.sqrt((3*t+1)/t)
sol=sp.integrate(exp,t)
print(sol)
print('finished...')
def stop(self):
self._isRunning = False
#this is displayed when the "stop button" is clicked
#but actually the integration process won´t stop
print("Integration too long - User terminated")
class SimulationUi(QDialog):
'PyQt interface'
def __init__(self):
super(SimulationUi, self).__init__()
self.goButton = QPushButton('Run Integration')
self.stopButton = QPushButton('Stop if too long')
self.layout = QHBoxLayout()
self.layout.addWidget(self.goButton)
self.layout.addWidget(self.stopButton)
self.setLayout(self.layout)
self.simulThread = QThread()
self.simulThread.start()
self.simulIntegration = IntegrationRunner()
self.simulIntegration.moveToThread(self.simulThread)
#self.simulIntegration.stepIncreased.connect(self.currentStep.setValue)
# call stop on Integr.Runner from this (main) thread on click
self.stopButton.clicked.connect(lambda: self.simulIntegration.stop())
self.goButton.clicked.connect(self.simulIntegration.longRunning)
if __name__ == '__main__':
app = QApplication(sys.argv)
simul = SimulationUi()
simul.show()
sys.exit(app.exec_())
After clicking the "start" button and the "stop" button before the integration stops automatically
i get this output:
>>preparing Integration
>>Integration too long - User terminated
>>Integral(sqrt((3*t + 1)/t), t)
>>finished...
This isn´t exactly my problem but I want to understand how I can use a thread where I can put
time consuming calculations and stop them in order to try other "parameters" or "initial guesses"
when using something like Scipy Minimize
Is it even possible to stop an iterative function from "outside" and restart it without getting into "non responding"?
Any help to improve here is appreciated.
I took these example here as a guideline
how-to-signal-from-a-running-qthread-back-to-...
and pastebin

PyQt doesn't switch the screen

I've written a GUI program which do a lot of calculation in the back, at the same time, display a progress bar. After it was done, a new screen will show the result.
Then I want to make another interface before the calculation to let the user select if they want to use the last calculation result, thus skipping the calculation.
I made a screen with a button which connected to the calculation, and a combo box to select last calculation result file.
However, when I clicked the button, it did nothing. And after around 10 secs( the duration for the calculation ), the result screen pop open. Thus, it skipped the progress bar screen. Why?
This it part of the original program:
import sys
import configparser
import getpass
import telnetlib
import time
import subprocess
from datetime import *
from log_tracker import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import QtCore
from PyQt4 import QtGui
class Task_Checker(QMainWindow):
def __init__(self):
super(Task_Checker, self).__init__()
config = configparser.RawConfigParser()
config.read('profile.cfg')
self.log_path = config.get('Config', 'log_path')
self.log_prefix = config.get('Config', 'log_prefix')
self.log_suffix = config.get('Config', 'log_suffix')
self.initUI()
def check_production(self):
self.log_tracker = Log_Tracker(self)
self.log_tracker.tick.connect(self.pbar.setValue)
self.log_tracker.parseConfig()
self.log_tracker.connectDb()
self.log_tracker.trackLog()
def initUI(self):
self.resize(1400, 768)
self.center()
self.statusBar().showMessage('Checking Production Programs')
self.wait_message = QLabel('Checking Production Programs')
self.wait_message.setAlignment(Qt.Alignment(Qt.AlignHCenter))
self.pbar = QProgressBar(self)
self.pbar.setMinimum(0)
self.pbar.setMaximum(100)
vbox = QVBoxLayout()
vbox.addWidget(self.wait_message)
vbox.addWidget(self.pbar)
tmpWidget2 = QWidget()
tmpWidget2.setLayout(vbox)
self.setCentralWidget(tmpWidget2)
self.show()
self.check_production()
self.pbar.hide()
self.statusBar().showMessage('Processing Information')
self.tabs = QTabWidget()
mua_table = self.processInfo('MUA')
bps_table = self.processInfo('BPS')
obdua_table = self.processInfo('OBDUA')
sua_table = self.processInfo('SUA')
ngr_ftp_table = self.processInfo('NGR_FTP')
bpspdfbill_table = self.processInfo('BpsPdfBill')
disk_space_table = self.processInfo('Disk_Space')
self.tabs.addTab(mua_table, 'MUA')
self.tabs.addTab(bps_table, 'BPS')
self.tabs.addTab(obdua_table, 'ODBUA')
self.tabs.addTab(sua_table, 'SUA')
self.tabs.addTab(ngr_ftp_table, 'NGR_FTP')
self.tabs.addTab(bpspdfbill_table, 'BpsPdfBill')
self.tabs.addTab(disk_space_table, 'Disk_Space')
self.setCentralWidget(self.tabs)
self.statusBar().showMessage('Ready')
self.setWindowTitle('Task Checker')
self.show()
def center(self):
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def processInfo(self, project_name):
...the processing...
In order to add a new screen before the progress bar ( originally it loads the calculation right away ), I made a fews changes to initUI() and move the calculation part to a new sub routine checkProd(), then connect it with a button:
import sys
import configparser
import getpass
import telnetlib
import time
import subprocess
from datetime import *
from log_tracker import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import QtCore
from PyQt4 import QtGui
class Task_Checker(QMainWindow):
def __init__(self):
super(Task_Checker, self).__init__()
config = configparser.RawConfigParser()
config.read('profile.cfg')
self.log_path = config.get('Config', 'log_path')
self.log_prefix = config.get('Config', 'log_prefix')
self.log_suffix = config.get('Config', 'log_suffix')
self.initUI()
def check_production(self):
self.log_tracker = Log_Tracker(self)
self.log_tracker.tick.connect(self.pbar.setValue)
self.log_tracker.parseConfig()
self.log_tracker.connectDb()
self.log_tracker.trackLog()
def initUI(self):
self.resize(1400, 768)
self.center()
btn_check = QPushButton('Check Lastest Status', self)
btn_check.setToolTip('Click this if you want to check the lastest status in production')
combo = QComboBox()
dirlist = os.listdir(self.log_path)
for f in dirlist:
combo.addItem(f)
QtCore.QObject.connect(btn_check, QtCore.SIGNAL('clicked()'), self.checkProd)
hbox = QHBoxLayout()
hbox.addWidget(btn_check)
hbox.addWidget(combo)
tmpWidget = QWidget()
tmpWidget.setLayout(hbox)
self.setCentralWidget(tmpWidget)
self.show()
def checkProd(self):
self.statusBar().showMessage('Checking Production Programs')
self.wait_message = QLabel('Checking Production Programs')
self.wait_message.setAlignment(Qt.Alignment(Qt.AlignHCenter))
self.pbar = QProgressBar(self)
self.pbar.setMinimum(0)
self.pbar.setMaximum(100)
vbox = QVBoxLayout()
vbox.addWidget(self.wait_message)
vbox.addWidget(self.pbar)
tmpWidget2 = QWidget()
tmpWidget2.setLayout(vbox)
self.setCentralWidget(tmpWidget2)
self.show()
self.check_production()
self.pbar.hide()
self.statusBar().showMessage('Processing Information')
self.tabs = QTabWidget()
mua_table = self.processInfo('MUA')
bps_table = self.processInfo('BPS')
obdua_table = self.processInfo('OBDUA')
sua_table = self.processInfo('SUA')
ngr_ftp_table = self.processInfo('NGR_FTP')
bpspdfbill_table = self.processInfo('BpsPdfBill')
disk_space_table = self.processInfo('Disk_Space')
self.tabs.addTab(mua_table, 'MUA')
self.tabs.addTab(bps_table, 'BPS')
self.tabs.addTab(obdua_table, 'ODBUA')
self.tabs.addTab(sua_table, 'SUA')
self.tabs.addTab(ngr_ftp_table, 'NGR_FTP')
self.tabs.addTab(bpspdfbill_table, 'BpsPdfBill')
self.tabs.addTab(disk_space_table, 'Disk_Space')
self.setCentralWidget(self.tabs)
self.statusBar().showMessage('Ready')
self.setWindowTitle('Task Checker')
self.show()
def center(self):
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def processInfo(self, project_name):
...the processing...
This is a lot of unnecessary code to read, but what I have managed to parse out of it, is that you are doing heavy calculations in the main thread, while expecting another widget in the main thread to also maintain responsiveness.
This is a common pitfall when working with PyQt4 (and probably most GUI frameworks that use a main thread event loop). The issue is that when you start your app, the main event loop constantly polls for new events to process from your GUI. Its expecting that every operation should either take a very short time to complete, or, periodically relinquish control back to the eventloop. Right now, your calculation is hogging up all the availability of the main thread, and anything that your progress window is trying to do is just backing up into a queue waiting for a chance for the event loop to pick it up.
The simple answer to this is: Do any heavy calculations in a separate QThread, and communicate back to your main thread via signals. This will require a little bit of retooling on you part. You should not put anything heavy into an init of any class. But you have managed to address this in your second example by attaching the computation to a button. Good start. The button should actually start the calc in another thread, thus not blocking up the main thread.
Qt4.7 Threading Basics
GUI Thread and Worker Thread
As mentioned, each program has one thread
when it is started. This thread is called the "main thread" (also
known as the "GUI thread" in Qt applications). The Qt GUI must run in
this thread. All widgets and several related classes, for example
QPixmap, don't work in secondary threads. A secondary thread is
commonly referred to as a "worker thread" because it is used to
offload processing work from the main thread.
...
Using Threads
There are basically two use cases for threads:
1. Make
processing faster by making use of multicore processors.
2. Keep the GUI
thread or other time critical threads responsive by offloading long
lasting processing or blocking calls to other threads.
As a quick fix if you just want to see some results, you can periodically call QtGui.QApplication.processEvents() from your calculation method. This will every so often allow the event loop to flush out pending operations. Doing so will let something like your progress widget actually function. Essentially what you are doing is manually "pumping" the event loop. This isn't the best approach though. Usually its reserved for lighter weight one off stuff. If you want the best performance, move it to a thread.

Resources