Я создаю интерфейс в Tkinter, в котором главное окно (назовем его «корень») содержит кнопку (скажем, «создать»). Кроме того, предположим, что я уже определил функцию 'f'. Я хотел бы создать следующий эффект: щелчок по «создать» выполнял бы «f» в фоновом режиме и в то же время открывал неопределенный индикатор выполнения в новом окне. Более того, и это сложная часть для меня, я хочу, чтобы индикатор выполнения автоматически закрывался после выполнения «f». Как я могу этого добиться? Не могли бы вы привести минимальный рабочий пример? Я думаю, что ключ заключается в создании правильной функции для передачи в качестве параметра «команда» для «создания».
Это то, что у меня есть до сих пор. Он даже не работает должным образом, так как индикатор выполнения работает бесконечно, и задача начинает выполняться только после закрытия индикатора выполнения (или после закрытия «root»). Однако мне кажется, что это действительно близко, и есть небольшая проблема, которую я должен исправить, но которую я не вижу:
from tkinter import *
from tkinter.ttk import *
import threading
import time
root = Tk() # Main window
def create_command():
stop_flag = threading.Event() # create a flag to stop the progress bar
def f():
# function to do some task
print("Starting task...")
time.sleep(5) # simulate some time-consuming task
print("Task complete.")
stop_flag.set() # set the stop flag to indicate that progress_check() should stop
progress_bar_window = Toplevel(root) # Progress bar window
progress_bar = Progressbar(progress_bar_window, orient= 'horizontal', length= 300, mode= 'indeterminate') # Create progress bar
progress_bar.pack()
progress_bar.start()
def progress_check():
# function to run an infinite loop
while not stop_flag.is_set():
print("Running infinite loop...")
time.sleep(1)
progress_bar.stop()
progress_bar_window.destroy()
progress_bar_window.mainloop() # Start mainloop for progress bar window
# create separate threads to run the functions
thread1 = threading.Thread(target=f, args=())
thread2 = threading.Thread(target=progress_check, args=())
thread1.start() # start executing f
thread2.start() # start the progress_check
# wait for f to finish before stopping the infinite loop
thread2.join()
stop_flag.set() # set the stop flag to indicate that progress_bar() should stop
create_button = Button(root, text= "Create", command= create_command)
create_button.pack()
root.mainloop()
Привет @Thingamabobs! Я не смог найти в Интернете пример, аналогичный тому, что я спрашиваю здесь. Кроме того, пока f работает, я не буду нажимать кнопку: я подожду, пока он не остановится. Кроме того, я хочу получить вывод «f» из работающего потока и закрыть индикатор выполнения после завершения выполнения f, чтобы двигаться дальше по жизни :). Заранее спасибо!
Вы должны обновить свой вопрос с этой информацией. Я мог бы изучить его в ближайшие дни, если у вас нет хорошего ответа до сих пор.
Посмотри на это:
from tkinter import ttk
import tkinter as tk
import threading
import time
root = tk.Tk() # Main window
def create_command():
# create a flag to stop the progress bar
stop_flag = threading.Event()
def f():
print("Starting task...\n", end = "")
time.sleep(5)
print("Task complete.\n", end = "")
# set the stop flag to indicate that progress_check() should stop
stop_flag.set()
progress_bar_window = tk.Toplevel(root)
progress_bar = ttk.Progressbar(progress_bar_window, orient = "horizontal",
length=300, mode = "indeterminate")
progress_bar.pack()
progress_bar.start()
def progress_check():
# If the flag is set (function f has completed):
if stop_flag.is_set():
# Stop the progressbar and destroy the toplevel
progress_bar.stop()
progress_bar_window.destroy()
else:
# If the function is still running:
print("Running infinite loop...\n", end = "")
# Schedule another call to progress_check in 100 milliseconds
progress_bar.after(100, progress_check)
# start executing f in another thread
threading.Thread(target=f, daemon=True).start()
# Start the tkinter loop
progress_check()
create_button = tk.Button(root, text= "Create", command=create_command)
create_button.pack()
root.mainloop()
Объяснение:
Чтобы запустить цикл вместе с tkinter, вы должны использовать .after, как в этом вопросе. Я изменил progress_check так, чтобы tkinter вызывал его каждые 100 миллисекунд, пока не будет установлено stop_flag. Когда установлено stop_flag, индикатор выполнения останавливается, а Toplevel уничтожается.
Несколько незначительных моментов:
from ... import *
не рекомендуетсяtkinter
вам не нужно больше 1 .mainloop()
, если вы не используете .quit()
. .mainloop()
не остановится, пока все tk.Tk
окна не будут уничтожены..join()
.Во-первых, не используйте импорт с подстановочными знаками! Импорт подстановочных знаков может привести к конфликтам имен, например, поменять местами импорт подстановочных знаков из ttk и tkinter. В конечном итоге вы используете кнопки tkinter, даже если хотите использовать кнопки ttk. Та же проблема может возникнуть с PhotoImage и подушкой. Волшебное слово "квалифицированные имена".
Также мне нравится иметь какую-то структуру в моем коде, я предпочитаю классы. Однако даже в процедурном коде может быть какая-то структура. Например:
Каждый логический блок может быть разделен комментариями, указывающими, что следующий код может делать или представлять. Это также может быть полезно для «перехода» с помощью функции поиска вашей IDE к точке, с которой вы хотите работать дальше, в больших сценариях и модулях это становится удобным.
Несколько иную версию вашего кода можно найти ниже, и она не предназначена для использования:
import tkinter as tk
from tkinter import ttk
import threading
import time
def start_worker_thread():
'This function starts a thread and pops up a progressbar'
def generate_waiting_window():
'nested function to generate progressbar'
#disable button to inform user of intended use
start_btn.configure(state=tk.DISABLED)
#toplevel definitions
toplevel = tk.Toplevel(root)
toplevel.focus()
#progressbar definitions
progress = ttk.Progressbar(
toplevel, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress.pack(fill=tk.BOTH, expand=True)
progress.start()
return toplevel
def long_blocking_function():
'This function simulates a long blocking call'
stopped = threading.Event()
n = 0
while not stopped.is_set():
n += 1
print('working in turn', n)
time.sleep(0.5)
if n == 10:
stopped.set()
nonlocal thread_info
thread_info = n
#important!! last logical line
toplevel.destroy()
return None
toplevel = generate_waiting_window()
thread_info = None
thread = threading.Thread(target=long_blocking_function)
thread.start()
toplevel.wait_window()
start_btn.configure(state='normal')
result_lbl.configure(text='Result is: '+str(thread_info))
print('thread exited on turn', thread_info)
#Main window definitions
root = tk.Tk()
start_btn = ttk.Button(root, text = "Start", command=start_worker_thread)
start_btn.pack()
result_lbl = tk.Label(root, text='Result is: None')
result_lbl.pack()
#start the application
root.mainloop()
#after application is destroyed
Хотя этот код эффективен для этой простой задачи, он требует понимания того, что он делает для его отладки. Вот почему вы не часто найдете такой код. Это здесь для демонстрационных целей. Так что же не так с кодом и чем он отличается от пока что канонического способа использования тредов в tkinter.
Прежде всего, он использует вложенную функцию. Хотя здесь это может и не быть проблемой, повторное вычисление одной и той же функции может значительно замедлить работу вашего кода. Во-вторых, он использует tkwait и поэтому имеет некоторые оговорки по поводу связанного ответа. Также threading.Event является низкоуровневым примитивом для общения, хотя есть случаи, когда вы могли бы его использовать, tkinter предлагает для него собственные инструменты, и им следует отдавать предпочтение. Кроме того, он не использует потокобезопасное хранилище для данных, что также может привести к путанице и недостоверности данных.
Лучший подход и небольшое улучшение канонического способа можно найти здесь:
import tkinter as tk
from tkinter import ttk
import threading
import sys
import queue
import time
inter_thread_storage = queue.Queue()
temporary_toplevel = None
EXIT = False
def on_thread_ended_event(event):
start_btn.configure(state=tk.NORMAL)
result = inter_thread_storage.get_nowait()
result_lbl.configure(text='Result is: '+str(result))
global temporary_toplevel
temporary_toplevel.destroy()
temporary_toplevel = None
def worker_thread_function():
'Simulates a long blocking function'
n = 0
while n < 10 and not EXIT:
n += 1
print('working in turn', n)
time.sleep(0.5)
if not EXIT:
inter_thread_storage.put(n)
root.event_generate('<<ThreadEnded>>')
def start_worker_thread():
'This function starts a thread and pops up a progressbar'
#toplevel definitions
toplevel = tk.Toplevel(root)
toplevel.focus()
#progressbar definitions
progress = ttk.Progressbar(
toplevel, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress.pack(fill=tk.BOTH, expand=True)
progress.start()
#thread definitions
thread = threading.Thread(target=worker_thread_function)
thread.start()
#disable button to inform user of intended use
start_btn.configure(state=tk.DISABLED)
#store toplevel temporary
global temporary_toplevel
temporary_toplevel = toplevel
#Main window definitions
root = tk.Tk()
root.bind('<Destroy>',lambda e:setattr(sys.modules[__name__], 'EXIT', True))
root.bind('<<ThreadEnded>>', on_thread_ended_event)
start_btn = ttk.Button(root, text = "Start", command=start_worker_thread)
start_btn.pack()
result_lbl = tk.Label(root, text='Result is: None')
result_lbl.pack()
#start the application
root.mainloop()
#after application is destroyed
Вот как это работает:
Дайте мне знать, если у вас есть вопросы к моему ответу.
Вызов методов tkinter из потоков, отличных от того, в котором был создан tk.Tk, не является хорошей идеей. И какой смысл звонить wait_window?
@TheLizzard, ты не читал мой ответ. Вы? Там какой-то жирный текст.
Посмотрите на это, я немного изменил ваш 2-й фрагмент кода, и теперь он вызывает RuntimeError, если главное окно закрывается до завершения 2-го потока. Поэтому я думаю, что утверждение «небольшое улучшение канонического образа» неверно.
@TheLizzard, верно, исправил. Что-нибудь еще ? Также обратите внимание, что у вас может быть несколько событий для разных задач, и они всегда работают ТОЛЬКО при необходимости. Так что я думаю, что это улучшение.
Это просто придирка, но теоретически (на практике этого никогда не произойдет), если пользователь закроет главное окно сразу после вызова event_generate, единственная ссылка на root будет во втором потоке. Поэтому сборщик мусора Python удалит root из второго потока, что может привести к сбою Python. Вот почему я всегда советую избегать вызова методов tkinter из потоков, отличных от потока tk.Tk.
@TheLizzard Я тоже мог бы это исправить, но чтобы этот пример не усложнялся так, как должен быть, я оставлю его так. Для тех, кто заинтересован в этом, вы можете попробовать блокировку try и exclude, фильтруя сообщение об ошибке tcl и игнорируя конкретный случай.
Спасибо за ответ и советы по структуре кода (+1)!
@JohnD, надеюсь, вы однажды заметите, насколько полезен этот подход.
Вы знаете, что в сети уже есть примеры, в том числе и на этом сайте? Это помогло бы нам написать лучший ответ на то, с чем у вас возникли проблемы. Также было бы неплохо знать, планируете ли вы использовать кнопку несколько раз во время работы f или хотите просто повторно использовать ее после выполнения или что-то еще. Также вам нужна другая информация из работающего потока, кроме того, когда это делается, какие-то данные объекта или что-то еще?