tkinter 滚动条只向下滚动并切断内容

tkinter scrollbar only scrolls downwards and cuts off content

编辑[已解决]

基于下面的 ,该方法只是确保您生成的元素被分配以正确 parents。

值得创建一个框架来容纳每个可滚动元素,因为使用 pack/place 时您无法在 parents 之间移动小部件。因此,所有 movable 元素之间的公共 parents 将使您可以自由移动它们,同样只需创建一个支架框架。有关详细信息,请参阅下面的答案和讨论。

编辑2

有非常好的信息和仅使用 canvas 的替代方法。核心概念仍然相同。

原题从这里开始


情况

所以基于 Bryan 的 ,我使用这种方法在 scrollable frame 上生成小部件(这基本上是一个 Canvas 在内部包裹一个框架,因为我们知道框架不'有滚动属性)。

本例中的小部件只是 tk.Button,我使用 lambda 在循环中生成这些小部件以保存状态。那方面完全没问题。

问题

现在一切正常,除了当我生成更多元素(再次只是按钮)时,它们似乎被切断了。而且我似乎无法向下滚动以查看其余的。我只能向上滚动只能看到空的 space.

请观看下面的视频了解我的意思(请原谅我糟糕的颜色选择,这是为了快速演示)

在视频中,template47 下面还有更多元素,但我无法滚动到它们。我可以向上滚动,但那里很寂寞。所有名称实际上都是带有零 bdHLthickness.

的按钮

我试过的

首先,我的第一直觉是将 ttk.scrollbar 附加到 canvas+frame,我这样做了,但观察到完全相同的行为。

然后我尝试查看是否可以使用 .moveTo('1.0') 向下滚动到最后一个条目,然后由于向上滚动已经工作,应该没有问题。但这也没有做任何事情。元素仍然被切断,显然向上滚动也搞砸了。

我认为我不能使用 pack/grid geoManagers,因为正如我在上面链接的 bryan 的回答所建议的那样,使用 place 及其 in_ arg 是首选方式。如果可能的话,请告诉我。

我的用例简述

如上面链接的答案中所述,我也有两个框架,我正在使用 on_click 回调函数来切换 parents(很像答案中的示例代码)。事实证明,地方是实现这一目标的最佳选择。 确实,所有这些方面都运作良好。只是滚动的东西不起作用。

一些代码(我敢说 MCVE)

我如何绑定到鼠标滚轮

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

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

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

创建 window

self.canvas.create_window((5, 6), window=self.scrolled_frame, anchor="w")

当然还有里面的小部件

        for f in self.fav_templates:
            btn = tk.Button(self.root, text='text', command=lambda te=f: self._on_click(te))

            btn.place(in_=self.ft_frame, x=20, y=p)

所以

我相信上面的片段会让我了解我所做的事情。如果有人需要我创建一个完整的 MCVE,他们可以 运行,很乐意这样做,让我知道。

我在设置中遗漏了什么?欢迎任何想法或建议。

谢谢你:)

主要问题是您正在使用 place 并且在 place 中您将成为小部件的全能者,这意味着将没有 requested size 到 master 或 tkinter 中提供的任何其他魔法的背景。所以我确实建议使用另一个几何管理器,它可以像 pack 一样为您带来魔力。另请注意,我将锚点设置为 nw.

此外,您似乎只能在普通母版中使用可选参数 in_。所以这个概念的关键是有一个用作主参数的支架框架,并将 in_ 用于 children 能够容纳小部件。

import tkinter as tk

class Example:
    def __init__(self):
        self.root = tk.Tk()
        test_frame = tk.Frame(self.root,height=200,width=400,bg='orange')
        test_frame.pack()
        self.DISPLAY_WIDTH = 200
        self.DISPLAY_HEIGHT= 200
        self.create_holder()
        self.create_displays()
        self.add_scrollbars()
        self.populate_holder()
    def create_holder(self):
        '''creates a canvas for the displays and is needed
         to have a common border/window'''
        self.holder = tk.Canvas(self.root,width=self.DISPLAY_WIDTH*2,height=self.DISPLAY_HEIGHT)
        self.holder_frame = tk.Frame(self.holder)
        self.holder.create_window((5, 6), window=self.holder_frame, anchor="nw")
        self.holder.pack()
    def create_displays(self):
        '''creates 2 displays to have seperate scrollregions'''
        self.cnvs1 = tk.Canvas(self.holder_frame,
                               width=self.DISPLAY_WIDTH,
                               height=self.DISPLAY_HEIGHT)
        self.cnvs2 = tk.Canvas(self.holder_frame,
                               width=self.DISPLAY_WIDTH,
                               height=self.DISPLAY_HEIGHT)
        self.lf1 = tk.Frame(self.cnvs1);self.cnvs1.create_window((5, 6), window=self.lf1, anchor="nw")
        self.lf2 = tk.Frame(self.cnvs2);self.cnvs2.create_window((5, 6), window=self.lf2, anchor="nw")
    def add_scrollbars(self):        
        self.vsb1 = tk.Scrollbar(self.holder_frame, orient="vertical", command=self.cnvs1.yview)
        self.vsb2 = tk.Scrollbar(self.holder_frame, orient="vertical", command=self.cnvs2.yview)        
        self.cnvs1.configure(yscrollcommand=self.vsb1.set)
        self.lf1.bind("<Configure>", self.onFrameConfigure)
        self.cnvs2.configure(yscrollcommand=self.vsb2.set)
        self.lf2.bind("<Configure>", self.onFrameConfigure)
    def populate_holder(self):
        self.cnvs1.pack(side="left", fill="both", expand=True)
        self.vsb1.pack(side="left", fill="y")
        self.cnvs2.pack(side="right", fill="both", expand=True)
        self.vsb2.pack(side="right", fill="y")

        for i in range(20):
            button = tk.Button(self.holder_frame,text="Click me")
            button.config(command=lambda b=button:self.on_click(b))
            button.pack(in_=self.lf1)
    def start(self):
        self.root.mainloop()
    def onFrameConfigure(self, event):
        self.cnvs1.configure(scrollregion=self.cnvs1.bbox("all"))
        self.cnvs2.configure(scrollregion=self.cnvs2.bbox("all"))

    def on_click(self,button):
        current_frame = button.pack_info().get("in")
        new_frame = self.lf1 if current_frame == self.lf2 else self.lf2
        button.pack(in_=new_frame)


Example().start()

如果您正在创建一个垂直堆栈的小部件,那么在 canvas 中嵌入一个框架会使解决方案过于复杂。相反,您可以直接在 canvas 中创建按钮。嵌入式框架唯一有用的时候是当你想利用 packgrid 的力量将小部件排列成复杂的布局时。在这种情况下,您正在创建一个垂直堆栈,因此框架是不必要的。

以下代码需要注意几点:

  • 这些按钮是 canvas 的子项,而不是 root 的子项。我不知道你为什么要让按钮成为根 window 的子按钮,因为它们在 canvas 中。它们需要是 canvas 的子元素,这样它们就不会在 canvas.

    的边界之外绘制
  • 一个名为 lasty 的 属性 将 return 底部的 y 坐标。每个新按钮都将添加到此坐标下方。

  • 为简洁起见,此代码未使用 self.fav_templates


import tkinter as tk

class ScrolledButtons(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.canvas = tk.Canvas(self)
        self.vsb = tk.Scrollbar(self, command=self.canvas.yview, orient="vertical")
        self.canvas.configure(yscrollcommand=self.vsb.set)

        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)

        for n in range(20):
            x = 2
            y = self.lasty + 2
            btn = tk.Button(self.canvas, text=f'Button #{n}', command=lambda te=n: self._on_click(te))
            self.canvas.create_window(20, y, anchor="nw", window=btn)
        bbox = self.canvas.bbox("all")
        self.canvas.configure(scrollregion=(0, 0, bbox[2], bbox[3]))

    @property
    def lasty(self):
        bbox = self.canvas.bbox("all")
        lasty = bbox[3] if bbox else 0
        return lasty

root = tk.Tk()
w = ScrolledButtons(root)
w.pack(fill="both", expand=True)
root.mainloop()