执行长函数时如何使 Tkinter GUI 不崩溃?

How to make the Tkinter GUI not crash when executing a long function?

我目前正在开发一个程序,该程序使用 Python 和 Tkinter 构建,用于与我们实验室中的不同仪器交互并测试一些电子设备。我有一个函数 (isDutAlive()),它可能需要很长时间(最多 10-15 分钟)才能完成。 现在,Tkinter GUI 在此期间没有响应,对用户来说,程序似乎崩溃了。也没有其他操作是可能的。 这是代码:

import os
import sys
import requests
import pyvisa
import time
from datetime import datetime
import threading
from jsonrpcclient import request
from tkinter import *
from tkinter import ttk

print("-----------------------------------------")
print("              Q-Center V0.1              ")
print("-----------------------------------------")

port = ":8080"
rm = pyvisa.ResourceManager()



def listArticles():
    #print("PK IP Address: ")
    #pkIpAddress = input()
    response = request("http://" + ipEntry.get() + port, "temperature_test_list_articles")
    print(response.text)
    print(response.data.result)
    answer = response.data.result
    pkReply.insert(END, answer)
    pkReply.insert(END, '\n\n')

def setArticle():
    response = request("http://" + ipEntry.get() + port,  "temperature_test_set_article", articleEntry.get())
    print(response.text)
    print(response.data.result)
    answer = response.text
    pkReply.insert(END, answer)
    pkReply.insert(END, '\n\n')

def unsetArticle():
    response = request("http://" + ipEntry.get() + port,  "temperature_test_unset_article")
    print(response.text)
    print(response.data.result)
    answer = response.text
    pkReply.insert(END, answer)
    pkReply.insert(END, '\n\n')

def clearLog():
    pkReply.delete('1.0', END)

def listInstruments():
    instruments = rm.list_resources()
    print(instruments)
    print('\n\n')
    visaReplyText.insert(END, instruments)
    visaReplyText.insert(END, '\n\n')
    return 0

def isDutAlive():
    timestamp = time.strftime("%Y_%m_%d-%H_%M_%S.txt")
    logFile = open(timestamp, "a")
    logFile.write("Timestamp               DUT     Result       Voltage         Current\n\n")
    for x in range (int(firstSocketEntry.get()), int(lastSocketEntry.get())+1):
        print(x)
        time.sleep(1)
        response = request("http://" + ipEntry.get() + port, "temperature_test_is_dut_alive", x)
        time.sleep(1)
        print(response.text)
        print(response.data.result)
        answer = response.data.result
        pkReply.insert(END, answer)
        pkReply.insert(END, '\n\n')
        logFile.write(time.strftime("%Y_%m_%d-%H_%M_%S\t\t") + str(x) + "\t\t" + str(answer) + "\t\t" + str(readVoltage()) + "\t\t" + str(readCurrent()) + "\n")  
    print("TEST")

def clearPyvisaLog():
    visaReplyText.delete('1.0', END)
    return 0

def readVoltage():
    choosenInstrument = rm.open_resource(instrumentEntry.get())
    voltageMeasurement = choosenInstrument.query_ascii_values("MEAS:VOLT? (@" + setChannelEntry.get() + ")")
    visaReplyText.insert(END, voltageMeasurement)
    visaReplyText.insert(END, " V")
    visaReplyText.insert(END, "\n\n")
    return voltageMeasurement

def readCurrent():
    choosenInstrument = rm.open_resource(instrumentEntry.get())
    currentMeasurement = choosenInstrument.query_ascii_values("MEAS:CURR? (@" + setChannelEntry.get() + ")")
    visaReplyText.insert(END, currentMeasurement)
    visaReplyText.insert(END, " A")
    visaReplyText.insert(END, "\n\n")
    return currentMeasurement

def setVoltage():
    choosenInstrument = rm.open_resource(instrumentEntry.get())
    print(choosenInstrument.query('*IDN?'))
    print(choosenInstrument.write("VOLT:LEV " + setVoltageEntry.get() + ", (@" + setChannelEntry.get() + ")"))
    return 0


def setCurrent():
    choosenInstrument = rm.open_resource(instrumentEntry.get())
    print(choosenInstrument.query('*IDN?'))
    print(choosenInstrument.write("CURR:LEV " + setCurrentEntry.get() + ", (@" + setChannelEntry.get() + ")"))
    return 0

def toggleChannelOutput():
    choosenInstrument = rm.open_resource(instrumentEntry.get())
    #choosenInstrument.write("OUTPut OFF, (@2)")
    choosenInstrument.write(output.get() + ", (@" + setChannelEntry.get() + ")")
    return 0

def sendRelayCommand():
    send = requests.get('http://' + relayIPEntry.get() + '/io.cgi?DO' + on_off.get() + relayNumberEntry.get())
    print(on_off.get())

def cycleRelay():
    send = requests.get('http://' + relayIPEntry.get() + '/io.cgi?DOI' + relayNumberEntry.get() + '=100')

root = Tk()
#root.geometry("800x400")
root.title("Q-Center")
root.iconbitmap("./icons/icon.ico")
root.call('wm', 'iconphoto', root._w, PhotoImage(file='./icons/icon.png'))
# root.iconphoto(False, PhotoImage(file='./icons/icon.png'))

on_off = StringVar()
output = StringVar()

##############################################################################################################################

tabControl = ttk.Notebook(root)
tab1 = ttk.Frame(tabControl)
tab2 = ttk.Frame(tabControl)
tab3 = ttk.Frame(tabControl)

tabControl.add(tab1, text="PK Control")
tabControl.add(tab2, text="Keysight Control")
tabControl.add(tab3, text="Relay Board Control")
tabControl.pack(expand=1, fill="both")

##############################################################################################################################

ipEntry = ttk.Entry(tab1)
ipEntry.pack()
ipEntry.insert(0, "192.168.1.73")

listArticlesButton = Button(tab1, text="List Articles", command=listArticles)
listArticlesButton.pack()

articleEntry = ttk.Entry(tab1)
articleEntry.pack()

setArticleButton = Button(tab1, text="Set Article", command=setArticle)
setArticleButton.pack()

unsetArticleButton = Button(tab1, text="Unset Article", command=unsetArticle)
unsetArticleButton.pack()

firstSocketTestLabel = Label(tab1, text="Enter First Socket To Test")
firstSocketTestLabel.pack()

firstSocketEntry = ttk.Entry(tab1)
firstSocketEntry.pack()

lastSocketTestLabel = Label(tab1, text="Enter Last Socket To Test")
lastSocketTestLabel.pack()

lastSocketEntry = ttk.Entry(tab1)
lastSocketEntry.pack()

isDutAliveButton = Button(tab1, text="Is DUT Alive", command=isDutAlive)
isDutAliveButton.pack()

clearLogEntryButton = Button(tab1, text="Clear Log", command=clearLog)
clearLogEntryButton.pack()

pkReply = Text(tab1)
pkReply.pack()

##############################################################################################################################

listInstrumentsButton = Button(tab2, text="List Instruments", command=listInstruments)
listInstrumentsButton.pack()

instrumentLabel = Label(tab2, text="Enter The Instruments VISA Address")
instrumentLabel.pack()

instrumentEntry = ttk.Entry(tab2)
instrumentEntry.pack()
instrumentEntry.insert(0, "USB0::0x0957::0x0C07::MY54005119::INSTR")

setVoltageLabel = Label(tab2, text="Enter The Voltage")
setVoltageLabel.pack()

setVoltageEntry = ttk.Entry(tab2)
setVoltageEntry.pack()

setCurrentLabel = Label(tab2, text="Enter The Current")
setCurrentLabel.pack()

setCurrentEntry = ttk.Entry(tab2)
setCurrentEntry.pack()

setChannelLabel = Label(tab2, text="Enter The Channel")
setChannelLabel.pack()

setChannelEntry = ttk.Entry(tab2)
setChannelEntry.pack()
setChannelEntry.insert(0, "2")

readVoltageButton = Button(tab2, text="Read Voltage", command=readVoltage)
readVoltageButton.pack()

readCurrentButton = Button(tab2, text="Read Current", command=readCurrent)
readCurrentButton.pack()

setVoltageButton = Button(tab2, text="Set Voltage", command=setVoltage)
setVoltageButton.pack()

setCurrentButton = Button(tab2, text="Set Current", command=setCurrent)
setCurrentButton.pack()

outputOnRadioButton = ttk.Radiobutton(tab2, variable=output, text="On", value="OUTPut ON", command=toggleChannelOutput)
outputOnRadioButton.pack()

relayOffRadioButton = ttk.Radiobutton(tab2, variable=output, text="Off", value="OUTPut OFF", command=toggleChannelOutput) 
relayOffRadioButton.pack()

clearLogButton = Button(tab2, text="Clear Log", command=clearPyvisaLog)
clearLogButton.pack()

visaReplyText = Text(tab2)
visaReplyText.pack()

##############################################################################################################################

relayIPLabel = ttk.Label(tab3, text="Relay Board IP")
relayIPLabel.pack()

relayIPEntry = ttk.Entry(tab3)
relayIPEntry.insert(0, "192.168.1.199")
relayIPEntry.pack()

relayNumberLabel = ttk.Label(tab3, text="Relay Number")
relayNumberLabel.pack()

relayNumberEntry = ttk.Entry(tab3)
relayNumberEntry.insert(0, "2")
relayNumberEntry.pack()

relayOnRadioButton = ttk.Radiobutton(tab3, variable=on_off, text="On", value="A") # A for active (turn on)
relayOnRadioButton.pack()

relayOffRadioButton = ttk.Radiobutton(tab3, variable=on_off, text="Off", value="I") # I for inactive (turn off)
relayOffRadioButton.pack()

sendRelayCommandButton = Button(tab3, text="Send Command", command=sendRelayCommand)
sendRelayCommandButton.pack()

cycleRelayButton = Button(tab3, text="Cycle Relay 10s", command=cycleRelay)
cycleRelayButton.pack()

##############################################################################################################################

root.mainloop()

现在我读到多线程将是一个合适的解决方案,但我不确定如何以最佳方式实现它。 我试图从函数“isDutAlive()”创建一个线程,并在按下触发该线程启动的按钮后启动它。但是当我想再次按下按钮时(为了再次执行函数和测试),它告诉我,一个线程只能启动一次。 有没有办法在执行函数后“杀死”线程?有什么建议可以在执行此功能期间使 GUI 响应吗? 我感谢任何帮助和提示。感谢和问候

我认为您的工作进展顺利。多线程确实是解决这个问题的一种方式。你实际上是在尝试做一些异步的事情。

最快的方法可能是像您提到的那样加入讨论帖。 this page on realpython.com. Specifically on the topic of barriers or deadlocks 是一个很好的资源,我认为它可以回答您的问题。

或者,如果您计算出这种情况发生得更多,并且结果可以在 X 时间后显示,而用户仍然没有积极等待,则可以使用某种带有代理的异步系统。我在类似情况下使用的是Celery。我将它与易于使用的 RabbitMQ 实例相结合。

使用此设置,您的用户将在预定义的队列中创建“todo”任务,然后这些任务将被处理。输出可以存储在您的应用程序可以订阅的结果队列中。使用此解决方案有一些优点和缺点,在您的情况下可能会或可能不会破坏。

优点:

  1. 真正的异步,所以只要您的应用程序可以到达 RMQ 服务器,它对您的系统的影响应该几乎为 0,并且 GUI 将始终保持“运行”状态。
  2. 无论用户按下多少次按钮,都无需等待结果。

缺点:

  1. 每个项目将单独执行。将 5 个项目放入队列意味着相同的操作将被执行 5 次。
  2. 需要额外的软件(RMQ 或其他代理)以及额外的库 (celery)
  3. 用户将主动获得他们行为的结果。这可能是一个障碍。

希望我已经为您提供了一些可能的解决方案?