2

Summary: In Python when I update a Tkinter label text from a spawned process, the label on the GUI is not updated, although the spawned process is executed. How can I make it update from the spawned process?

Im am working with Python 2.7.2 in Lubuntu 20.04

EDIT: I also tried with Python 3.8, had to install python3-tk extra, change a little syntax (parentheses after print command and replacing Tkinter with tkinter), but problem still there looking identical. END EDIT

Here is my sample code working standalone to try out:

from Tkinter import *
from multiprocessing import Process

# simple Label change invoked from "simple" button
def bnAction_sync():
   print "bnAction_sync"
   changeLabel()
   print "bnAction_sync done"

# asynchronous label change invoked from Async button
def bnAction_async():
   print "bnAction_async"
   p = Process(target=changeLabel)
   p.start()
   print "bnAction_Async done"

def changeLabel():
   print "change label"
   lbl['text'] = "Text changed"
   ### Apr 19 2021: 
   ### uncommenting the following line really updates label but makes program crash ###
   # root.update_idletasks
   print "change label done"

root = Tk()

btnSync = Button(root, text="simple", command=bnAction_sync)
btnSync.pack()
btnAsync = Button(root, text="async", command=bnAction_async)
btnAsync.pack()
lbl = Label(root, text="Initial text")
lbl.pack()

root.mainloop()

If I press the "simple" button, text is updated in label. All good.

But: If I press "async" button,

  • as you can verify by the prints I have provided, the asynchronous process starts,
  • the label text updation line is executed.
  • But: Here is my problem: The label on the GUI is not displaying the updated text.

The reason I want to do it this way: Because I am starting a long running spawned process, after which I want to update the label. All other processes however should run in parallel. So I created a function f, containing the long running function and the label update function sequentially. I want to call f asynchronously. So in principle:

def longprocess():
   code...
def updatelabel_after_longprocess():
   code...
def f():
   longprocess()
   updatelabel_after_longprocess()

p = Process(target=f)
p.start()
do other things immediately

Somewhere I read, that refresh is suspended, while a script is still running. I tried some p.join inserts with no luck.

Please help, thanks!

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Is there a good reason you are still stuck on Python 2? You are more likely to get help if you upgrade to the currently recommended and supported version of the language, which is Python 3. – tripleee Apr 18 '21 at 08:38
  • I started out with python 2, because, that's what Ubuntu 18.2 gave me. If I now change to python 3 I have to change some syntax. No need currently, unless of course my problem is related to the version. :-) – user3599802 Apr 18 '21 at 08:52
  • @tripleee: Did what you recommended. See edited description. No change. – user3599802 Apr 18 '21 at 09:57
  • update: a little progress, when I add the line root.update_idletasks at the end of the changeLabel function. Label gets really updated(!), visible for a fraction of a second. Unfortunately however the whole python program crashes with the message "[xcb] Most likely this is a multi-threaded client and XInitThreads has not been called". At least I could prove that my original issue is a GUI refresh issue. – user3599802 Apr 19 '21 at 11:24

1 Answers1

1

It's unlikely that you'll be able to make updating your label from another process work. It might be possible, but it will be very complicated. What I'd suggest instead is that you make a thread that launches the expensive code in another process, and then waits to update the GUI until after the process is done:

from multiprocessing import Process
from threading import Thread

def longprocess():
    # do whatever you need here, it can take a long time

def updatelabel_after_longprocess():
    # do whatever GUI stuff you need here, this will be run in the main process

def thread_helper():
    process = Process(target=longprocess)
    process.start()
    process.join() # this waits for the process to end
    updatelabel_after_longprocess()

if __name__ == "__main__":
    t = Thread(target=thread_helper)
    t.start()

    # do whatever else you have to do in parallel here

    t.join()
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Wow: That worked immediately. Thank a lot. However I still have to understand everything. I may return with a follow-up question, if you don't mind. – user3599802 Apr 19 '21 at 14:05
  • Looking at it again and tampering with it, it also seems to work, if in function thread_helper() I simply call longprocess(), then updatelabel_after_longprocess, so apparently no need to fork out another Process within the Thread. – user3599802 Apr 19 '21 at 14:18
  • 1
    Whether you need a process depends what's taking the time in `longprocess`. The GIL limits what Python code can do in parallel effectively. If it's CPU limited, the two threads will be fighting over the GIL and not accomplish much more than you'd get without any threading at all. But if the work is IO limited (downloading files from the net, or even doing lots of disk operations), or if the CPU-intensive parts are happening in certain clever libraries, then you can get parallel work done with only threads, since the code will release the GIL when it's waiting on results from outside of Python. – Blckknght Apr 19 '21 at 19:18
  • In my case longprocess is an internet download process that takes long, so it's mostly IO or waiting. – user3599802 Apr 20 '21 at 08:09