Я разделю ответ на две части. Первая часть решает проблему оперативного обновления данных, используя два потока, как предложено @Martineau. Связь между потоками осуществляется с помощью простой блокировки и глобальной переменной.
Вторая часть создает виджет калибровочной шкалы, используя алгоритм вычисления градиента, определенный @ Martineau.
ЧАСТЬ 1:
В этом примере кода показано небольшое окно с одним номером. Номер генерируется в одном потоке, а графический интерфейс отображается в другом потоке.
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()
ЧАСТЬ 2: Ниже показан код для простого виджета калибровочной панели. Панель содержит только 5 тиков, которые вы можете адаптировать, чтобы добавить больше, если хотите Обратите внимание на необходимые форматы ввода. Для проверки виджета генерируется случайное значение, которое отображается на виджете каждые 0,5 с.
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()
Вот как это выглядит:
![Calibration bar screenshot](https://i.stack.imgur.com/A50k9.png)
Чтобы получить обновленную калибровочную линейку в режиме реального времени, вам нужно просто объединить первую и вторую части в вашем приложении.