Tkinter GUI、I/O 和线程:何时使用队列、何时使用事件?

2024-02-01

我正在使用 TKinter 构建一个 GUI(用于与多通道分析仪的套接字连接),以定期(约 15 秒)接收和绘制数据(约 15.000.000 个值)。

在接收数据时,我不希望 GUI 冻结,因此我使用多线程进行连接处理、数据接收和绘图操作。正如可重现的代码中所示,我通过设置事件来完成此操作threading.Event()并处理一个又一个线程(几行代码initSettings() & acquireAndPlotData)。我唯一一次干扰 GUI 是在画布上绘图时,我使用 tkinter 来完成此操作after() method.

启动后,只要窗口打开并按预期工作,代码就会运行而不会冻结并接收和绘图。

当我阅读有关在 tkinter GUI 中处理阻塞 I/O 操作的内容时,我只找到了递归排队和检查队列的示例(使用Queue & after(), 1 https://medium.com/@mattia512maldini/how-to-setup-correctly-an-application-with-python-and-tkinter-107c6bc5a45 2 https://stupidpythonideas.blogspot.com/2013/10/why-your-gui-app-freezes.html 3 https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html 4 https://www.thetopsites.net/article/54237067.shtml 5 https://benedictwilkinsai.github.io/post/tkinter-mp/),但我发现用它来处理这些操作更方便、更容易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)

所以我这样做了,但我不知道它是否适合你,或者这是否是一个好方法,但它可以让你安全.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()

一些研究:

[Tcl] https://hg.python.org/cpython/file/05e8f92b58ff/Modules/_tkinter.c#l142

解释器仅在创建它的线程中有效,并且所有 Tk 活动也必须发生在该线程中。这意味着 mainloop 必须在创建该线程的线程中调用 口译员。可以从其他线程调用命令; _tkinter 将为解释器线程排队一个事件,这将 然后执行命令并传回结果。

#l1493 https://hg.python.org/cpython/file/05e8f92b58ff/Modules/_tkinter.c#l1483 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;

在 python 线程中使用 intvar-doublevar 是否安全 https://stackoverflow.com/questions/25351829/is-it-safe-to-use-a-intvar-doublevar-in-a-python-thread

当你设置一个变量时,它会调用 globalsetvar 方法 与变量关联的主小部件。 _tk.globalsetvar 方法是用C实现的,内部调用var_invoke,即 内部调用 WaitForMainLoop,它将尝试调度 在主线程中执行的命令,如引用中所述 来自我上面包含的 _tkinter 来源。

wiki.tcl https://wiki.tcl-lang.org/page/Tcl+event+loop

     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                                                 |
       |                                                           |
       +-----------------------------------------------------------+

常识

from 错误跟踪器 https://bugs.python.org/msg316447:

Tkinter 和线程。

如果你想同时使用 tkinter 和线程,最安全的方法是 在主线程中进行所有 tkinter 调用。如果工作线程生成 tkinter调用所需的数据,使用queue.Queue将数据发送到 主线程。为了干净关闭,添加一个方法来等待 当窗口关闭按钮 [X] 按下时,线程停止并调用它 按下。

effbot http://effbot.org/zone/tkinter-threads.htm

只需在主线程中运行所有 UI 代码,然后让编写者写入 一个队列对象;例如

结论

你做的方式和我做的方式似乎并不一样ideal https://stackoverflow.com/a/10556698/13629335但他们似乎一点都没有错。这取决于需要什么。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Tkinter GUI、I/O 和线程:何时使用队列、何时使用事件? 的相关文章

  • Condition 接口中的 signalAll 与对象中的 notificationAll

    1 昨天我才问过这个问题条件与等待通知机制 https stackoverflow com questions 10395571 condition vs wait notify mechanism 2 我想编辑相同的内容并在我的问题中添加
  • 如何迭代按值排序的 Python 字典?

    我有一本字典 比如 a 6 b 1 c 2 我想迭代一下by value 不是通过键 换句话说 b 1 c 2 a 6 最直接的方法是什么 sorted dictionary items key lambda x x 1 对于那些讨厌 la
  • Python逻辑运算符优先级[重复]

    这个问题在这里已经有答案了 哪个运算符优先4 gt 5 or 3 lt 4 and 9 gt 8 这会被评估为真还是假 我知道该声明3 gt 4 or 2 lt 3 and 9 gt 10 显然应该评估为 false 但我不太确定 pyth
  • 从 ffmpeg 获取实时输出以在进度条中使用(PyQt4,stdout)

    我已经查看了很多问题 但仍然无法完全弄清楚 我正在使用 PyQt 并且希望能够运行ffmpeg i file mp4 file avi并获取流式输出 以便我可以创建进度条 我看过这些问题 ffmpeg可以显示进度条吗 https stack
  • 在 Python distutils 中从 setup.py 查找脚本目录的正确方法?

    我正在分发一个具有以下结构的包 mymodule mymodule init py mymodule code py scripts script1 py scripts script2 py The mymodule的子目录mymodul
  • Pandas 中允许重复列

    我将一个大的 CSV 包含股票财务数据 文件分割成更小的块 CSV 文件的格式不同 像 Excel 数据透视表之类的东西 第一列的前几行包含一些标题 公司名称 ID 等在以下列中重复 因为一家公司有多个属性 而不是一家公司只有一栏 在前几行
  • 从零开始的 numpy 形状意味着什么

    好的 我发现数组的形状中可以包含 0 对于将 0 作为唯一维度的情况 这对我来说是有意义的 它是一个空数组 np zeros 0 但如果你有这样的情况 np zeros 0 100 让我很困惑 为什么这么定义呢 据我所知 这只是表达空数组的
  • 为什么Python的curses中escape键有延迟?

    In the Python curses module I have observed that there is a roughly 1 second delay between pressing the esc key and getc
  • 从 python 发起 SSH 隧道时出现问题

    目标是在卫星服务器和集中式注册数据库之间建立 n 个 ssh 隧道 我已经在我的服务器之间设置了公钥身份验证 因此它们只需直接登录而无需密码提示 怎么办 我试过帕拉米科 它看起来不错 但仅仅建立一个基本的隧道就变得相当复杂 尽管代码示例将受
  • 如何设置 Celery 来调用自定义工作器初始化?

    我对 Celery 很陌生 我一直在尝试设置一个具有 2 个独立队列的项目 一个用于计算 另一个用于执行 到目前为止 一切都很好 我的问题是执行队列中的工作人员需要实例化一个具有唯一 object id 的类 每个工作人员一个 id 我想知
  • 按元组分隔符拆分列表

    我有清单 print L I WW am XX newbie YY ZZ You WW are XX cool YY ZZ 我想用分隔符将列表拆分为子列表 ZZ print new L I WW am XX newbie YY ZZ You
  • 将 matplotlib 颜色图集中在特定值上

    我正在使用 matplotlib 颜色图 seismic 绘制绘图 并且希望白色以 0 为中心 当我在不进行任何更改的情况下运行脚本时 白色从 0 下降到 10 我尝试设置 vmin 50 vmax 50 但在这种情况下我完全失去了白色 关
  • 在 Pandas 中使用正则表达式的多种模式

    我是Python编程的初学者 我正在探索正则表达式 我正在尝试从 描述 列中提取一个单词 数据库名称 我无法给出多个正则表达式模式 请参阅下面的描述和代码 描述 Summary AD1 Low free DATA space in data
  • 如何在 python 中没有 csv.reader 迭代器的情况下解析单行 csv 字符串?

    我有一个 CSV 文件 需要重新排列和重新编码 我想跑 line line decode windows 1250 encode utf 8 在由 CSV 读取器解析和分割之前的每一行 或者我想自己迭代行 运行重新编码 并仅使用单行解析表单
  • 无法在 osx-arm64 上安装 Python 3.7

    我正在尝试使用 Conda 创建一个带有 Python 3 7 的新环境 例如 conda create n qnn python 3 7 我收到以下错误 Collecting package metadata current repoda
  • 将数据打印到文件

    我已经超载了 lt lt 运算符 使其写入文件并写入控制台 我已经为同一个函数创建了 8 个线程 并且我想输出 hello hi 如果我在无限循环中运行这个线程例程 文件中的o p是 hello hi hello hi hello hi e
  • 迭代 my_dict.keys() 并修改字典中的值是否会使迭代器失效?

    我的例子是这样的 for my key in my dict keys my dict my key mutate 上述代码的行为是否已定义 假设my dict是一本字典并且mutate是一个改变其对象的方法 我担心的是 改变字典中的值可能
  • 是否有一种更简单的方法可以并行运行命令,同时在 Windows PowerShell 中保持高效?

    此自我回答旨在为那些受困于 Windows PowerShell 并由于公司政策等原因而无法安装模块的用户提供一种简单且高效的并行替代方案 在 Windows PowerShell 中 built in可用的替代方案local并行调用是St
  • 具有自定义值的 Django 管理外键下拉列表

    我有 3 个 Django 模型 class Test models Model pass class Page models Model test models ForeignKey Test class Question model M
  • Elastic Beanstalk 中的 enum34 问题

    我正在尝试在 Elastic Beanstalk 中设置 django 环境 当我尝试通过requirements txt 文件安装时 我遇到了python3 6 问题 File opt python run venv bin pip li

随机推荐