为什么Tkinter文本组件在窗口高度增加时更新缓慢?

5 投票
2 回答
1881 浏览
提问于 2025-04-18 10:16

我现在正在为neovim实现一个示例界面,选择使用Tkinter和Python是因为这两个平台既流行又简单。不过,我遇到的问题是,当窗口高度超过某个阈值时,Tkinter似乎会“堆叠”界面更新。

这里有一个视频展示了这个问题。

右边的窗口是一个运行neovim的终端模拟器,左边的窗口是连接到它的Tkinter界面程序。我的想法是,Tkinter界面应该能够反映neovim终端的屏幕,包括尺寸。在这个视频中,终端窗口始终保持焦点,所以Tkinter只需要处理来自neovim的连接事件(这些是描述屏幕更新的虚拟“nvim”事件)。

视频的前半部分显示,当窗口高度较小时,一切运行得很好,但当我增加高度时,更新就开始变得滞后。

这里是Tkinter程序的代码。虽然neovim的API非常新,仍在积极开发中(代码可能对某些读者来说不太容易理解),但我认为我想解决的问题与实现终端模拟器(使用Tk文本小部件)很相似:它必须有效地处理大量格式化文本的更新。

我在GUI编程方面经验不足。使用Tkinter来完成这个任务是否明智?如果是的话,能否给我一些提示,告诉我哪里做错了?

简单解释一下发生了什么:Neovim的API是线程安全的,vim.next_event()方法会阻塞(不会忙等待,它底层使用libuv事件循环),直到接收到事件。

vim.next_event()调用返回时,它会使用generate_event通知Tkinter线程,这样就会进行实际的事件处理(它还会在redraw:startredraw:stop之间缓冲事件,以优化屏幕更新)。

所以实际上有两个事件循环并行运行,后台事件循环以线程安全的方式为Tkinter事件循环提供数据(generate_event方法是少数几个可以从其他线程调用的方法之一)。

2 个回答

0

我也发现了Tkinter小部件有类似的问题,感觉这主要是因为Tkinter的性能下降导致的。如果不对Tkinter进行大规模的改进,这个问题似乎是无法解决的。

1

我建议你先确认一下,问题真的是出在Tkinter上。你可以通过在终端输出一些信息来检查,当你收到一个事件时,看看是否有反应。

不过现在仔细想想,这可能是你遇到的问题:

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

线程和事件循环不太兼容,而Tkinter本身就有一个事件循环。我不太确定neovim的API是否设置了回调,但通常来说,处理变化时你会想用回调。

因为你说你对图形界面编程不太熟悉,我就假设你对事件循环的概念也不太了解。简单来说,想象一下你有一段代码,看起来像这样:

while True:
    if something_to_do:
        do_it_now()

显然,这是一种忙碌的循环,会消耗你的CPU资源,所以通常情况下,事件循环会阻塞或者和操作系统设置回调,这样它就可以释放CPU,当发生一些有趣的事情时,操作系统会通知你,比如“有人在这里点击了”或者“有人按了一个键”或者“嘿,你叫我现在叫醒你!”

所以作为一个图形界面开发者,你的工作就是接入这个事件循环。你并不在乎事情发生的具体时间——你只想对它做出反应。在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)

当然,也有可能你可以完全绕过事件循环,让事件直接执行你想要的操作。

撰写回答