Python tkinter的自动调整大小

2 投票
2 回答
10977 浏览
提问于 2025-04-16 12:02

在下面这个方法中,我想创建一个框架,并在里面放一个标签和一个文本小部件,然后把它们放到另一个文本小部件里。结果有两个问题。应该怎么改才能:

  1. 让里面的文本对象根据插入的文本有正确的高度?
  2. 让框架和文本能够根据外部小部件的当前尺寸自动调整大小?

如果有建议就太好了!在代码中让消息按预期显示有点困难。它们应该能够自动换行,并在主小部件被拉伸时调整大小。

def display(self, name, message):
    frame = tkinter.ttk.Frame(self.__text, borderwidth=1)
    frame.grid_rowconfigure(0, weight=1)
    frame.grid_columnconfigure(1, weight=1)
    name = tkinter.ttk.Label(frame, text=name)
    name.grid(row=0, column=0)
    text = tkinter.Text(frame, wrap=tkinter.WORD, height=1)
    text.grid(row=0, column=1, sticky=tkinter.EW)
    text.insert('1.0', message)
    text.configure(state=tkinter.DISABLED)
    self.__text.window_create('1.0', window=frame, stretch=tkinter.TRUE)

这段代码应该生成一个带有标签的框架,旁边还有自动换行的文本。每条新显示的消息应该在旧消息的上面,随着消息列表的增长,应该可以滚动查看旧消息(无限制)。不幸的是,这个效果并没有比上面的代码好多少。

def display(self, name, message):
    frame = tkinter.ttk.Frame(self.__text, borderwidth=1, relief='solid')
    name = tkinter.ttk.Label(frame, text=name)
    text = tkinter.Text(frame, wrap=tkinter.WORD, height=1)
    frame.pack(expand=tkinter.TRUE, fill=tkinter.BOTH)
    name.pack(fill=tkinter.BOTH, side=tkinter.LEFT)
    text.pack(expand=tkinter.TRUE, fill=tkinter.BOTH)
    text.insert('1.0', message)
    text.configure(state=tkinter.DISABLED)
    self.__text.window_create('1.0', window=frame)

框架的配置看起来是正确的,但让外部文本框像几何管理器一样工作,以及设置内部文本框的高度属性似乎是主要问题。当前,外部文本框没有调整框架的大小,我也不确定该写什么代码来根据里面的文本量调整内部文本框的高度。以下是程序的完整代码:

import tkinter
import tkinter.ttk

import datetime
import getpass
import os
import uuid

################################################################################

class DirectoryMonitor:

    def __init__(self, path):
        self.__path = path
        self.__files = {}

    def update(self, callback):
        for name in os.listdir(self.__path):
            if name not in self.__files:
                path_name = os.path.join(self.__path, name)
                self.__files[name] = FileMonitor(path_name)
        errors = set()
        for name, monitor in self.__files.items():
            try:
                monitor.update(callback)
            except OSError:
                errors.add(name)
        for name in errors:
            del self.__files[name]


################################################################################

class FileMonitor:

    def __init__(self, path):
        self.__path = path
        self.__modified = 0
        self.__position = 0

    def update(self, callback):
        modified = os.path.getmtime(self.__path)
        if modified != self.__modified:
            self.__modified = modified
            with open(self.__path, 'r') as file:
                file.seek(self.__position)
                text = file.read()
                self.__position = file.tell()
            callback(self.__path, text)

################################################################################

class Aggregator:

    def __init__(self):
        self.__streams = {}

    def update(self, path, text):
        if path not in self.__streams:
            self.__streams[path] = MessageStream()
        parts = text.split('\0')
        assert not parts[-1], 'Text is not properly terminated!'
        self.__streams[path].update(parts[:-1])

    def get_messages(self):
        all_messages = set()
        for stream in self.__streams.values():
            all_messages.update(stream.get_messages())
        return sorted(all_messages, key=lambda message: message.time)

################################################################################

class MessageStream:

    def __init__(self):
        self.__name = None
        self.__buffer = None
        self.__waiting = set()

    def update(self, parts):
        if self.__name is None:
            self.__name = parts.pop(0)
        if self.__buffer is not None:
            parts.insert(0, self.__buffer)
            self.__buffer = None
        if len(parts) & 1:
            self.__buffer = parts.pop()
        for index in range(0, len(parts), 2):
            self.__waiting.add(Message(self.__name, *parts[index:index+2]))

    def get_messages(self):
        messages = self.__waiting
        self.__waiting = set()
        return messages

################################################################################

class Message:

    def __init__(self, name, timestamp, text):
        self.name = name
        self.time = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
        self.text = text

################################################################################

class MessageWriter:

    def __init__(self, path, name):
        assert '\0' not in name, 'Name may not have null characters!'
        self.__name = str(uuid.uuid1())
        self.__path = os.path.join(path, self.__name)
        with open(self.__path, 'w') as file:
            file.write(name + '\0')

    @property
    def name(self):
        return self.__name

    def write(self, text):
        assert '\0' not in text, 'Text may not have null characters!'
        timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
        with open(self.__path, 'a') as file:
            file.write(timestamp + '\0' + text + '\0')

################################################################################

class Logos(tkinter.ttk.Frame):

    @classmethod
    def main(cls, path):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.title('Logos 2.0')
        root.minsize(320, 240)  # QVGA
        view = cls(root, path)
        view.grid(row=0, column=0, sticky=tkinter.NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        root.mainloop()

    def __init__(self, master, path, **kw):
        super().__init__(master, **kw)
        self.configure_widgets()
        self.__writer = MessageWriter(path, getpass.getuser())
        self.__monitor = DirectoryMonitor(path)
        self.__messages = Aggregator()
        self.after_idle(self.update)

    def configure_widgets(self):
        # Create widgets.
        self.__text = tkinter.Text(self, state=tkinter.DISABLED)
        self.__scroll = tkinter.ttk.Scrollbar(self, orient=tkinter.VERTICAL,
                                              command=self.__text.yview)
        self.__entry = tkinter.ttk.Entry(self, cursor='xterm')
        # Alter their settings.
        self.__text.configure(yscrollcommand=self.__scroll.set)
        # Place everything on the grid.
        self.__text.grid(row=0, column=0, sticky=tkinter.NSEW)
        self.__scroll.grid(row=0, column=1, sticky=tkinter.NS)
        self.__entry.grid(row=1, column=0, columnspan=2, sticky=tkinter.EW)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        # Setup box for typing.
        self.__entry.bind('<Control-Key-a>', self.select_all)
        self.__entry.bind('<Control-Key-/>', lambda event: 'break')
        self.__entry.bind('<Return>', self.send_message)
        self.__entry.focus_set()

    def select_all(self, event):
        event.widget.selection_range(0, tkinter.END)
        return 'break'

    def send_message(self, event):
        text = self.__entry.get()
        self.__entry.delete(0, tkinter.END)
        self.__writer.write(text)

    def update(self):
        self.after(1000, self.update)
        self.__monitor.update(self.__messages.update)
        for message in self.__messages.get_messages():
            self.display(message.name, message.text)

    def display(self, name, message):
        frame = tkinter.ttk.Frame(self.__text, borderwidth=1, relief='solid')
        name = tkinter.ttk.Label(frame, text=name)
        text = tkinter.Text(frame, wrap=tkinter.WORD, height=1)
        name.grid(row=0, column=0)
        text.grid(row=0, column=1, sticky=tkinter.EW)
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(1, weight=1)
        text.insert('1.0', message)
        text.configure(state=tkinter.DISABLED)
        self.__text.window_create('1.0', window=frame)

################################################################################

if __name__ == '__main__':
    Logos.main('Feeds')

2 个回答

3

.grid 方法一直让我觉得有点麻烦,特别是在调整大小和拉伸方面。

对于你的代码,我建议把 .grid 的调用改成下面的 .pack 调用:

frame.pack(expand=1, fill='both')
name.pack(fill='both', side='left')
text.pack(expand=1, fill='both')

这样你就可以去掉 .grid_{row,column}configure 的调用了。

你的 __text 组件调整大小正常吗?如果它不调整大小,那么这个框架组件也无法调整大小。

2

你的描述有点难懂。你是说虽然你把框架和文本组合放在另一个文本小部件里,但你希望这个框架和文本组合能随着外面的文本小部件的大小变化而变化吗?如果是这样,为什么要用文本小部件呢?

可能你用错了小部件类型,没法实现你想要的效果。你到底想做什么,为什么需要把文本小部件放在其他文本小部件里面呢?

撰写回答