I’ve been working on a minor python project recently, and went back to my old friend TkInter. TkInter is the standard GUI library for Python. I had used it previously in another small project, so when I wanted to quickly get a GUI going for a project, it made sense to use TkInter. There are many other GUI libraries, but TkInter is supposedly one of the quicker and easier to get going.
TkInter has a number of shortcomings. It’s clunky and not completely intuitive. But the biggest issues that I experience with it is trying to get the interface to update from an outside perspective. My specific implementation required me to update about 20 values on the GUI by reading data from a serial port.
The easiest way to implement this is to make use of the TkInter Label widget. The Label is used for displaying any kind of text. A label can be linked to a TkInter variable, so that if you update the variable, the label automatically gets updated. The difficulty is that you need an event to trigger an update of the variables.
The way to get around this is threads. Python has built in support for threads, via the Threading class. And so you create a thread, and pass it the Tk variables and update them from the thread. This allows you to continue doing other things in the GUI, and still have your thread run. Or does it.
The immediate issue is with how TkInter queues up tasks. Because the label uses Tk variables, you can only update them by calling a set method. Not just by declaring the variable equal to a value. As such this set gets added to a queue. I found that when I clicked a drop down menu (Tk.OptionMenu), my updates would pause. My thread would effectively pause as it waited to be allowed to call the variable set method.
This was not the end of the world, and I probably would have left it like this if I didn’t continue to have the program crash unexpectedly. It would happen often, but at different times, and only when I was interacting with another aspect of the GUI. For example, when I clicked a button or selected a menu item. The program would freeze, causing the python itself to crash, and not reporting any error.
I battled to find much info on this topic. All I’ve been able to come across is the repeating statement that “TkInter is not thread safe” and that you shouldn’t try to access TkInter widgets, except from within the main thread.
There are a couple ways around this. What I ended up using is the TkInter after method. It’s janky. But it works, and has proven reliable. Since I made the change, I have not had my app crashing any more. Now I have a global variable to store the data from serial in. I have a function within my GUI object that updates the Tk variables from the global array. It then calls itself using the after method.
The after method adds a note to TK to do something after a specific amount of time has lapsed, and appears to operate much like an interrupt, ignoring whatever else I am doing and updating on schedule. I first came across the after method as described by Furas on StackOverflow.
Below is a summary of what I was doing before, and what I am doing now.
class SerialThread(threading.Thread): def __init__(self, gui_object): threading.Thread.__init__(self) self.gui = gui_object def run(self): #Read serial info self.gui.variable1.set(SERIALDATA) class GUI(): def __init__(self): self.root = tk.Tk() self.variable1 = tk.StringVariable(self.root) label1 = tk.Label(self.root, textvariable=self.variable1) label1.grid(row=1,column=1) thread1 = SerialThread(self) thread1.start() self.root.mainloop() if __name__ == "__main__": GUI();
TempVariable = "XXXX" class SerialThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): global TempVariable #Read serial info TempVariable = SERIALDATA class GUI(): def __init__(self): self.root = tk.Tk() self.variable1 = tk.StringVariable(self.root) label1 = tk.Label(self.root, textvariable=self.variable1) label1.grid(row=1,column=1) self.updateLabels() thread1 = SerialThread() thread1.start() self.root.mainloop() def updateLabels(self): global TempVariable self.variable1.set(TempVariable) self.root.after(10,self.updateLabels) if __name__ == "__main__": GUI();
UPDATE – below code skips the global variable and uses a thread lock:class SerialThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.serial_data_to_display = "" self.lock = threading.Lock() def run(self): while True: with self.lock: # serial read stuff self.serial_data_to_display = "SERIAL DATA" sleep(0.1) class GUI(): def __init__(self): self.root = tk.Tk() self.variable1 = tk.StringVariable(self.root) label1 = tk.Label(self.root, textvariable=self.variable1) label1.grid(row=1,column=1) self.thread1 = SerialThread() self.thread1.start() self.root.mainloop() self.updateLabels() def updateLabels(self): with self.thread1.lock: self.variable1.set(self.thread1.serial_data_to_display) self.root.after(10,self.updateLabels) if __name__ == "__main__": GUI()