I shall split up the answer in two parts. The first part solves the issue of live updating the data, by using two threads as suggested by @Martineau. The communication between the threads is done by a simple lock and a global variable.
The second part creates the calibration bar widget using the gradient calculation algorithm defined by @Martineau.
PART 1:
This example code shows a small window with one number. The number is generated in one thread and the GUI is shown by another thread.
import threading
import time
import copy
import tkinter as tk
import random
class ThreadCreateData(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
#Declaring data global allows to access it between threads
global data
# create data for the first time
data_original = self.create_data()
while True: # Go in the permanent loop
print('Data creator tries to get lock')
lock.acquire()
print('Data creator has it!')
data = copy.deepcopy(data_original)
print('Data creator is releasing it')
lock.release()
print('Data creator is creating data...')
data_original = self.create_data()
def create_data(self):
'''A function that returns a string representation of a number changing between one and ten.'''
a = random.randrange(1, 10)
time.sleep(1) #Simulating calculation time
return str(a)
class ThreadShowData(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
# Declaring data global allows to access it between threads
global data
root = tk.Tk()
root.geometry("200x150")
# creation of an instance
app = Window(root, lock)
# mainloop
root.mainloop()
# Here, we are creating our class, Window, and inheriting from the Frame
# class. Frame is a class from the tkinter module. (see Lib/tkinter/__init__)
class Window(tk.Frame):
# Define settings upon initialization. Here you can specify
def __init__(self, master=None,lock=None):
# parameters that you want to send through the Frame class.
tk.Frame.__init__(self, master)
# reference to the master widget, which is the tk window
self.master = master
#Execute function update_gui after 1ms
self.master.after(1, self.update_gui(lock))
def update_gui(self, lock):
global data
print('updating')
print('GUI trying to get lock')
lock.acquire()
print('GUI got the lock')
new_data = copy.deepcopy(data)
print('GUI releasing lock')
lock.release()
data_label = tk.Label(self.master, text=new_data)
data_label.grid(row=1, column=0)
print('GUI wating to update')
self.master.after(2000, lambda: self.update_gui(lock)) #run update_gui every 2 seconds
if __name__ == '__main__':
# creating the lock
lock = threading.Lock()
#Initializing data
data = None
#creating threads
a = ThreadCreateData("Data_creating_thread")
b = ThreadShowData("Data_showing_thread")
#starting threads
b.start()
a.start()
PART 2: Below the code for a simple calibration bar widget is shown. The bar only contains 5 ticks you can adapt the code to add more if wanted. Pay attention to the needed input formats. To test the widget a random value is generated and shown on the widget every 0.5s.
import tkinter as tk
from PIL import ImageTk, Image
import sys
EPSILON = sys.float_info.epsilon # Smallest possible difference.
###Functions to create the color bar (credits to Martineau)
def convert_to_rgb(minval, maxval, val, colors):
for index, color in enumerate(colors):
if color == 'YELLOW':
colors[index] = (255, 255, 0)
elif color == 'RED':
colors[index] = (255, 0, 0)
elif color == 'GREEN':
colors[index] = (0, 255, 0)
# "colors" is a series of RGB colors delineating a series of
# adjacent linear color gradients between each pair.
# Determine where the given value falls proportionality within
# the range from minval->maxval and scale that fractional value
# by the total number in the "colors" pallette.
i_f = float(val - minval) / float(maxval - minval) * (len(colors) - 1)
# Determine the lower index of the pair of color indices this
# value corresponds and its fractional distance between the lower
# and the upper colors.
i, f = int(i_f // 1), i_f % 1 # Split into whole & fractional parts.
# Does it fall exactly on one of the color points?
if f < EPSILON:
return colors[i]
else: # Otherwise return a color within the range between them.
(r1, g1, b1), (r2, g2, b2) = colors[i], colors[i + 1]
return int(r1 + f * (r2 - r1)), int(g1 + f * (g2 - g1)), int(b1 + f * (b2 - b1))
def create_gradient_img(size, colors):
''''Creates a gradient image based on size (1x2 tuple) and colors (1x3 tuple with strings as entries,
possible entries are GREEN RED and YELLOW)'''
img = Image.new('RGB', (size[0],size[1]), "black") # Create a new image
pixels = img.load() # Create the pixel map
for i in range(img.size[0]): # For every pixel:
for j in range(img.size[1]):
pixels[i,j] = convert_to_rgb(minval=0,maxval=size[0],val=i,colors=colors) # Set the colour accordingly
return img
### The widget
class CalibrationBar(tk.Frame):
""""The calibration bar widget. Takes as arguments the parent, the start value of the calibration bar, the
limits in the form of a 1x5 list these will form the ticks on the bar and the boolean two sided. In case it
is two sided the gradient will be double."""
def __init__(self, parent, limits, name, value=0, two_sided=False):
tk.Frame.__init__(self, parent)
#Assign attributes
self.value = value
self.limits = limits
self.two_sided = two_sided
self.name=name
#Test that the limits are 5 digits
assert len(limits)== 5 , 'There are 5 ticks so you should give me 5 values!'
#Create a canvas in which we are going to put the drawings
self.canvas_width = 400
self.canvas_height = 100
self.canvas = tk.Canvas(self,
width=self.canvas_width,
height=self.canvas_height)
#Create the color bar
self.bar_offset = int(0.05 * self.canvas_width)
self.bar_width = int(self.canvas_width*0.9)
self.bar_height = int(self.canvas_height*0.8)
if two_sided:
self.color_bar = ImageTk.PhotoImage(create_gradient_img([self.bar_width,self.bar_height],['RED','GREEN','RED']))
else:
self.color_bar = ImageTk.PhotoImage(create_gradient_img([self.bar_width,self.bar_height], ['GREEN', 'YELLOW', 'RED']))
#Put the colorbar on the canvas
self.canvas.create_image(self.bar_offset, 0, image=self.color_bar, anchor = tk.NW)
#Indicator line
self.indicator_line = self.create_indicator_line()
#Tick lines & values
for i in range(0,5):
print(str(limits[i]))
if i==4:
print('was dees')
self.canvas.create_line(self.bar_offset + int(self.bar_width - 2), int(self.canvas_height * 0.7),
self.bar_offset + int(self.bar_width - 2), int(self.canvas_height * 0.9), fill="#000000", width=3)
self.canvas.create_text(self.bar_offset + int(self.bar_width - 2), int(self.canvas_height * 0.9), text=str(limits[i]), anchor=tk.N)
else:
self.canvas.create_line(self.bar_offset + int(i * self.bar_width / 4), int(self.canvas_height * 0.7), self.bar_offset + int(i * self.bar_width / 4), int(self.canvas_height * 0.9), fill="#000000", width=3)
self.canvas.create_text(self.bar_offset + int(i * self.bar_width / 4), int(self.canvas_height * 0.9), text=str(limits[i]), anchor=tk.N)
#Text
self.label = tk.Label(text=self.name+': '+str(self.value),font=14)
#Positioning
self.canvas.grid(row=0,column=0,sticky=tk.N)
self.label.grid(row=1,column=0,sticky=tk.N)
def create_indicator_line(self):
""""Creates the indicator line"""
diff = self.value-self.limits[0]
ratio = diff/(self.limits[-1]-self.limits[0])
if diff<0:
ratio=0
elif ratio>1:
ratio=1
xpos = int(self.bar_offset+ratio*self.bar_width)
return self.canvas.create_line(xpos, 0, xpos, 0.9 * self.canvas_height, fill="#000000", width=3)
def update_value(self,value):
self.value = value
self.label.config(text = self.name+': '+str(self.value))
self.canvas.delete(self.indicator_line)
self.indicator_line = self.create_indicator_line()
###Creation of window to place the widget
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry('400x400')
self.calibration_bar = CalibrationBar(self, value= -5, limits=[-10, -5, 0, 5, 10], name='Inclination angle', two_sided=True)
self.calibration_bar.grid(column=0, row=4)
self.after(500,self.update_data)
def update_data(self):
""""Randomly assing values to the widget and update the widget."""
import random
a = random.randrange(-15, 15)
self.calibration_bar.update_value(a)
self.after(500, self.update_data)
###Calling our window
if __name__ == "__main__":
app=App()
app.mainloop()
This is how it looks like:

To get a live updating calibration bar you should just combine part one and two in your application.