如何逐字符更新customtkinter标签,并在其间设置时间延迟?

1 投票
3 回答
67 浏览
提问于 2025-04-13 17:02

我正在尝试使用CustomTkinter来创建一个游戏,我想让< a href="https://customtkinter.tomschimansky.com/documentation/widgets/label" rel="nofollow noreferrer">CTkLabel中的文字慢慢地一个一个字符地打印出来。我想要的效果是让文字看起来像是一个人在实时输入。大部分我找到的资料都是关于Tkinter的,所以我不太确定该怎么做。

这是我正在挣扎的代码部分:

dialogue_var = ("Hello User")
dialogue_var = ("Ready To Protect It")

dialogue = customtkinter.CTkLabel(app, width=350, height=80, text=dialogue_var)
dialogue.pack(padx=1, pady=1)

DirkS树莓派论坛上发布了以下的print_slow函数:

from time import sleep

def print_slow(txt):
    # cycle through the text one character at a time
    for x in txt:
        # print one character, no new line, flush buffer
        print(x, end='', flush=True)
        sleep(0.3)
    # go to new line
    print()

print_slow("Hello. I'm feeling a bit slow today")

但是我不知道怎么把它和CTkLabel结合起来。它总是提示说命令没有关闭。

3 个回答

0

我该如何正确使用customtkinter,让我可以慢慢打印ctk标签的输出呢?

这个问题是可以解决的。只需要一小段代码来绕过这个问题。

总共只需要33行代码。

  • 添加一个按钮。
  • 创建两个函数。

代码示例:

import tkinter as tk
import customtkinter as ctk
 
app = tk.Tk()

def clicked() -> None:
    n = 0
    txt = r"Hello User. Ready To Protect It"
    showChar(n, txt)


def showChar(n: int, txt: str) -> int and str:
    n += 1
    diaglogue.configure(text = txt[:n])
    if n < len(txt):
        app.after(1000, lambda: showChar(n, txt))

        
diaglogue = ctk.CTkLabel(master=app, width=350,
                         height=80,
                         text_color="#000",
                         )


#diaglogue_var = ("Hello User")
#diaglogue_var = ("Ready To Protect It")  
diaglogue.pack(padx=1, pady=1)
 
button = ctk.CTkButton(master=app, text="Click Me", 
                   command=clicked)
button.pack()

app.mainloop()
0

这里有一个纯Tkinter的解决方案。根据我从文档中的理解,你只需要把Tkinter中的类名换成CTkinter就可以了。

如果你对事件驱动的应用程序不太熟悉,TkDocs教程提供了一个快速的介绍:

像大多数用户界面工具包一样,Tk运行一个事件循环,这个循环会接收来自操作系统的事件。这些事件包括按钮点击、键盘输入、鼠标移动、窗口调整大小等等。

通常,Tk会为你管理这个事件循环。它会判断这个事件是针对哪个控件的(比如用户点击了哪个按钮?如果按下了某个键,哪个文本框是当前活动的?),然后相应地处理。每个控件都知道如何响应事件;例如,当鼠标移动到按钮上时,按钮可能会变色,而当鼠标离开时又会恢复原样。

换句话说,像下面这样的代码

dialogue = ""
buffer   = "Ready To Protect It"
for x in buffer:
    dialogue.append(x)
    sleep(0.3)

是行不通的,因为这个sleep调用会不断暂停执行,导致事件循环无法正常工作;这样应用程序几乎会完全无响应。

相反,我们需要设置一些结构,能够基于定时器以小的、独立的步骤工作。首先,导入几个模块:

import tkinter as tk
from collections import deque

接下来,我们定义一个自定义的StringVar类。这个类可以让我们一个字符一个字符地添加到字符串中。每次调用step方法时,就会把一个字符从缓冲区移动到显示字符串的末尾:

class TeletypeVar(tk.StringVar):
    """StringVar that appends characters from a buffer one at a time.
    
    Parameters
    ----------
    value : string, optional
        Initial string value.
    buffer : string, optional
        Initial buffer content.
    """

    def __init__(self, *args, **kwargs):
        buffer = kwargs.pop("buffer", "")
        super().__init__(*args, **kwargs)
        self.buffer = deque(buffer)
        
    def clear(self):
        """Clear contents of the string and buffer."""
        self.set("")
        self.buffer.clear()
        
    def enqueue(self, str):
        """Add the given string to the end of the buffer."""
        self.buffer.extend(str)
        
    def step(self, _event=None):
        """Move 1 character from the buffer to the string."""
        if len(self.buffer) > 0:
            self.set(self.get() + self.buffer.popleft())

然后我们定义一个类,它会以稳定的速度重复调用像TeletypeVar.step这样的回调函数。这个类也有一个step方法,用于调用指定的回调函数,并启动下一个步骤的定时器:

class Clock():
    """A clock that calls ``cb`` every ``T`` milliseconds.

    Parameters
    ----------
    T : int
        Delay between calls, in milliseconds.
    cb : function
        Callback function to be repeatedly called.
    """

    def __init__(self, T, cb):
        self.T = T
        self.cb = cb
        self.after = root.after
    
    def step(self):
        """Called every T milliseconds."""
        self.cb()
        self.after(self.T, self.step)
        
    def start(self):
        """Start running the clock."""
        self.after(self.T, self.step)

这就完成了准备工作。创建框架和启动主循环的代码应该看起来很熟悉:

class App(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack()

        # create our custom StringVar pre-populated with "Hello User"
        #   and "_Ready To Protect It" waiting in the buffer
        self.tt_dynamic = TeletypeVar(value="Hello User",
                                      buffer="_Ready To Protect It")

        # create our clock, but don't start it yet
        #   this replaces a line of code like `sleep(0.150)`
        self.clk = Clock(150, self.tt_dynamic.advance)  

        # create a TkLabel; same as using a CTkLabel
        # using `textvariable` instead of `text` lets the label
        #   know that the string is a dynamic property
        dialogue = tk.Label(self, width=35, height=8,
                            textvariable=self.tt_dynamic)
        dialogue.pack(padx=1, pady=1)

        # call self.delayed_cb1() after 2 seconds
        #   note how there is no delay between the two print statements
        print("before after")
        root.after(2_000, self.delayed_cb1)
        print("after after")

    def delayed_cb1(self):
        # start the clock defined in `__init__`
        self.clk.start()

        # call self.delayed_cb2() after a further 5 seconds
        root.after(5_000, self.delayed_cb2)
    
    def delayed_cb2(self):
        # clear out the teletype string
        self.tt_dynamic.clear()

        # speed up the clock
        self.clk.T = 75

        # and queue up more text
        self.tt_dynamic.enqueue("_And Protect It Some More")


root = tk.Tk()
myapp = App(root)
myapp.mainloop()

我写这个代码的方式希望能让你方便地尝试不同的部分,看看一切是如何工作的。如果有什么不清楚的地方,随时告诉我。

免责声明:我之前没有使用过Tkinter或CustomTkinter。这一切都是基于快速浏览文档和与许多其他事件驱动工具包的相似性,所以这可能不是最优雅的解决方案。

0

尊重一下@drmuelr的详细回答,我会选择创建一个自己的Label类,让它可以直接输入文本,而不是使用StringVar。另外,他的Clock类其实主要就是after方法的功能。

这是我的做法:

from customtkinter import CTk, CTkLabel, CTkButton, StringVar


class TypedLabel(CTkLabel):
    """ Label that can slowly type the provided text """
    def __init__(self, master, text, type_delay):
        self.delay = type_delay
        self.display_text = StringVar(master, value='')
        self.text_buffer = text
        super().__init__(master, textvariable=self.display_text)

    def change_delay(self, new_delay):
        """change the speed for the typing by setting lower millisecond intervalls"""
        self.delay = new_delay

    def set_buffer(self, text):
        """Change buffer text"""
        self.text_buffer = text

    def append_buffer(self, text, newline=True):
        """append the buffer with new text. Default will print a newline before the appended text"""
        if newline:
            text = '\n' + text
        self.text_buffer += text

    def clear_text(self):
        """reset both bufffer and display text to empty string"""
        self.text_buffer = ''
        self.display_text.set('')

    def type_text(self):
        if len(self.text_buffer) > 0:
            self.display_text.set(self.display_text.get() + self.text_buffer[0])  # append first character from buffer
            self.text_buffer = self.text_buffer[1:]  # remove first character from buffer
            self.after(self.delay, self.type_text)  # type next char after given delay


class TestGui(CTk):
    def __init__(self):
        super().__init__()
        self.type_text_lbl = TypedLabel(self, 'Test', 300)
        self.type_text_lbl.pack()
        test_btn = CTkButton(self, text='new test', command=self._new_test)  # test buffer append
        test_btn.pack()
        self.after(2000, self.type_text_lbl.type_text)  # type text 2 sec after instantiation of the GUI

    def _new_test(self):
        self.type_text_lbl.clear_text()
        self.type_text_lbl.set_buffer('Test 2')
        self.type_text_lbl.append_buffer('succesfull!')
        self.type_text_lbl.type_text()


if __name__ == '__main__':
    gui = TestGui()
    gui.mainloop()

撰写回答