I'm trying to create simple progress status label for my Pyqt5 Python code and update it after every iteration of a loop in which a function does a bunch of stuff. The label I want to be updated is "status_label_counter". The below code only shows the part for creating labels and the exact place where I want to use the functionality I've mentioned.
#initialisation code, etc...
self.status_label_counter = QLabel()
self.status_label_from = QLabel(' from: ')
self.status_label_total = QLabel()
status_hbox = QHBoxLayout()
status_hbox.addStretch()
status_hbox.addWidget(self.status_label_counter)
status_hbox.addWidget(self.status_label_from)
status_hbox.addWidget(self.status_label_total)
status_hbox.addStretch()
#bunch of other code...
def create_ics(self):
counter = 0
self.status_label_total.setText(str(len(self.groups)))
for group in self.groups:
#does a bunch of stuff inside
group_manager.create_calendar_for(self.rows, group, self.term)
counter += 1
#for console output
print('iteration: ', counter)
#trying to update status counter
self.status_label_counter.setText(str(counter))
The problem is that I only see the update of both labels when the loop is done with the nested function. When I click a button that calls for "create_ics" function window becomes inactive for about 5 seconds, I see logs on console with count of iterations, but nothing happens in view.
The view (Qt) is locked in your main thread and never gets its chance to process its event loop and thus redraw itself. If you really want to do it this way, call:
self.status_label_counter.repaint()
After you set the text (and if you have some complex layout measuring call QApplication.processEvents() instead).
However, much better option would be to run your create_ics() function in a separate thread leaving your main thread to deal with the view and Qt's event processing. You can do it either through standard Python's threading module, or using Qt's own QThread: https://nikolak.com/pyqt-threading-tutorial/ .
Related
I would like to make it so that I can see the progress of the execution of the loop, but the ProgressBar only exits after the execution of this loop. How can I fix this?
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.progressbar import ProgressBar
class MyApp(App):
def build(self):
self.wid = FloatLayout()
self.prog_bar = ProgressBar(max=99999, pos = [0, -150])
self.wid.add_widget(self.prog_bar)
self.prog_bar.value = 0
for i in range(0, 99999):
self.prog_bar.value = i
print(i/99999)
return self.wid
if __name__ == '__main__':
MyApp().run()
Kivy uses the main thread of your App to update its widgets. You are running your loop on the main thread, so Kivy cannot update the ProgressBar until that loop completes. The fix is to do the loop in another thread:
class MyApp(App):
def build(self):
self.wid = FloatLayout()
self.prog_bar = ProgressBar(max=99999, pos=[0, -150])
self.wid.add_widget(self.prog_bar)
self.prog_bar.value = 0
threading.Thread(target=self.do_progress).start()
return self.wid
def do_progress(self):
for i in range(0, 99999):
self.prog_bar.value = i
print(i / 99999)
This is not about composition, but rather about multitasking, the thing is, if you do a locking loop like this, kivy can’t update anything until the loop is done (because nothing else happens while the function runs, no update of the UI or processing of any event). So what you want to do is allow the loop to run "concurrently" to the kivy event loop.
There are multiple ways to do this kind of things, they are all more or less suited to different situations.
kivy Clock allows scheduling a function to run later (schedule_once), possibly multiple times (schedule_interval), so you could have a more atomic function, that just increments the value of the progress bar once (not in a loop), and call this function using schedule interval. The issue is if you want to do actual work in the function, that could take significant enough time for the UI to be visibly blocked between increment of the progressbar value.
If that’s your case, because the function you are trying to run is slow, then you might want to run the function in a Thread instead, so it doesn’t block the kivy UI. You’ll be able to update the value of your progress bar from the the thread, if necessary, using the Clock to schedule a function doing just that (you need to be careful about not doing things that update the OpenGL context from a thread, as OpenGL doesn’t specify the behavior in such situation, so some graphic cards/drivers can be fine with it, but others will crash your program, so anything updating the UI from a thread must use the clock, there is a mainthread decorator provided in the clock module for that).
In some situation, you can (and thus want) avoid doing the long work beforehand, and find a way to do the minimal amount of work each time needed, and go on with the work, there are some abstractions of that idea, like RecycleView, that avoids creating widgets altogether, when you want a very long list of items to display, just creating the data beforehand, and letting it handle the creation/update of the widgets as needed. Of course, this might not apply to your situation, but it’s worth mentioning in case it is.
So in short: don’t do such loops, or do them in threads, think really hard if you actually need such a loop, and use clock events to update your UI when using threads.
I borrowed a design that I found on stackoverflow to redirect console output to a PyQt5 GUI textEdit widget. This works fine, but the text is not displayed in "real-time". It seems to output the text to the GUI once a process has completed. This has not been a problem until I tried to use time.sleep(secs) to print something, pause, then print something else. What ends up happening is that the program pauses for secs, then it prints all of the statements at once.
This class is in the mainWindow file for the GUI:
class EmittingStream(QtCore.QObject):
textWritten = QtCore.pyqtSignal(str)
def write(self, text):
self.textWritten.emit(str(text))
This is in the __init__ method of my event handling file:
sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)
self.case_setup_console.setReadOnly(True)
self.main_console.setReadOnly(True)
This function is in the main class of event handling file (outside __init__):
def normalOutputWritten(self, text):
"""Append text to the QTextEdit."""
# Maybe QTextEdit.append() works as well, but this is how I do it:
cursor = self.case_setup_console.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(text)
self.case_setup_console.setTextCursor(cursor)
self.case_setup_console.ensureCursorVisible()
This works as intended to re-route the output to the text edit widget self.case_setup_console. But, when I try to run a code such as:
print('This is the first message')
time.sleep(5)
print('This should print 5 seconds later')
What happens is that the program waits 5 seconds, then it prints both statements together.
When programing for GUI code, there is a fundamental shift in how the program is designed. To make it short: after building and initialisation, the program is all the time running in an "event loop" provided by the GUI framework, and your code is only called when specific events take place.
That is in contrast with a terminal application where your code is running all the time, and you tell when to do "print"s, "input"s and pauses with "time.sleep".
The GUI code is responsible for taking notes of events (keyboard, UI, network, etc...), redrawing window contents and calling your code in response to events, or just when it is time to redraw a content that is defined in your code (like updating a background image).
So, it can only render the text that is supposed to show up in a window, with your redirected "print", when the control is passed back to its event loop. When you do time.sleep you pause the return - no code in the event loop is run, and it can't, of course, do any screen drawing.
What is needed is that you write your pauses in the program in a way that during the pause, the GUI event loop is running - not "time.sleep", that just suspends your whole thread.
In Qt the way to do that is create a QTimer object to call the code you want to use to print text at a particular moment, and then just surrender the execution to the the QtMainloop by returning from your function.
Thanks to Python's support for nested functions, that can be done in painless ways, even using lambda functions when setting the timer itself.
...
print('This is the first message')
timer = QtCore.QTimer
timer.singleShot(5000, lambda *_: print('This should print 5 seconds later'))
Should work for the given example. (The call, as usual for UIs, takes the pause time in miliseconds rather than in seconds).
If you will need to schedule more text to be printed after each phrase is output, you will need to call the scheduling inside the callback itself, and will need a little more sophistication, but it still could be as simple as:
phrases = iter(("hello", "world", "I", "am", "talking", "slowly!"))
timer = QtCore.QTimer()
def talker(*_):
phrase = next(phrases, None)
if not phrase:
return
print(phrase)
timer.singleShot(1000, talker)
timer.singleShot(1000, talker)
(Note that there is nothing special about the *_ parameter name either: I am just indicating that there might be any number of positional arguments for the callback - (The "*" part, that is Python syntax) - and that I won't care about then (I call the argument sequence as "_" to indicate I don't care how this is called, as it won't be used anyway - that is a coding convention) )
The iter and next calls are more "Python dialect" than one might be used, but one could just use a list and a counter up to the list length all the same.
Let's say we have three tkinter widgets: a label, a treeview and an Optionmenu (will be called 'menu' for short below). I successfully make the menu execute a function once an option is chosen. The brief function looks like this:
def data_process():
# do something takes time
def print_data()
data_process() # do something takes time too
# print stuff to treeview
def refresh_table(self, args):
label['text'] = 'Executing' # change label text to Executing
print_data() # a func. which takes time to run
label['text'] = 'Done' # change text to Done
label = tk.Label(parent, text = 'ready')
label.pack()
menu = tk.OptionMenu(parent, var, *list, command = lambda _:refresh_table(self, args))
menu.pack()
table = tk.Treeview(parent)
table.pack()
Function print_data is going to print something to a treeview widget (table). The label widget is like a status bar, telling users what's going on now. So the workflow that I attempt to do is:
Select an option in menu and call refresh_table.
Change label text to 'Executing'.
Execute print_data and print stuff in treeview.
When print_data is done, change the label to 'Done'.
Here is the thing. When I select the option, the program stocks (as expected) to do stuff. However, the label isn't changed to 'Executing' at the beginning. Instead, it's changed to 'Done' when the print_data is done executing (almost simultaneously). I suspect that the command refresh_table effects the target widgets once all demands are done. Because I does see the label flashes out 'Executing' but immediately shows 'Done'. Are there any thoughts about this situation? Any suggestion is appreciated. Thank you!
I suspect that the command refresh_table effects the target widgets once all demands are done.
That's true. Your GUI will update every time the mainloop is entered. While your function refresh_table is running, no events (like updating your label) will be processed, since those only occur during idle times (hence their other name, "idle tasks"). But because the UI isn't idle during the execution of refresh_table, the event loop is not accessible until all code in the function is done (because your whole program runs on the same thread) and the redrawing event is pending. You can fix this by calling update_idletasks, which will process all pending events immediately. Would be done like this (assuming that your main window is called parent and you are inside a class definition, the important thing is that you call update_idletasks on your main window directly after changin the label's text):
def refresh_table(self, args):
label['text'] = 'Executing'
self.parent.update_idletasks()
print_data()
label['text'] = 'Done'
There's also update, which will not only call pending idle tasks, but rather process basically everything that there is to process, which is unnecessary in your case. A very nice explanation why update_idletasks is mostly preferable to update can be found in the answer to this question.
Using Python 3.5. I have a tkinter form. The user clicks a button to import many files into a Listbox. Another button loops thru the files and reads and extracts data from them.
I have a Label on the form that indicates the status of the loop. The status, for the most part, works as expected except that extra characters are added on the end. I'm not sure where the characters come from. I also print() the same content as the Label back to the screen and the print() displays exactly what it should.
My question is why is my Label not displaying the correct string?
My code, greatly shortened:
class tk_new_db:
def __init__(self, master):
self.master = master # sets 'root' to the instance variable 'master'
self.var_text_2 = StringVar()
self.var_text_2.set('STATUS: Active')
self.label_6 = Label(master, textvariable=self.var_text_2,
font=self.font_10)
self.label_6.grid(row=15, sticky=W, padx=15)
def execute_main(self): # extract data from files
file_num = 0
nf = len(self.listbox_1.get(0, END))
for fr in li:
file_num += 1
print('STATUS: Extracting Loads from File '
'{} in {}'.format(file_num, nf))
self.var_text_2.set('STATUS: Extracting Loads from File '
'{} in {}'.format(file_num, nf))
self.master.update_idletasks()
The print() is writing the following:
STATUS: Extracting Loads from File 1 in 5
The Label is writing the following:
STATUS: Extracting Loads from File 1 in 5 nce...
It always adds ' nce...' on the Form.
EDIT:
I do use self.var_text_2 earlier in the program. The ' nce...' looks like it is a fragment of the previous string. I've since tried resetting the variable in two different ways, but I'm still getting the same result.
self.var_text_2.set('STATUS: Checking .F06 Files for Convergence...')
self.master.update_idletasks()
self.var_text_2.__del__()
self.var_text_2.set('STATUS: Checking .F06 Files for Convergence...')
self.master.update_idletasks()
self.var_text_2.set('')
How do you properly delete the StringVar() for reuse?
The only explanation I can think of is that you're stacking labels on top of each other in the same row and column, so when you add a long string that ends in "nce..." and later update the screen by adding a shorter label in the same grid cell, the trailing text of the longer label underneath is showing through.
The reasons I draw this conclusion are:
this is a common mistake I've seen several times
there are no bugs in tkinter that would cause this
there is nothing in the code that you've shown that could possibly add "nce..." to the end of the string
the fact that you use sticky="w" rather than sticky="ew", which will cause a shorter label to not completely overlay a longer label if placed in the same row and column
I think I can explain. Root.mainloop repeatedly calls root.update. Root.update executes tasks on the 'main' event queue. In brief, it only executes idletasks when the event queue is empty after checking for file events (on non-Windows), window-gui events, and timer events. Idletasks, which are not well documented but seem to primarily be screen update tasks, are never added to the main event queue. It is therefore possible that idletasks will remain undone indefinitely. Root.update_idletasks exists to force execution of idletasks anyway.
When mainloop is running, calling update within a task run by update is usually unnecessary and possibly worse. It is something only for adventurous experts. Hence the warnings you have read (which assume that you are running mainloop).
When mainloop is not running and update is not being called repeatedly, either because you never called mainloop or because you block it with a long running loop, then you likely must call update yourself. What you seem to have discovered is that part of completely handling a StringVar update is a main event and not just an idletask.
I sympathize with your desire to do repeated tasks with a for loop, but doing so currently means taking responsibility for event handling yourself. I hope in the future that it will be possible to have 'async for' (new in 3.5) interoperate with Tk.mainloop, but it is not as easy as I hoped.
In the meanwhile, you could put the body of the for loop into a callback and loop with root.after.
I am trying to embed a matplotlib graph that updates every second into a PyQt GUI main window.
In my program I call an update function every second using threading.Timer via the timer function shown below. I have a problem: my program grows bigger every second - at a rate of about 1k every 4 seconds. My initial thoughts are that the append function (that returns a new array in update_figure) does not delete the old array? Is it possible this is the cause of my problem?
def update_figure(self):
self.yAxis = np.append(self.yAxis, (getCO22()))
self.xAxis = np.append(self.xAxis, self.i)
# print(self.xAxis)
if len(self.yAxis) > 10:
self.yAxis = np.delete(self.yAxis, 0)
if len(self.xAxis) > 10:
self.xAxis = np.delete(self.xAxis, 0)
self.axes.plot(self.xAxis, self.yAxis, scaley=False)
self.axes.grid(True)
self.i = self.i + 1
self.draw()
This is my timer function - this is triggered by the click of a button in my PyQt GUI and then calls itself as you can see:
def timer(self):
getCH4()
getCO2()
getConnectedDevices()
self.dc.update_figure()
t = threading.Timer(1.0, self.timer)
t.start()
EDIT: I cant post my entire code because it requires a lot of .dll includes. So i'll try to explain what this program does.
In my GUI I want to show the my CO2 value over time. My get_co22 function just returns a float value and I'm 100% sure this works fine. With my timer, shown above, I want to keep append a value to a matplotlib graph - the Axes object is available to me as self.axes. I try to plot the last 10 values of the data.
EDIT 2: After some discussion in chat, I tried putting the call to update_figure() in a while loop and using just one thread to call it and was able to make this minimal example http://pastebin.com/RXya6Zah. This changed the structure of the code to call update_figure() to the following:
def task(self):
while True:
ui.dc.update_figure()
time.sleep(1.0)
def timer(self):
t = Timer(1.0, self.task())
t.start()
but now the program crashes after 5 iterations or so.
The problem is definitely not with how you are appending to your numpy array, or truncating it.
The problem here is with your threading model. Integrating calculation loops with a GUI control loop is difficult.
Fundamentally, you need your GUI threading to have control of when your update code is called (spawning a new thread to handle it if necessary) - so that
your code does not block the GUI updating,
the GUI updating does not block your code executing and
you don't spawn loads of threads holding multiple copies of objects (which might be where your memory leak comes from).
In this case, as your main window is controlled by PyQt4, you want to use a QTimer (see a simple example here)
So - alter your timer code to
def task(self):
getCH4()
getCO2()
getConnectedDevices()
self.dc.update_figure()
def timer(self):
self.t = QtCore.QTimer()
self.t.timeout.connect(self.task)
self.t.start(1000)
and this should work. Keeping the reference to the QTimer is essential - hence self.t = QtCore.QTimer() rather than t = QtCore.QTimer(), otherwise the QTimer object will be garbage collected.
Note:
This is a summary of a long thread in chat clarifying the issue and working through several possible solutions. In particular - the OP managed to mock up a simpler runnable example here: http://pastebin.com/RXya6Zah
and the fixed version of the full runnable example is here: http://pastebin.com/gv7Cmapr
The relevant code and explanation is above, but the links might help anyone who wants to replicate / solve the issue. Note that they require PyQt4 to be installed
if you are creating a new figure for every time this is quite common.
matplotlib do not free the figures you create, unless you ask it, somethink like:
pylab.close()
see How can I release memory after creating matplotlib figures