简体   繁体   English

Tkinter 框架滚动条

[英]Tkinter scrollbar for frame

My objective is to add a vertical scroll bar to a frame which has several labels in it.我的目标是向其中包含多个标签的框架添加垂直滚动条。 The scroll bar should automatically enabled as soon as the labels inside the frame exceed the height of the frame.一旦框架内的标签超过框架的高度,滚动条就会自动启用。 After searching through, I found this useful post.搜索之后,我发现了这个有用的帖子。 Based on that post I understand that in order to achieve what i want, (correct me if I am wrong, I am a beginner) I have to create a Frame first, then create a Canvas inside that frame and stick the scroll bar to that frame as well.基于那篇文章,我明白为了实现我想要的,(如果我错了请纠正我,我是初学者)我必须先创建一个Frame ,然后在该框架内创建一个Canvas并将滚动条粘贴到该框架上框架也是如此。 After that, create another frame and put it inside the canvas as a window object. So, I finally come up with this:之后,创建另一个框架并将其放入 canvas 中作为 window object。所以,我终于想出了这个:

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. Am I doing it right?我做对了吗? Is there better/smarter way to achieve the output this code gave me?有没有更好/更智能的方法来实现此代码给我的 output?
  2. Why must I use grid method?为什么我必须使用网格方法? (I tried place method, but none of the labels appear on the canvas.) (我尝试了 place 方法,但没有任何标签出现在 canvas 上。)
  3. What so special about using anchor='nw' when creating window on canvas?在 canvas 上创建 window 时使用anchor='nw'有什么特别之处?

Please keep your answer simple, as I am a beginner.请保持你的答案简单,因为我是初学者。

Please note that the proposed code is only valid with Python 2请注意,建议的代码仅适用于 Python 2

Here is an example:下面是一个例子:

from Tkinter import *   # from x import * is bad practice
from ttk import *

# http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame

class VerticalScrolledFrame(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):
        Frame.__init__(self, parent, *args, **kw)            

        # create a canvas object and a vertical scrollbar for scrolling it
        vscrollbar = Scrollbar(self, orient=VERTICAL)
        vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
        canvas = 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 = 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):
        def __init__(self, *args, **kwargs):
            root = Tk.__init__(self, *args, **kwargs)


            self.frame = VerticalScrolledFrame(root)
            self.frame.pack()
            self.label = Label(text="Shrink the window to activate the scrollbar.")
            self.label.pack()
            buttons = []
            for i in range(10):
                buttons.append(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)到 1)
IMHO scrolling frames is somewhat tricky in Tkinter and does not seem to be done a lot.恕我直言,滚动帧在 Tkinter 中有些棘手,似乎并没有做太多。 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)到 2)
You are talking about the data function?你说的是数据功能? Place works for me, too. Place 也适用于我。 (In general I prefer grid). (一般来说我更喜欢网格)。

to 3)到 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.除了画布之外,Tkinter 没有本机可滚动容器。 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.如您的示例所示,只需 5 或 6 行代码即可使其工作——这取决于您如何计算行数。

"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?" “在画布上创建窗口时使用 anchor='nw' 有什么特别之处?”

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.对于上面的代码,您希望左上角(“西北”)位于坐标处。

Please see my class that is a scrollable frame.请参阅我的类是一个可滚动框架。 It's vertical scrollbar is binded to <Mousewheel> event as well.它的垂直滚动条也绑定到<Mousewheel>事件。 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 .所以,你所要做的就是创建一个框架,用你喜欢的方式填充小部件,然后让这个框架成为我的ScrolledWindow.scrollwindow的子框架。 Feel free to ask if something is unclear.如果有什么不清楚的,请随时询问。

Used a lot from @ Brayan Oakley answers to close to this questions使用了很多@ Brayan Oakley 的回答来解决这个问题

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

We can add scroll bar even without using Canvas.即使不使用 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)

对于任何偶然发现此问题的人(就像在寻找我自己的要点时所做的那样),我在https://gist.github.com/mp035/9f2027c3ef9172264532fcd6262f3b01 上为此目的维护了一个要点 它具有对各种操作系统的滚轮支持,已评论,并且在文件中有一个内置的演示。

It is nessesery to configure Scrollbar in case of using with CanvasCanvas使用时需要配置Scrollbar
by sending to Canvas xscrollcommand attribute Scrollbar.set method and通过发送到Canvas xscrollcommand属性Scrollbar.set方法和
to Scrollbar command attribute Canvas.yview (xview) method. Scrollbar command属性Canvas.yview (xview) 方法。
Canvas.yview method after scrollbar was moved recieve *args in next formatting:移动滚动条后的Canvas.yview方法在下一个格式中接收 *args:
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.必须创建接收区域和翻译 scrollbar_region(元素可见和不可见)功能。
Region is `tuple(float, float)' representing open to see part of all elements. Region 是 `tuple(float, float)' 表示打开可以看到所有元素的一部分。
Not ideal bechavior showed in this solution (without using tk.Canvas)此解决方案中显示的行为不理想(不使用 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()

After I watching many answers, I got it:看了很多答案后,我明白了:

import tkinter as tk
root = tk.Tk()
root.title("音樂編輯器")
root.geometry("600x480")

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

canvas = tk.Canvas(root)
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(fill="both", expand=1, anchor="nw")
wrapFrame = canvas.create_window((0,0), window=frame, anchor="nw")

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

L1 = tk.Label(frame, text="音樂編輯器", bg="#556644", font=("",25))
L1.pack(anchor="n")

for i in range(100):
    input = tk.Entry(frame)
    input.pack()

root.mainloop()

According:根据:
https://stackoverflow.com/a/3092341/19470749 https://stackoverflow.com/a/3092341/19470749
https://stackoverflow.com/a/16198198/19470749 https://stackoverflow.com/a/16198198/19470749

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM