如何在Tkinter中为按钮创建新线程/杀死活动线程



我目前正在为我的DJI Tello开发Tkinter GUI,我正在努力使其在命令无人机起飞/降落时,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

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

以下是到目前为止我所拥有的(在takeff_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)

最新更新