Disable elements when running QThread with PySide/PyQt - python

This is what I get when I click on the OK button.
But I would like to disable the two buttons while the run_action() is still running and finally reset the bar to 0.
This is my current code:
import sys
import time
from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QProgressBar,
QPushButton,
QWidget,
)
class External(QThread):
progressChanged = Signal(int)
def run(self):
progress = 0
while progress < 100:
progress += 10
time.sleep(1)
self.progressChanged.emit(progress)
class Window(QWidget):
"""The main application Window."""
def __init__(self):
super().__init__()
self.setWindowTitle("Example")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(6, 6, 6, 6)
self.bar = QProgressBar()
self.bar.setTextVisible(False)
self.bar.setValue(0)
self.layout.addWidget(self.bar)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.close)
self.layout.addWidget(self.cancel_btn)
self.ok_btn = QPushButton("OK")
self.ok_btn.clicked.connect(self.run_action)
self.layout.addWidget(self.ok_btn)
self.setLayout(self.layout)
def run_action(self):
self.ok_btn.setEnabled(False)
self.cancel_btn.setEnabled(False)
self.calc = External()
self.calc.progressChanged.connect(self.onProgressChanged)
self.calc.start()
self.cancel_btn.setEnabled(True)
self.ok_btn.setEnabled(True)
self.bar.setValue(0)
def onProgressChanged(self, value):
self.bar.setValue(value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

void QThread::finished()
This signal is emitted from the associated thread right before it finishes executing.
When this signal is emitted, the event loop has already stopped running.
No more events will be processed in the thread, except for deferred deletion events.
This signal can be connected to QObject::deleteLater(), to free objects in that thread.
import sys
#import time
#from PySide6.QtCore import QThread, Signal
from PyQt5.QtCore import QThread, pyqtSignal
#from PySide6.QtWidgets import (
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QProgressBar,
QPushButton,
QWidget,
)
class External(QThread):
# progressChanged = Signal(int)
progressChanged = pyqtSignal(int)
def run(self):
progress = 0
while progress <= 100:
self.progressChanged.emit(progress)
self.msleep(500)
progress += 10
class Window(QWidget):
"""The main application Window."""
def __init__(self):
super().__init__()
self.setWindowTitle("Example")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(6, 6, 6, 6)
self.bar = QProgressBar()
self.bar.setTextVisible(False)
self.bar.setValue(0)
self.layout.addWidget(self.bar)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.close)
self.layout.addWidget(self.cancel_btn)
self.ok_btn = QPushButton("OK")
self.ok_btn.clicked.connect(self.run_action)
self.layout.addWidget(self.ok_btn)
self.setLayout(self.layout)
def run_action(self):
self.ok_btn.setEnabled(False)
self.cancel_btn.setEnabled(False)
self.calc = External()
self.calc.progressChanged.connect(self.onProgressChanged)
self.calc.finished.connect(self.onFinished) # +++
self.calc.start()
# self.cancel_btn.setEnabled(True)
# self.ok_btn.setEnabled(True)
# self.bar.setValue(0)
def onProgressChanged(self, value):
self.bar.setValue(value)
def onFinished(self): # +++
self.cancel_btn.setEnabled(True)
self.ok_btn.setEnabled(True)
self.bar.setValue(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

Related

What is the signal for user clicks the down arrow on the QComboBox?

I need to execute a method whenever the user clicks the down arrow on the combo box. I've tried the signals listed in the documentations but none of the worked.
from PyQt5.QtWidgets import *
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
self.combo = QComboBox(self)
self.combo.signal.connect(self.mymethod)
self.show()
def mymethod(self):
print('hello world')
app = QApplication(sys.argv)
win = Window()
sys.exit(app.exec_())
There is no signal that is emitted when the down arrow is pressed but you can create override the mousePressEvent method and verify that this element was pressed:
import sys
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QStyle,
QStyleOptionComboBox,
QVBoxLayout,
QWidget,
)
class ComboBox(QComboBox):
arrowClicked = pyqtSignal()
def mousePressEvent(self, event):
super().mousePressEvent(event)
opt = QStyleOptionComboBox()
self.initStyleOption(opt)
sc = self.style().hitTestComplexControl(
QStyle.CC_ComboBox, opt, event.pos(), self
)
if sc == QStyle.SC_ComboBoxArrow:
self.arrowClicked.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.combo = ComboBox()
self.combo.arrowClicked.connect(self.mymethod)
lay = QVBoxLayout(self)
lay.addWidget(self.combo)
lay.setAlignment(Qt.AlignTop)
def mymethod(self):
print("hello world")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

Opening new window while out of focus using "keyboard"

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)>

PyQt5: How to show another window/widget for a short time

Hi in my application I have main windows and when a data comes from another thread, I need to show it in another screen for 2 seconds and then go back to previous screen. Screens has many components so I made a simple version to demonstrate my purpose.
Data comes from another thread successfully I can change the text of label. However I can not make disappear the old one and apeear the new one.
from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import QApplication,QMainWindow, QLabel, QWidget, QGridLayout, QVBoxLayout, QGroupBox
from PyQt5.QtGui import QTextDocument
from PyQt5 import QtCore, Qt
from PyQt5.QtGui import QIcon, QPixmap, QFont
from time import strftime
import datetime
from babel.dates import format_date, format_datetime, format_time
import sys
import worker
import time
class Form(QWidget):
def __init__(self):
super().__init__()
self.label_main = QLabel("Welcome")
self.label_uid = QLabel("Exit")
self.left = 0
self.top = 0
self._width = 480
self._height = 800
self.layout_main = QVBoxLayout()
self.layout_access = QVBoxLayout()
self.obj = worker.Worker() # no parent!
self.thread = QThread() # no parent!
self.obj.return_uid.connect(self.onCardRead)
self.obj.moveToThread(self.thread)
self.obj.finished.connect(self.thread.quit)
self.thread.started.connect(self.obj.get_uid)
self.thread.start()
self.initUI()
def initUI(self):
self.setLayout(self.layout_main)
self.layout_main.addWidget(self.label_main)
self.setWindowTitle('Main Thread')
self.show()
def secondUI(self):
self.setLayout(self.layout_access)
self.layout_access.addWidget(self.label_uid)
self.setWindowTitle('Access Thread')
self.show()
add self.windowname.close() or just self.close() after the show()
Try it:
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Worker(QtCore.QObject):
return_uid = QtCore.pyqtSignal(int)
finished = QtCore.pyqtSignal()
def get_uid(self):
print("start")
count = 0
QtCore.QThread.msleep(1000)
while count < 10:
QtCore.QThread.msleep(200)
self.return_uid.emit(count)
count += 1
self.finished.emit()
class Form(QWidget):
def __init__(self):
super().__init__()
self.label_main = QLabel("Welcome")
self.label_uid = QLabel("Exit")
self.layout_main = QVBoxLayout()
self.layout_access = QVBoxLayout()
# self.obj = worker.Worker() # no parent!
self.obj = Worker()
self.thread = QThread()
self.obj.return_uid.connect(self.onCardRead)
self.obj.moveToThread(self.thread)
# self.obj.finished.connect(self.thread.quit)
self.obj.finished.connect(self.close)
self.thread.started.connect(self.obj.get_uid)
self.thread.start()
self.initUI()
def initUI(self):
self.setLayout(self.layout_main)
self.layout_main.addWidget(self.label_main)
self.setWindowTitle('Main Thread')
self.show()
def secondUI(self):
self.setLayout(self.layout_access)
self.layout_access.addWidget(self.label_uid)
self.setWindowTitle('Access Thread')
self.show()
def onCardRead(self, id):
self.label_main.setNum(id)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Form()
sys.exit(app.exec_())
Update
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Worker(QtCore.QObject):
return_uid = QtCore.pyqtSignal(int)
finished = QtCore.pyqtSignal()
def get_uid(self):
print("start")
count = 0
QtCore.QThread.msleep(1000)
while count < 10:
QtCore.QThread.msleep(500)
self.return_uid.emit(count)
count += 1
self.finished.emit()
class Form(QWidget):
def __init__(self):
super().__init__()
self.label_main = QLabel("Welcome")
self.label_uid = QLabel("Exit")
self.layout_main = QVBoxLayout()
# self.layout_access = QVBoxLayout()
self.obj = Worker()
self.thread = QThread()
self.obj.return_uid.connect(self.onCardRead)
self.obj.moveToThread(self.thread)
# self.obj.finished.connect(self.close)
self.obj.finished.connect(self.secondUI) # <---
self.thread.started.connect(self.obj.get_uid)
self.thread.start()
self.initUI()
def initUI(self):
self.setLayout(self.layout_main)
self.layout_main.addWidget(self.label_main)
self.setWindowTitle('Main Thread')
self.resize(300, 100)
self.show()
def secondUI(self): # <---
self.hide()
self.windowSecond = QWidget()
self.layout_access = QVBoxLayout(self.windowSecond)
self.layout_access.addWidget(self.label_uid)
self.windowSecond.setWindowTitle('Main Screen')
self.windowSecond.resize(300, 200)
self.windowSecond.show()
def onCardRead(self, id):
self.label_main.setNum(id)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Form()
sys.exit(app.exec_())

PyQt4 threading properly

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_())

Why do external functions cause the PyQt5 window to freeze?

Here is some sample code that breaks:
import sys
import time
from PyQt5.QtWidgets import (QApplication, QDialog,
QProgressBar)
class Actions(QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.progress = QProgressBar(self)
self.progress.setGeometry(0, 0, 300, 25)
self.show()
self.count = 0
while self.count < 100:
self.count += 1
time.sleep(1) # Example external function
self.progress.setValue(self.count)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Actions()
sys.exit(app.exec_())
Running this will cause it to freeze and become unresponsive particularly in windows environments. Replacing the time.sleep function with any non-PyQt5 function will yield the same results.
From what I understand this has to do with the function not being called in a separate thread using QThread. I used this answer as a reference and came up with a partial solution.
import sys
import time
from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import (QApplication, QDialog,
QProgressBar)
class External(QThread):
def run(self):
count = 0
while count < 100:
count += 1
print(count)
time.sleep(1)
class Actions(QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.progress = QProgressBar(self)
self.progress.setGeometry(0, 0, 300, 25)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Actions()
calc = External()
calc.finished.connect(app.exit)
calc.start()
sys.exit(app.exec_())
This will run time.sleep in the background and keep the main window responsive. But, I don't know how to update the values using self.progress.setValue since it's not accessible in class External.
So far from what I know, I have to use signals to accomplish this. Most of the documentation out there is for PyQt4 making it harder to find a solution.
Another problem I am faced with is being able to start the External thread from within class Actions.
Answers to this problem will also serve as valuable documentation for PyQt5.
Thanks in advance.
You must use the signals to update the values.
import sys
import time
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import (QApplication, QDialog,
QProgressBar)
class External(QThread):
countChanged = pyqtSignal(int)
def run(self):
count = 0
while count < 100:
count += 1
self.countChanged.emit(count)
print(count)
time.sleep(1)
class Actions(QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.progress = QProgressBar(self)
self.progress.setGeometry(0, 0, 300, 25)
self.show()
def onCountChanged(self, value):
self.progress.setValue(value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Actions()
calc = External()
calc.countChanged.connect(window.onCountChanged)
calc.start()
sys.exit(app.exec_())
Here is a version that starts the thread from inside class Actions and uses a button:
import sys
import time
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import (QApplication, QDialog,
QProgressBar, QPushButton)
class External(QThread):
countChanged = pyqtSignal(int)
def run(self):
count = 0
while count < 100:
count += 1
time.sleep(1)
print(count)
self.countChanged.emit(count)
class Actions(QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.progress = QProgressBar(self)
self.progress.setGeometry(0, 0, 300, 25)
self.button = QPushButton('Start', self)
self.button.move(0, 30)
self.show()
self.button.clicked.connect(self.onButtonClick)
def onButtonClick(self):
self.calc = External()
self.calc.countChanged.connect(self.onCountChanged)
self.calc.start()
def onCountChanged(self, value):
self.progress.setValue(value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Actions()
sys.exit(app.exec_())

Categories

Resources