Попытка исправить зависания в графическом интерфейсе tkinter (используя потоки) - PullRequest
0 голосов
/ 28 ноября 2018

У меня есть создатель отчетов Python 3.x, который так привязан к вводу / выводу (из-за SQL, а не python), что главное окно будет «заблокировано» на минут во время создания отчетов,

Все, что нужно, - это возможность использовать стандартные действия окна (перемещение, изменение размера / сворачивания, закрытие и т. Д.), Пока графический интерфейс пользователя заблокирован (все остальное в графическом интерфейсе может оставаться «замороженным» довсе отчеты закончены).

Добавлено 20181129: Другими словами, tkinter должен контролировать только СОДЕРЖАНИЕ окна приложения и оставлять обработку всех стандартных (внешних) оконных элементов управления для O / S.Если я могу это сделать, моя проблема исчезает, и мне не нужно использовать все потоки / подпроцессы (замораживание становится приемлемым поведением, аналогичным отключению кнопки «Делать отчеты»).

Что является самым простым /Простейший способ (= минимум нарушение существующего кода) сделать это - в идеале способом, который работает с Python> = 3.2.2, и кросс-платформенным способом (то есть работает по крайней мере на Windows и Linux),


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

Что следует учитывать:

  • Пользователи выбирают свои отчеты, затем нажимают кнопку «Создать отчеты» в главном окне (когда начинается настоящая работа и происходит остановка).После того, как все отчеты завершены, код создания отчета отображает (Toplevel) окно «Готово».Закрытие этого окна активирует все в главном окне, позволяя пользователям выходить из программы или создавать дополнительные отчеты.

  • Добавлено 20181129: через, по-видимому, случайные интервалы (с интервалом в несколько секунд) я могу перемещать окно.

  • За исключением отображения окна «Готово»код создания отчета никак не связан с GUI или tkinter.

  • Некоторые данные, полученные с помощью кода создания отчета, должны появиться в окне «Готово».

  • Нет смысла «распараллеливать» создание отчетов, тем более что один и тот же сервер SQL и база данных используются для создания всех отчетов.

  • В случае, если это повлияет на решение: в конечном итоге мне нужно будет отображать имена отчетов (теперь отображаются на консоли) в графическом интерфейсе при создании каждого отчета.

  • В первый раз я выполнял многопоточность / подпроцессирование с python, но знаком с обоими языками.

  • Добавлено 20181129: среда разработки 64-битная Python 3.6.4 на Win10 с использованием Eclipse Oxygen (плагин pydev).Приложение должно быть переносимо как минимум на linux.


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

DoChosenReports()  # creates all reports (and the "Done" window)

при изменении на:

from threading import Thread

CreateReportsThread = Thread( target = DoChosenReports )
CreateReportsThread.start()
CreateReportsThread.join()  # 20181130: line omitted in original post, comment out to unfreeze GUI 

успешно создает отчеты с их именами, отображаемыми в консоли по мере их создания.
Однако графический интерфейсостается замороженным, и окно «Готово» (теперь вызывается новым потоком) никогда не появляется.Это оставляет пользователя в подвешенном состоянии, неспособным сделать что-либо и задается вопросом, что, если что-нибудь, произошло (вот почему я хочу отображать имена файлов в графическом интерфейсе по мере их создания).

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

Я также пытался использовать

from multiprocessing import Process

ReportCreationProcess = Process( target = DoChosenReports )
ReportCreationProcess.start()

, но это застрялоиз основных программ "if (__name__ == '__main__):'" test.


Добавлено 20181129: только что обнаружен метод универсального виджета waitvariable (см. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html). Основная идея состоит в том, чтозапустить код создания отчета как поток do-forever (демон?), управляемый этим методом (с выполнением, управляемым кнопкой «Делать отчеты» в графическом интерфейсе).


Из веб-исследований, которые я знаючто все действия tkinter должны выполняться из основного (родительского) потока,Это означает, что я должен переместить окно «Готово» в этот поток.
Мне также нужно, чтобы это окно отображало некоторые данные (три строки), которые он получает из «дочернего» потока.Я подумываю об использовании глобалов уровня приложения в качестве семафоров (записываемых только потоком создания отчета и считываемых только основной программой) для передачи данных.Я знаю, что это может быть рискованно с более чем двумя потоками, но делать что-то большее (например, с использованием очередей?) Для моей простой ситуации кажется излишним.


Подводя итог: Какой самый простой способ разрешить пользователю манипулировать (перемещать, изменять размер, минимизировать и т. Д.) Главным окном приложения, пока оно по какой-либо причине заморожено.Другими словами, O / S, а не tkinter, должен управлять фреймом (снаружи) главного окна.
Ответ должен работать на Python 3.2.2+ кросс-платформенным способом (по крайней мере, на Windows &Linux)

Ответы [ 3 ]

0 голосов
/ 29 ноября 2018

Я нашел хороший пример, похожий на то, что вы хотите сделать из одной из моих книг, которая, как мне кажется, показывает хороший способ использования потоков с tkinter.Это рецепт 9.6 для Объединение Tkinter и асинхронного ввода-вывода с потоками в первом издании книги Алексея Мартинелли и Дэвида Ашера Python Cookbook .Код был написан для Python 2.x, но для работы в Python 3 требовались лишь незначительные изменения.

Как я уже говорил в комментарии, вы должны поддерживать графический интерфейс GUI, если вы хотите иметь возможность взаимодействоватьс ним или просто изменить размер или переместить окно.Приведенный ниже пример кода делает это с помощью Queue для передачи данных из потока фоновой обработки в основной поток GUI.

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

С момента after() является частью tkinter, он позволяет mainloop() продолжать работу, что поддерживает «живой» графический интерфейс пользователя между этими периодическими проверками очереди.Он также может делать tkinter вызовы для обновления графического интерфейса при желании (в отличие от кода, который выполняется в отдельных потоках).

from itertools import count
import sys
import tkinter as tk
import tkinter.messagebox as tkMessageBox
import threading
import time
from random import randint
import queue

# Based on example Dialog 
# http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
class InfoMessage(tk.Toplevel):
    def __init__(self, parent, info, title=None, modal=True):
        tk.Toplevel.__init__(self, parent)
        self.transient(parent)
        if title:
            self.title(title)
        self.parent = parent

        body = tk.Frame(self)
        self.initial_focus = self.body(body, info)
        body.pack(padx=5, pady=5)

        self.buttonbox()

        if modal:
            self.grab_set()

        if not self.initial_focus:
            self.initial_focus = self
        self.protocol("WM_DELETE_WINDOW", self.cancel)
        self.geometry("+%d+%d" % (parent.winfo_rootx()+50, parent.winfo_rooty()+50))
        self.initial_focus.focus_set()

        if modal:
            self.wait_window(self)  # Wait until this window is destroyed.

    def body(self, parent, info):
        label = tk.Label(parent, text=info)
        label.pack()
        return label  # Initial focus.

    def buttonbox(self):
        box = tk.Frame(self)
        w = tk.Button(box, text="OK", width=10, command=self.ok, default=tk.ACTIVE)
        w.pack(side=tk.LEFT, padx=5, pady=5)
        self.bind("<Return>", self.ok)
        box.pack()

    def ok(self, event=None):
        self.withdraw()
        self.update_idletasks()
        self.cancel()

    def cancel(self, event=None):
        # Put focus back to the parent window.
        self.parent.focus_set()
        self.destroy()


class GuiPart:
    TIME_INTERVAL = 0.1

    def __init__(self, master, queue, end_command):
        self.queue = queue
        self.master = master
        console = tk.Button(master, text='Done', command=end_command)
        console.pack(expand=True)
        self.update_gui()  # Start periodic GUI updating.

    def update_gui(self):
        try:
            self.master.update_idletasks()
            threading.Timer(self.TIME_INTERVAL, self.update_gui).start()
        except RuntimeError:  # mainloop no longer running.
            pass

    def process_incoming(self):
        """ Handle all messages currently in the queue. """
        while self.queue.qsize():
            try:
                info = self.queue.get_nowait()
                InfoMessage(self.master, info, "Status", modal=False)
            except queue.Empty:  # Shouldn't happen.
                pass


class ThreadedClient:
    """ Launch the main part of the GUI and the worker thread. periodic_call()
        and end_application() could reside in the GUI part, but putting them
        here means all the thread controls are in a single place.
    """
    def __init__(self, master):
        self.master = master
        self.count = count(start=1)
        self.queue = queue.Queue()

        # Set up the GUI part.
        self.gui = GuiPart(master, self.queue, self.end_application)

        # Set up the background processing thread.
        self.running = True
        self.thread = threading.Thread(target=self.workerthread)
        self.thread.start()

        # Start periodic checking of the queue.
        self.periodic_call(200)  # Every 200 ms.

    def periodic_call(self, delay):
        """ Every delay ms process everything new in the queue. """
        self.gui.process_incoming()
        if not self.running:
            sys.exit(1)
        self.master.after(delay, self.periodic_call, delay)

    # Runs in separate thread - NO tkinter calls allowed.
    def workerthread(self):
        while self.running:
            time.sleep(randint(1, 10))  # Time-consuming processing.
            count = next(self.count)
            info = 'Report #{} created'.format(count)
            self.queue.put(info)

    def end_application(self):
        self.running = False  # Stop queue checking.
        self.master.quit()


if __name__ == '__main__':  # Needed to support multiprocessing.
    root = tk.Tk()
    root.title('Report Generator')
    root.minsize(300, 100)
    client = ThreadedClient(root)
    root.mainloop()  # Display application window and start tkinter event loop.
0 голосов
/ 01 декабря 2018

Я изменил вопрос, включив в него случайно пропущенную, но критическую строку.Ответ об избежании зависаний графического интерфейса оказывается смущающим простым:

Don't call ".join()" after launching the thread.

В дополнение к вышесказанному, полное решение включает в себя:

  • Отключение кнопки «Делать отчеты» допоток «создать отчет» завершается (технически это не нужно, но предотвращение создания дополнительных потоков отчетов также предотвращает путаницу);
  • Наличие потока «создание отчета» обновляет основной поток, используя следующие события:
    • «Завершенный отчет X» (расширение, отображающее ход выполнения в графическом интерфейсе) и
    • «Завершенные все отчеты» (откройте окно «Готово» и снова нажмите кнопку «Создать отчеты»);
  • Перемещение вызова окна «Готово» в основной поток, вызванный вышеуказанным событием;и
  • Передача данных с событием вместо использования общих глобальных переменных.

Простой подход с использованием модуля multiprocessing.dummy (доступен начиная с 3.0 и 2.6):

    from multiprocessing.dummy import Process

    ReportCreationProcess = Process( target = DoChosenReports )
    ReportCreationProcess.start()

еще раз, обратите внимание на отсутствие строки .join ().

В качестве временного взлома окно «Готово» все еще создается потоком создания отчета непосредственно перед его выходом.Это работает, но вызывает эту ошибку времени выполнения:

RuntimeError: Calling Tcl from different appartment  

, однако, похоже, что ошибка не вызывает проблем.И, как указывали другие вопросы, эту ошибку можно устранить, переместив создание окна «ВЫПОЛНЕНО» в основной поток (и сделайте так, чтобы поток создания отчетов отправил событие, чтобы «запустить» это окно).

Наконец, спасибо @ TigerhawkT3 (который опубликовал хороший обзор подхода, который я использую) и @martineau, который рассказал о том, как обрабатывать более общий случай, и включил ссылку на то, что выглядит как полезный ресурс.Оба ответа стоит прочитать.

0 голосов
/ 28 ноября 2018

Вам понадобятся две функции: первая инкапсулирует длительную работу вашей программы, а вторая создает поток, который обрабатывает первую функцию.Если вам нужно, чтобы поток немедленно остановился, если пользователь закрывает программу, пока поток все еще работает (не рекомендуется), используйте флаг daemon или изучите объекты Event.Если вы не хотите, чтобы пользователь мог снова вызывать функцию до ее завершения, отключите кнопку при ее запуске и затем установите кнопку в нормальное состояние в конце.

import threading
import tkinter as tk
import time

class App:
    def __init__(self, parent):
        self.button = tk.Button(parent, text='init', command=self.begin)
        self.button.pack()
    def func(self):
        '''long-running work'''
        self.button.config(text='func')
        time.sleep(1)
        self.button.config(text='continue')
        time.sleep(1)
        self.button.config(text='done')
        self.button.config(state=tk.NORMAL)
    def begin(self):
        '''start a thread and connect it to func'''
        self.button.config(state=tk.DISABLED)
        threading.Thread(target=self.func, daemon=True).start()

if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...