【Stack Overflow】在 tkinter 中给一组 widgets 添加滚动条
这是一个新的系列,筛选来自 stack overflow 的问题和解答并翻译成中文。
本文来自https://stackoverflow.com/questions/3085696/adding-a-scrollbar-to-a-group-of-widgets-in-tkinter
问题
我正在用 python 来读取日志文件记录,并用 tkinter 把记录内容显示出来,目前为止一切正常。输出的结果是一组利用 grid 方法布局的 label 组件,但有时数量过多以至于没法全部显示在屏幕上。我想添加一个滚动条,看似很简单,但是我却没法搞定。
官方文档说只有List
、Textbox
、Canvas
和Entry
组件支持滚动条的接口。这些组件对于显示一组利用 grid 方法布局的组件来说都不合适。可以试着把所有的组件都放在一个Canvas
里面,但这样的话貌似必须使用绝对坐标了,然后我就没法再使用 grid 布局管理器了?
我试着把组件都放在一个Frame
里面,因为Frame
里面可以使用 grid 布局,但它不支持滚动条接口,所以下面的代码不起作用:
mainframe = Frame(root, yscrollcommand=scrollbar.set)
谁有办法解开这个限制吗?我可不想只是为了增加一个滚动条就用 PyQt 来重写,还大大增加了我程序的占用空间。
解答
滚动条只支持少数组件,很遗憾root
和Frame
都不包括在内。
最常见的解决办法就是创建一个 canvas 组件然后和滚动条绑定。然后在 canvas 里面嵌入一个 Frame,把你的 label 组件都放到 frame 里去。确定好 frame 的长度和宽度,同时设置 canvas 的scrollregion
选项,这样滚动区域就可以准确匹配 frame 的大小。
为什么把 label 组件都放在一个 frame 而不是直接放在 canvas 中呢?这是因为一个绑定到 canvas 的滚动条只能滚动那些利用某种create_
前缀方法创建的对象。你不能滚动那些利用pack
、place
或者grid
方法添加到 canvas 的对象(也就是把 label 组件直接放在 canvas 中)。通过使用一个 frame,你可以在其中布局 label 组件,然后调用 frame 的create_window
方法即可。
在 canvas 上直接绘制文本不太难,所以如果 canvas 嵌套 frame 的办法太复杂的话你可能应该重新考虑一下。既然你用了 grid 布局管理器,每个文本对象的坐标应该很容易计算出来,尤其是每行都是一样高度的情况下(当你只用一种字体的时候更是如此)。
要想直接在 canvas 上绘制,只要算出当前使用的字体对应每一行的高度(可以用现有的代码)。然后,每个 y 坐标就是row*(lineheight+spacing)
。x 的坐标根据最宽的那个对象来定,也是个固定值。如果你给每个对象提供 tag,你只需要用很简单的命令来调整 x 坐标和宽度就行。
面向对象版本的解法
下面是一个 canvas 嵌套 frame 解法的示例,用了面向对象方法:
import tkinter as tk # python 3
# import Tkinter as tk # python 2
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(root, 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(root).pack(side="top", fill="both", expand=True)
root.mainloop()
面向过程版本的解法
不使用类也可以:
import tkinter as tk # python 3
# import Tkinter as tk # python 2
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()