在 Tkinter 中向一组小部件添加滚动条

新手上路,请多包涵

我正在使用 Python 来解析日志文件中的条目,并使用 Tkinter 显示条目内容,到目前为止效果非常好。输出是标签小部件的网格,但有时行数多于屏幕上可以显示的行数。我想加一个滚动条,看起来应该很容易,但我想不通。

文档暗示只有 List、Textbox、Canvas 和 Entry 小部件支持滚动条界面。这些似乎都不适合显示小部件网格。可以在 Canvas 小部件中放置任意小部件,但您似乎必须使用绝对坐标,所以我无法使用网格布局管理器?

我试过将小部件网格放入框架中,但这似乎不支持滚动条界面,所以这不起作用:

 mainframe = Frame(root, yscrollcommand=scrollbar.set)

任何人都可以提出解决此限制的方法吗?我不想不得不在 PyQt 中重写并增加我的可执行图像大小,只是为了添加一个滚动条!

原文由 Simon Hibbs 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 473
2 个回答

概述

您只能将滚动条与几个小部件相关联,而根小部件和 Frame 不属于该组小部件。

至少有几种方法可以做到这一点。如果您需要一组简单的垂直或水平小部件,您可以使用文本小部件和 window_create 方法来添加小部件。此方法很简单,但不允许小部件的复杂布局。

一个更常见的通用解决方案是创建一个画布小部件并将滚动条与该小部件相关联。然后,在该画布中嵌入包含标签小部件的框架。确定框架的宽度/高度并将其输入画布 scrollregion 选项,以便滚动区域与框架的大小完全匹配。

为什么将小部件放在框架中而不是直接放在画布中?附加到画布的滚动条只能滚动使用 create_ 方法之一创建的项目。您无法使用 packplacegrid 滚动添加到画布的项目。通过使用框架,您可以在框架内部使用这些方法,然后为框架调用 create_window 一次。

直接在画布上绘制文本项并不难,因此如果框架嵌入画布解决方案看起来过于复杂,您可能需要重新考虑该方法。由于您正在创建一个网格,因此每个文本项的坐标将非常容易计算,尤其是当每一行的高度相同时(如果您使用的是单一字体,则可能是这样)。

要直接在画布上绘图,只需计算出您正在使用的字体的行高(并且有相应的命令)。然后,每个 y 坐标是 row*(lineheight+spacing) 。 x 坐标将是基于每列中最宽项目的固定数字。如果你给每样东西一个它所在列的标签,你可以用一个命令调整列中所有项目的 x 坐标和宽度。

面向对象的解决方案

下面是框架嵌入画布解决方案的示例,使用面向对象的方法:

 import tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent):

        tk.Frame.__init__(self, parent)
        self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
        self.frame = tk.Frame(self.canvas, background="#ffffff")
        self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.vsb.set)

        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas.create_window((4,4), window=self.frame, anchor="nw",
                                  tags="self.frame")

        self.frame.bind("<Configure>", self.onFrameConfigure)

        self.populate()

    def populate(self):
        '''Put in some fake data'''
        for row in range(100):
            tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1",
                     relief="solid").grid(row=row, column=0)
            t="this is the second column for row %s" %row
            tk.Label(self.frame, text=t).grid(row=row, column=1)

    def onFrameConfigure(self, event):
        '''Reset the scroll region to encompass the inner frame'''
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

if __name__ == "__main__":
    root=tk.Tk()
    example = Example(root)
    example.pack(side="top", fill="both", expand=True)
    root.mainloop()

程序解决方案

这是一个不使用类的解决方案:

 import tkinter as tk

def populate(frame):
    '''Put in some fake data'''
    for row in range(100):
        tk.Label(frame, text="%s" % row, width=3, borderwidth="1",
                 relief="solid").grid(row=row, column=0)
        t="this is the second column for row %s" %row
        tk.Label(frame, text=t).grid(row=row, column=1)

def onFrameConfigure(canvas):
    '''Reset the scroll region to encompass the inner frame'''
    canvas.configure(scrollregion=canvas.bbox("all"))

root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)

vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")

frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))

populate(frame)

root.mainloop()

原文由 Bryan Oakley 发布,翻译遵循 CC BY-SA 4.0 许可协议

使其可滚动

使用这个方便的类使包含小部件的框架可滚动。按着这些次序:

  1. 创建框架
  2. 显示它(包、网格等)
  3. 使其可滚动
  4. 在其中添加小部件
  5. 调用更新()方法

import tkinter as tk
from tkinter import ttk

class Scrollable(tk.Frame):
    """
       Make a frame scrollable with scrollbar on the right.
       After adding or removing widgets to the scrollable frame,
       call the update() method to refresh the scrollable area.
    """

    def __init__(self, frame, width=16):

        scrollbar = tk.Scrollbar(frame, width=width)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)

        self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar.config(command=self.canvas.yview)

        self.canvas.bind('<Configure>', self.__fill_canvas)

        # base class initialization
        tk.Frame.__init__(self, frame)

        # assign this obj (the inner frame) to the windows item of the canvas
        self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)

    def __fill_canvas(self, event):
        "Enlarge the windows item to the canvas width"

        canvas_width = event.width
        self.canvas.itemconfig(self.windows_item, width = canvas_width)

    def update(self):
        "Update the canvas and the scrollregion"

        self.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))


使用示例

root = tk.Tk()

header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()

ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()

scrollable_body = Scrollable(body, width=32)

for i in range(30):
    ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()

scrollable_body.update()

root.mainloop()

原文由 Tarqez 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题