Лучший способ реализовать калибровочную полосу tkinter? - PullRequest
0 голосов
/ 15 января 2019

У меня есть датчик, который необходимо откалибровать. Ошибка зависит от ориентации датчика и может быть оценена и показана пользователю. Я хотел бы сделать это визуально, используя tkinter для python 3.x.

Идеальным результатом было бы что-то вроде этого с черным обновлением в реальном времени в зависимости от ошибки:

calibration bar

Как я могу сделать это лучше всего в tkinter? Я посмотрел на виджеты Scale и Progressbar, но у них не было необходимой функциональности.

Я думал о том, чтобы показать цветную полосу как изображение и наложить черную индикаторную полосу и постоянно обновлять положение этой черной полосы. Это будет возможно?

1 Ответ

0 голосов
/ 17 января 2019

Я разделю ответ на две части. Первая часть решает проблему оперативного обновления данных, используя два потока, как предложено @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

Чтобы получить обновленную калибровочную линейку в режиме реального времени, вам нужно просто объединить первую и вторую части в вашем приложении.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...