如何在 Tkinter 中为按钮创建新的 Threads/Kill 个活动线程?

How Do I Create New Threads/Kill Alive Threads for a Button in Tkinter?

我目前正在为我的 DJI Tello 开发 Tkinter GUI,我正在努力做到这一点,以便当我命令无人机 takeoff/land 时,GUI 上的流式视频不会冻结.我对多线程不太熟悉,但我查了一下这个问题,看来我不是唯一遇到这个问题的人。所以我使用了我发现的关于线程和启动线程的内容,并以这一行结束(或多或少):

forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=threading.Thread(target=lambda: takeoff_land(flydo)).start)

现在按下按钮,无人机起飞,视频不再卡顿。然而,当我再次点击它时,代码抛出错误:

RuntimeError: threads can only be started once

但我希望我的按钮能够让无人机在降落时起飞,然后在飞行时降落。有什么办法可以做到吗?

这是我目前所拥有的(在 takeoff_land() 函数中,我设置了一些测试代码来代替实际命令。基本上,我希望它能够开始打印 0, 1、2...之后,即使它已经在打印了。)大部分只是 GUI 的东西,但我不想遗漏任何会破坏代码的东西。

import cv2
import threading
from djitellopy import tello
from tkinter import *
from PIL import Image, ImageTk

import time

def takeoff_land(flydo):
    '''Flydo takes off if not flying, lands if flying.'''
    global flying
    if flying:
        for i in range(10):
            print(i)
            time.sleep(1)
        # flydo.land()
        flying = False
    else:
        for i in range(10):
            print(i)
            time.sleep(1)
        # flydo.takeoff()
        flying = True
    

def run_app(HEIGHT=800, WIDTH=800):
    root = Tk()
    
    flydo = tello.Tello()
    flydo.connect()
    flydo.streamon()

    global flying
    flying = False # To toggle between takeoff and landing for button

    canvas = Canvas(root, height=HEIGHT, width=WIDTH)
    
    # For background image
    bg_dir = "C:\Users\charl\Desktop\flydo\Tacit.jpg"
    img = Image.open(bg_dir).resize((WIDTH, HEIGHT))
    bg_label = Label(root)
    bg_label.img = ImageTk.PhotoImage(img)
    bg_label["image"] = bg_label.img
    bg_label.place(x=0, y=0, relwidth=1, relheight=1)

    # Display current battery
    battery = Label(text=f"Battery: {int(flydo.get_battery())}%", font=("Verdana", 18), bg="#95dff3")
    bat_width = 200
    bat_height = 50
    battery.config(width=bat_width, height=bat_height)
    battery.place(x=(WIDTH - bat_width - 0.1*HEIGHT + bat_height), rely=0.9, relwidth=bat_width/WIDTH, relheight=bat_height/HEIGHT)

    # Takeoff/Land button
    forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=threading.Thread(target=lambda: takeoff_land(flydo)).start)
    if threading.Thread(target=lambda: takeoff_land(flydo)).is_alive():
        threading.Thread(target=lambda: takeoff_land(flydo)).join() # This doesn't kill the thread the way I want it to...
    fb_width = 200
    fb_height = 100
    forward_button.config(width=fb_width, height=fb_height)
    forward_button.place(x=(WIDTH/2 - fb_width/2), rely=0.61, relwidth=fb_width/WIDTH, relheight=fb_height/HEIGHT)

    cap_label = Label(root)
    cap_label.pack()
    
    def video_stream():
        h = 480
        w = 720
        frame = flydo.get_frame_read().frame
        frame = cv2.resize(frame, (w, h))
        cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
        img = Image.fromarray(cv2image)
        imgtk = ImageTk.PhotoImage(image=img)
        cap_label.place(x=WIDTH/2 - w/2, y=0)
        cap_label.imgtk = imgtk
        cap_label.configure(image=imgtk)
        cap_label.after(5, video_stream)

    video_stream()
    canvas.pack()
    root.mainloop()


if __name__ == "__main__":
    HEIGHT = 800
    WIDTH = 800

    run_app(HEIGHT, WIDTH)

好的,今天早上我实际上根据这篇文章找到了解决方案:

https://bhaveshsingh0124.medium.com/multi-threading-on-python-tkinter-button-f0d9f759ad3e

基本上,我必须做的是将 Tello 命令线程化到我使用按钮调用的函数中,而不是函数本身。由于无人机只能降落或起飞,因此每次调用这两个命令之一时,它都可以创建一个新线程。这是固定代码:

import cv2
import threading
from djitellopy import tello
from tkinter import *
from PIL import Image, ImageTk # You have to import this last or else Image.open throws an error
import time

def dummy_tello_fn():
    for i in range(3):
        print(i)
        time.sleep(1)


def takeoff_land(flydo):
    '''Flydo takes off if not flying, lands if flying.'''
    global flying
    if flying:
        # threading.Thread(target=lambda: dummy_tello_fn()).start()
        threading.Thread(target=lambda: flydo.land()).start()
        flying = False
    else:
        # threading.Thread(target=lambda: dummy_tello_fn()).start()
        threading.Thread(target=lambda: flydo.takeoff()).start()
        flying = True
    

def run_app(HEIGHT=800, WIDTH=800):
    root = Tk()
    
    flydo = tello.Tello()
    flydo.connect()
    flydo.streamon()

    global flying
    flying = False # To toggle between takeoff and landing for button

    canvas = Canvas(root, height=HEIGHT, width=WIDTH)
    
    # For background image
    bg_dir = "C:\Users\charl\Desktop\flydo\Tacit.jpg"
    img = Image.open(bg_dir).resize((WIDTH, HEIGHT))
    bg_label = Label(root)
    bg_label.img = ImageTk.PhotoImage(img)
    bg_label["image"] = bg_label.img
    bg_label.place(x=0, y=0, relwidth=1, relheight=1)

    # Display current battery
    battery = Label(text=f"Battery: {int(flydo.get_battery())}%", font=("Verdana", 18), bg="#95dff3")
    bat_width = 200
    bat_height = 50
    battery.config(width=bat_width, height=bat_height)
    battery.place(x=(WIDTH - bat_width - 0.1*HEIGHT + bat_height), rely=0.9, relwidth=bat_width/WIDTH, relheight=bat_height/HEIGHT)

    # Takeoff/Land button
    forward_button = Button(root, text="Takeoff/Land", font=("Verdana", 18), bg="#95dff3", command=lambda: takeoff_land(flydo))
    fb_width = 200
    fb_height = 100
    forward_button.config(width=fb_width, height=fb_height)
    forward_button.place(x=(WIDTH/2 - fb_width/2), rely=0.61, relwidth=fb_width/WIDTH, relheight=fb_height/HEIGHT)

    cap_label = Label(root)
    cap_label.pack()
    
    def video_stream():
        h = 480
        w = 720
        frame = flydo.get_frame_read().frame
        frame = cv2.resize(frame, (w, h))
        cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
        img = Image.fromarray(cv2image)
        imgtk = ImageTk.PhotoImage(image=img)
        cap_label.place(x=WIDTH/2 - w/2, y=0)
        cap_label.imgtk = imgtk
        cap_label.configure(image=imgtk)
        cap_label.after(5, video_stream)

    video_stream()
    canvas.pack()
    root.mainloop()


if __name__ == "__main__":
    HEIGHT = 800
    WIDTH = 800

    run_app(HEIGHT, WIDTH)