Язык программирования Python

Фоновые задачи с помощью Tcl/Tk (tkinter) в Python

При разработке приложения с помощью стандартного модуля tkinter иногда возникает ситуация, когда “тяжелая” операция (т.е. та, на выполнение которой требуется не менее нескольких секунд) замораживает наше окно и все элементы управления.

Это приводит к тому, что пользователь больше не может взаимодействовать с окном, а наш код не может вносить изменения (например, увеличивать значение индикатора выполнения).

Например, когда мы пытаемся загрузить файл через HTTP, открыть большой системный файл, отправить письмо через SMTP, выполнить команду через подпроцесс и т.д.

Рассмотрим следующий код:

import tkinter as tk from tkinter import ttk from urllib.request import urlopen def download_file(): info_label["text"] = "Загрузка файла..." # Отключение кнопки на время загрузки файла. download_button["state"] = "disabled" url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" # Открываем адрес URL. with urlopen(url) as r: with open(filename, "wb") as f: # Чтение удаленного файла и запись локального файла. f.write(r.read()) info_label["text"] = "Файл загружен!" # Сброс кнопки. download_button["state"] = "normal" root = tk.Tk() root.title("Загрузка файла с помощью Tcl/Tk") info_label = ttk.Label(text="Нажмите кнопку , чтобы загрузить файл.") info_label.pack() download_button = ttk.Button(text="Скачать файл", command=download_file) download_button.pack() root.mainloop()
Code language: PHP (php)

Здесь у нас есть окно с кнопкой для загрузки файла через стандартный модуль urllib.request, которая при нажатии выполняет функцию download_file().

Внутри этой функции тяжелая операция происходит в строке 14, когда вызывается метод r.read(), который отвечает за загрузку содержимого удаленного файла.

Обратите внимание, что в строке 6, перед загрузкой файла, код отключает кнопку загрузки, а затем в строке 18, после загрузки, она снова включается.

Однако если мы запустим код, то увидим, что окно замирает в процессе загрузки, а пользователь так и не видит отключенную кнопку:

Чтобы увидеть разницу, вот как выглядит отключенная кнопка:

Почему так происходит?

Ответ прост: tkinter (в частности, функция mainloop()) и download_file() выполняются в одном потоке.

В то время как r.read() (или любая другая тяжелая функция) занимает внимание процессора, Tk не может выполнять свои операции по реагированию на события, происходящие в окне. Поэтому замирает.

В Python есть модуль threading, который позволяет запускать новые потоки, поэтому мы могли бы использовать его для перемещения функции download_file() в независимый поток, чтобы она не блокировала основной поток программы, в котором работает Tk.

Но вопрос не так прост: Tk позволяет изменять элементы управления интерфейса только из того же потока, в котором выполняется функция mainloop() (то есть он не является потоконезависимым).

Поэтому мы не можем изменить текст info_label или отключить и снова включить кнопку загрузки из нового потока.

Одно из решений этой проблемы следующее: перенесите в новый поток только те строки download_file(), которые связаны с загрузкой файла (в основном r.read(). Которая является тяжелой операцией), и оставьте в основном потоке те, которые связаны с Tk (изменение состояний элементов управления).

Но как главный поток узнает о завершении выполнения потока?

Для этого существует функция: threading.Thread.is_alive().

Получается, что мы можем каждый определенный момент времени проверять, завершился ли новый поток, чтобы знать, нужно ли нам снова включать кнопку (и другие операции, которые мы хотим выполнить по окончании тяжелой задачи).

Для выполнения этой проверки мы должны использовать функцию Tk after(), которая позволяет нам запланировать выполнение функции в течение нескольких секунд, чтобы избежать использования собственного цикла, который в итоге также блокирует интерфейс.

Без лишних слов, код выглядит так:

import threading import tkinter as tk from tkinter import ttk from urllib.request import urlopen def download_file_worker(): url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" # Открываем адрес URL. with urlopen(url) as r: with open(filename, "wb") as f: # Чтение удаленного файла и запись локального файла. f.write(r.read()) def schedule_check(t): """ Планирование выполнения функции `check_if_done()` в течение одной секунды. """ root.after(1000, check_if_done, t) def check_if_done(t): # Если поток закончился, сбросим кнопку и выведем сообщение. if not t.is_alive(): info_label["text"] = "Файл загружен!" # Сброс кнопки. download_button["state"] = "normal" else: # Если нет, проверим еще раз через некоторое время. schedule_check(t) def download_file(): info_label["text"] = "Загрузка файла..." # Отключение кнопки на время загрузки файла. download_button["state"] = "disabled" # Запустим загрузку в новом потоке. t = threading.Thread(target=download_file_worker) t.start() # Начнем периодически проверять, закончился ли поток. schedule_check(t) root = tk.Tk() root.title("Загрузка файла с помощью Tcl/Tk") info_label = ttk.Label(text="Нажмите кнопку , чтобы загрузить файл.") info_label.pack() download_button = ttk.Button(text="Скачать файл", command=download_file) download_button.pack() root.mainloop()
Code language: PHP (php)

Вот как это выглядит на этот раз.

Используя этот код в качестве основы, вы можете переместить любую тяжелую задачу в функцию download_file_worker(), а затем указать операции, которые должны быть выполнены в конце этой функции в check_if_done().

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *