当用户在应用程序中嵌入的交互式绘图上选择值时,如何暂停 tkinter 应用程序

How to put a tkinter application on hold while the user pick values on an interactive plot embedded in the app

我正在尝试构建一个 tkinter 应用程序来处理数据,在这个过程中的某个时刻,用户必须在交互式图形上选择值。我把这项工作做得很好,但我遇到了一个问题。

基本上,我的应用程序中有一个启动进程的按钮。时间到了,我创建了一个 TopLevel 小部件,其中嵌入了一个交互式图表,用户可以通过单击选择一个出现在图表上的值。但是,即使用户尚未选择值,该过程也不会暂停。所以我想要的是暂停该过程,直到用户选择一个值。

正如您可能已经从标签中注意到的那样,我尝试使用线程来完成,但没有成功。由于我以前从未尝试过使用线程,所以很可能我做错了什么。

这是我到目前为止编写的一段代码以及我尝试使用线程模块所做的代码:

    def __init__(self, parameters, main_menu):

        # We use this attributes for the TopLevel widget
        self.main_menu = main_menu
        self.gui = self.main_menu.gui

        self.color_font = main_menu.color_font
        self.background_color = main_menu.background_color
        self.button_font = main_menu.button_font

        # Proper data related attributes
        super().__init__(parameters)
        self.worksheet = None
        self.pressure = []
        self.V60 = []
        self.corrected_volume = []
        self.membrane_pressure = []
        self.corrected_pressure = []

        self.figure = None
        self.ax = None

        self.P1 = DoubleVar(value=0)
        self.P2 = DoubleVar(value=0)
        self.V1 = DoubleVar(value=0)
        self.V2 = DoubleVar(value=0)

        self.filename = self.parameters["Corrected data filename"].get()
        self.new_filename = self.filename

        # We use this attributes for the TopLevel widget used for the choice of (P1,V1) and (P2,V2)
        self.selection_window = None
        self.selection_window_width = 1080
        self.selection_window_height = 720

        self.frame = None
        self.canvas = None

        self.toolbar = None

        self.index1 = [0]
        self.index2 = [0]
        self.artist = None

        self.clicked1 = 0
        self.clicked2 = 0

        self.choice_over = BooleanVar(value=False)
        self.result = threading.Event()

    def animate(self, i):
        if self.artist is None and self.clicked1 > 0:
            self.artist = self.ax.scatter(self.P1.get(), self.V1.get(), color='r', s=100)

    def on_pick(self, event):
        self.index1 = event.ind
        self.P1.set(value=self.corrected_pressure[self.index1[0]])
        self.V1.set(value=self.corrected_volume[self.index1[0]])
        if self.artist is not None:
            self.artist.remove()
            self.artist = None
        self.clicked1 += 1

    def display_selection_window(self):

        try:
            self.selection_window = Toplevel(self.gui.root, bg=self.background_color)
            self.selection_window.withdraw()
            self.selection_window.iconbitmap('assets/Logo.ico')
            self.selection_window.geometry(f'{self.selection_window_width}x{self.selection_window_height}'
                                           f'+{int((self.selection_window.winfo_screenwidth() - self.selection_window_width) / 2)}'
                                           f'+{- 50 + int((self.selection_window.winfo_screenheight() - self.selection_window_height) / 2)}')
            self.selection_window.resizable(width=False, height=False)

            self.frame = Frame(self.selection_window, bg=self.background_color)

            self.canvas = FigureCanvasTkAgg(self.figure, master=self.frame)
            self.figure.canvas.mpl_connect('pick_event', self.on_pick)
            self.toolbar = CustomToolbar(self.canvas, self.frame)
            self.toolbar.config(background=self.background_color)
            self.toolbar.update()

            self.confirmation_button = Button(self.selection_window, text="confirm",
                                              command=lambda: [self.selection_window.destroy(), self.result.set()])

            self.frame.pack()
            self.canvas.draw()
            self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH)
            self.toolbar.pack(fill=X)
            self.confirmation_button.pack()
            self.selection_window.deiconify()

            ani = FuncAnimation(self.figure, lambda i: self.animate(i), interval=100, cache_frame_data=False)
            
            
        except:
            Log().write_log_exception()

    def complete_data_process(self):

        progress_bar_canvas = 0

        try:

            assert os.path.exists(self.gui.parameters["Data path"].get())

            number_of_tasks = 4

            progress_bar_canvas = Canvas(self.gui.root, width=400, height=20, bd=0, highlightthickness=0)
            progress_bar_canvas.create_rectangle(0, 0, progress_bar_canvas.cget("width"),
                                                 progress_bar_canvas.cget("height"), fill="", width=3)

            progress_bar_canvas.place(relx=0.5, rely=0.9, anchor=CENTER)

            self.tube_calibration.load_data()
            self.tube_calibration.save_figure()

            self.update_progress_bar(progress_bar_canvas, 1, number_of_tasks)

            self.air_calibration.load_data()
            self.air_calibration.save_figure()

            self.update_progress_bar(progress_bar_canvas, 2, number_of_tasks)

            self.corrected_data.load_data()
            self.corrected_data.save_figure(coefficient=self.tube_calibration.line_equation[0],
                                            interpolator=self.air_calibration.interpolator)

            self.update_progress_bar(progress_bar_canvas, 3, number_of_tasks)

            thread = threading.Thread(target=self.corrected_data.display_selection_window)
            thread.start()

            self.corrected_data.result.wait()
            
            self.update_progress_bar(progress_bar_canvas, 4, number_of_tasks)

            progress_bar_canvas.place_forget()

            answer = messagebox.askquestion('Succès',
                                            "Le traitement complet des données a été effectué avec succès.\n"
                                            f"Les graphes ont été sauvegardés dans {self.tube_calibration.user_dir}"
                                            f"\{self.tube_calibration.dir}.\n\nVoulez-vous ouvrir le dossier?")

            if answer == "yes":
                os.startfile(f"{self.tube_calibration.user_dir}/{self.tube_calibration.dir}/")

我也试过这个

while not self.choice_over.get():
    ani = FuncAnimation(self.figure, lambda i: self.animate(i), interval=100, cache_frame_data=False)

但是没用。

因此,如果您提出解决方案,即使它不使用线程(老实说会容易得多),我也提前感谢您。

抱歉,如果我的英语有时不准确,抱歉这么长 post。

感谢您的宝贵时间,

保罗

tkinter 有专门针对这种情况的功能。 wait_window 将等到 window 被销毁。您还可以等待使用 wait_variable.

设置变量

让您的代码正常工作需要付出太多的努力,但这里有一个非常简单的示例来说明这一点。单击“单击我”,您将看到一个对话框,让您选择一种颜色。当您单击一种颜色时,它会设置一个变量。 show 方法将等待变量设置后再返回。

import tkinter as tk

class CustomDialog(tk.Toplevel):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.color_var = tk.StringVar()
        self.withdraw()
        self.label = tk.Label(self, text="Choose a color")
        self.canvas = tk.Canvas(self, background="bisque", width=200, height=200)
        self.label.pack(side="top")
        self.canvas.pack(fill="both", expand=True)

        x = 5
        y = 10
        for color in ("red", "yellow", "orange", "green", "blue"):
            self.canvas.create_rectangle(x, y, x+40, y+40, fill=color, outline="")
            x += 40

        self.canvas.bind("<1>", self.set_color)

    def show(self):
        self.deiconify()
        self.wait_variable(self.color_var)
        return self.color_var.get()

    def set_color(self, event):
        item = self.canvas.find_closest(event.x, event.y)
        self.color_var.set(self.canvas.itemcget(item[0], "fill"))
        self.withdraw()

def do_something():
    dialog = CustomDialog()
    color = dialog.show()
    root.configure(background=color)
    dialog.destroy()

root = tk.Tk()
button = tk.Button(root, text="Click me", command=do_something)
button.pack(padx=20, pady=20)

root.mainloop()