Вопрос
Как заставить приложение Python tkinter корректно закрываться в конце нормальной работы, если пользователь закрывает окно, прерывая выполняемую обработку?
Подробнее
Приложение tkinter запускает долговременную функцию long.work() при нажатии кнопки, эта функция запускается в новом потоке, что позволяет приложению tkinter реагировать во время длительного выполнения.
Некоторые функции, вызываемые в work(), являются многопоточными через ThreadPoolExecutor< /код> (concurrent_tasks.do_multi_threads()), а другие обрабатываются в многопроцессорном режиме с помощью joblib Parallel() и Delay() (parallel_tasks.do_multi_process()).
Поэтому плавное завершение должно обрабатывать случаи, когда завершение пользователя не происходит, завершение пользователя происходит после завершения функции, прерывание пользователя происходит в многопоточном и многопроцессном состояниях, а также обрабатывать закрытие средств ведения журнала, которые используют QueueListener и QueueHandler для записи сообщений журнала в файл журнала.
Я рассмотрел несколько подобных вопросов здесь, на SO, но ни одного охватить все необходимые варианты использования.
Пример
Мой подход до сих пор выдает ошибку при закрытии окна, прерывая многопоточную функцию (среди прочего):< /p>
app.py
import logging
from logging.handlers import QueueHandler, QueueListener
import sys
import multiprocessing
import threading
import tkinter as tk
from tkinter import Tk, ttk
from joblib.externals.loky import get_reusable_executor
import long
def finish(
logger: logging.Logger,
listener: QueueListener = None,
log_queue: multiprocessing.Queue = None,
threads: bool = False,
lock: threading.Lock = None,
root: tk.Tk = None,
exit_code: int = 0,
):
# QueueHandler
if log_queue is not None:
log_queue.put_nowait(None)
# QueueListener
if listener is not None:
listener.stop()
# joblib
get_reusable_executor(kill_workers=True).shutdown(wait=True)
# multiprocessing
for p in multiprocessing.active_children():
p.terminate()
p.join()
# threading
if threads is True:
for thread in threading.enumerate():
if thread is not threading.current_thread():
thread.join(timeout=1)
# logger
for handler in logger.handlers:
handler.flush()
handler.close()
logger.handlers.clear()
# lock
if lock is not None and lock.locked():
lock.release()
# tkinter
if root is not None:
root.destroy()
sys.exit(exit_code)
def main():
root = Tk()
# logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
log_fmt = logging.Formatter(
"%(asctime)s (%(levelname)s) %(message)s", datefmt="%H:%M:%S"
)
file_handler = logging.FileHandler("log_file.log")
file_handler.setFormatter(log_fmt)
file_handler.setLevel(logging.DEBUG)
# note also a StreamHandler is added to QueueListener
manager = multiprocessing.Manager()
log_queue = manager.Queue()
queue_handler = QueueHandler(log_queue)
logger.addHandler(queue_handler)
listener = QueueListener(log_queue, file_handler, respect_handler_level=True)
listener.start()
def begin():
# entry_vals = get Entry values...
exit_code = 0
lock = threading.Lock()
def terminate():
finish(logger, listener, log_queue, threads=True, lock=lock, root=root, exit_code=exit_code)
root.protocol("WM_DELETE_WINDOW", terminate)
def start(): # EDIT
try:
long.work(entry_vals)
finally:
button.config(state=tk.NORMAL) # re-enable
finish(logger, listener, log_queue, threads=True, lock=lock, root=None, exit_code=exit_code)
try: # EDIT (indentation)
set_progress = entry_vals.get("update_status")
if set_progress: # EDIT
set_progress("begin", 1) # EDIT
logging. Debug("Starting GUI...")
button.config(state=tk.DISABLED) # disable button
threading.Thread(target=start).start() # EDIT
except Exception as e:
exit_code = 1
logging.exception(e)
finish(logger, listener, log_queue, threads=False, lock=None, root=None, exit_code=exit_code)
frame = ttk.Frame(root)
frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
button = ttk.Button(frame, text="Start", command=begin)
root.mainloop()
if __name__ == "__main__":
main()
long.py
import logging
from concurrent. Futures import ThreadPoolExecutor
from functools import partial
from joblib import Parallel, delayed
import concurrent_tasks
import parallel_tasks
def work(entry_vals):
set_progress = entry_vals.get("update_status")
partial_func = partial(concurrent_tasks.do_multi_threads, entry_vals)
if set_progress: # EDIT
set_progress("work", 10) # EDIT
logging.debug("Starting multithreaded function...") # EDIT
with ThreadPoolExecutor() as executor:
res_1 = list(executor.map(partial_func, range(10)))
if set_progress: # EDIT
set_progress("work", 60) # EDIT
logging.debug("Starting multiprocessed function...") # EDIT
with Parallel(n_jobs=-1) as parallel_work:
res_2 = list(parallel_work(delayed(parallel_tasks.do_multi_process)(entry_vals, i) for i in range(10)))
return res_1, res_2
Дополнительная информация
Это изменение дополняет упрощенное представление кода, приведенное выше: Существует некоторое функциональное программирование, которое позволяет увеличивать tk.ttk.Progressbar< /code> путем установки tk.IntVar, который является аргументом переменной параметра Progressbar.
app.pydef update_status(
progress_var,
progress_bar,
percent_label,
status_label,
lock
):
def callback(section, percent):
with lock:
if percent is not None:
progress_var.set(percent)
percent_label["text"] = f"{progress_var.get()}%"
status_label["text"] = section
progress_bar.update_idletasks()
percent_label.update_idletasks()
status_label.update_idletasks()
progress_bar.after(10000, callback)
return callback
Ошибка
Exception has occurred: RuntimeError
main thread is not in main loop
in start:
long.work(entry_vals)
in work:
set_progress("work", 60)
in callback:
progress_var.set(percent)
RuntimeError: main thread is not in main loop
During handling of the above exception, another exception occurred:
in start:
button.config(state=tk.NORMAL)
RuntimeError: main thread is not in main loop
Подробнее здесь: https://stackoverflow.com/questions/791 ... essing-que
Грамотно завершить работу приложения tkinter (включая многопоточность, многопроцессорность, QueueHandler, QueueListener) ⇐ Python
-
- Похожие темы
- Ответы
- Просмотры
- Последнее сообщение