PyQt5: Thread Not Ending? Process finished with exit code -1 - python

Below code runs well and does its job. However, when I exit the GUI, the python file does not finish processing.
When I run through pycharm debug run then force terminate the debug, I get the message
"Process finished with exit code -1".
Could anyone help me to find what is causing this error?
from PyQt5 import uic
from PyQt5.QtWidgets import QWidget
from pybithumb import WebSocketManager
from PyQt5.QtCore import QThread, pyqtSignal
class OverViewWorker(QThread):
data24Sent = pyqtSignal(int, float, int, float, int, int)
dataMidSent = pyqtSignal(int, float, float)
def __init__(self, ticker):
super().__init__()
self.ticker = ticker
self.alive = True
def run(self):
wm = WebSocketManager("ticker", [f"{self.ticker}_KRW"], ["24H", "MID"])
while self.alive:
data = wm.get()
if data['content']['tickType'] == "MID":
self.dataMidSent.emit(int(data['content']['closePrice']),
## Deleted
else:
self.data24Sent.emit(int(data['content']['closePrice']),
## Deleted
def close(self):
self.alive = False
class OverviewWidget(QWidget):
def __init__(self, parent=None, ticker="BTC"):
super().__init__(parent)
uic.loadUi("resource/overview.ui", self)
self.ticker = ticker
self.ovw = OverViewWorker(ticker)
self.ovw.data24Sent.connect(self.fill24Data)
self.ovw.dataMidSent.connect(self.fillMidData)
self.ovw.start()
def fill24Data(self, currPrice, volume, highPrice, value, lowPrice, PrevClosePrice):
## Deleted
def fillMidData(self, currPrice, chgRate, volumePower):
## Deleted
def closeEvent(self, event):
self.ovw.close()
if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
ob = OverviewWidget()
ob.show()
exit(app.exec_())

In your code, the QThread.run() method enters a while True loop, which never allows any CPU time to execute the close method and change self.alive to False, therefore that loop never ends even though you emit the close signal from the QWidget.
You are then trying to quit a thread which is still running, and that gives you the -1 return code as your QThread is forcefully killed without finishing its tasks.
If you add the possibility to update the alive parameter from within the loop, you can get out of this issue, but it would need the QThread to access a parameter of the QWidget, which may not be what you want.
Moreover, using QThread.wait() method may help you for synchronizing, as it will forcefully delay the exit of your QWidget until ovw returns from run.
Since your -1 return code may just be due to your QApplication exiting moments before the thread finishes, synchronizing the exits will alleviate that.
Here is an example of that based on your code :
from PyQt5 import uic
from PyQt5.QtWidgets import QWidget
from pybithumb import WebSocketManager
from PyQt5.QtCore import QThread, pyqtSignal
class OverViewWorker(QThread):
data24Sent = pyqtSignal(int, float, int, float, int, int)
dataMidSent = pyqtSignal(int, float, float)
def __init__(self, ticker, master):
super().__init__()
self.ticker = ticker
self.master = master
self.alive = True
def run(self):
wm = WebSocketManager("ticker", [f"{self.ticker}_KRW"], ["24H", "MID"])
while self.alive:
data = wm.get()
if data['content']['tickType'] == "MID":
self.dataMidSent.emit(int(data['content']['closePrice']),
## Deleted
else:
self.data24Sent.emit(int(data['content']['closePrice']),
## Deleted
self.get_alive_from_master()
# Do here anything you want in the thread before quitting.
def get_alive_from_master(self):
self.alive = master.ovw_alive
class OverviewWidget(QWidget):
def __init__(self, parent=None, ticker="BTC"):
super().__init__(parent)
uic.loadUi("resource/overview.ui", self)
self.ticker = ticker
self.ovw = OverViewWorker(ticker, self)
self.ovw.data24Sent.connect(self.fill24Data)
self.ovw.dataMidSent.connect(self.fillMidData)
self.ovw_alive = True
self.ovw.start()
def fill24Data(self, currPrice, volume, highPrice, value, lowPrice, PrevClosePrice):
## Deleted
def fillMidData(self, currPrice, chgRate, volumePower):
## Deleted
def closeEvent(self, event):
self.ovw_alive = False
self.ovw.wait()
if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
ob = OverviewWidget()
ob.show()
exit(app.exec_())

Qt application exits if last window closed, if there are running threads left, it terminates them. It can be overriden with app.setQuitOnLastWindowClosed(False). In that case you must call app.quit() on thread finished to terminate application.

Related

PyQt delaying function's computation without blocking main thread

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

Pyqt5 - how to go back to hided Main Window from Secondary Window?

If I click Back from the second window, the program will just exit. How do I go back to mainwindow in this case? I assume I will need some more code in that clickMethodBack function.
import os
import PyQt5
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QPushButton
import time
from PyQt5.QtCore import QSize
class GUI_Window():
def __init__(self):
self.main_window()
return
def main_window(self):
app = PyQt5.QtWidgets.QApplication(sys.argv)
self.MainWindow = MainWindow_()
self.MainWindow.show()
app.exec_()
return
class MainWindow_(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.TestAButton = QPushButton("TestA", self)
self.TestAButton.clicked.connect(self.TestA_clickMethod)
self.TestAButton.move(20, 0)
self.CloseButton = QPushButton("Close", self)
self.CloseButton.clicked.connect(self.Close_clickMethod)
self.CloseButton.move(20, 40)
self.TestingA = TestA_MainWindow()
def TestA_clickMethod(self):
self.TestAButton.setEnabled(False)
time.sleep(0.2)
self.TestingA.show()
self.hide()
try:
if self.TestingA.back == True:
self.show()
except:
None
def Close_clickMethod(self):
self.Test_Choice = 'Exit'
self.close()
class TestA_MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setMinimumSize(QSize(980,700))
self.setWindowTitle("TestA")
self.Back_Button = False
self.closeButton = QPushButton("Close", self)
self.closeButton.clicked.connect(self.clickMethodClose)
self.returnButton = QPushButton("Back", self)
self.returnButton.clicked.connect(self.clickMethodBack)
self.returnButton.move(0,30)
def clickMethodClose(self):
self.Back_Button = False
self.close()
def clickMethodBack(self):
self.returnButton.setEnabled(False)
time.sleep(0.5)
self.back = True
self.close()
# Run if Script
if __name__ == "__main__":
main = GUI_Window() # Initialize GUI
Your code has two very important issues.
you're using a blocking function, time.sleep; Qt, as almost any UI toolkit, is event driven, which means that it has to be able to constantly receive and handle events (coming from the system or after user interaction): when something blocks the event queue, it completely freezes the whole program until that block releases control;
you're checking for the variable too soon: even assuming the sleep would work, you cannot know if the window is closed after that sleep timer has ended;
The solution is to use signals and slots. Since you need to know when the second window has been closed using the "back" button, create a custom signal for the second window that will be emitted whenever the function that is called by the button is closed.
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(central)
self.testButton = QtWidgets.QPushButton('Test A')
self.closeButton = QtWidgets.QPushButton('Close')
layout.addWidget(self.testButton)
layout.addWidget(self.closeButton)
self.setCentralWidget(central)
self.testButton.clicked.connect(self.launchWindow)
self.closeButton.clicked.connect(self.close)
def launchWindow(self):
self.test = TestA_MainWindow()
self.test.backSignal.connect(self.show)
self.hide()
self.test.show()
class TestA_MainWindow(QtWidgets.QWidget):
backSignal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
self.closeButton = QtWidgets.QPushButton('Close')
self.backButton = QtWidgets.QPushButton('Back')
layout.addWidget(self.closeButton)
layout.addWidget(self.backButton)
self.closeButton.clicked.connect(self.close)
self.backButton.clicked.connect(self.goBack)
def goBack(self):
self.close()
self.backSignal.emit()
def GUI_Window():
import sys
app = QtWidgets.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
GUI_Window()
Notes:
I removed the GUI_Window class and made a function, as using a class for that is not really useful;
you should always prefer layout managers instead of setting manual geometries;
widgets should not be added to a QMainWindow as direct children, and a central widget should always be used (see the creation and use of central in the example); read more about it in the documentation;
only classes and constants should be capitalized, while variables, attributes and functions should always have names starting with a lowercase letter;

Python program subclasses a PyQt5 Window cant setwindow.title in Function

I have a sample Python program that sub classes a PyQt5 window. I am teaching myself and still a little new PyQt5 and python Classes.
The program does what I want to do I ran into an error that I don't understand how to fix. To start this program functions as is, I am currently learning how to run Threads. I imported and sub classed the PyQt5 window. In the __init__ section of the subclass I can set the window title and it works fine.
If I move the statement to a function "def initUI(self):" I am unable to set the window title, mind you I have tried various versions of the statement and nothing works. It's the first line of the def and I have it commented out.
My questions are:
Is this property setable in a def.
If it io what is the proper format of the statement.
from PyQt5.QtCore import pyqtSlot, pyqtSignal
from PyQt5 import QtCore, QtGui, QtWidgets
from Threads import Ui_MainWindow
import sys, time
from time import sleep
class MainWindow_EXEC():
def __init__(self): # This section has to be laid out this way
app = QtWidgets.QApplication(sys.argv)
win = QtWidgets.QMainWindow()
self.ui = Ui_MainWindow()
self.ui.setupUi(win)
self.initUI() # Inits that need to happen before start up
win.setWindowTitle("This is the Title!") # Works fine
win.resize(800,600)
win.show()
sys.exit(app.exec_())
def initUI(self): # Button assignments
#self.ui.setWindowTitle("This is the Title!") # AttributeError: 'Ui_MainWindow' object has no attribute 'setWindowTitle'
self.ui.btn_Start.clicked.connect(self.start_progressbar)
self.ui.btn_Stop.clicked.connect(self.stop_progressbar)
self.ui.btn_Reset.clicked.connect(self.reset_progressbar)
self.progress_value = 0
self.stop_progress = False
def progressbar_counter(self, start_value=0):
# have to use member: self.run_thread NOT local var: run_thread
self.run_thread = RunThread(parent=None, counter_start=start_value)
self.run_thread.start()
self.run_thread.counter_value.connect(self.get_thread_value)
def get_thread_value(self, counter): #This updates the progress bar
print(counter)
if not self.stop_progress:
self.ui.progressBar.setValue(counter)
def start_progressbar(self): # This is the button that starts the progress bar
self.stop_progress = False # This is a switch
self.progress_value = self.ui.progressBar.value() # Updating the progress bar
self.progressbar_counter(self.progress_value)
def stop_progressbar(self): # This is a button to stop the progress bar
self.stop_progress = True
self.run_thread.stop()
def reset_progressbar(self): # This is a button to reset the progress bar
self.stop_progressbar()
self.progress_value = 0
self.stop_progress = False
self.ui.progressBar.reset()
class RunThread(QtCore.QThread):
counter_value = QtCore.pyqtSignal(int) # define new Signal
def __init__(self, parent=None, counter_start=0):
super(RunThread, self).__init__(parent)
self.counter = counter_start
self.isRunning = True
def run(self):
while self.counter < 100 and self.isRunning == True:
sleep(0.1)
self.counter += 1
print(self.counter)
self.counter_value.emit(self.counter) # emit new Signal with value
def stop(self):
self.isRunning = False
print('stopping thread...')
self.terminate()
if __name__ == "__main__":
MainWindow_EXEC()
The objective of the following classes must be distinguished:
QMainWindow is a widget that has the setWindowTitle method.
Ui_MainWindow is not a widget but a class that is used to fill a widget so it does not have the setWindowTitle method.
The solution is to make win a class member and then use that object to modify the title:
class MainWindow_EXEC():
def __init__(self): # This section has to be laid out this way
app = QtWidgets.QApplication(sys.argv)
self.win = QtWidgets.QMainWindow()
self.ui = Ui_MainWindow()
self.ui.setupUi(self.win)
self.initUI()
self.win.setWindowTitle("This is the Title!")
self.win.resize(800,600)
self.win.show()
sys.exit(app.exec_())
def initUI(self): # Button assignments
self.win.setWindowTitle("This is the Title!")
self.ui.btn_Start.clicked.connect(self.start_progressbar)
# ...

PySide2 use QProgressBar as signal argument

I am trying to solve a problem with PySide2, QThread and the Signal/Slot mechanism.
What I want to achieve is updating a specific progressbar which is passed as reference via signal/slot mechanism.
The problem is the call of self.gui_connection.signal_progress_value.emit(lambda: self.progressbar, value) in my ProgressBarThread class.
The program stops after emitting the signal with a QProgressBar and an int value as arguments and causes a Seg Fault. If I just pass an int value to the signal and call emit in my ProgressBarThread everything is working fine.
Is it just not possible to pass an Object via Signal/Slot mechanism?
(Reduced) Code Samples:
GUISignal.py
from PySide2.QtCore import QObject, Signal
from PySide2.QtWidgets import QProgressBar
class GUISignal(QObject):
signal_progress_value = Signal(QProgressBar, int)
main.py
# File: main.py
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QPushButton
from PySide2.QtCore import QFile
from config.configManager import ConfigManager
from layout.srcConfigLayout import SrcConfigLayout
from layout.ingestLayout import IngestLayout
if __name__ == "__main__":
app = QApplication(sys.argv)
ui_file = QFile("main_window.ui")
ui_file.open(QFile.ReadOnly)
loader = QUiLoader()
window = loader.load(ui_file)
ui_file.close()
window.show()
src_config_layout = SrcConfigLayout(window)
ingestLayout = IngestLayout(window)
src_config_layout.load_config()
ingestLayout.load_config()
sys.exit(app.exec_())
ingestLayout.py
from PySide2.QtWidgets import QTableWidget, QTableWidgetItem, QPushButton, QProgressBar
from PySide2.QtCore import QObject, Slot
from util.ingestManager import IngestManager
from config.configManager import ConfigManager
from util.progressBarThread import ProgressBarThread
from functools import partial
class IngestLayout(QObject):
def __init__(self, parent):
super().__init__()
self.parent = parent
def handleIngestClicked(self, srcpath, destpath, progressbar):
ingestManager = IngestManager()
ingestManager.copy_to_destination(self, srcpath, destpath)
progressThread = ProgressBarThread(srcpath, destpath, progressbar, self.parent)
progressThread.gui_connection.signal_progress_value.connect(self.updateProgressbar)
progressThread.start()
#Slot(QProgressBar, int)
def updateProgressbar(self, progressbar: QProgressBar, value: int):
pass
# progressbar.setValue(value)
progressBarThread.py
from PySide2.QtCore import QThread
import os
import threading
import time
from signals.GUISignal import GUISignal
class ProgressBarThread(QThread):
def __init__(self, srcpath, destpath, progressbar, parent):
super(ProgressBarThread, self).__init__(parent)
self.gui_connection = GUISignal()
self.srcpath = srcpath
self.destpath = destpath
self.src_size = self.size(srcpath)
self.progressbar = progressbar
def run(self):
self.periodically_compare_folder_size()
def periodically_compare_folder_size(self):
dest_size = self.size(self.destpath)
while self.src_size > dest_size:
value = self.calc_progress(self.src_size, dest_size)
self.gui_connection.signal_progress_value.emit(lambda: self.progressbar, value)
dest_size = self.size(self.destpath)
time.sleep(2)
else:
self.gui_connection.signal_progress_value.emit(lambda: self.progressbar, 100)
print(100)
return
def size(self, path, *, follow_symlinks=False):
try:
with os.scandir(path) as it:
return sum(self.size(entry, follow_symlinks=follow_symlinks) for entry in it)
except NotADirectoryError:
return os.stat(path, follow_symlinks=follow_symlinks).st_size
def calc_progress(self, src_size, dest_size):
return dest_size / src_size * 100
The problem is that the program stops after emitting the signal with a QProgressBar and a value as arguments and causes a Seg Fault. If I just pass an int value to the signal and call emit in my ProgressBarThread everything is working fine.
Is it just not possible to pass Object via Signal/Slot mechanism?
It is not necessary for the signal to send as QProgressBar dates, in addition to the GUI is not thread-safe, on the other hand it is not necessary to use a lambda.
Considering the above, the solution is:
class GUISignal(QObject):
signal_progress_value = Signal(int)
class ProgressBarThread(QThread):
def __init__(self, srcpath, destpath, parent):
super(ProgressBarThread, self).__init__(parent)
self.gui_connection = GUISignal()
self.srcpath = srcpath
self.destpath = destpath
self.src_size = self.size(srcpath)
def run(self):
self.periodically_compare_folder_size()
def periodically_compare_folder_size(self):
dest_size = self.size(self.destpath)
while self.src_size > dest_size:
value = self.calc_progress(self.src_size, dest_size)
self.gui_connection.signal_progress_value.emit(value)
dest_size = self.size(self.destpath)
time.sleep(2)
else:
self.gui_connection.signal_progress_value.emit(100)
print(100)
return
# ...
def handleIngestClicked(self, srcpath, destpath, progressbar):
ingestManager = IngestManager()
ingestManager.copy_to_destination(self, srcpath, destpath)
progressThread = ProgressBarThread(srcpath, destpath, self.parent)
progressThread.gui_connection.signal_progress_value.connect(progressbar.setValue)
progressThread.start()
Update:
In the previous part of my answer I separate the GUI from the business logic as this will have a more scalable SW. But the OP seems to not want that so the simple solution is not to use the lambda method since it is unnecessary:
self.gui_connection.signal_progress_value.emit(self.progressbar, value)
self.gui_connection.signal_progress_value.emit(self.progressbar, 100)
SOLVED:
The problem was the lambda function in the emit function inside the QThread class. It seems like the object was not passed by the lambda function. Just using .emit(self,progressbar, 100) is working.

Concerned about race conditions while accessing SQLite3 database connection that's accessed in thread invoked by Pynput listener inside a QThread

I'm writing a Windows application with Pyside2. Due to the nature of how I'm using multithreading, I'm having to interact with the same Sqlite3 database in multiple threads. I've created a <100 line Minimal, complete, verifiable example that nearly identically replicates the problem.
The problem: I'm currently using the pynput module to monitor key activity in the background once the PushButton has been pressed, while the Qt GUI is out of focus for a hotkey combination of "j" + "k". Once the hot key combination is pressed, a screenshot is taken, the image is processed via OCR and saved to a database along with the OCR text. The image path is sent through a a series of connected signals to the main GUI thread. The key monitoring happens in another QThread to prevent the key monitoring and image processing from affecting the main Qt event loop from running. Once the QThread starts and emits it's start signal I call the monitor_for_hot_key_combo function in the key_monitor instance which instantiates listeneras a threading.Thread, which is assigned key_monitor member functions on_release and on_press as callbacks, which are called every time a key is pressed.
This is where the problem lies. Those callbacks interact with the imageprocessing_obj instance of the image_processclass in a different thread than the class was instantiated in. Therefore, when image_process member functions are interacted with that use the SQlite database, they do so in a separate thread than the database connection was created in. Now, SQLite "can be safely used by multiple threads provided that no single database connection is used simultaneously in two or more threads". To allow this you
have to set the check_same_thread argument for sqlite3.connect() to False. However, Id rather avoid this mulithreaded access of the database if possible to prevent undefined behavior.
The Possible solution: I've been wondering if the two threads, both threading.Thread and QThread aren't necessary and it can all be done within the Pynput thread. However, I can't seem to figure out how to just use the Pynput thread while still being able to send signals back to the main Qt event loop.
qtui.py
from PySide2 import QtCore, QtWidgets
from PySide2.QtCore import *
import HotKeyMonitor
class Ui_Form(object):
def __init__(self):
self.worker = None
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(400, 300)
self.pressbutton = QtWidgets.QPushButton(Form)
self.pressbutton.setObjectName("PushButton")
self.pressbutton.clicked.connect(self.RunKeyMonitor)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1))
self.pressbutton.setText(QtWidgets.QApplication.translate("Form", "Press me", None, -1))
def RunKeyMonitor(self):
self.Thread_obj = QThread()
self.HotKeyMonitor_Obj = HotKeyMonitor.key_monitor()
self.HotKeyMonitor_Obj.moveToThread(self.Thread_obj)
self.HotKeyMonitor_Obj.image_processed_km.connect(self.print_OCR_result)
self.Thread_obj.started.connect(self.HotKeyMonitor_Obj.monitor_for_hotkey_combo)
self.Thread_obj.start()
def print_OCR_result(self, x):
print("Slot being called to print image path string")
print(x)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec_())
HotKeyMonitor.py
from pynput import keyboard
from PySide2.QtCore import QObject, Signal
import imageprocess
class key_monitor(QObject):
image_processed_km = Signal(str)
def __init__(self):
super().__init__()
self.prev_key = None
self.listener = None
self.imageprocessing_obj = imageprocess.image_process()
self.imageprocessing_obj.image_processed.connect(self.image_processed_km.emit)
def on_press(self,key):
pass
def on_release(self,key):
if type(key) == keyboard._win32.KeyCode:
if key.char.lower() == "j":
self.prev_key = key.char.lower()
elif key.char.lower() == "k" and self.prev_key == "j":
print("key combination j+k pressed")
self.prev_key = None
self.imageprocessing_obj.process_image()
else:
self.prev_key = None
def stop_monitoring(self):
self.listener.stop()
def monitor_for_hotkey_combo(self):
with keyboard.Listener(on_press=self.on_press, on_release = self.on_release) as self.listener:self.listener.join()
imageprocess.py
import uuid,os,sqlite3,pytesseract
from PIL import ImageGrab
from PySide2.QtCore import QObject, Signal
class image_process(QObject):
image_processed = Signal(str)
def __init__(self):
super().__init__()
self.screenshot = None
self.db_connection = sqlite3.connect("testdababase.db", check_same_thread=False)
self.cursor = self.db_connection.cursor()
self.cursor.execute("CREATE TABLE IF NOT EXISTS testdb (OCRstring text, filepath text)")
def process_image(self):
self.screenshot = ImageGrab.grab()
self.screenshot_path = os.getcwd() + "\\" + uuid.uuid4().hex + ".jpg"
self.screenshot.save(self.screenshot_path )
self.ocr_string = pytesseract.image_to_string(self.screenshot)
self.cursor.execute("INSERT INTO testdb (OCRstring, filepath) VALUES (?,?)",(self.ocr_string, self.screenshot_path))
self.image_processed.emit(self.screenshot_path)
First of all a QThread is not a Qt thread, that is, it is not a new type of thread, QThread is a class that manages the native threads of each platform. so the thread that handles QThread has the same characteristics of threading.Thread.
On the other hand the goal of using threads in a GUI is not to block the main thread called GUI thread, in your pynput it already has its thread so there would be no problems. The other task that is blocking is that of the OCR, so we must execute it in a new thread. The task of the database is not expensive, so it is not necessary to create a thread.
keymonitor.py
from pynput import keyboard
import time
from PySide2 import QtCore
class KeyMonitor(QtCore.QObject):
letterPressed = QtCore.Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.listener = keyboard.Listener(on_release = self.on_release)
def on_release(self,key):
if type(key) == keyboard._win32.KeyCode:
self.letterPressed.emit(key.char.lower())
def stop_monitoring(self):
self.listener.stop()
def start_monitoring(self):
self.listener.start()
imageprocess.py
import uuid
import pytesseract
from PIL import ImageGrab
from PySide2 import QtCore
class ProcessWorker(QtCore.QObject):
processSignal = QtCore.Signal(str, str)
def doProcess(self):
screenshot = ImageGrab.grab()
screenshot_path = QtCore.QDir.current().absoluteFilePath(uuid.uuid4().hex+".jpg")
screenshot.save(screenshot_path )
print("start ocr")
ocr_string = pytesseract.image_to_string(screenshot)
print(ocr_string, screenshot_path)
self.processSignal.emit(ocr_string, screenshot_path)
self.thread().quit()
main.py
from keymonitor import KeyMonitor
from imageprocess import ProcessWorker
from PySide2 import QtCore, QtWidgets
import sqlite3
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.last_letter = ""
self.current_letter = ""
lay = QtWidgets.QVBoxLayout(self)
button = QtWidgets.QPushButton("Start")
button.clicked.connect(self.onClicked)
lay.addWidget(button)
self.keymonitor = KeyMonitor()
self.keymonitor.letterPressed.connect(self.onLetterPressed)
self.db_connection = sqlite3.connect("testdababase.db")
self.cursor = self.db_connection.cursor()
self.cursor.execute("CREATE TABLE IF NOT EXISTS testdb (OCRstring text, filepath text)")
self.threads = []
def onClicked(self):
self.keymonitor.start_monitoring()
def onLetterPressed(self, letter):
if self.last_letter:
if self.current_letter:
self.last_letter = self.current_letter
self.current_letter = letter
else:
self.last_letter = letter
if self.last_letter == "j" and self.current_letter == "k":
print("j+k")
self.start_processing()
def start_processing(self):
thread = QtCore.QThread()
self.worker = ProcessWorker()
self.worker.processSignal.connect(self.onProcessSignal)
self.worker.moveToThread(thread)
thread.started.connect(self.worker.doProcess)
thread.finished.connect(self.worker.deleteLater)
thread.finished.connect(lambda th=thread: self.threads.remove(th))
thread.start()
self.threads.append(thread)
def onProcessSignal(self, ocr, path):
print(ocr, path)
self.cursor.execute("INSERT INTO testdb (OCRstring, filepath) VALUES (?,?)",(ocr, path))
self.db_connection.commit()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

Categories

Resources