I have created a UI using PyQt5 that contains a progress bar. I have coded this progress bar in python so that it increases to 99% in real time. The problem I am facing is that the window does not load until the job is complete. I have read upon similar posts which talk about threading and I cannot possibly grasp anything from it. I would appreciate if someone could explain to me a solution and if it does require threading, an aspect I am yet to learn, please explain it to me in laymans term.
import sys, os, sqlite3
import random, datetime
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton,
QMessageBox, QProgressBar, QSplashScreen
import sqlite3
import time
window2 = uic.loadUiType("login_loadingbar.ui")[0]
class LoadingBar(QtWidgets.QMainWindow, window2):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
#title
self.setWindowTitle('Loading')
#makes progress bar go from 0-100, time scaled
def progress(self):
for i in range(100):
time.sleep(0.1)
self.login_progressBar.setValue(i)
app = QApplication(sys.argv)
w2 = LoadingBar(None)
w2.show()
w2.progress()
app.exec_()
When you create gui application you have one thread, in this thread there's thing called event loop - its essentially while true loop that handles events - redraws widgets when necessary, dispatches keyboard and mouse input to related widgets.
Since you have only one thread you can only do one thing at a time, so when you do long operation like time.sleep(), event loop is frozen - no widgets gets redrawn, no inputs handled until it completes and next iteration of event loop reached.
You can create second thread and move operation there or in simple cases when operation can be split into pieces - process events in event loop in between pieces:
for i in range(100):
time.sleep(0.1)
self.login_progressBar.setValue(i)
QtWidgets.QApplication.processEvents()
This way event loop kinda works, but application is not 100% responsive, because every time.sleep(0.1) freezes it, so mouse clicks and other events may not be handled.
Related
I am trying to run the following script inside a software called Anki. This is basically an addon (a plugin) that makes the program to be always on top. This is the MRE:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget
import time
app = QApplication([])
window = QWidget()
window.show()
def start():
while True:
window.setWindowFlags(Qt.WindowStaysOnTopHint)
#window.show()
#app.exec()
time.sleep(1)
start()
This is the closer I could get to the actual error I'm getting. Whenever the program runs the line window.setWindowFlags(Qt.WindowStaysOnTopHint), it immediatly closes the window and becomes unable to reach window.show(). Since this is just a plugin to the base app, I assume all plugins are loaded AFTER the main window has opened and within the main window. So if you close the main window, all plugins also stop running. How can I prevent the main window from closing?
There should only be one QApplication, and you don't have to create a new QApplication since anki creates it. Nor is a while True necessary since it will block the eventloop and even less use time.sleep since it also blocks the eventloop.
import time
from aqt import gui_hooks
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget
window = QWidget()
window.setWindowFlags(window.windowFlags() | Qt.WindowStaysOnTopHint)
gui_hooks.main_window_did_init.append(window.show)
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
window = uic.loadUi("exercise3.ui")
timer = QTimer()
def start():
timer.start(10)
timer.timeout.connect(updateDisplay)
time = 0
def updateDisplay():
global time
time += 1
text = str(time/100)
window.timeDisplay.setText(text)
def stop():
timer.stop()
window.startButton.clicked.connect(start)
window.stopButton.clicked.connect(stop)
window.show()
app.exec_()
Hi, super beginner here, not sure why, but every time I press the start button in the GUI, the stopwatch seems to get faster and faster, almost as if the interval is changing. Not quite sure what's going on, would appreciate if someone could help, thank you!
I seem to have solved it! The 'timeout.connect()' call should be outside of the start() slot. Somehow, when it was in the start() slot, it seems like the signal was connected multiple times to the same slot, resulting in the slot being called multiples times too, making it accelerate every time it was called.
In developing an application using Qt5 with Python, you are generally event driven. No sweat, works like a charm. However, there are instances when you need to poll the status of some hardware GPIO (i.e. a button push), or get some information from a serial port, or something like a gpsd daemon.
What is the preferred way to handle this? Via a QTimer, say, running every 50 msec? Or is there some other method I haven't found? Is it better to set up a trigger on a GPIO pi (https://www.ics.com/blog/control-raspberry-pi-gpio-pins-python) or is there any conflict with the Qt5 Gui?
Basic documentation doesn't look horrible, and I can follow some examples, of course, but didn't know if there was a better/canonical/more Pythonic method.
https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html
https://python-catalin.blogspot.com/2019/08/python-qt5-qtimer-class.html
I don't think there is a pythonic solution, not because you can't use python but because python is not relevant to the topic. And there is no canonical solution either, everything will depend on the application.
From my experience I have found it much easier to reuse libraries that handle GPIOs like Rpi.GPIO or gpiozero. These libraries have as a strategy to create threads where the state of the pins is monitored, so you cannot use the callbacks directly to update the GUI but you must implement wrapper(see this for example).
trivial example:
import sys
from gpiozero import Button
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from PyQt5.QtWidgets import QMainWindow, QApplication
class ButtonManager(QObject):
pressed = pyqtSignal()
def __init__(self, parent=None):
super(ButtonManager, self).__init__(parent)
self._button = Button(20)
self._button.when_pressed = self._on_when_pressed
def _on_when_pressed(self):
self.pressed.emit()
class MainWindow(QMainWindow):
#pyqtSlot()
def on_pressed(self):
print("pressed")
def main():
app = QApplication(sys.argv)
w = MainWindow()
button_manager = ButtonManager()
button_manager.pressed.connect(w.on_pressed)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
If you are going to use serial ports then the best option is to use Qt Serial Port since unlike pyserial it is not necessary to use threads but the notification is through signals(See this for example)
Using a QTimer is another option.
How can I architect code to run a pyqt GUI multiple times consecutively in a process?
(pyqtgraph specifically, if that is relevant)
The context
A python script that performs long running data capture on measurement equipment (a big for loop). During each capture iteration a new GUI appear and displays live data from the measurement equipment to the user, while the main capture code is running.
I'd like to do something like this:
for setting in settings:
measurement_equipment.start(setting)
gui = LiveDataStreamGUI(measurement_equipment)
gui.display()
measurement_equipment.capture_data(300) #may take hours
gui.close()
The main issue
I'd like the data capture code to be the main thread. However pyqt doesn't seems to allow this architecture, as its app.exec_() is a blocking call, allowing a GUI to be created only once per process (e.g., in gui.display() above).
An application is an executable process that runs on one or more foreground threads each of which can also start background threads to perform parallel operations or operations without blocking the calling thread. An application will terminate after all foreground threads have ended, therefore, you need at least one foreground thread which in your case is created when you call the app.exec_() statement. In a GUI application, this is the UI thread where you should create and display the main window and any other UI widget. Qt will automatically terminate your application process when all widgets are closed.
IMHO, you should try to follow the normal flow described above as much as possible, the workflow could be as follows:
Start Application > Create main window > Start a background thread for each calculation > Send progress to UI thread > Show results in a window after each calculation is finished > Close all windows > End application
Also, you should use ThreadPool to make sure you don't run out of resources.
Here is a complete example:
import sys
import time
import PyQt5
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QRunnable, pyqtSignal, QObject
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDialog
class CaptureDataTaskStatus(QObject):
progress = pyqtSignal(int, int) # This signal is used to report progress to the UI thread.
captureDataFinished = pyqtSignal(dict) # Assuming your result is a dict, this can be a class, a number, etc..
class CaptureDataTask(QRunnable):
def __init__(self, num_measurements):
super().__init__()
self.num_measurements = num_measurements
self.status = CaptureDataTaskStatus()
def run(self):
for i in range(0, self.num_measurements):
# Report progress
self.status.progress.emit(i + 1, self.num_measurements)
# Make your equipment measurement here
time.sleep(0.1) # Wait for some time to mimic a long action
# At the end you will have a result, for example
result = {'a': 1, 'b': 2, 'c': 3}
# Send it to the UI thread
self.status.captureDataFinished.emit(result)
class ResultWindow(QWidget):
def __init__(self, result):
super().__init__()
# Display your result using widgets...
self.result = result
# For this example I will just print the dict values to the console
print('a: {}'.format(result['a']))
print('b: {}'.format(result['b']))
print('c: {}'.format(result['c']))
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.result_windows = []
self.thread_pool = QtCore.QThreadPool().globalInstance()
# Change the following to suit your needs (I just put 1 here so you can see each task opening a window while the others are still running)
self.thread_pool.setMaxThreadCount(1)
# You could also start by clicking a button, menu, etc..
self.start_capturing_data()
def start_capturing_data(self):
# Here you start data capture tasks as needed (I just start 3 as an example)
for setting in range(0, 3):
capture_data_task = CaptureDataTask(300)
capture_data_task.status.progress.connect(self.capture_data_progress)
capture_data_task.status.captureDataFinished.connect(self.capture_data_finished)
self.thread_pool.globalInstance().start(capture_data_task)
def capture_data_progress(self, current, total):
# Update progress bar, label etc... for this example I will just print them to the console
print('Current: {}'.format(current))
print('Total: {}'.format(total))
def capture_data_finished(self, result):
result_window = ResultWindow(result)
self.result_windows.append(result_window)
result_window.show()
class App(QApplication):
"""Main application wrapper, loads and shows the main window"""
def __init__(self, sys_argv):
super().__init__(sys_argv)
self.main_window = MainWindow()
self.main_window.show()
if __name__ == '__main__':
app = App(sys.argv)
sys.exit(app.exec_())
If you want your GUI to keep updating in realtime and to not be freezed, you have two main ways to do it:
Refresh the GUI from time to time calling QApplication.processEvents() inside your time consuming function.
Create a separate thread (I mean, QThread) where you run your time consuming function
My personal preference is to go for the latter way. Here is a good tutorial for getting started on how to do multi-threading in Qt.
Having a look at your code:
...
gui.display()
measurement_equipment.capture_data(300) #may take hours
gui.close()
...
it seems you are calling app.exec_ inside gui.display. Its very likely you will have to decouple both functions and call app.exec_ outside of gui.display and after calling capture_data. You will also have to connect the finished signal of the new thread to gui.close. It will be something like this:
...
gui.display() # dont call app.exec_ here
thread = QThread.create(measurement_equipment.capture_data, 300)
thread.finished.connect(gui.close)
app.exec_()
...
I hope this can help you and to not be late!!
You can have only One graphic GUI thread. This would imply to have some Threads to capture data and sync data with the graphic application when needed.
We need to know if the GUI data display is displaying realtime data or only oneshot.
I need to respond to a drag-and-drop re-ordering of items in QListWidget. I can't find an QEvent to use. Any help would be appreciated!
I'm using an eventFilter routine to capture events in my GUI:
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def eventFilter(self, sourceObj, event):
if event.type()== QtCore.QEvent.Drop:
print("got to drop event")
(processing code here)
Perhaps this event doesn't trigger on a drag-and-drop re-order of items because the drag-and-drop is internal to the widget (e.g., nothing from outside the widget is dropped on it)?
I've tried QtCore.QEvent.Drop and QtCore.QEvent.MouseButtonRelease without success. Which event should I use?
(I'm using Python3, PyQt4, on Ubuntu 16.10. My eventFilter routine works for other actions in the GUI like clicks, lostFocus ,etc.)
Here's the solution I found to work. The solution was to use the eventFilter to listen for the ChildRemoved QEvent. Apparently, QT triggers this event when items in a QListWidget are re-ordered via drag-and-drop.
Step 1. In the init(self) for your class that includes the QListWidget, install the event filter for the widget whose event you want to capture.
def __init__(self):
self.lstYourWidgetNameHere.installEventFilter(self)
Step 2. In the eventFilter routine in the same class, check for the ChildRemoved QEvent.
def eventFilter(self, sourceObj, event):
objName = str(sourceObj.objectName())
if event.type()== QtCore.QEvent.ChildRemoved:
if objName == "lstYourWidgetNameHere":
(your code here)