框架滚动条

我的目标是添加一个垂直滚动条到一个框架,其中有几个标签。当框架内的标签超过框架的高度时,滚动条应该自动启用。通过搜索,我发现 这个有用的职位。基于这篇文章,我理解为了达到我想要的效果,(如果我错了请纠正我,我是一个初学者)我必须首先创建一个 Frame,然后在这个框架内创建一个 Canvas,并将滚动条粘贴到这个框架上。然后,创建另一个框架,并将其作为窗口对象放在画布中。所以,我终于想到了这个:

from Tkinter import *


def data():
for i in range(50):
Label(frame,text=i).grid(row=i,column=0)
Label(frame,text="my text"+str(i)).grid(row=i,column=1)
Label(frame,text="..........").grid(row=i,column=2)


def myfunction(event):
canvas.configure(scrollregion=canvas.bbox("all"),width=200,height=200)


root=Tk()
sizex = 800
sizey = 600
posx  = 100
posy  = 100
root.wm_geometry("%dx%d+%d+%d" % (sizex, sizey, posx, posy))


myframe=Frame(root,relief=GROOVE,width=50,height=100,bd=1)
myframe.place(x=10,y=10)


canvas=Canvas(myframe)
frame=Frame(canvas)
myscrollbar=Scrollbar(myframe,orient="vertical",command=canvas.yview)
canvas.configure(yscrollcommand=myscrollbar.set)


myscrollbar.pack(side="right",fill="y")
canvas.pack(side="left")
canvas.create_window((0,0),window=frame,anchor='nw')
frame.bind("<Configure>",myfunction)
data()
root.mainloop()
  1. 我做的对吗? 有没有更好/更聪明的方法来实现这段代码给我的输出?
  2. 为什么我必须使用网格方法? (我尝试了 place 方法,但是没有一个标签出现在画布上。)
  3. 在画布上创建窗口时使用 anchor='nw'有什么特别之处吗?

请简单回答,因为我是初学者。

179397 次浏览

Here's example code adapted from the VerticalScrolledFrame page on the now defunct Tkinter Wiki that's been modified to run on Python 2.7 and 3+.

try:  # Python 2
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.constants import *
except ImportError:  # Python 2
import Tkinter as tk
import ttk
from tkinter.constants import *




# Based on
#   https://web.archive.org/web/20170514022131id_/http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame


class VerticalScrolledFrame(ttk.Frame):
"""A pure Tkinter scrollable frame that actually works!
* Use the 'interior' attribute to place widgets inside the scrollable frame.
* Construct and pack/place/grid normally.
* This frame only allows vertical scrolling.
"""
def __init__(self, parent, *args, **kw):
ttk.Frame.__init__(self, parent, *args, **kw)


# Create a canvas object and a vertical scrollbar for scrolling it.
vscrollbar = ttk.Scrollbar(self, orient=VERTICAL)
vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set)
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
vscrollbar.config(command=canvas.yview)


# Reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)


# Create a frame inside the canvas which will be scrolled with it.
self.interior = interior = ttk.Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor=NW)


# Track changes to the canvas and frame width and sync them,
# also updating the scrollbar.
def _configure_interior(event):
# Update the scrollbars to match the size of the inner frame.
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# Update the canvas's width to fit the inner frame.
canvas.config(width=interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)


def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# Update the inner frame's width to fill the canvas.
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)




if __name__ == "__main__":


class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
root = tk.Tk.__init__(self, *args, **kwargs)


self.frame = VerticalScrolledFrame(root)
self.frame.pack()
self.label = ttk.Label(self, text="Shrink the window to activate the scrollbar.")
self.label.pack()
buttons = []
for i in range(10):
buttons.append(ttk.Button(self.frame.interior, text="Button " + str(i)))
buttons[-1].pack()


app = SampleApp()
app.mainloop()

It does not yet have the mouse wheel bound to the scrollbar but it is possible. Scrolling with the wheel can get a bit bumpy, though.

edit:

to 1)
IMHO scrolling frames is somewhat tricky in Tkinter and does not seem to be done a lot. It seems there is no elegant way to do it.
One problem with your code is that you have to set the canvas size manually - that's what the example code I posted solves.

to 2)
You are talking about the data function? Place works for me, too. (In general I prefer grid).

to 3)
Well, it positions the window on the canvas.

One thing I noticed is that your example handles mouse wheel scrolling by default while the one I posted does not. Will have to look at that some time.

"Am i doing it right?Is there better/smarter way to achieve the output this code gave me?"

Generally speaking, yes, you're doing it right. Tkinter has no native scrollable container other than the canvas. As you can see, it's really not that difficult to set up. As your example shows, it only takes 5 or 6 lines of code to make it work -- depending on how you count lines.

"Why must i use grid method?(i tried place method, but none of the labels appear on the canvas?)"

You ask about why you must use grid. There is no requirement to use grid. Place, grid and pack can all be used. It's simply that some are more naturally suited to particular types of problems. In this case it looks like you're creating an actual grid -- rows and columns of labels -- so grid is the natural choice.

"What so special about using anchor='nw' when creating window on canvas?"

The anchor tells you what part of the window is positioned at the coordinates you give. By default, the center of the window will be placed at the coordinate. In the case of your code above, you want the upper left ("northwest") corner to be at the coordinate.

We can add scroll bar even without using Canvas. I have read it in many other post we can't add vertical scroll bar in frame directly etc etc. But after doing many experiment found out way to add vertical as well as horizontal scroll bar :). Please find below code which is used to create scroll bar in treeView and frame.

f = Tkinter.Frame(self.master,width=3)
f.grid(row=2, column=0, columnspan=8, rowspan=10, pady=30, padx=30)
f.config(width=5)
self.tree = ttk.Treeview(f, selectmode="extended")
scbHDirSel =tk.Scrollbar(f, orient=Tkinter.HORIZONTAL, command=self.tree.xview)
scbVDirSel =tk.Scrollbar(f, orient=Tkinter.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scbVDirSel.set, xscrollcommand=scbHDirSel.set)
self.tree["columns"] = (self.columnListOutput)
self.tree.column("#0", width=40)
self.tree.heading("#0", text='SrNo', anchor='w')
self.tree.grid(row=2, column=0, sticky=Tkinter.NSEW,in_=f, columnspan=10, rowspan=10)
scbVDirSel.grid(row=2, column=10, rowspan=10, sticky=Tkinter.NS, in_=f)
scbHDirSel.grid(row=14, column=0, rowspan=2, sticky=Tkinter.EW,in_=f)
f.rowconfigure(0, weight=1)
f.columnconfigure(0, weight=1)

Please see my class that is a scrollable frame. It's vertical scrollbar is binded to <Mousewheel> event as well. So, all you have to do is to create a frame, fill it with widgets the way you like, and then make this frame a child of my ScrolledWindow.scrollwindow. Feel free to ask if something is unclear.

Used a lot from @ Brayan Oakley answers to close to this questions

class ScrolledWindow(tk.Frame):
"""
1. Master widget gets scrollbars and a canvas. Scrollbars are connected
to canvas scrollregion.


2. self.scrollwindow is created and inserted into canvas


Usage Guideline:
Assign any widgets as children of <ScrolledWindow instance>.scrollwindow
to get them inserted into canvas


__init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs)
docstring:
Parent = master of scrolled window
canv_w - width of canvas
canv_h - height of canvas


"""




def __init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs):
"""Parent = master of scrolled window
canv_w - width of canvas
canv_h - height of canvas


"""
super().__init__(parent, *args, **kwargs)


self.parent = parent


# creating a scrollbars
self.xscrlbr = ttk.Scrollbar(self.parent, orient = 'horizontal')
self.xscrlbr.grid(column = 0, row = 1, sticky = 'ew', columnspan = 2)
self.yscrlbr = ttk.Scrollbar(self.parent)
self.yscrlbr.grid(column = 1, row = 0, sticky = 'ns')
# creating a canvas
self.canv = tk.Canvas(self.parent)
self.canv.config(relief = 'flat',
width = 10,
heigh = 10, bd = 2)
# placing a canvas into frame
self.canv.grid(column = 0, row = 0, sticky = 'nsew')
# accociating scrollbar comands to canvas scroling
self.xscrlbr.config(command = self.canv.xview)
self.yscrlbr.config(command = self.canv.yview)


# creating a frame to inserto to canvas
self.scrollwindow = ttk.Frame(self.parent)


self.canv.create_window(0, 0, window = self.scrollwindow, anchor = 'nw')


self.canv.config(xscrollcommand = self.xscrlbr.set,
yscrollcommand = self.yscrlbr.set,
scrollregion = (0, 0, 100, 100))


self.yscrlbr.lift(self.scrollwindow)
self.xscrlbr.lift(self.scrollwindow)
self.scrollwindow.bind('<Configure>', self._configure_window)
self.scrollwindow.bind('<Enter>', self._bound_to_mousewheel)
self.scrollwindow.bind('<Leave>', self._unbound_to_mousewheel)


return


def _bound_to_mousewheel(self, event):
self.canv.bind_all("<MouseWheel>", self._on_mousewheel)


def _unbound_to_mousewheel(self, event):
self.canv.unbind_all("<MouseWheel>")


def _on_mousewheel(self, event):
self.canv.yview_scroll(int(-1*(event.delta/120)), "units")


def _configure_window(self, event):
# update the scrollbars to match the size of the inner frame
size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight())
self.canv.config(scrollregion='0 0 %s %s' % size)
if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width():
# update the canvas's width to fit the inner frame
self.canv.config(width = self.scrollwindow.winfo_reqwidth())
if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
# update the canvas's width to fit the inner frame
self.canv.config(height = self.scrollwindow.winfo_reqheight())

For anyone who stumbles across this (as it did when looking for my own gist) I maintain a gist for exactly this purpose at https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01 It has scrollwheel support for various operating systems, is commented, and has a built-in demo in the file.

It is nessesery to configure Scrollbar in case of using with Canvas
by sending to Canvas xscrollcommand attribute Scrollbar.set method and
to Scrollbar command attribute Canvas.yview (xview) method.
Canvas.yview method after scrollbar was moved recieve *args in next formatting:
tuple('move_to', '<some_absolute_float_value_of_top_of_scrollbar_region>')
In case of implementing scrollability to widget,
Recieving region and translating scrollbar_region (whith element viewable and whith not) features must be created.
Region is `tuple(float, float)' representing open to see part of all elements.
Not ideal bechavior showed in this solution (without using tk.Canvas)

import tkinter as tk
from tkinter import ttk
    

class ItemizeFrame(ttk.Frame, list):
def __init__(self,
*args,
scroll_upd_callback = lambda x: x,
visible_els: int = 10,
**kwargs):
list.__init__(self)
ttk.Frame.__init__(self, *args, **kwargs)
        

ttk.Style().configure('Small.TButton', background='red', width=2, height=2, padx=3, pady=3)
ttk.Style().configure('Sep.TFrame', padx=3, pady=3)
        

self.scroll_upd_callback = scroll_upd_callback
self.visible_els = visible_els
self.visible_st_idx = 0
self.pseudo_scroll_element_cursor_line = 0.5*1/visible_els
        

def append(self, item: ttk.Widget, **kw):
e = item(self, **kw)
super().append(e)
e.pack(fill='x')
self._update_visible_els()
    

def _update_visable_id_callback(self):
for id_, entry_ in enumerate(self):
entry_.set_id(id_)
            

def pop(self, index=None):
e = super().pop(index)
e.destroy()
self._update_visible_els()


def __getitem__(self, idx) -> ttk.Widget:
return list.__getitem__(self, idx)
        

# indicators computing and application
@property
def visible_end_idx(self):
return self.visible_st_idx + self.visible_els -1
    

@property
def visible_area_ratio(self) -> tuple[float, float]:
total = len(self)
st_val = 0.0
end_val = 1.0
if total > self.visible_els:
end_val = 1.0 - (total-self.visible_end_idx)/total
st_val = self.visible_st_idx / total
st_val = st_val + self.pseudo_scroll_element_cursor_line
end_val = end_val + self.pseudo_scroll_element_cursor_line
return (st_val, end_val)
    

def _update_scroll_widget(self):
self.scroll_upd_callback(*self.visible_area_ratio)
        

    

def set_yview(self, move_to_ratio):
base_pseudo_ratio = 0.5*1/self.visible_els
total = len(self)
max_ratio = (total - self.visible_els)/total+base_pseudo_ratio
if move_to_ratio < 0:
possible_st_el_pseudo_part = base_pseudo_ratio
possible_st_el_idx = 0
if max_ratio < move_to_ratio:
possible_st_el_idx = total - self.visible_els
possible_st_el_pseudo_part = base_pseudo_ratio
else :
el_idx_raw = move_to_ratio * total
el_idx_round = round(el_idx_raw)
el_idx_pseudo = (el_idx_raw - el_idx_round)*1/self.visible_els
possible_st_el_idx = el_idx_round
possible_st_el_pseudo_part = el_idx_pseudo
self.visible_st_idx = possible_st_el_idx
self.pseudo_scroll_element_cursor_line = possible_st_el_pseudo_part
self._update_visible_els()
    

def _update_visible_els(self):
for el in self:
el.pack_forget()
for num, el in enumerate(self):
if self.visible_st_idx <= num and num <= self.visible_end_idx:
el.pack()
self._update_scroll_widget()
    

class ScrollableFrame(ttk.Frame):
def __init__(self, *args, **kwargs):
kw = dict(width=400, height=300)
kw.update(kwargs)
super().__init__(*args, **kw)
self.scroll = ttk.Scrollbar(self, command=self.on_scroll)
self.scroll.pack(expand=True, fill='y', side='right')
        

self.view = ItemizeFrame(
self,
scroll_upd_callback=self.scroll.set,
**kwargs
)
self.view.pack(expand=True, fill='both')#, side='left')
    

def on_scroll(self, *args, **kwargs):
value_raw = float(args[1])
self.view.set_yview(value_raw)
    

     

Usecase

class App(tk.Tk):
def __init__(self):
super().__init__()
self.frame = ScrollableFrame(self)
self.frame.pack()
    

def test_fill(self):
for i in range(15):
self.frame.view.append(ttk.Entry)
 

class Test:
@staticmethod
def v2():
app = App()
app.test_fill()
app.mainloop()
       

Test.v2()