Tkinter - Обмен потоками и общий результат - с индикатором выполнения

Я создаю интерфейс в 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()

Вы знаете, что в сети уже есть примеры, в том числе и на этом сайте? Это помогло бы нам написать лучший ответ на то, с чем у вас возникли проблемы. Также было бы неплохо знать, планируете ли вы использовать кнопку несколько раз во время работы f или хотите просто повторно использовать ее после выполнения или что-то еще. Также вам нужна другая информация из работающего потока, кроме того, когда это делается, какие-то данные объекта или что-то еще?

Thingamabobs 12.01.2023 19:51

Привет @Thingamabobs! Я не смог найти в Интернете пример, аналогичный тому, что я спрашиваю здесь. Кроме того, пока f работает, я не буду нажимать кнопку: я подожду, пока он не остановится. Кроме того, я хочу получить вывод «f» из работающего потока и закрыть индикатор выполнения после завершения выполнения f, чтобы двигаться дальше по жизни :). Заранее спасибо!

John D 12.01.2023 20:18

Вы должны обновить свой вопрос с этой информацией. Я мог бы изучить его в ближайшие дни, если у вас нет хорошего ответа до сих пор.

Thingamabobs 12.01.2023 20:23
Как подобрать выигрышные акции с помощью анализа и визуализации на Python
Как подобрать выигрышные акции с помощью анализа и визуализации на Python
Отказ от ответственности: Эта статья предназначена только для демонстрации и не должна использоваться в качестве инвестиционного совета.
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
Потяните за рычаг выброса энергососущих проектов
Потяните за рычаг выброса энергососущих проектов
На этой неделе моя команда отменила проект, над которым я работал. Неделя усилий пошла насмарку.
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Библиотека для работы с мороженым
Библиотека для работы с мороженым
Лично я попрощался с операторами print() в python. Без шуток.
Эмиссия счетов-фактур с помощью Telegram - Python RPA (BotCity)
Эмиссия счетов-фактур с помощью Telegram - Python RPA (BotCity)
Привет, люди RPA, это снова я и я несу подарки! В очередном моем приключении о том, как создавать ботов для облегчения рутины. Вот, думаю, стоит...
0
3
142
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Посмотри на это:

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 и подушкой. Волшебное слово "квалифицированные имена".

Также мне нравится иметь какую-то структуру в моем коде, я предпочитаю классы. Однако даже в процедурном коде может быть какая-то структура. Например:

  1. импорт
    1.0) встроенные модули
    1.1) импортировать внешние модули
    1.2) импортировать собственные модули
  2. Константы и глобальные переменные
  3. бесплатные функции
  4. определения главного окна
    ...

Каждый логический блок может быть разделен комментариями, указывающими, что следующий код может делать или представлять. Это также может быть полезно для «перехода» с помощью функции поиска вашей 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 безопасно вызвать вашу функцию в mainloop.
  • у него есть флаг для пограничного случая, когда пользователь закрывает главное окно до завершения потока.

Дайте мне знать, если у вас есть вопросы к моему ответу.

Вызов методов tkinter из потоков, отличных от того, в котором был создан tk.Tk, не является хорошей идеей. И какой смысл звонить wait_window?

TheLizzard 12.01.2023 23:23

@TheLizzard, ты не читал мой ответ. Вы? Там какой-то жирный текст.

Thingamabobs 12.01.2023 23:25

Посмотрите на это, я немного изменил ваш 2-й фрагмент кода, и теперь он вызывает RuntimeError, если главное окно закрывается до завершения 2-го потока. Поэтому я думаю, что утверждение «небольшое улучшение канонического образа» неверно.

TheLizzard 12.01.2023 23:35

@TheLizzard, верно, исправил. Что-нибудь еще ? Также обратите внимание, что у вас может быть несколько событий для разных задач, и они всегда работают ТОЛЬКО при необходимости. Так что я думаю, что это улучшение.

Thingamabobs 12.01.2023 23:54

Это просто придирка, но теоретически (на практике этого никогда не произойдет), если пользователь закроет главное окно сразу после вызова event_generate, единственная ссылка на root будет во втором потоке. Поэтому сборщик мусора Python удалит root из второго потока, что может привести к сбою Python. Вот почему я всегда советую избегать вызова методов tkinter из потоков, отличных от потока tk.Tk.

TheLizzard 12.01.2023 23:59

@TheLizzard Я тоже мог бы это исправить, но чтобы этот пример не усложнялся так, как должен быть, я оставлю его так. Для тех, кто заинтересован в этом, вы можете попробовать блокировку try и exclude, фильтруя сообщение об ошибке tcl и игнорируя конкретный случай.

Thingamabobs 13.01.2023 00:02

Спасибо за ответ и советы по структуре кода (+1)!

John D 13.01.2023 17:36

@JohnD, надеюсь, вы однажды заметите, насколько полезен этот подход.

Thingamabobs 14.01.2023 14:57

Другие вопросы по теме