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

2022-01-29 00:00:00 python tkinter tkinter.scrollbar

问题描述

我正在使用 Python 从日志文件中解析条目,并使用 Tkinter 显示条目内容,到目前为止它非常出色.输出是一个标签小部件的网格,但有时行数超出了屏幕上可以显示的数量.我想加一个滚动条,看起来应该很简单,但我想不通.

I am using Python to parse entries from a log file, and display the entry contents using Tkinter and so far it's been excellent. The output is a grid of label widgets, but sometimes there are more rows than can be displayed on the screen. I'd like to add a scrollbar, which looks like it should be very easy, but I can't figure it out.

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

The documentation implies that only the List, Textbox, Canvas and Entry widgets support the scrollbar interface. None of these appear to be suitable for displaying a grid of widgets. It's possible to put arbitrary widgets in a Canvas widget, but you appear to have to use absolute co-ordinates, so I wouldn't be able to use the grid layout manager?

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

I've tried putting the widget grid into a Frame, but that doesn't seem to support the scrollbar interface, so this doesn't work:

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

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

Can anyone suggest a way round this limitation? I'd hate to have to rewrite in PyQt and increase my executable image size by so much, just to add a scrollbar!


解决方案

概览

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

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

There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.

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

A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion option so that the scrollregion exactly matches the size of the frame.

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

Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_ methods. You cannot scroll items added to a canvas with pack, place, or grid. By using a frame, you can use those methods inside the frame, and then call create_window once for the frame.

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

Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).

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

For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing). The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.

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

Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:

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()

程序解决方案

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

Procedural solution

Here is a solution that doesn't use a class:

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()

相关文章