Графический интерфейс Tkinter, ввод-вывод и потоки: когда использовать очереди, когда события?
Я использую TKinter для создания графического интерфейса (для подключения сокета к многоканальному анализатору) для получения и построения данных (~15000000 значений) через равные промежутки времени (~15 секунд).
При получении данных я не хочу, чтобы графический интерфейс зависал, поэтому я использую многопоточность для обработки соединений, приема данных и операций построения графиков. Я сделал это, как видно из воспроизводимого кода, установив событие с помощьюthreading.Event()
и обрабатывать один поток за другим (несколько строк кода в initSettings()
& acquireAndPlotData
). Единственный раз, когда я вмешиваюсь в графический интерфейс, - это когда я рисую на холсте, и я делаю это с помощью tkintersafter()
метод.
При запуске код работает без зависания, получает и строит график, пока окно открыто и работает должным образом.
Когда я читал об обработке блокирующих операций ввода-вывода в графическом интерфейсе tkinter, я нашел только примеры с постановкой в очередь и рекурсивной проверкой очереди (с Queue
& after()
,12345), но я обнаружил, что эти операции намного удобнее и проще выполнять сthreading.Event()
.
Теперь мой вопрос:
Я использую правильный подход или мне здесь не хватает чего-то важного?(Что касается безопасности потоков, условий гонки, что, если построение графика не удается и занимает больше времени, чем сбор данных? Я не думаю о чем-то? Плохие методы? и т. д.)
Буду очень благодарен за отзыв по этому поводу!
Воспроизводимый код
#####################*** IMPORTS ***#######################################################
import tkinter
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
import numpy as np
################### *** FUNCTIONS *** #########################################################
# *** initializes two threads for initializing connection & receiving/plotting data ***
def onStartButtonClick(event):
#
init_settings_thread.start()
acquire_and_plot_data_thread.start()
#
# *** inizialize connection & set event when finished & ready for sending data ***
def initSettings():
#time.sleep() simulates the time it takes to inizialize the connection
time.sleep(2)
start_data_acquisition_event.set()
# *** waiting for event/flag from initSettings() & start data receiving/plotting loop afer event set ***
def acquireAndPlotData():
start_data_acquisition_event.wait()
while start_data_acquisition_event.is_set():
# time.sleep() simulates the time it takes the connection to fill up the buffer
time.sleep(4)
# send updateGuiFigure to tkinters event queue, so that it won't freeze
root.after(0, updateGuiFigure)
# *** set new data points on existing plot & blit GUI canvas ***
def updateGuiFigure():
# simulate data -> 15.000.000 points in real application
line.set_xdata(np.random.rand(10))
#
line.set_ydata(np.random.rand(10))
#
plotting_canvas.restore_region(background) # restore background
ax.draw_artist(line) # redraw just the line -> draw_artist updates axis
plotting_canvas.blit(ax.bbox) # fill in the axes rectangle
#
# *** update background for resize events ***
def update_background(event):
global background
background = plotting_canvas.copy_from_bbox(ax.bbox)
##########################*** MAIN ***#########################################################
# Init GUI
root = tkinter.Tk()
# Init frame & canvas
frame = ttk.Frame(root)
plotting_area = tkinter.Canvas(root, width=700, height=400)
#
frame.grid(row=0, column=1, sticky="n")
plotting_area.grid(row=0, column=0)
# Init button & bind to function onStartButtonClick
start_button = tkinter.Button(frame, text="Start")
start_button.bind("<Button-1>", onStartButtonClick)
start_button.grid(row=0, column=0)
# Init figure & axis
fig = Figure(figsize=(7, 4), dpi=100)
ax = fig.add_subplot(111)
# Connect figure to plotting_area from GUI
plotting_canvas = FigureCanvasTkAgg(fig, master=plotting_area)
# Set axis
ax.set_title('Test')
ax.grid(True)
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set(xlim=[0,1], ylim=[0, 1])
# Init plot
line, = ax.plot([], [])
# if animated == True: artist (= line) will only be drawn when manually called draw_artist(line)
line.set_animated(True)
# Draw plot to GUI canvas
plotting_canvas.draw()
plotting_canvas.get_tk_widget().pack(fill=tkinter.BOTH)
background = plotting_canvas.copy_from_bbox(ax.bbox) # cache background
plotting_canvas.mpl_connect('draw_event', update_background) # update background with 'draw_event'
# Init threads
start_data_acquisition_event = threading.Event()
#
init_settings_thread = threading.Thread(name='init_settings_thread', target=initSettings, daemon=True)
acquire_and_plot_data_thread = threading.Thread(name='acquire_and_plot_data_thread', target=acquireAndPlotData, daemon=True)
# Start tkinter mainloop
root.mainloop()
Пример с фрагментами кода, обработанный с несколькими классами, выглядит следующим образом (то же, что и код выше, но не воспроизводимый, им можно пренебречь):
def onStartButtonClick(self):
#
.
# Disable buttons and get widget values here etc.
.
#
self.start_data_acquisition_event = threading.Event()
self.init_settings_thread = threading.Thread(target=self.initSettings)
self.acquire_and_plot_data_thread = threading.Thread(target=self.acquireAndPlotData)
#
self.init_settings_thread.start()
self.acquire_and_plot_data_thread.start()
# FUNCTION END
def initSettings(self):
self.data_handler.setInitSettings(self.user_settings_dict)
self.data_handler.initDataAcquisitionObject()
self.start_data_acquisition_event.set()
def acquireAndPlotData(self):
self.start_data_acquisition_event.wait()
while self.start_data_acquisition_event.is_set():
self.data_handler.getDataFromDataAcquisitionObject()
self.master.after(0, self.data_plotter.updateGuiFigure)
1 ответ
Итак, я сделал это так, но я не знаю, подходит ли он вам или это хороший способ сделать это, но он защищает вас от .after
как указано в комментариях, что имеет то преимущество, что ваша функция do_stuff
вызывается только при необходимости.
import tkinter as tk
import time
import threading
def get_data():
time.sleep(3)
print('sleeped 3')
_check.set(1)
def do_stuff():
try:
root.configure(bg='#'+str(_var.get()))
_var.set(_var.get()+101010)
except:
_var.set(101010)
root = tk.Tk()
_check = tk.IntVar(value=0)
_var = tk.IntVar(value=101010)
def callback(event=None, *args):
t1 = threading.Thread(target=get_data)
t1.start()
do_stuff()
_check.trace_add('write', callback) #kepp track of that variable and trigger callback if changed
callback() # start the loop
root.mainloop()
Некоторые исследования:
Интерпретатор действителен только в потоке, который его создал, и все действия Tk также должны происходить в этом потоке. Это означает, что основной цикл должен быть вызван в потоке, создавшем интерпретатор. Возможен вызов команд из других потоков; _tkinter поставит событие в очередь для потока интерпретатора, который затем выполнит команду и вернет результат.
#l1493 var_invoke
The current thread is not the interpreter thread. Marshal
the call to the interpreter thread, then wait for
completion. */
if (!WaitForMainloop(self))
return NULL;
безопасно ли использовать-intvar-doublevar-in-a-python-thread
Когда вы устанавливаете переменную, он вызывает метод globalsetvar на главном виджете, связанном с переменной. Метод _tk.globalsetvar реализован на C и внутренне вызывает var_invoke, который внутренне вызывает WaitForMainLoop, который пытается запланировать выполнение команды в основном потоке, как описано в цитате из источника _tkinter, который я включил выше.
Start
|
|<----------------------------------------------------------+
v ^
Do I have No[*] Calculate how Sleep for at |
work to do? -----> long I may sleep -----> most that much --->|
| time |
| Yes |
| |
v |
Do one callback |
| |
+-----------------------------------------------------------+
Здравый смысл
из багтрекера:
Tkinter и потоки.
Если вы хотите использовать как tkinter, так и потоки, самый безопасный метод - выполнять все вызовы tkinter в основном потоке. Если рабочие потоки генерируют данные, необходимые для вызовов tkinter, используйте queue.Queue для отправки данных в основной поток. Для чистого завершения работы добавьте метод ожидания остановки потоков и его вызова при нажатии кнопки закрытия окна [X].
Просто запустите весь код пользовательского интерфейса в основном потоке и позвольте авторам писать в объект Queue; например
Вывод
То, как вы это сделали, и то, как я это сделал, кажется идеальным, но они не кажутся неправильными. Это зависит от того, что нужно.