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.
Related
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.
I am trying to track my keypresses, using the module "keyboard" while a PySide2 Widget is not in focus, which works fine. However when I try to create a new Widget using a "keyboard" shortcut the program crashes. Opening a window on a button press works fine. I can also call non UI functions using "keyboard" eg. the print function without any problem.
Do you know a way to fix this and open a new window using "keyboard" or any other method, while a PySide2 window is not in focus. In this example I want to open a new window on "CTRL+D". The Problem exists both for PySide2 and PyQt5.
This is my shortened code:
import sys
import json
import os
import keyboard
from PySide2.QtWidgets import QApplication, QWidget, QGridLayout, QKeySequenceEdit, QLabel, QPushButton, QShortcut
from PySide2.QtCore import Qt, QObject, Signal, Slot # Qt.Key_W beispielsweise
#from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QKeySequenceEdit, QLabel, QPushButton, QShortcut
#from PyQt5.QtCore import Qt, QObject, pyqtSignal as Signal, pyqtSlot as Slot # Qt.Key_W beispielsweise
class ConfigWindow(QWidget):
def __init__(self):
super().__init__()
self.initUi()
self.init_shortcuts()
self.show()
def initUi(self):
self.setGeometry(300,300, 400, 250)
self.setWindowTitle("Settings")
grid = QGridLayout()
self.setLayout(grid)
self.keyseq = QKeySequenceEdit("CTRL+D")
grid.addWidget(self.keyseq, 0, 0)
s_button = QPushButton("Safe")
grid.addWidget(s_button, 1, 0)
cl_button = QPushButton("Close")
grid.addWidget(cl_button, 1, 1)
cl_button.clicked.connect(self.close)
open_button = QPushButton("openw")
grid.addWidget(open_button, 2, 0)
open_button.clicked.connect(self.call_item_parser)
def keyPressEvent(self, event): #event:PySide2.QtGui.QKeyEvent
if event.key() == Qt.Key_Escape:
self.close()
# shortcuts are listened to, while program is running
def init_shortcuts(self):
str_value = self.keyseq.keySequence().toString()
print("Binding _price_keyseq to {}".format(str_value))
keyboard.add_hotkey(str_value, self.call_item_parser)
# keyboard.add_hotkey(str_value, print, args=("this works")) # this would work
def call_item_parser(self):
self.h_w = ParseWindow()
self.h_w.setWindowTitle("New Window")
self.h_w.setGeometry(100, 100, 100, 100)
self.h_w.show()
class ParseWindow(QWidget):
def __init__(self):
super().__init__()
app = QApplication(sys.argv)
w = ConfigWindow()
sys.exit(app.exec_())
The problem is caused because the callback registered in keyboard is executed in a secondary thread as can be verified by modifying the following part of the code and printing threading.current_thread(). In Qt it is forbidden to create any widget in another thread since they are not thread-safe.
def call_item_parser(self):
print(threading.current_thread())
self.h_w = ParseWindow()
self.h_w.setWindowTitle("New Window")
self.h_w.setGeometry(100, 100, 100, 100)
self.h_w.show()
print(threading.current_thread())
app = QApplication(sys.argv)
w = ConfigWindow()
sys.exit(app.exec_())
Output:
<_MainThread(MainThread, started 140144979916608)>
Binding _price_keyseq to ctrl+a
<Thread(Thread-10, started daemon 140144220817152)>
One possible solution is to use a signal to send the information to the main thread, and invoke the callback in the main thread.
import sys
from functools import partial
import platform
import threading
import keyboard
from PySide2.QtCore import Qt, QObject, Signal, Slot
from PySide2.QtGui import QKeySequence
from PySide2.QtWidgets import (
QApplication,
QWidget,
QGridLayout,
QKeySequenceEdit,
QPushButton,
)
class KeyBoardManager(QObject):
activated = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._callbacks = dict()
self.activated.connect(self._handle_activated)
#property
def callbacks(self):
return self._callbacks
def register(self, shortcut, callback, *, args=(), kwargs=None):
self.callbacks[shortcut] = (callback, args, kwargs or {})
keyboard.add_hotkey(shortcut, partial(self.activated.emit, shortcut))
#Slot(str)
def _handle_activated(self, shortcut):
values = self.callbacks.get(shortcut)
if values is not None:
callback, args, kwargs = self._callbacks[shortcut]
callback(*args, **kwargs)
class ConfigWindow(QWidget):
def __init__(self):
super().__init__()
self.initUi()
self.init_shortcuts()
self.show()
def initUi(self):
self.setGeometry(300, 300, 400, 250)
self.setWindowTitle("Settings")
grid = QGridLayout(self)
self.keyseq = QKeySequenceEdit("CTRL+A")
grid.addWidget(self.keyseq, 0, 0)
s_button = QPushButton("Safe")
grid.addWidget(s_button, 1, 0)
cl_button = QPushButton("Close")
grid.addWidget(cl_button, 1, 1)
cl_button.clicked.connect(self.close)
open_button = QPushButton("openw")
grid.addWidget(open_button, 2, 0)
open_button.clicked.connect(self.call_item_parser)
def keyPressEvent(self, event): # event:PySide2.QtGui.QKeyEvent
if event.key() == Qt.Key_Escape:
self.close()
# shortcuts are listened to, while program is running
def init_shortcuts(self):
self.keyboard_manager = KeyBoardManager()
str_value = self.keyseq.keySequence().toString()
if platform.system() == "Linux":
str_value = str_value.lower()
print("Binding _price_keyseq to {}".format(str_value))
self.keyboard_manager.register(str_value, self.call_item_parser)
def call_item_parser(self):
print(threading.current_thread())
self.h_w = ParseWindow()
self.h_w.setWindowTitle("New Window")
self.h_w.setGeometry(100, 100, 100, 100)
self.h_w.show()
class ParseWindow(QWidget):
pass
def main():
print(threading.current_thread())
app = QApplication(sys.argv)
w = ConfigWindow()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Output:
<_MainThread(MainThread, started 140037641176896)>
Binding _price_keyseq to ctrl+a
<_MainThread(MainThread, started 140037641176896)>
I am trying to emit custom events in PyQt. One widget would emit and another would listen to events, but the two widgets would not need to be related.
In JavaScript, I would achieve this by doing
// Component 1
document.addEventListener('Hello', () => console.log('Got it'))
// Component 2
document.dispatchEvent(new Event("Hello"))
Edit: I know about signals and slots, but only know how to use them between parent and child. How would I this mechanism (or other mechanism) between arbitrary unrelated widgets?
In PyQt the following instruction:
document.addEventListener('Hello', () => console.log('Got it'))
is equivalent
document.hello_signal.connect(lambda: print('Got it'))
In a similar way:
document.dispatchEvent(new Event("Hello"))
is equivalent
document.hello_signal.emit()
But the big difference is the scope of the "document" object, since the connection is between a global element. But in PyQt that element does not exist.
One way to emulate the behavior that you point out is by creating a global object:
globalobject.py
from PyQt5 import QtCore
import functools
#functools.lru_cache()
class GlobalObject(QtCore.QObject):
def __init__(self):
super().__init__()
self._events = {}
def addEventListener(self, name, func):
if name not in self._events:
self._events[name] = [func]
else:
self._events[name].append(func)
def dispatchEvent(self, name):
functions = self._events.get(name, [])
for func in functions:
QtCore.QTimer.singleShot(0, func)
main.py
from PyQt5 import QtCore, QtWidgets
from globalobject import GlobalObject
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton(text="Press me", clicked=self.on_clicked)
self.setCentralWidget(button)
#QtCore.pyqtSlot()
def on_clicked(self):
GlobalObject().dispatchEvent("hello")
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
GlobalObject().addEventListener("hello", self.foo)
self._label = QtWidgets.QLabel()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._label)
#QtCore.pyqtSlot()
def foo(self):
self._label.setText("foo")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w1 = MainWindow()
w2 = Widget()
w1.show()
w2.show()
sys.exit(app.exec_())
I would like to dynamically create then manipulate lots of widgets. My idea is to store widgets in a dict (mywidgets) and to trigger them with signals stored in another dict (mysignals). Both dict shared same keys defined in a list (names), dicts are initialized with for loops.
When I connect signals to slots, I'm currently facing an AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'.
I have tried to disable signal/slot connections: the GUI looks good, QLineEdit are well stored in mywidgets. Types of mysignals items are correct: class 'PyQt5.QtCore.pyqtSignal'.
Can you, please, explain me where the issue come from ?
Thanks.
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QVBoxLayout
from PyQt5.QtCore import pyqtSlot, pyqtSignal
class App(QWidget):
names = ["foo","bar"]
mysignals = {} # Store several signals in a dict
for name in names:
mysignals[name] = pyqtSignal(str)
def __init__(self):
super().__init__()
# Create Widgets
self.btn_go = QPushButton("Go") #Simple push button
self.mywidgets = {} #Store several QLineEdit in a dict
for name in self.names:
self.mywidgets[name] = QLineEdit()
# Connect signals
self.btn_go.clicked.connect(self.on_click) #Connect push button
for name in self.names:
print(type(self.mysignals[name]))
self.mysignals[name].connect(self.mywidgets[name].setText) #Connect several signals
# Configure layout
layout = QVBoxLayout()
layout.addWidget(self.btn_go)
for name in self.names:
layout.addWidget(self.mywidgets[name])
self.setLayout(layout)
# Show widget
self.show()
#pyqtSlot()
def on_click(self):
data = {"foo":"Python3","bar":"PyQt5"}
for key,value in data.items():
self.mysignals[key].emit(value)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The expected result is to display respectivelly Python3 and PyQt5 in mywidgets["foo"] and mywidgets["bar"] QLineEdit widgets when push button is clicked.
As the docs point out:
A signal (specifically an unbound signal) is a class attribute. When a
signal is referenced as an attribute of an instance of the class then
PyQt5 automatically binds the instance to the signal in order to
create a bound signal. This is the same mechanism that Python itself
uses to create bound methods from class functions.
A signal is declared as an attribute of the class but when referenced through self a bind is made with the object, that is, the declared signal is different from the instantiated signal:
from PyQt5 import QtCore
class Foo(QtCore.QObject):
fooSignal = QtCore.pyqtSignal()
print("declared:", fooSignal)
def __init__(self, parent=None):
super(Foo, self).__init__(parent)
print("instantiated:", self.fooSignal)
if __name__ == '__main__':
import sys
app = QtCore.QCoreApplication(sys.argv)
obj = Foo()
Output:
declared: <unbound PYQT_SIGNAL )>
instantiated: <bound PYQT_SIGNAL fooSignal of Foo object at 0x7f4beb998288>
That is the reason for the error you get, so if you want to use the signals you must obtain it using the object so we can inspect the attributes and get the signals:
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
foo = QtCore.pyqtSignal(str)
bar = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.fill_signals()
self.names = ["foo", "bar"]
self.btn_go = QtWidgets.QPushButton("Go")
self.mywidgets = {}
for name in self.names:
self.mywidgets[name] = QtWidgets.QLineEdit()
signal = self.mysignals.get(name)
if signal is not None:
signal.connect(self.mywidgets[name].setText)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.btn_go)
for name in self.names:
layout.addWidget(self.mywidgets[name])
self.btn_go.clicked.connect(self.testing)
def fill_signals(self):
self.mysignals = dict()
for p in dir(self):
attr = getattr(self, p)
if isinstance(attr, QtCore.pyqtBoundSignal):
self.mysignals[p] = attr
def testing(self):
self.foo.emit("foo")
self.bar.emit("bar")
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Sorry, I think you have complicated the algorithm to obtain the expected result. Try it:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QVBoxLayout
from PyQt5.QtCore import pyqtSlot, pyqtSignal
class App(QWidget):
def __init__(self, names):
super().__init__()
self.names = names
layout = QVBoxLayout()
# Create Widgets
self.btn_go = QPushButton("Go") # Simple push button
self.btn_go.clicked.connect(self.on_click) # Connect push button
layout.addWidget(self.btn_go)
self.mywidgets = {} # Store several QLineEdit in a dict
for name in self.names:
self.mywidgets[name] = QLineEdit()
layout.addWidget(self.mywidgets[name])
self.setLayout(layout)
#pyqtSlot()
def on_click(self):
data = {"foo":"Python3", "bar":"PyQt5"}
for key, value in data.items():
self.mywidgets[key].setText(value)
if __name__ == '__main__':
app = QApplication(sys.argv)
names = ["foo", "bar"]
ex = App(names)
ex.show()
sys.exit(app.exec_())
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_())