In this small PyQt based program, I have created a counter that counts from 0 to 10, in 10 seconds. While I use QRunnable to run counter in countBox() in a separate thread but after pressing start button GUI becomes not responding and counter does not work at all. Do you have any idea about what's wrong with this? Thanks.
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QLabel
from PyQt5.QtCore import Qt, QRunnable, pyqtSlot, QThreadPool
import time, sys
class Worker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
#pyqtSlot()
def run(self):
self.fn(*self.args, **self.kwargs)
def createCounterWorker():
threadpool = QThreadPool()
worker = Worker(countBox)
threadpool.start(worker)
def countBox():
i = 0
while i < 10:
print(f'i: {i}')
time.sleep(1.0)
i += 1
class Counter():
def __init__(self, layout):
self.startButton = QPushButton(f'Start')
self.startButton.pressed.connect(lambda: createCounterWorker())
layout.addWidget(self.startButton)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.layout = QVBoxLayout()
self.btn = QPushButton('+')
self.btn.pressed.connect(lambda: self.addNewCounter())
self.layout.addWidget(self.btn)
w = QWidget()
w.setLayout(self.layout)
self.setCentralWidget(w)
self.show()
def addNewCounter(self):
newCount = Counter(self.layout)
app = QApplication([])
window = MainWindow()
sys.exit(app.exec_())
The problem is simple: threadpool is the object that creates the threads, it is a local variable that will be destroyed instantly along with the threads, so countBox will be executed in the main thread, causing the GUI to freeze, so the solution is to extend the life cycle of that variable so there are the following options:
Make threadpool a global variable:
threadpool = QThreadPool()
def createCounterWorker():
worker = Worker(countBox)
threadpool.start(worker)
Use QThreadPool.globalInstance():
def createCounterWorker():
worker = Worker(countBox)
QThreadPool.globalInstance().start(worker)
Related
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import time
import sys
import numpy as np
class Mainthread(QThread):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.running = None
self.mutex = QMutex()
def run(self):
while self.running:
self.mutex.lock()
print ("test")
time.sleep(1)
self.mutex.unlock()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.mainthread = Mainthread(self)
self.mainthread.running = True
self.mainthread.start()
self.mainthread1 = Mainthread(self)
self.mainthread1.running = True
self.mainthread1.start()
app = QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.show()
app.exec_()
I have this code where I run two instances of the same MainThread class.
What I was expecting was that mainthread's message (which is "test") would print, then wait for a sec and then mainthread1's would be printed. Instead, it seems like both threads are running at the same time. Is there something I'm missing?
In your code, each thread creates its own separate mutex, so no relation is enforced between the two. Create a single mutex first, and pass it to the theads:
import time
import sys
from PyQt5.QtCore import QThread, QMutex
from PyQt5.QtWidgets import QMainWindow, QApplication
class Mainthread(QThread):
def __init__(self, mutex, parent):
super().__init__(parent)
self.parent = parent
self.running = None
self.mutex = mutex
def run(self):
while self.running:
self.mutex.lock()
print ("test")
time.sleep(1)
self.mutex.unlock()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
mutex = QMutex()
self.mainthread = Mainthread(mutex, self)
self.mainthread.running = True
self.mainthread.start()
self.mainthread1 = Mainthread(mutex, self)
self.mainthread1.running = True
self.mainthread1.start()
app = QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.show()
app.exec_()
Note: I don't have PyQt5 installed (and doing so on my architecture is tricky), but I tested this in PySide6 and as far as I know the behavior should be consistent.
I'm trying to make a UI which communicates in the background with several BLE devices.
For that I've implemented a separate thread which runs an asyncio.loop. This is necessary because I use bleak 0.9.1 to connect to the devices.
Using signals and slots to get data from the UI-thread to the worker thread works fine. However, it does not work in the other direction. As far as I know this is because the thread is busy running the loop and never stops doing that. Therefore, it cannot process the inputs from the UI-thread.
Below there is an example code which shows the problem.
Is there any way to process the input slots in the thread while running the asyncio loop?
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QVBoxLayout
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
import asyncio
class Test_Thread(QObject):
signal_back = pyqtSignal(int)
def __init__(self,
loop: asyncio.AbstractEventLoop,
parent=None):
super(Test_Thread, self).__init__(parent)
self.text = "Task1 not configured"
self.loop = loop
self.counter = 0
#pyqtSlot(str)
def set_text_slot(self, txt):
self.text = txt
async def do_stuff1(self):
while True:
print(self.text)
await asyncio.sleep(2.0)
async def do_stuff2(self):
while True:
self.counter += 1
self.signal_back.emit(self.counter)
await asyncio.sleep(1.0)
def work(self):
#run the event loop
try:
asyncio.ensure_future(self.do_stuff1(), loop=self.loop)
asyncio.ensure_future(self.do_stuff2(), loop=self.loop)
self.loop.run_forever()
finally:
print("Disconnect...")
class Window(QWidget):
set_text_signal = pyqtSignal(str)
def __init__(self, parent=None):
super(Window, self).__init__()
self.initUi()
self.startThread()
def initUi(self):
layout = QVBoxLayout()
self.button = QPushButton('User input')
self.button.clicked.connect(self.sendtotask)
layout.addWidget(self.button)
self.setLayout(layout)
self.show()
def startThread(self):
loop = asyncio.get_event_loop()
self.asyciothread = Test_Thread(loop)
self.thread = QThread()
self.asyciothread.moveToThread(self.thread)
self.set_text_signal.connect(self.asyciothread.set_text_slot)
self.asyciothread.signal_back.connect(self.receivefromthread)
self.thread.started.connect(self.asyciothread.work)
self.thread.start()
#pyqtSlot(int)
def receivefromthread(self, number):
print(str(number))
def sendtotask(self):
self.set_text_signal.emit("Task: Configured")
if __name__ == "__main__":
app = QApplication(sys.argv)
ui = Window()
ui.show()
sys.exit(app.exec_())
It is not necessary to use threads to use asyncio with Qt since there are libraries like asyncqt and qasync that enable it:
import asyncio
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QVBoxLayout
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from asyncqt import QEventLoop
# from qasync import QEventLoop
class Worker(QObject):
signal_back = pyqtSignal(int)
def __init__(self, loop: asyncio.AbstractEventLoop, parent=None):
super(Worker, self).__init__(parent)
self.text = "Task1 not configured"
self.loop = loop
self.counter = 0
#pyqtSlot(str)
def set_text_slot(self, txt):
self.text = txt
async def do_stuff1(self):
while True:
print(self.text)
await asyncio.sleep(2.0)
async def do_stuff2(self):
while True:
self.counter += 1
self.signal_back.emit(self.counter)
await asyncio.sleep(1.0)
def work(self):
asyncio.ensure_future(self.do_stuff1(), loop=self.loop)
asyncio.ensure_future(self.do_stuff2(), loop=self.loop)
class Window(QWidget):
set_text_signal = pyqtSignal(str)
def __init__(self, parent=None):
super(Window, self).__init__()
self.initUi()
self.start_task()
def initUi(self):
layout = QVBoxLayout(self)
self.button = QPushButton("User input")
self.button.clicked.connect(self.sendtotask)
layout.addWidget(self.button)
def start_task(self):
loop = asyncio.get_event_loop()
self.worker = Worker(loop)
self.set_text_signal.connect(self.worker.set_text_slot)
self.worker.signal_back.connect(self.receive_from_worker)
self.worker.work()
#pyqtSlot(int)
def receive_from_worker(self, number):
print(str(number))
def sendtotask(self):
self.set_text_signal.emit("Task: Configured")
if __name__ == "__main__":
app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
ui = Window()
ui.show()
with loop:
loop.run_forever()
When I click the minimize button to perform this operation in the background,Threads often execute beyond the limited time,
Waking up the program in the background is back to normal.
Please be patient, it will appear in about a minute.
import sys, random, time, functools
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMainWindow, QHBoxLayout
from PyQt5.QtCore import QThread, QObject
def clock(func):
#functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
l_arg = []
if args:
l_arg.append(', '.join(repr(arg) for arg in args))
arg_str = ', '.join(l_arg)
print('[%0.5fs] %s(%s)' % (elapsed, name, arg_str))
return result
return clocked
#clock
def go_sleep(sleep_time):
time.sleep(sleep_time)
def go_run():
for i in range(100):
go_sleep(random.randint(1, 3))
class WorkThread(QObject):
def __int__(self):
super(WorkThread, self).__init__()
def run(self):
go_run()
class WinForm(QMainWindow):
def __init__(self, parent=None):
super(WinForm, self).__init__(parent)
self.button1 = QPushButton('Run')
self.button1.clicked.connect(self.onButtonClick)
self._thread = QThread(self)
self.wt = WorkThread()
self.wt.moveToThread(self._thread)
layout = QHBoxLayout()
layout.addWidget(self.button1)
main_frame = QWidget()
main_frame.setLayout(layout)
self.setCentralWidget(main_frame)
def onButtonClick(self):
self.button1.setText('Running')
self._thread.started.connect(self.wt.run)
self._thread.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
form = WinForm()
form.show()
sys.exit(app.exec_())
turn App Nap back off
defaults write NSGlobalDomain NSAppSleepDisabled -bool YES
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 want to make a progressbar which runs on a thread and I want to be able to move the widget during the process:
import sys
from PyQt4.QtGui import QApplication, QMainWindow, QPushButton, QLineEdit, QLabel, QComboBox, QProgressBar, QFileDialog
from PyQt4.QtCore import QSize, pyqtSlot, QCoreApplication, SIGNAL, QThread
class App(QMainWindow):
def __init__(self):
super(App, self).__init__()
self.setGeometry(500, 300, 820, 350)
self.setWindowTitle("Program")
self.initUI()
def initUI(self):
#Buttons
btnposx = 30
btnposy = 50
self.btn4 = QPushButton('Load', self)
self.btn4.move(btnposx,btnposy+220)
self.connect(self.btn4, SIGNAL("released()"), self.test)
#ProgressBar
self.pb = QProgressBar(self)
self.pb.move(btnposx+150,btnposy+220)
self.pb.resize(470,27)
self.show()
def load(self, val):
self.pb.setValue(val)
def test(self):
self.workThread = WorkThread()
self.connect( self.workThread, SIGNAL('pb_update'), self.load)
self.workThread.start()
class WorkThread(QThread):
def __init__(self):
super(WorkThread, self).__init__()
QThread.__init__(self)
def __del__(self):
self.wait()
#pyqtSlot()
def run(self):
val = 0
l = range(1000000)
for i in l:
if i < len(l):
val += 100/len(l)
self.emit(SIGNAL('pb_update'), val)
return
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
ex.show()
sys.exit(app.exec_())
So far this works, but it does very poorly. The Widget does barely run on my machine, when I try to move it during the process. Is there a way to make this work better so that the Widget doesn't lag or stop responding?
The improvements that your code can have are the following:
Use the new connection style between signals and slots
You must leave a little time for the secondary thread to send the information to the primary thread.
You must indicate the type of connection, in your case Qt::QueuedConnection.
Use pyqtSlot decorator.
You only have to emit the signal when it is necessary, in your case when the whole value of the value changes since the QProgressBar does not recognize floating.
import sys
from PyQt4.QtGui import QApplication, QMainWindow, QPushButton, QLineEdit, QLabel, QComboBox, QProgressBar, QFileDialog
from PyQt4.QtCore import QSize, pyqtSlot, pyqtSignal, QThread, Qt
class App(QMainWindow):
def __init__(self):
super(App, self).__init__()
self.setGeometry(500, 300, 820, 350)
self.setWindowTitle("Program")
self.initUI()
def initUI(self):
#Buttons
btnposx = 30
btnposy = 50
self.btn4 = QPushButton('Load', self)
self.btn4.move(btnposx,btnposy+220)
self.btn4.released.connect(self.test)
#ProgressBar
self.pb = QProgressBar(self)
self.pb.move(btnposx+150,btnposy+220)
self.pb.resize(470,27)
self.show()
#pyqtSlot(int)
def load(self, val):
self.pb.setValue(val)
def test(self):
self.workThread = WorkThread()
self.workThread.pb_update.connect(self.load, Qt.QueuedConnection)
#self.workThread.pb_update.connect(self.pb.setValue)
self.workThread.start()
class WorkThread(QThread):
pb_update = pyqtSignal(float)
def __init__(self, *args, **kwargs):
QThread.__init__(self, *args, **kwargs)
self.value = 0
def __del__(self):
self.wait()
#pyqtSlot()
def run(self):
val = 0
l = range(1000000)
for i in l:
if i < len(l):
val += 100/len(l)
int_val = int(val)
if int_val != self.value:
self.value = int_val
self.pb_update.emit(self.value)
QThread.msleep(1)
return
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
ex.show()
sys.exit(app.exec_())