В некоторых случаях случаются ситуации, когда перерыв в работе приложения слишком большой. Причем «слишком большой» – это понятие растяжимое. В некоторых случаях даже секунда может быть на счету. Поэтому недопустимо зависание интерфейса на это время.
Сегодня рассмотрим, что делать для того, чтобы предотвращать такие ситуации, не задействуя для этого отдельный поток. В качестве примера будем использовать тот, который был в материале о «Запланированных действиях», только пауза у нас будет в 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(). Это позволяет изменять состояние кнопки до того, как будет вызван time.sleep(). И, следовательно, мы можем сделать так, чтобы кнопка выглядела правильно. Технически Tkinter задал это состояние еще до того, как была исполнена функция обратного вызова.
Чтобы проиллюстрировать этот пример, мы использовали метод time.sleep(), но это очень упрощенный вариант. В реальных же ситуациях придется использовать гораздо более сложные вычисления.
Как создавать отдельные процессы?
В некоторых случаях нет возможности обеспечить необходимый результат, используя разные потоки. Например, возможно, потребуется вызвать программу, для создания которой использовался совсем другой язык программирования.
В таких случаях используется модуль subprocess, с помощью которого можно вызывать определенную программу из процесса Python.
Приведем пример приложения, который выполняет запросы на адрес, который был указан пользователем в специальном поле ввода.
Как правило, для этого определяется метод 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)