I'm trying to create a simple class recitation app where it randomly picks from the list of students. It works, but I want it to change in real time, so there's a bit of suspense who is being picked.
You can see in the illustration video below that the console updates the names in real time but the QtWidget does not: https://www.dropbox.com/s/rwcbhhj58tevshl/py003_pyqt_real_time_update_widget.mp4?dl=0
Here is the working code so far:
import sys import os from PySide2.QtCore import Qt from PySide2.QtWidgets import QApplication, QMainWindow, QSpinBox, QWidget, QPushButton, QTextEdit, QVBoxLayout, QHBoxLayout, QLineEdit, QLabel from PySide2.QtGui import QPixmap from time import sleep, perf_counter import time import random class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Class Recitation") # Widget root_img_path = 'img' self.image_list =  for root, dirs, files in os.walk(root_img_path): for f in files: img_path = os.path.join(root_img_path, f) first_name = f.split('_') last_name = f.split('_') self.image_list.append( (img_path, first_name + " " + last_name ) ) # creating label self.label = QLabel(self) # loading image #self.pixmap = QPixmap('img/Eijirou_Kirishima_Portrait.png') self.pixmap = QPixmap(self.image_list) # adding image to label self.label.setPixmap(self.pixmap) self.name_label = QLabel() self.name_label.setText(self.image_list) self.pick_btn = QPushButton("Pick") self.pick_btn.setObjectName("Pick") self.pick_btn.clicked.connect(self.random_pick) # Layout Creations hbox = QHBoxLayout() # hbox.addWidget(self.search_button) # hbox.addWidget(self.search_bar) vbox = QVBoxLayout() vbox.addWidget(self.label) vbox.addWidget(self.name_label) vbox.addWidget(self.pick_btn) # vbox.addWidget(self.result_text_edit) layout = QVBoxLayout() layout.addLayout(hbox) layout.addLayout(vbox) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) def random_pick(self): choice_list = self.image_list time_started = time.perf_counter() counter = 0 for x in range(0,10000): random_pick = random.choice(choice_list) self.name_label.setText(random_pick) self.label.setPixmap(random_pick) print (random_pick) sleep(counter) current_time = time.perf_counter() time_elapse = current_time - time_started counter += 0.0001 * (2**4) if time_elapse >= 5: break return random_pick def change_name(self): self.name_label.setText("Name Changed") app = QApplication(sys.argv) window = MainWindow() window.show() app.exec_()
Is there a way around this?
Hi Bentraje welcome to the forum!
The problem is the
First some background -- In GUI applications the update of the UI is handled by an event loop, and a queue of things to do. The event loop takes items form the queue, does them, then takes the next, and so on. Things are done in order, and only one thing is done at a time.
When you update the text of a widget, with
self.name_label.setText(random_pick) that update happens immediately (the value in the widget changes) but the widget is not redrawn. Instead a "redraw" request is put onto the event queue, and that will happen once the event loop gets to it.
Why doesn't the event loop get to it in your case? Because your method is blocking the loop. In your code you've linked pressing the button to this method as follows
When the button is pressed it generates a signal, which you've correctly connected to your method. But when the event loop gets to that signal and calls your method, it has to wait for the method to finish before doing anything else. And "anything else" in this case includes redrawing the widgets you've updated.
The end result is you update the widget multiple times, but it never redraws, so you can't see it being updated.
How to solve it?
There are a few ways. Firstly, you can call
QApplication.processEvents() in your loop, to make Qt process the outstanding events (including the outstanding redraws). I wouldn't recommend this, as it's better to structure your application to use the event loop, rather than try and work around it -- but it's there as a quick and dirty solution.
Another option is to run this selection process in a thread. This would stop it blocking your main GUI thread, and allow you to keep the code more or less the same (aside from creating the workers). See a tutorial on multithreading here.
A third option is to use a
QTimer to trigger the random selection and another to stop it. Remove the loop code from
random_pick and have it make just one choice. Then add a new method which starts the two timers. I think this is the approach I would prefer in this case.
Something like the following (untested)
class MainWindow(QMainWindow): def __init__(self): super().__init__() # snip self.pick_btn = QPushButton("Pick") self.pick_btn.setObjectName("Pick") self.pick_btn.clicked.connect(self.start_selection) def start_selection(self): # Repeating timer, calls random_pick over and over. self.picktimer = QTimer() self.picktimer.setInterval(500) self.picktimer.timeout.connect(self.random_pick) self.picktimer.start() # Single oneshot which stops the selection after 5 seconds QTimer.singleShot(5000, self.stop_selection) def stop_selection(self): # Stop the random selection self.picktimer.stop() # The current pick is in self.current_selection print("Current selection is: ", self.current_selection) def random_pick(self): random_pick = random.choice(self.image_list) self.name_label.setText(random_pick) self.label.setPixmap(random_pick) print (random_pick) # Store the current selection, so we have it at the end in stop_selection. self.current_selection = random_pick
Thanks for the response. It works as expected. Just to confirm, if I have to loop (other than based on time) in Qt, I guess I'll have to use the threading/process feature right?
Great, glad it did the trick!
Just to confirm, if I have to loop (other than based on time) in Qt, I guess I’ll have to use the threading/process feature right?
It depends on how long the loop lasts. The key thing to remember is that whenever your code is running the UI is blocked. But you can get away with doing a lot before it becomes noticeable.
If you have any long-running tasks that can't be broken down into smaller units, then yes you would ideally put these in a thread. The timer approach works well for anything that can be broken down into smaller steps -- like your random selection example, it's a series of very quick steps, that you want to happen regularly, but the subsequent steps don't rely on one another so can be easily split.
Ah gotcha. Thanks for the clarification!