Скачайте код уроков с 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)