Почему текстовый виджет Tkinter «отстает» от обновлений при увеличении высоты окна?

#python #user-interface #tkinter #tk #neovim

#python #пользовательский интерфейс #tkinter #tk-toolkit #neovim

Вопрос:

В настоящее время я внедряю образец пользовательского интерфейса для neovim и решил использовать Tkinter / python из-за популярности / простоты платформ. Проблема, с которой я сталкиваюсь, заключается в том, что tkinter, похоже, «складывает» обновления пользовательского интерфейса, когда высота окна пересекает определенный порог.

Вот видео, в котором показана проблема.

Правое окно — это эмулятор терминала, в котором запущен neovim, а левое окно — подключенная к нему программа пользовательского интерфейса Tkinter. Идея в том, что пользовательский интерфейс tkinter должен отражать экран терминала neovim, включая размеры. В этом видео никогда не отвлекайтесь от окна терминала, поэтому единственные события, которые Tk должен обрабатывать, исходят от подключения к neovim (виртуальные события «nvim», которые описывают обновления экрана)

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

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

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

Чтобы немного объяснить, что происходит: API Neovim потокобезопасен, и vim.next_event() метод блокируется (без ожидания занятости, он использует цикл событий libuv внизу) до получения события.

Когда vim.next_event() вызов вернется, он уведомит поток Tkinter generate_event , который будет выполнять фактическую обработку событий (он также буферизует события между redraw:start redraw:stop обновлениями экрана и для оптимизации).).

Таким образом, на самом деле параллельно выполняются два цикла событий, при этом фоновый цикл событий передает цикл событий Tkinter потокобезопасным способом ( generate_event метод является одним из немногих, который может быть вызван из других потоков)

Комментарии:

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

Ответ №1:

Я бы дважды проверил, что на самом деле это задержка Tkinter. Я бы сделал это, просто записав в терминал, когда вы получаете событие.

Но теперь, когда я присмотрелся повнимательнее, это может быть вашей проблемой:

     t = Thread(target=get_nvim_events, args=(self.nvim_events,
                                             self.vim,
                                             self.root,))
  

Потоки плохо работают с циклами событий, один из которых уже есть в Tkinter. Я не уверен, настроен ли API neovim на использование обратных вызовов, но обычно именно так вы хотите распространять изменения.

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

 while True:
    if something_to_do:
        do_it_now()
  

Очевидно, что это цикл занятости и приведет к потере вашего процессора, поэтому обычно ваш цикл событий блокирует или настраивает обратные вызовы с ОС, что позволяет ему отказаться от процессора, и когда происходит что-то интересное, ОС скажет что-то вроде: «Кто-то нажал здесь» или «Кто-то нажал клавишу,» или «Эй, ты сказал мне разбудить тебя сейчас!»

Итак, ваша задача как разработчика GUI — подключиться к этому циклу событий. На самом деле вам все равно, когда что-то происходит — вы просто хотите отреагировать на это. С помощью Tkinter вы можете сделать это с .after помощью методов, см. «Обратные вызовы без событий». Что может быть хорошим выбором, так это .after_idle метод:

Регистрирует обратный вызов, который вызывается, когда система простаивает. Будет вызван обратный вызов, в основном цикле больше нет событий для обработки. Обратный вызов вызывается только один раз для каждого вызова after_idle .

Это означает, что вы не будете блокировать нажатие клавиши или щелчок мыши, и он будет запущен только после того, как Tkinter завершит обработку других своих данных (например, рисование, обратные вызовы и т. Д.).

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

 def do_something(arg):
    # do something with `arg` here


def event_happened(event_args): #whatever args the event generates
    root.after_idle(lambda: do_something(event_args))

vim.bind("did_something", event_happened)
  

Конечно, также возможно, что вы можете просто полностью обойти цикл событий и заставить событие делать то, что вы хотите.

Комментарии:

1. Я обновил вопрос, чтобы лучше объяснить, что происходит.

Ответ №2:

Я также наблюдал аналогичные проблемы с виджетами Tkinter и обнаружил, что, похоже, это результат проблем с Tkinter, снижающих его производительность. Кажется, что эта проблема не может быть решена без капитального ремонта Tkinter.

Комментарии:

1. Мне кажется, это ответ: иногда «вы не можете» — это ответ. Это мог бы быть гораздо лучший ответ, если бы в нем говорилось о том, какие аспекты tk обеспечивают такую производительность, но я думаю, что это действительно ответ на вопрос.