tkinter redraw rates


F

fronagzen

Hm. So I've written a GUI in tkinter. I've found two performance issues, I was hoping someone could point me in the right direction.

Firstly, I'm using an image as a border, namely:

from tkinter import *
from tkinter import ttk

root_frame = Tk()
root_frame.configure(background = 'black')

img1 = PhotoImage("frameBorder", data="""
R0lGODlhQABAAMIHAAAAABkfLTMrMzMrZjNVZjNVmUFch////ywAAAAAQABAAAAD9A
i63P4wykmrvTjrzbu/hlGMZGmeaBp2QmgIQSzPdG3fbShk+u3/wFkONAgaj7aBoWIo
Ip9P5aQFrSJfkpd1C2xluWDfEhIKm2mrh/bM9qrZ8MDYYYiz54263Yxn6PdgfQt/gF
uCCoSFVYcAiYpPjI6PR5GTVpWWUJiZV2SckJ6flKGiQZulP6eoN6qrNa2uM7CxMbO0
trG4rrqrvKi+pcCiwp/EnMaZyJbKk8yPzorQhdKA1HuRMLQ0bnSSuYyN2mhZ2eLcD1
TicjtZ3sPgfu7J8A0EBOWfQxg5a4/87BtcCBxIsKDBgh8SKlzIsKHDhxAVJgAAOw==""")

style = ttk.Style()
style.element_create("RoundedFrame", "image", "frameBorder",
border=30, sticky="nsew")
style.layout("RoundedFrame", [("RoundedFrame", {"sticky": "nsew"})])

input_frame = ttk.Frame(root_frame,
style = "RoundedFrame",
padding = 15,
width = 640,
height = 180
)
input_frame.pack(padx=10, pady=10)

This works, yes, but is annoyingly laggy on an older computer when I try tomove the window around. I figure it's because the program has to keep redrawing the image border when dragged around, and is exacerbated by the fact that I have two of the imageborder frames in my application. How can I remedy this? I've tried using a hard-drawn image on a Canvas instead of the image border, but it's suboptimal because that prevents resizing the window.


The other performance issue I've found is that when the logic is running, the app doesn't redraw. Ordinarily this would be acceptable, but as part of my program, it loads data from a website, and during the load, the window completely freezes up and doesn't respond until the download is done; as I understand it, tkinter doesn't redraw until it is forced to by .update() or control is given back to the mainloop. How can I force a more frequent redraw rate?
 
Ad

Advertisements

D

Dave Angel

Hm. So I've written a GUI in tkinter. I've found two performance issues, I was hoping someone could point me in the right direction.

Firstly, I'm using an image as a border, namely:

This works, yes, but is annoyingly laggy on an older computer when I try to move the window around. I figure it's because the program has to keep redrawing the image border when dragged around, and is exacerbated by the fact that I have two of the imageborder frames in my application. How can I remedy this? I've tried using a hard-drawn image on a Canvas instead of the image border, but it's suboptimal because that prevents resizing the window.

This part I can't help with, as I'm not that familiar with tkinter in
particular. If I had to guess an approach, I'd tell you to create an
object that represents the scaled border image, and then when it moves,
you just reblit it. And if/when the scale changes, you then recreate
the object at the new scale. Most of the time you won't be scaling.

But what that means in tkinter calls, I don't know and don't have time
tonight to figure out.
The other performance issue I've found is that when the logic is running, the app doesn't redraw. Ordinarily this would be acceptable, but as part of my program, it loads data from a website, and during the load, the window completely freezes up and doesn't respond until the download is done; as I understand it, tkinter doesn't redraw until it is forced to by .update() or control is given back to the mainloop. How can I force a more frequent redraw rate?

Tkinter, like every other GUI I know of, is event driven. You're not
intended to do "logic is running" kinds of events. Break up larger
problems into little tasks, and daisy chain them, returning to the event
loop after each little piece.

A second approach that works with some GUI's is to fire up the event
loop at periodic intervals in your long function; get it to do a few
other events and then return to you. This isn't recommended usually
because it can get very messy. And it may not even be possible in tkinter.

Third approach is to start another thread to do the "logic is running."
Make sure that thread never calls any GUI stuff, but just updates
values that can be seen by the main thread and its GUI stuff. When
you're done, post a message to the GUI to tell it to redraw whichever
parts are now changed.
 
F

fronagzen

This part I can't help with, as I'm not that familiar with tkinter in

particular. If I had to guess an approach, I'd tell you to create an

object that represents the scaled border image, and then when it moves,

you just reblit it. And if/when the scale changes, you then recreate

the object at the new scale. Most of the time you won't be scaling.



But what that means in tkinter calls, I don't know and don't have time

tonight to figure out.








Tkinter, like every other GUI I know of, is event driven. You're not

intended to do "logic is running" kinds of events. Break up larger

problems into little tasks, and daisy chain them, returning to the event

loop after each little piece.



A second approach that works with some GUI's is to fire up the event

loop at periodic intervals in your long function; get it to do a few

other events and then return to you. This isn't recommended usually

because it can get very messy. And it may not even be possible in tkinter.



Third approach is to start another thread to do the "logic is running."

Make sure that thread never calls any GUI stuff, but just updates

values that can be seen by the main thread and its GUI stuff. When

you're done, post a message to the GUI to tell it to redraw whichever

parts are now changed.

Thanks for the responses.

Yeah, I understand that tkinter isn't really designed for 'logic is running' style issues, but I do need to load that data, and sometimes, that can take a while. I am in fact experimenting with threading another process to update the window, yes.
 
D

Dave Angel

On 07/16/2013 09:51 PM, (e-mail address removed) wrote:

If you are going to use googlegroups, then at least bypass its worst
bugs, like double-spacing everything it quotes.
http://wiki.python.org/moin/GoogleGroupsPython
Yeah, I understand that tkinter isn't really designed for 'logic is running' style issues, but I do need to load that data, and sometimes, that can take a while. I am in fact experimenting
with threading another process to update the window, yes.
--------- ------- -----------------
Those three words/phrases strike a real discord.

Multithreading is not the same as multiprocessing. And you probably
don't ever want to update a window with a different thread than the one
that created it. Not that it can't be done, at least in some languages,
but that it's real rat-hole of problems.

The extra thread or process (with different problems to each) should be
doing the "logic is running" but NOT the gui.
 
F

fronagzen

Noted on the quoting thing.

Regarding the threading, well, first, I'm not so much a programmer as someone who knows a bit of how to program.

And it seems that the only way to update a tkinter window is to use the .update() method, which is what I was experimenting with. Start up a new thread that just loops the .update() with a 1ms sleep until the download is done. It seems to work, actually.
 
D

Dave Angel

Noted on the quoting thing.

Regarding the threading, well, first, I'm not so much a programmer as someone who knows a bit of how to program.

And it seems that the only way to update a tkinter window is to use the .update() method, which is what I was experimenting with. Start up a new thread that just loops the .update() with a 1ms sleep until the download is done. It seems to work, actually.

update() is to be used when it's too awkward to return to mainloop. In
my second approach, you would periodically call it inside the processing
loop. But unless tkinter is unique among GUI's, it's unsafe to do that
in any thread besides the GUI thread.
 
Ad

Advertisements

F

fronagzen

update() is to be used when it's too awkward to return to mainloop. In

my second approach, you would periodically call it inside the processing

loop. But unless tkinter is unique among GUI's, it's unsafe to do that

in any thread besides the GUI thread.

Ok. Well, what I'm currently doing, based on advice from this thread, is tocreate a new thread that handles the downloading, as well as updating a variable for text display on the GUI, and in the main thread, just after the thread is created, a while loop that updates the GUI while the thread is running.
 
F

fronagzen

update() is to be used when it's too awkward to return to mainloop. In
my second approach, you would periodically call it inside the processing
loop. But unless tkinter is unique among GUI's, it's unsafe to do that
in any thread besides the GUI thread.

Yes, based on advice from this thread, I'm doing that. From my main thread,I create a thread that handles the download while updating a variable thatthe mainloop displays as a text output, and in that mainloop, I have a while loop that updates the GUI until the downloading is done.
 
D

Dave Angel

Yes, based on advice from this thread, I'm doing that. From my main thread, I create a thread that handles the download while updating a variable that the mainloop displays as a text output, and in that mainloop, I have a while loop that updates the GUI until the downloading is done.

I can't figure out what you're really doing, since each message from you
says something different. You don't need a separate while loop, since
that's exactly what app.mainloop() is.
 
F

fronagzen

I can't figure out what you're really doing, since each message from you
says something different. You don't need a separate while loop, since
that's exactly what app.mainloop() is.

Hm. My apologies for not being very clear. What I'm doing is this:

self.loader_thread = Thread(target=self.loadpages,
name="loader_thread")
self.loader_thread.start()
while self.loader_thread.isAlive():
self.root_window.update()
sleep(0.05)

Where loadpages is a function defined elsewhere.
 
P

Peter Otten

Hm. My apologies for not being very clear. What I'm doing is this:

self.loader_thread = Thread(target=self.loadpages,
name="loader_thread")
self.loader_thread.start()
while self.loader_thread.isAlive():
self.root_window.update()
sleep(0.05)

Where loadpages is a function defined elsewhere.

I suppose the while loop runs in the main thread? That's not a good idea
with Tkinter (and GUIs in general). I suggest using

http://effbot.org/zone/tkinter-threads.htm

as a template. The main difference is that instead of your while-loop and
sleep() it calls the after() method which keeps tkinter in control so that
it can still respond to events.
 
Ad

Advertisements

F

fronagzen

Presumably this fragment is from a method of some class you've written.
Is it an event handler, or is this happening before you finish setting
up the GUI? Somewhere at top-level, you're supposed to fall into a call
to mainloop(), which doesn't return till the user cancels the app.
This is, indeed, an event handler from a class for my GUI. My entire GUI isa bit large, so I'll not copy the entire thing here, but it roughly goes:

class GUI(object):
def __init__(self):
[stuff]

def init_button(self):
self.execute = ttk.Button(self.input_frame, text='Tally',
command=self.execute_now)
self.execute.grid(column=1, row=2, sticky=(N, S, E, W), columnspan=4)

def execute_now(self):
[stuff]
self.loader_thread = Thread(target=self.loadpages,
name="loader_thread")
self.loader_thread.start()
while self.loader_thread.isAlive():
self.root_window.update()
sleep(0.05)
[morestuff]

if __name__ == "__main__":
APP = GUI()
APP.root_window.mainloop()
 
F

fronagzen

This is, indeed, an event handler from a class for my GUI. My entire GUI is a bit large, so I'll not copy the entire thing here, but it roughly goes:
class GUI(object):
def __init__(self):
[stuff]
def init_button(self):
self.execute = ttk.Button(self.input_frame, text='Tally',
command=self.execute_now)
self.execute.grid(column=1, row=2, sticky=(N, S, E, W), columnspan=4)
def execute_now(self):
[stuff]
self.loader_thread = Thread(target=self.loadpages,
name="loader_thread")
self.loader_thread.start()
self.root_window.after(100, self.test_thread)
return
while self.loader_thread.isAlive():
self.root_window.update()
Nope - don't use that. Instead, post an event on the queue, and return
to the mainloop() from whence we came.
def test_thread(self):
if self.loader_thread.isAlive():
self.root_window.after(100, self.test_thread)
return
[morestuff]
sleep(0.05)
[morestuff]
if __name__ == "__main__":
APP = GUI()
APP.root_window.mainloop()
I probably don't have it quite right, but hopefully you'll get the idea.
self.test_thread() is now a new event that will get repeatedly
invoked, to do the check on the thread status. It returns rapidly
unless the condition has occurred.
There are other things that should be done, like blocking the specific
events that would create duplicate threads.

I see, though it should be noted that your method doesn't actually block the rest of the even handler code from running, had to fiddle with it a bit to get that to work. May I ask what exactly is the rationale behind implementing it like this, though?
 
Ad

Advertisements

F

fronagzen

Am 18.07.13 06:38, schrieb (e-mail address removed):
Exactly this is the goal of it. Event handlers are supposed to run in as
short time as possible, and should never block. The reason is that you
want the events to be processed in the order they come in, such that he
user can still move the window, resize it, iconify/maximize etc.
That said, the code still looks odd to me. I have used the Tk with
multithreading in the past, but directly from Tcl, and not from Python.
The basic idea is to have the background thread (which does the work)
signal the main thread about its status, i.e. in the worker thread:
for i in range(50):
some_odd_computation()
signal('progress', i)
signal('finished')
and in the main thread you bind() to the events fired from the worker
thread. That way you don't run any periodic polling.
I fear that Tkinter has a shortcoming which does not allow this pattern
to be implemented. The tricky thing is to implement this signal()
function, which must post an event to another thread. From the C level,
there is Tcl_ThreadQueueEvent() which does this. It arranges for a C
function to be run from the event loop of another thread. From Tcl,
thread::send does this. To use it from Tkinter, it would be necessary to
create a Tcl interpreter in the worker thread *without* loading Tk.
Some day I should dive into the innards of Tkinter to see if this is
possible. Then you could implement signal() simply by
def signal(sig, data=''):
tclinterp.eval('thread::send -async $mainthread {event generate .
<<%s>> -data {%s}'%sig%data)
and in the main thread bind() to the virtual events.
Christian
Ah, I see. Thank you for your help!
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top