I want to update the GIF while pool process are executing in parallel. Now GIF is getting stuck, it is responding with respect to progressbar value update. I want GIF to execute independently. I have tried using it thread but that is not solution for my process. Once control is going to pool-process it getting GUI freeze, not responding. I have given high level piece of code that in this GIF should not stuck. I am copying number of files through process in the original code there it will take much time to complete the task till then GUI gets not responding. So pool-process I can not remove or replace. Let me know if this GIF continuous movement is possible irrespective of progressbar and process executing. I would appreciate code changes because i am new to python and PYQT5.
Thanks in advance.
from PyQt5.QtGui import QMovie
import sys
import time
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import (QApplication, QDialog,
QProgressBar, QPushButton, QVBoxLayout, QLabel)
from multiprocessing import Pool, freeze_support
TIME_LIMIT = 100
def func(a, b):
print(a +b)
class External (QThread):
countChanged = pyqtSignal(int)
def run(self):
count = 0
print("check1")
while count < TIME_LIMIT:
count += 1
time.sleep(1)
print("thread1 update")
self.countChanged.emit(count)
class Actions (QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Progress Bar')
self.setGeometry(200,300,200,200)
col = QVBoxLayout(self)
self.text = ['ON', 'OFF']
self.label = QLabel(self)
self.movie = QMovie("movie.gif")
self.label.setMovie(self.movie)
self.progress = QProgressBar(self)
self.progress.setGeometry(0, 0, 300, 25)
self.progress.setMaximum(100)
self.button = QPushButton('Start', self)
col.addWidget(self.label)
col.addWidget(self.progress)
col.addWidget(self.button)
self.label.move(0,90)
self.button.move(0, 30)
self.button.clicked.connect(self.onButtonClick)
self.show()
def startAnimation(self):
self.movie.start()
def onButtonClick(self):
self.startAnimation()
self.calc = External()
a_args = [1, 2, 3, 4, 5, 6]
second_arg = 1
self.calc.countChanged.connect(self.onCountChanged)
self.calc.start()
QApplication.processEvents()
for i in a_args:
pool = Pool(processes=3)
M = pool.starmap(func, zip(a_args, repeat(second_arg)))
QApplication.processEvents()
pool.close()
pool.join()
print("value of args",i)
def onCountChanged(self, value):
self.progress.setValue(value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Actions()
sys.exit(app.exec_())
Related
So I have this UI that loads ALOT of buttons, all with Images, problem is, it takes WAY to long, so I tried putting it into a QThread, I had it working, but there was no speed difference, so I tried a different solution, but now the Thread wont start.
Code:
import sys
import os
from PyQt5 import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtGui, QtPrintSupport, QtWidgets, uic
# I've tried using a QThread for this, but it still took the same amount of time.
class LoadImageThumbnailshread(QThread):
def __init__(self, listButtons, listPaths):
QThread.__init__(self)
self.listButtons = listButtons
self.listPaths = listPaths
def run(self):
self.process_images_Thread()
def process_images_Thread(self):
for i, j in enumerate(self.listButtons):
j.setIcon(QIcon(self.listPaths[i]))
j.setIconSize(QSize(150-6, 60-6))
class App(QDialog):
def __init__(self):
super().__init__()
self.title = 'PyQt5 layout - pythonspot.com'
self.left = 10
self.top = 10
self.width = 320
self.height = 100
self.images_path = []
self.button_images = []
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.createGridLayout()
windowLayout = QVBoxLayout()
windowLayout.addWidget(self.horizontalGroupBox)
self.setLayout(windowLayout)
self.show()
def createGridLayout(self):
self.horizontalGroupBox = QGroupBox("Grid")
layout = QGridLayout()
layout.setColumnStretch(1, 4)
layout.setColumnStretch(2, 4)
for i in range(100):
self.btnImage = QPushButton()
self.btnImage.setObjectName('btnImage')
self.images_path.append(os.path.dirname(
os.path.abspath(__file__)) + 'view.png')
# ! I have also tried using Pixmaps with Clickable labels, but it made no diffrence.
# pixmap = QPixmap(os.path.dirname(os.path.abspath(__file__)) + image_locations[i])
# pixmap = pixmap.scaled(60, 300, Qt.KeepAspectRatio, Qt.FastTransformation)
# self.btnImage.setPixmap(pixmap)
# ! Enableing this loads the images, but very slowly
# self.btnImage.setIcon(QIcon(os.path.dirname(os.path.abspath(__file__)) + '/view.png'))
# self.btnImage.setIconSize(QSize(150-6, 60-6))
self.button_images.append(self.btnImage)
layout.addWidget(self.btnImage, i, 0)
# ! This starts the QThread
converter = LoadImageThumbnailshread(
self.button_images, self.images_path)
converter.start()
self.horizontalGroupBox.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
So to explain more of what I want, I want to start a QThread AFTER the UI has loaded, that QThread will load all of the Images onto each of the buttons. Problem is: it doesn't work right now.
Another problem that I had, is that when I got the QThread the UI waited for it to finish, and then everything all of a sudden popped up. I want the QThread to be completely separate and pretty, much see all images loading one by one if you will.
The threads do not serve to accelerate any task!!! but to execute tasks that do not block any thread. Given the above, if I have n "task" and each one is executed in "n" threads then the total execution time will be 1 task, that is, no task is accelerated but the tasks are redistributed. In conclusion: If you want to speed up a task then threads is not a default option as it depends on the application.
On the other hand, the loading of the image does not consume time but there are many tasks that consume little time that in total is equivalent to a task that consumes a lot of time and unfortunately that task cannot be executed in another thread or process since the elements of the GUI are not thread-safe.
A workaround is that the load is of a small group of n elements every T seconds so the total task will be distributed and the user will not observe any effect of lag.
import os
import sys
from PyQt5.QtCore import QSize, QTimer
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
QApplication,
QDialog,
QGridLayout,
QGroupBox,
QPushButton,
QVBoxLayout,
)
class App(QDialog):
def __init__(self):
super().__init__()
self.title = "PyQt5 layout - pythonspot.com"
self.left = 10
self.top = 10
self.width = 320
self.height = 100
self.images_path = []
self.button_images = []
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.createGridLayout()
windowLayout = QVBoxLayout(self)
windowLayout.addWidget(self.horizontalGroupBox)
self._iter = iter(range(100))
self._timer = QTimer(interval=10, timeout=self.lazy_loading)
self._timer.start()
def createGridLayout(self):
self.horizontalGroupBox = QGroupBox("Grid")
self.grid_layout = QGridLayout()
self.grid_layout.setColumnStretch(1, 4)
self.grid_layout.setColumnStretch(2, 4)
self.horizontalGroupBox.setLayout(self.grid_layout)
def lazy_loading(self):
try:
i = next(self._iter)
except StopIteration:
self._timer.stop()
else:
btn = QPushButton()
image_path = os.path.join(os.path.abspath(__file__), "view.png")
btn.setIcon(QIcon(image_path))
btn.setIconSize(QSize(150 - 6, 60 - 6))
self.grid_layout.addWidget(btn, i, 0)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = App()
ex.show()
sys.exit(app.exec_())
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class MainWindow(QWidget):
signal = pyqtSignal()
def __init__(self):
super(MainWindow, self).__init__()
self.buttonList = list()
self.counter = 0
self.setUI()
self.signal.connect(self.updatebutton)
self.startTimer(10)
def timerEvent(self, *args, **kwargs):
self.signal.emit()
def updatebutton(self):
for button in self.buttonList: # type: QLabel or QPushButton
button.setText(str(self.counter))
self.counter += 1
def setUI(self):
gridLayout = QGridLayout()
for i in range(10):
for j in range(10):
button = QLabel(self)#QPushButton(self)
self.buttonList.append(button)
gridLayout.addWidget(button, i, j)
self.setLayout(gridLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
When I set the timer at 10 ms, all Labels can increase its value by 100 per second. However, when I change the timer to 1 ms, all Labels can not increase its value by 1000 per second.
When I use Pushbutton instead of Label, even the timer is set to 100 ms, all buttons can not increase their value by 100 per second. But when I reduce the number of buttons, e.g., totally 10 buttons instead of 100, these buttons can work well.
3.I meet with this problem recently when I used a data acquisition software called DEWESOFT, which can produce many subwidget or subwindow(very similar to QMidArea and QMidSubwindow) to draw curves and display values of high-frequency signals. Hence, I'd like to find out how and why. I wonder how to control the refresh rate of widget and how to determine the limit of UI refresh ability. If I wanna refresh more widgets at a high frequency, what should I do?
Later I tried Thread to improve, it seems to work a litter better, but I don't know why.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import threading
import sys
import time
class MainWindow(QWidget):
signal = pyqtSignal()
def __init__(self):
super(MainWindow, self).__init__()
self.buttonList = list()
self.counter = 0
self.setUI()
self.signal.connect(self.updatebutton)
self.thread = threading.Thread(target=self.timerevent, args=())
self.thread.setDaemon(True)
self.thread.start()
# self.startTimer(10)
def timerEvent(self, *args, **kwargs):
self.signal.emit()
def timerevent(self):
while True:
self.signal.emit()
time.sleep(0.01)
def updatebutton(self):
print(self.counter)
for button in self.buttonList: # type: QPushButton
button.setText(str(self.counter))
self.counter += 1
def setUI(self):
gridLayout = QGridLayout()
for i in range(10):
for j in range(10):
button = QPushButton(self)
self.buttonList.append(button)
gridLayout.addWidget(button, i, j)
self.setLayout(gridLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Please excuse me if my description isn't perfect, I'm still pretty new at PyQt and also Python in general. If you have recommendations on how to improve the question, please let me know.
I'm trying to draw on a Pixmap-QLabel, which is part of a QMainWindow, with QPainter. The QPainter is called in a loop, because the drawing is updated after a fixed duration. Drawing on the Pixmap works as intended, the problem I have is that the label always opens in a new window, instead of being placed on the QLabel inside the original QMainWindow.
I suspect that the reason for that is that I'm calling the QPainter from a Worker-class-object which is created by the QThreadpool-object. If I call the QPainter from inside the initialization of the GUI, the Pixmap-label is created as part of the QMainWindow as intended. Unfortunately the multithreading is necessary so the GUI stays responsive while the QLabel is updating.
The GUI itself is created with QtCreator, and simply loaded into the script.
Here's my code:
import os
import sys
import time
from PyQt5 import QtWidgets, QtCore, uic
from PyQt5.QtWidgets import QLabel, QPushButton, QMainWindow
from PyQt5.QtGui import QPixmap, QPainter, QPen, QPaintEvent
from PyQt5.QtCore import *
class Ui(QMainWindow):
def __init__(self):
super(Ui, self).__init__()
self.counter = 0
# load ui which can be designed with Qt Creator
uic.loadUi("ui/paintEvent_Loop.ui", self)
# find the QLabel where the picture should be placed
self.pixmap_label = self.findChild(QtWidgets.QLabel, "pixmap_label")
# creating the pixmap-label here works as intended
'''self.draw_label = PixmapLabel(self.pixmap_label)
self.draw_label.setGeometry(130, 50, 911, 512)
self.draw_label.show()'''
self.label = self.findChild(QLabel, "label")
# find the button with the name "cancel_button"
self.cancel_button = self.findChild(QtWidgets.QPushButton, "cancel_button")
self.cancel_button.clicked.connect(self.close_application)
# find the start_button button
self.start_button = self.findChild(QtWidgets.QPushButton, "start_button")
self.start_button.clicked.connect(self.start_loop)
self.pause_cont_button = self.findChild(QPushButton, "pause_cont_button")
self.pause_cont_button.clicked.connect(self.toggle_pause_continue)
self.pause_cont_button.hide()
# create the QThreadPool object to manage multiple threads
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.run_loop = True
# show application
self.show()
def close_application(self):
app.quit()
def toggle_pause_continue(self):
"""
changes the value of boolean run_loop to pause and continue the loop through the samples in the chosen scene
:return:
"""
if self.run_loop:
self.run_loop = False
else:
self.run_loop = True
def start_loop(self):
# hide start_button and show pause_cont_button
self.start_button.hide()
self.pause_cont_button.show()
self.pause_cont_button.setCheckable(True)
# start one further thread managed by threadpool
worker = Worker()
self.threadpool.start(worker)
class PixmapLabel(QLabel):
def __init__(self, parent=None):
super(PixmapLabel, self).__init__(parent=parent)
def paintEvent(self, a0: QPaintEvent) -> None:
# initiate QPainter instance
painter = QPainter(window.draw_label)
# open image
picture = QPixmap(os.getcwd() + '/test-image.png')
myPicturePixmap = picture.scaled(self.size(), QtCore.Qt.KeepAspectRatio)
self.setPixmap(myPicturePixmap)
# draw red box on it
painter.drawPixmap(self.rect(), myPicturePixmap)
pen = QPen(Qt.red, 3)
painter.setPen(pen)
painter.drawRect(10, 10, 100, 100)
class Worker(QRunnable):
# worker thread
def __init__(self):
super().__init__()
#pyqtSlot()
def run(self):
print("Thread start")
for self.i in range(0, 50):
# create pixmap_label with drawings
# FIXME: make pixmap-label part of original GUI
window.draw_label = PixmapLabel(window.pixmap_label)
window.draw_label.setGeometry(130, 50, 911, 512)
window.draw_label.show()
window.label.setText(str(self.i))
while window.run_loop == False:
time.sleep(0.05)
# show image for 0.5 seconds, then update image
time.sleep(0.5)
window.draw_label.destroy()
time.sleep(0.05)
# print in terminal to know that we are finished
print("Thread complete")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = Ui()
app.exec_()
The image I'm using:
I use two widgets: a QSpinBox and a QLineEdit. valueChanged slot of the QSpinBox widget is connected to the update function. This function consist of a time-consuming processing (a loop with calculations or a time.sleep() call) and a QLineEdit.setText() call. At the beginning, i thought it worked as expected but I noticed that the signal seems to be emitted twice when the calculations takes a long time.
Bellow is the code:
import time
from PyQt5.QtWidgets import QWidget, QSpinBox, QVBoxLayout, QLineEdit
class Window(QWidget):
def __init__(self):
# parent constructor
super().__init__()
# widgets
self.spin_box = QSpinBox()
self.line_edit = QLineEdit()
# layout
v_layout = QVBoxLayout()
v_layout.addWidget(self.spin_box)
v_layout.addWidget(self.line_edit)
# signals-slot connections
self.spin_box.valueChanged.connect(self.update)
#
self.setLayout(v_layout)
self.show()
def update(self, param_value):
print('update')
# time-consuming part
time.sleep(0.5) # -> double increment
#time.sleep(0.4) # -> works normally!
self.line_edit.setText(str(param_value))
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
win = Window()
sys.exit(app.exec_())
Another version of update:
# alternative version, calculations in a loop instead of time.sleep()
# -> same behaviour
def update2(self, param_value):
print('update2')
for i in range(2000000): # -> double increment
x = i**0.5 * i**0.2
#for i in range(200000): # -> works normally!
# x = i**0.5 * i**0.2
self.line_edit.setText(str(param_value))
There is no real mystery here. If you click a spin-box button, the value will increase by a single step. But if you press and hold down the button, it will increase the values continually. In order to tell the difference between a click and a press/hold, a timer is used. Presumably, the threshold is around half a second. So if you insert a small additional delay, a click may be interpreted as a short press/hold, and so the spin-box will increment by two steps instead of one.
UPDATE:
One way to work around this behaviour is by doing the processing in a worker thread, so that the delay is eliminated. The main problem with this is avoiding too much lag between spin-box value changes and line-edit updates. If you press and hold the spin-box button, a large number of signal events could be queued by the worker thread. A simplistic approach would wait until the spin-box button was released before handling all those queued signals - but that would result in a long delay whilst each value was processed separately. A better approach is to compress the events, so that only the most recent signal is handled. This will still be somewhat laggy, but if the processing time is not too long, it should result in acceptable behaviour.
Here is a demo that implements this approach:
import sys, time
from PyQt5.QtWidgets import (
QApplication, QWidget, QSpinBox, QVBoxLayout, QLineEdit,
)
from PyQt5.QtCore import (
pyqtSignal, pyqtSlot, Qt, QObject, QThread, QMetaObject,
)
class Worker(QObject):
valueUpdated = pyqtSignal(int)
def __init__(self, func):
super().__init__()
self._value = None
self._invoked = False
self._func = func
#pyqtSlot(int)
def handleValueChanged(self, value):
self._value = value
if not self._invoked:
self._invoked = True
QMetaObject.invokeMethod(self, '_process', Qt.QueuedConnection)
print('invoked')
else:
print('received:', value)
#pyqtSlot()
def _process(self):
self._invoked = False
self.valueUpdated.emit(self._func(self._value))
class Window(QWidget):
def __init__(self):
super().__init__()
self.spin_box = QSpinBox()
self.line_edit = QLineEdit()
v_layout = QVBoxLayout()
v_layout.addWidget(self.spin_box)
v_layout.addWidget(self.line_edit)
self.setLayout(v_layout)
self.thread = QThread(self)
self.worker = Worker(self.process)
self.worker.moveToThread(self.thread)
self.worker.valueUpdated.connect(self.update)
self.spin_box.valueChanged.connect(self.worker.handleValueChanged)
self.thread.start()
self.show()
def process(self, value):
time.sleep(0.5)
return value
def update(self, param_value):
self.line_edit.setText(str(param_value))
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
sys.exit(app.exec_())
I am using Python 3.5, PyQt5 on OSX and I was wondering if there was a possibility to update the QProgressBar without slowing down the whole computing work.
Here was my code and if I did just the task without the progressbar update it was soo much faster!!
from PyQt5.QtWidgets import (QWidget, QProgressBar, QPushButton, QApplication)
from jellyfish import levenshtein_distance, jaro_winkler
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.pbar = QProgressBar(self)
self.pbar.setGeometry(30, 40, 200, 25)
self.btn = QPushButton('Start', self)
self.btn.move(40, 80)
self.btn.clicked.connect(self.doAction)
self.setGeometry(300, 300, 280, 170)
self.show()
def doAction(self):
#setup variables
step = 0
m = 1000
n = 500
step_val = 100 / (m * n)
#make task
for i in range(m):
for j in range(n):
jaro_winkler(str(i), str(j))
#show task
print(i,j)
#update progressbar
step += step_val
self.pbar.setValue(step)
QApplication.processEvents()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Then with help from stackoverflow users I got the hint to make a separate working thread and connect the updating signal to the GUI. I did it and it looks now like the following code. It also works and is much faster, but I can't figure out how to connect the emited signal to the GUI. Can somebody please help me? Many thanks in advance!
from jellyfish import jaro_winkler
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QProgressBar, QMainWindow
import time
import numpy as np
class Main_Window(QMainWindow):
def __init__(self):
super(Main_Window,self).__init__()
self.initUI()
def initUI(self):
self.pbar = QProgressBar(self)
self.pbar.setGeometry(30, 40, 200, 25)
self.btn = QPushButton('Start', self)
self.btn.move(40, 80)
self.btn.clicked.connect(MyThread.doAction)
self.setGeometry(300, 300, 280, 170)
self.show()
def updateProgressBar(self, val):
self.pbar.setValue.connect(val)
class MySignal(QWidget):
pbar_signal = QtCore.pyqtSignal(int)
class MyThread(QtCore.QThread):
def __init__(self):
super().__init__()
def doAction(self):
t = time.time() #for time measurement
#setup variables
step = 0
m = 1000
n = 500
pbar_val = 100 / m
signal_instance = MySignal()
#make task
for i in range(m):
for j in range(n):
jaro_winkler(str(i), str(j))
signal_instance.pbar_signal.emit(pbar_val)
#measuring task time
print(np.round_(time.time() - t, 3), 'sec elapsed')
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Main_Window()
sys.exit(app.exec_())
There are three things slowing the code down:
Printing to stdout is very expensive - especially when you do it 500,000 times! On my system, commenting out print(i,j) roughly halves the time doAction takes to run.
It's also quite expensive to call processEvents 500,000 times. Commenting out QApplication.processEvents() reduces the run time by another two-thirds.
Commenting out self.pbar.setValue(step) halves the time again.
Hopefully it should be obvious by now that attempting to update the gui 500,000 times during a task that should take less than a second is massive overkill! Most user's reaction times are about 200ms at best, so you only need to update the gui about once every 100ms.
Given that, one simple fix is to move the updates into the outer loop:
for i in range(m):
for j in range(n):
jaro_winkler(str(i), str(j))
# show task
# print(i,j)
step += step_val
# update progressbar
self.pbar.setValue(step)
QApplication.processEvents()
But an even better solution would be to move the calculations into a separate worker thread and have it periodically emit a custom signal to update the progress bar:
class Main_Window(QMainWindow):
...
def initUI(self):
...
self.btn.clicked.connect(self.doAction)
self.thread = MyThread()
self.thread.pbar_signal.connect(self.pbar.setValue)
def doAction(self):
if not self.thread.isRunning():
self.thread.start()
class MyThread(QtCore.QThread):
pbar_signal = QtCore.pyqtSignal(int)
def run(self):
#for time measurement
t = time.time()
#setup variables
m = 1000
n = 500
progress = step = 100 / m
#make task
for i in range(m):
for j in range(n):
jaro_winkler(str(i), str(j))
progress += step
self.pbar_signal.emit(progress)
#measuring task time
print(np.round_(time.time() - t, 3), 'sec elapsed')