Создание и обработка задач / tkinter 17

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Есть ситуации, когда определенная операция приводит к небольшому перерыву в работе программы. Он может занимать меньше секунды, но все равно будет заметен пользователю, ведь приведет к тому, что в это время графический интерфейс перестанет отвечать.

В этом материале рассмотрим, как справляться с такими ситуациями без необходимости обрабатывать целую задачу на отдельном потоке.

Возьмем пример из материала о «Запланированных действиях», но с паузой в 1, а не 5 секунд.

При изменении состояния кнопки на DISABLED функция обратного вызова продолжает выполнение, поэтому состояние кнопки не меняется до тех пор, пока система находится в состоянии ожидания. Это значит, что она будет ждать завершения time.sleep().

Однако можно сделать так, чтобы Tkinter принудительно обновил все элементы графического интерфейса в режиме ожидания в конкретный момент:


import time
import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать секунды")
self.button.pack(padx=30, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
self.update_idletasks()
time.sleep(1)
self.button.config(state=tk.NORMAL)
if __name__ == "__main__":
app = App()
app.mainloop()

Как работает обработка задач

Главная фишка здесь — вызов self.update_idletasks(). Благодаря этому изменение состояния кнопки обрабатывается Tkinter до вызова time.sleep(). И в ту секунду, пока функция обратного вызова приостановлена, кнопка выглядит так, как нужно, потому что Tkinter задает это состояние еще до вызова функции обратного вызова.

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

Создание отдельных процессов

В определенных ситуациях невозможно добиться нужного результата, просто используя потоки. Например, может потребоваться вызвать отдельную программу, написанную на другом языке.

В таком случае нужно использовать модуль subprocess для вызова определенной программы из процесса Python.

Следующий пример выполняет запрос на обозначенный DNS или IP адрес:

Создание отдельных процессов

Обычно определяется метод AsyncAction, но в этот раз вызовем subprocess.run() со значением в виджете Entry.

Эта функция запускает отдельный подпроцесс, который, в отличие от потоков, использует другую область памяти. Это значит, что для получения результата команды ping потребуется перенаправить его в стандартный вывод и прочесть в Python-программе:


import threading
import subprocess
import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.entry = tk.Entry(self)
self.button = tk.Button(self, text="Пинг!",
command=self.do_ping)
self.output = tk.Text(self, width=80, height=15)

self.entry.grid(row=0, column=0, padx=5, pady=5)
self.button.grid(row=0, column=1, padx=5, pady=5)
self.output.grid(row=1, column=0, columnspan=2,
padx=5, pady=5)

def do_ping(self):
self.button.config(state=tk.DISABLED)
thread = AsyncAction(self.entry.get())
thread.start()
self.poll_thread(thread)

def poll_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.poll_thread(thread))
else:
self.button.config(state=tk.NORMAL)
self.output.delete(1.0, tk.END)
self.output.insert(tk.END, thread.result) class AsyncAction(threading.Thread):
def __init__(self, ip):
super().__init__()
self.ip = ip

def run(self):
self.result = subprocess.run(["ping", self.ip], shell=True,
stdout=subprocess.PIPE).stdout.decode("CP866") if __name__ == "__main__":
app = App()
app.mainloop()

Как работает создание новых процессов

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

Эта функция вызывается с аргументом-ключевым словом shell и значением True, чтобы для процесса ping не открывалось новое окно терминала:


def run(self):
self.result = subprocess.run(["ping", self.ip], shell=True,
stdout=subprocess.PIPE).stdout.decode("CP866")

Наконец, когда основной поток подтверждает, что операция завершилась, он выводит результат в виджете Text:


def poll_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.poll_thread(thread))
else:
self.button.config(state=tk.NORMAL)
self.output.delete(1.0, tk.END)
self.output.insert(tk.END, thread.result)

Подписывайтесь на канал в Дзене

Полезный контент для начинающих и опытных программистов в канале Лента Python разработчика — Как успевать больше, делать лучше и не потерять мотивацию.

Обучение Python и Data Science

Профессия Data Scientist

Профессия Data Scientist

11 520 5 760 ₽/мес.
Профессия Python-разработчик

Профессия Python-разработчик

7 820 3 910 ₽/мес.
Профессия Python Fullstack

Профессия Python Fullstack

7 820 3 910 ₽/мес.
Курс Аналитик данных с нуля

Курс Аналитик данных с нуля

6 500 3 900 ₽/мес.

Появились вопросы? Задайте на Яндекс.Кью

У сайта есть сообщество на Кью >> Python Q <<. Там я, эксперты и участники отвечаем на вопросы по python и программированию.