PyQt: signal emitted twice when calculations are too long - python

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

Related

My PyQt5 script is skipping a line inside a function

I want to make a simple browser GUI for learning purposes with PyQt5. A function that I want is to have in the status bar a text "Online". If the user clicks somewhere else from the browser and the application loses focus, a message will appear in the status bar indicating that, and after a few seconds the browser will change the url to google.
If I run the following code everything works fine as expected, when app loses focus it navigates to google. However, the message "Application lost focus ..." doesn't appear in the statusbar. It simply skips that line. If I remove the seturl and time.sleep line, the script will change the text as expected.
Why is it skipping that line? (Line 55)
import sys
import time
from PyQt5.QtGui import QIcon, QFont
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMessageBox, QFrame ,QMainWindow, QLabel
from PyQt5.QtWebEngineWidgets import *
class VLine(QFrame):
# a simple VLine, like the one you get from designer
def __init__(self):
super(VLine, self).__init__()
self.setFrameShape(self.VLine|self.Sunken)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setFocus()
app.focusChanged.connect(self.on_focusChanged)
self.browser = QWebEngineView()
self.browser.setContextMenuPolicy(Qt.PreventContextMenu)
self.browser.setUrl(QUrl('http://stackoverflow.com'))
self.setCentralWidget(self.browser)
self.showMaximized()
self.date = QDate.currentDate()
font = QFont('Arial', 16, QFont.Bold)
self.statusBar().setFont(font)
timer = QTimer(self)
timer.timeout.connect(self.showTime)
timer.start(1000)
self.lbl1 = QLabel(self)
self.lbl1.setStyleSheet('border: 0; color: red;')
self.lbl1.setFont(font)
self.statusBar().reformat()
self.statusBar().setStyleSheet('border: 0; background-color: #FFF8DC;')
self.statusBar().setStyleSheet("QStatusBar::item {border: none;}")
self.statusBar().addPermanentWidget(VLine()) # <---
self.statusBar().addPermanentWidget(self.lbl1)
self.statusBar().addPermanentWidget(VLine())
def showTime(self):
current_time = QTime.currentTime()
label_time = current_time.toString('hh:mm:ss')
self.statusBar().showMessage('Time: ' + label_time + ' || Date: ' + self.date.toString('dd.MM.yyyy'))
def on_focusChanged(self):
if self.isActiveWindow() == False:
print(f"\nwindow is the active window: {self.isActiveWindow()}")
self.lbl1.setText('Application lost focus. Returning to Google in 5 seconds')
time.sleep(5)
self.browser.setUrl(QUrl('http://google.com'))
self.lbl1.setText('Online')
else:
print(f"window is the active window: {self.isActiveWindow()}")
self.lbl1.setText('Online')
app = QApplication(sys.argv)
QApplication.setApplicationName('Browser')
window = MainWindow()
app.exec_()
You are confusing how the focusChanged signal works: it emits a signal regarding the focus changes within the program.
What you need is to override the changeEvent of the window and intercept an ActivationChange event type.
class MainWindow(QMainWindow):
# ...
def changeEvent(self, event):
if event.type() == event.ActivationChange:
# ...
That said, NEVER put a blocking function within the main thread.
Remove the time.sleep and never think about using it again for this kind of things.
Add a function for that redirect, and create a QTimer that you can start when losing focus (and stop if regaining it again before the timeout).
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.focusLostTimer = QTimer(
interval=5000, singleShot=True, timeout=self.focusRedirect)
def focusRedirect(self):
self.browser.setUrl(QUrl('http://google.com'))
def changeEvent(self, event):
if event.type() == event.ActivationChange:
if not self.isActiveWindow():
self.focusLostTimer.start()
else:
self.focusLostTimer.stop()

How do I draw on a Qlabel with QPainter when multithreading with QThreadpool?

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:

PyQt5 QThread not working, gui still freezing

I have this code (if you have pyqt5, you should be able to run it yourself):
import sys
import time
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
class Worker(QObject):
def __init__(self):
super().__init__()
self.thread = None
class Tab(QObject):
def __init__(self, _main):
super().__init__()
self._main = _main
class WorkerOne(Worker):
finished = pyqtSignal()
def __init__(self):
super().__init__()
#pyqtSlot(str)
def print_name(self, name):
for _ in range(100):
print("Hello there, {0}!".format(name))
time.sleep(1)
self.finished.emit()
self.thread.quit()
class SomeTabController(Tab):
def __init__(self, _main):
super().__init__(_main)
self.threads = {}
self._main.button_start_thread.clicked.connect(self.start_thread)
# Workers
self.worker1 = WorkerOne()
#self.worker2 = WorkerTwo()
#self.worker3 = WorkerThree()
#self.worker4 = WorkerFour()
def _threaded_call(self, worker, fn, *args, signals=None, slots=None):
thread = QThread()
thread.setObjectName('thread_' + worker.__class__.__name__)
# store because garbage collection
self.threads[worker] = thread
# give worker thread so it can be quit()
worker.thread = thread
# objects stay on threads after thread.quit()
# need to move back to main thread to recycle the same Worker.
# Error is thrown about Worker having thread (0x0) if you don't do this
worker.moveToThread(QThread.currentThread())
# move to newly created thread
worker.moveToThread(thread)
# Can now apply cross-thread signals/slots
#worker.signals.connect(self.slots)
if signals:
for signal, slot in signals.items():
try:
signal.disconnect()
except TypeError: # Signal has no slots to disconnect
pass
signal.connect(slot)
#self.signals.connect(worker.slots)
if slots:
for slot, signal in slots.items():
try:
signal.disconnect()
except TypeError: # Signal has no slots to disconnect
pass
signal.connect(slot)
thread.started.connect(lambda: fn(*args)) # fn needs to be slot
thread.start()
#pyqtSlot()
def _receive_signal(self):
print("Signal received.")
#pyqtSlot(bool)
def start_thread(self):
name = "Bob"
signals = {self.worker1.finished: self._receive_signal}
self._threaded_call(self.worker1, self.worker1.print_name, name,
signals=signals)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Thread Example")
form_layout = QVBoxLayout()
self.setLayout(form_layout)
self.resize(400, 400)
self.button_start_thread = QPushButton()
self.button_start_thread.setText("Start thread.")
form_layout.addWidget(self.button_start_thread)
self.controller = SomeTabController(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
_main = MainWindow()
_main.show()
sys.exit(app.exec_())
However WorkerOne still blocks my GUI thread and the window is non-responsive when WorkerOne.print_name is running.
I have been researching a lot about QThreads recently and I am not sure why this isn't working based on the research I've done.
What gives?
The problem is caused by the connection with the lambda method since this lambda is not part of the Worker so it does not run on the new thread. The solution is to use functools.partial:
from functools import partial
...
thread.started.connect(partial(fn, *args))
Complete Code:
import sys
import time
from functools import partial
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
class Worker(QObject):
def __init__(self):
super().__init__()
self.thread = None
class Tab(QObject):
def __init__(self, _main):
super().__init__()
self._main = _main
class WorkerOne(Worker):
finished = pyqtSignal()
def __init__(self):
super().__init__()
#pyqtSlot(str)
def print_name(self, name):
for _ in range(100):
print("Hello there, {0}!".format(name))
time.sleep(1)
self.finished.emit()
self.thread.quit()
class SomeTabController(Tab):
def __init__(self, _main):
super().__init__(_main)
self.threads = {}
self._main.button_start_thread.clicked.connect(self.start_thread)
# Workers
self.worker1 = WorkerOne()
#self.worker2 = WorkerTwo()
#self.worker3 = WorkerThree()
#self.worker4 = WorkerFour()
def _threaded_call(self, worker, fn, *args, signals=None, slots=None):
thread = QThread()
thread.setObjectName('thread_' + worker.__class__.__name__)
# store because garbage collection
self.threads[worker] = thread
# give worker thread so it can be quit()
worker.thread = thread
# objects stay on threads after thread.quit()
# need to move back to main thread to recycle the same Worker.
# Error is thrown about Worker having thread (0x0) if you don't do this
worker.moveToThread(QThread.currentThread())
# move to newly created thread
worker.moveToThread(thread)
# Can now apply cross-thread signals/slots
#worker.signals.connect(self.slots)
if signals:
for signal, slot in signals.items():
try:
signal.disconnect()
except TypeError: # Signal has no slots to disconnect
pass
signal.connect(slot)
#self.signals.connect(worker.slots)
if slots:
for slot, signal in slots.items():
try:
signal.disconnect()
except TypeError: # Signal has no slots to disconnect
pass
signal.connect(slot)
thread.started.connect(partial(fn, *args)) # fn needs to be slot
thread.start()
#pyqtSlot()
def _receive_signal(self):
print("Signal received.")
#pyqtSlot(bool)
def start_thread(self):
name = "Bob"
signals = {self.worker1.finished: self._receive_signal}
self._threaded_call(self.worker1, self.worker1.print_name, name,
signals=signals)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Thread Example")
form_layout = QVBoxLayout()
self.setLayout(form_layout)
self.resize(400, 400)
self.button_start_thread = QPushButton()
self.button_start_thread.setText("Start thread.")
form_layout.addWidget(self.button_start_thread)
self.controller = SomeTabController(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
_main = MainWindow()
_main.show()
sys.exit(app.exec_())
To avoid blocking the gui for a slideshow function showing images i run the following
simple code.
A background Thread Class to wait one second, then signal to the gui waiting is finished.
class Waiter(QThread):
result = pyqtSignal(object)
def __init__(self):
QtCore.QThread.__init__(self)
def run(self):
while self.isRunning:
self.sleep(1)
self.result.emit("waited for 1s")
Then in main window connect the slideshow start and stop button to start and stop methods of main app and connect the nextImage function to the signal the Waiter Thread emits.
self.actionstopSlideShow.triggered.connect(self.stopSlideShow)
self.actionslideShowStart.triggered.connect(self.startSlideShow)
self.waitthread = Waiter()
self.waitthread.result.connect(self.nextImage)
Then two methods of main app allow to start and stop the time ticker
def startSlideShow(self):
"""Start background thread that waits one second,
on wait result trigger next image
use thread otherwise gui freezes and stop button cannot be pressed
"""
self.waitthread.start()
def stopSlideShow(self):
self.waitthread.terminate()
self.waitthread.wait()
Up to now i have no problems subclassing from QThread in pyqt5, gui changes are all handled inside the main (gui) thread.

QGraphicesItem emit a signal upon hoverEnterEvent

What is the best method/practice for emitting a signal upon entering either a QGraphicsWidget or a QGraphicsItem ?
In my MWE I would like to trigger a call to MainWindow.update, from Square.hoverEnterEvent, whenever the user mouse(s) over an item in a QGraphicsScene. The trouble is that QGraphicsItem/Widget is not really designed to emit signals. Instead these classes are setup to handle events passed down to them from QGraphicsScene. QGraphicsScene handles the case that the user has selected an item but does not appear to handle mouse entry events, At least there is no mechanism for entryEvent to percolate up to the parent widget/window.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, qApp, QMainWindow, QGraphicsScene, QGraphicsView, QStatusBar, QGraphicsWidget, QStyle
from PyQt5.QtCore import Qt, QSizeF
class Square(QGraphicsWidget) :
"""
doc string
"""
def __init__(self,*args, name = None, **kvps) :
super().__init__(*args, **kvps)
self.radius = 5
self.name = name
self.setAcceptHoverEvents(True)
def sizeHint(self, hint, size):
size = super().sizeHint(hint, size)
print(size)
return QSizeF(50,50)
def paint(self, painter, options, widget):
self.initStyleOption(options)
ink = options.palette.highLight() if options.state == QStyle.State_Selected else options.palette.button()
painter.setBrush(ink) # ink
painter.drawRoundedRect(self.rect(), self.radius, self.radius)
def hoverEnterEvent(self, event) :
print("Enter Event")
super().hoverEnterEvent(event)
class MainWindow(QMainWindow):
def __init__(self, *args, **kvps) :
super().__init__(*args, **kvps)
# Status bar
self.stat = QStatusBar(self)
self.setStatusBar(self.stat)
self.stat.showMessage("Started")
# Widget(s)
self.data = QGraphicsScene(self)
self.view = QGraphicsView(self.data, self)
item = self.data.addItem(Square())
self.view.ensureVisible(self.data.sceneRect())
self.setCentralWidget(self.view)
# Visibility
self.showMaximized()
def update(self, widget) :
self.stat.showMessage(str(widget.name))
if __name__ == "__main__" :
# Application
app = QApplication(sys.argv)
# Scene Tests
main = MainWindow()
main.show()
# Loop
sys.exit(app.exec_())
The docs state that QGraphicsItem is not designed to emit signals, instead it is meant to respond to the events sent to it by QGraphicsScene. In contrast it seems that QGraphicsWidget is designed to do so but I'm not entirely sure where the entry point ought to be. Personally I feel QGraphicsScene should really be emitting these signals, from what I understand of the design, but am not sure where the entry point ought to be in this case either.
Currently I see the following possible solutions, with #3 being the preferred method. I was wondering if anyone else had a better strategy :
Create a QGraphicsScene subclass, let's call it Scene, to each QGraphicsItem/QGraphicsWidget and call a custom trigger/signal upon the Scene from each widget. Here I would have to subclass any item I intend on including within the scene.
Set Mainwindow up as the event filter for each item in the scene or upon the scene itself and calling MainWindow.update.
Set Mainwindow.data to be a subclass of QGraphicsScene, let's call it Scene, and let it filter it's own events emitting a hoverEntry signal. hoverEntry is then connected to MainWindow.update as necessary.
As Murphy's Law would have it Ekhumoro already provides an answer.
It seems one should subclass QGraphicsScene and add the necessary signal. this is then triggered from the QGraphicsItem/Widget. This requires that all items within a scene be sub-classed to ensure that they call the corresponding emit function but it seems must do this do this anyhow when working with the graphics scene stuff.
I'll not mark this as answered for a bit in case some one has a better suggestion.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, qApp, QMainWindow, QGraphicsScene, QGraphicsView, QStatusBar, QGraphicsWidget, QStyle, QGraphicsItem
from PyQt5.QtCore import Qt, QSizeF, pyqtSignal
class Square(QGraphicsWidget) :
"""
doc string
"""
def __init__(self,*args, name = None, **kvps) :
super().__init__(*args, **kvps)
self.radius = 5
self.name = name
self.setAcceptHoverEvents(True)
self.setFlag(self.ItemIsSelectable)
self.setFlag(self.ItemIsFocusable)
def sizeHint(self, hint, size):
size = super().sizeHint(hint, size)
print(size)
return QSizeF(50,50)
def paint(self, painter, options, widget):
self.initStyleOption(options)
ink = options.palette.highLight() if options.state == QStyle.State_Selected else options.palette.button()
painter.setBrush(ink) # ink
painter.drawRoundedRect(self.rect(), self.radius, self.radius)
def hoverEnterEvent(self, event) :
super().hoverEnterEvent(event)
self.scene().entered.emit(self)
self.update()
class GraphicsScene(QGraphicsScene) :
entered = pyqtSignal([QGraphicsItem],[QGraphicsWidget])
class MainWindow(QMainWindow):
def __init__(self, *args, **kvps) :
super().__init__(*args, **kvps)
# Status bar
self.stat = QStatusBar(self)
self.setStatusBar(self.stat)
self.stat.showMessage("Started")
# Widget(s)
self.data = GraphicsScene(self)
self.data.entered.connect(self.itemInfo)
self.data.focusItemChanged.connect(self.update)
self.view = QGraphicsView(self.data, self)
item = Square(name = "A")
item.setPos( 50,0)
self.data.addItem(item)
item = Square(name = "B")
item.setPos(-50,0)
self.data.addItem(item)
self.view.ensureVisible(self.data.sceneRect())
self.setCentralWidget(self.view)
# Visibility
self.showMaximized()
def itemInfo(self, item):
print("Here it is -> ", item)
if __name__ == "__main__" :
# Application
app = QApplication(sys.argv)
# Scene Tests
main = MainWindow()
main.show()
# Loop
sys.exit(app.exec_())
The magic lines of interest are then the QGrahicsScene subclass.
class GraphicsScene(QGraphicsScene) :
entered = pyqtSignal([QGraphicsItem],[QGraphicsWidget])
The QGraphicsWidget.hoverEnterEvent triggers the entered signal. (This is where I got stuck)
def hoverEnterEvent(self, event) :
...
self.scene().entered.emit(self)
...
And the switcheroo from self.data = QGraphicsScene(...) to self.data = GraphicsScene in the MainWindow's init function.

CPU overload when updating QListView from QAbstractListModel

I'm building a little pyqt5 app that display a list of 512 values in a QListView. The list is updated through separate thread, with QThread.
It's working nice, except the fact that it is using 65/95 % of the CPU of the (old) Core 2 Duo 2,53 Ghz I'm developing on.
I simplify the code to remove dependancies, because the update is done from a network protocol. Updates are done 40 times per second (each 25 ms).
The simplified script below is refreshing the list 10 times per second and the CPU is still at 65 % when list is updated.
Is there anything to do for avoiding the overload?
Is there some best practices to follow for updating a View?
(the global is not in my last code, it's here to have a simple example)
from random import randrange
from time import sleep
from sys import argv, exit
from PyQt5.QtCore import QThread, QAbstractListModel, Qt, QVariant, pyqtSignal
from PyQt5.QtWidgets import QListView, QApplication, QGroupBox, QVBoxLayout, QPushButton
universe_1 = [0 for i in range(512)]
class SpecialProcess(QThread):
universeChanged = pyqtSignal()
def __init__(self):
super(SpecialProcess, self).__init__()
self.start()
def run(self):
global universe_1
universe_1 = ([randrange(0, 101, 2) for i in range(512)])
self.universeChanged.emit()
sleep(0.1)
self.run()
class Universe(QAbstractListModel):
def __init__(self, parent=None):
super(Universe, self).__init__(parent)
def rowCount(self, index):
return len(universe_1)
def data(self, index, role=Qt.DisplayRole):
index = index.row()
if role == Qt.DisplayRole:
try:
return universe_1[index]
except IndexError:
return QVariant()
return QVariant()
class Viewer(QGroupBox):
def __init__(self):
super(Viewer, self).__init__()
list_view = QListView()
self.list_view = list_view
# create a vertical layout
vbox = QVBoxLayout()
universe = Universe()
vbox.addWidget(list_view)
# Model and View setup
self.model = Universe(self)
self.list_view.setModel(self.model)
# meke a process running in parallel
my_process = SpecialProcess()
my_process.universeChanged.connect(self.model.layoutChanged.emit)
# set the layout on the groupbox
vbox.addStretch(1)
self.setLayout(vbox)
if __name__ == "__main__":
app = QApplication(argv)
group_widget = Viewer()
group_widget.show()
exit(app.exec_())
It seems to be a normal behavior…

Categories