【问题标题】:Python Tkinter Scrollbar and Frame not showing all CheckboxesPython Tkinter 滚动条和框架未显示所有复选框
【发布时间】:2021-10-14 05:05:33
【问题描述】:

一些 tkinter 代码有问题,我相信我离它太近了,看不到我面前的问题。我正在将复选框加载到框架中并将滚动条附加到该位置。

在我达到 1000 多个复选框之前,这很有效。然后它似乎被切断了,即使框架扩展了适合所有复选框的高度,它也没有在 gui 中显示它们。你可以在图片中看到他们停止显示Checkbox Malfunction

这是我的代码:(请原谅它看起来有多乱,它是一个更大的代码集的子集,我刚刚隔离了错误)

from tkinter import *


build_vars = {}
build_Radios = []
parent = Tk()

center_container = Frame(parent, width=5, height=5)
center_container.grid(row=1, sticky="nsew")

# Center Row Columns
center_center_container = Frame(center_container, width=150, height=200)
center_center_container.grid(row=0, column=2, sticky="ns")

build_canvas = Canvas(center_center_container, background='green')
build_canvas.grid(row=0, column=0, sticky=N+E+W+S)

# Create a vertical scrollbar linked to the canvas.
vsbar = Scrollbar(center_center_container, orient=VERTICAL, command=build_canvas.yview)
vsbar.grid(row=0, column=1, sticky=NS)
build_canvas.configure(yscrollcommand=vsbar.set)

# Create a frame on the canvas to contain the buttons.
frame_buttons = Frame(build_canvas, bd=2, background='red')

def create_build_radios():

    # for index, item in enumerate(filtered_builds):
    for index, item in enumerate(list(range(3000))):
        build_vars[item] = IntVar()
        radio = Checkbutton(frame_buttons, text=item, variable=build_vars[item], onvalue=1,
                        offvalue=0,
                        command=lambda item=item: sel(item))
        radio.grid(row=index, column=0, sticky=W)
        build_Radios.append(radio)

    # Create canvas window to hold the buttons_frame.
    build_canvas.create_window((0, 0), window=frame_buttons, anchor=NW)
    build_canvas.update_idletasks()  # Needed to make bbox info available.
    bbox = build_canvas.bbox(ALL)  # Get bounding box of canvas with Buttons.
    build_canvas.configure(scrollregion=bbox, width=150, height=400)

def sel(item):
    print(item)

create_build_radios()
parent.mainloop()

【问题讨论】:

  • 我不知道为什么,我的复选框列表停在 1310。但是您的示例代码,这两行似乎需要意图:radio.grid(row=index, column=0,sticky=W) build_Radios.append(收音机)
  • @moogoo 谢谢!是 Stackoverflow 不接受我的格式的复制和粘贴问题。此问题已修复。
  • 画布可以显示的数量有限制。你可能已经达到了这个限制。
  • 你能扩展一下吗?画布高度似乎扩大了,这就是我给它上色的原因。你的意思是它限制了它可以显示的内容?另外,如果这是一个解决方案,请将其放在解决方案部分。
  • @buzzard0190 为什么需要 3000 个Checkbuttons?如果将它们放在网格中,您应该能够显示更多?

标签: python tkinter checkbox scrollbar frame


【解决方案1】:

所以这是更好的解决方案(比另一个更好,可以轻松地在此上放置更多小部件,但请注意可能存在某种限制(至少是 CPU 的能力):

from tkinter import Tk, Canvas, Frame, Label, Scrollbar, Button, DoubleVar, StringVar, Entry
from tkinter.ttk import Progressbar


class PagedScrollFrame(Frame):
    def __init__(self, master, items_per_page=100, **kwargs):
        Frame.__init__(self, master, **kwargs)
        self.master = master
        self.items_per_page = items_per_page
        self.pages = None
        self.id_list = []
        self.bbox_tag = 'all'

        self._loading_frame = Frame(self)
        self.__load_progress_tracker = DoubleVar(master=self.master, value=0.0)
        self.__percent_tracker = StringVar(master=self.master, value='0.00%')

        self.frame = Frame(self)
        self.frame.pack(side='top', padx=20, pady=20)

        self.canvas = Canvas(self.frame)
        self.canvas.pack(side='left')

        self.bg_label = Label(self.canvas)
        self.bg_label.place(x=0, y=0, relwidth=1, relheight=1)

        self.scrollbar = Scrollbar(self.frame, orient='vertical', command=self.canvas.yview)
        self.scrollbar.pack(side='right', fill='y')
        self.canvas.config(yscrollcommand=self.scrollbar.set)
        self.canvas.bind('<Configure>',
                         lambda e: self.canvas.config(
                             scrollregion=self.canvas.bbox(self.bbox_tag)
                         ))

        self.button_frame = Frame(self)
        self.button_frame.pack(fill='x', side='bottom', padx=20, pady=20)

        self.canvas_frame = Frame(self.button_frame)
        self.button_canvas = Canvas(self.canvas_frame, height=20)
        self.button_canvas.pack(expand=True)
        self.inner_frame = Frame(self.button_canvas)
        self.button_canvas.create_window(0, 0, window=self.inner_frame, anchor='nw')

        self.button_scrollbar = Scrollbar(self.canvas_frame,
                                          orient='horizontal',
                                          command=self.button_canvas.xview)
        self.button_scrollbar.pack(fill='x')
        self.button_canvas.config(xscrollcommand=self.button_scrollbar.set)
        self.button_canvas.bind(
            '<Configure>', lambda e: self.button_canvas.config(
                scrollregion=self.button_canvas.bbox('all')
            )
        )

    def pack_items(self):
        if not self.pages:
            return
        self._loading_frame.place(x=0, y=0, relwidth=1, relheight=1)
        self._loading_frame.lift()
        self._loading_frame.update_idletasks()
        self.after(100, self._pack_items)

    def _pack_items(self):
        Label(self._loading_frame, text='Loading...').pack(expand=True)
        Progressbar(self._loading_frame,
                    orient='horizontal',
                    variable=self.__load_progress_tracker,
                    length=self._loading_frame.winfo_width()
                           - self._loading_frame.winfo_width() // 10).pack(expand=True)
        Label(self._loading_frame, textvariable=self.__percent_tracker).pack(expand=True)
        self.update_idletasks()
        widgets = [widget for page in self.pages for widget in page.winfo_children()]
        length = len(widgets)
        self.after(100, self.__pack_items, widgets, 0, length)

    def __pack_items(self, widgets, index, length):
        if index >= length:
            self._loading_frame.destroy()
            self.canvas.config(scrollregion=self.canvas.bbox('all'))
            return
        widgets[index].pack()
        percent = (index + 1) * 100 / length
        self.__load_progress_tracker.set(value=percent)
        self.__percent_tracker.set(value=f'{percent: .2f}%')
        self.after(1, self.__pack_items, widgets, index + 1, length)

    def change_frame(self, index):
        if not self.pages:
            return
        self.bbox_tag = self.id_list[index]
        self.canvas.config(scrollregion=self.canvas.bbox(self.bbox_tag))
        self.bg_label.lift()
        self.pages[index].lift()

    def create_pages(self, num_of_items, items_per_page=None):
        self.pages = None
        if not items_per_page:
            items_per_page = self.items_per_page
        num_of_pages = num_of_items // items_per_page
        if num_of_items % items_per_page != 0:
            num_of_pages += 1
        start_indexes = [items_per_page * page_num for page_num in range(num_of_pages)]
        end_indexes = [num + items_per_page for num in start_indexes]
        end_indexes[-1] += (num_of_items % items_per_page
                            - (items_per_page if num_of_items % items_per_page != 0 else 0))
        self.pages = [Frame(self.canvas) for _ in range(num_of_pages)]
        self.id_list = []
        for page, frame in enumerate(self.pages):
            self.id_list.append(self.canvas.create_window(0, 0, window=frame, anchor='nw'))
        self.pages[0].lift()
        if num_of_pages >= 2:
            Button(self.button_frame, text='1',
                   command=lambda: self.change_frame(0)).pack(
                side='left', expand=True, fill='both', ipadx=5
            )
            if num_of_pages > 2:
                self.canvas_frame.pack(fill='x', expand=True, side='left')
                for page_num in range(1, num_of_pages - 1):
                    Button(self.inner_frame, text=page_num + 1,
                           command=lambda index=page_num: self.change_frame(index)).pack(
                        expand=True, fill='both', side='left', ipadx=5
                    )
            Button(self.button_frame, text=num_of_pages,
                   command=lambda: self.change_frame(num_of_pages - 1)).pack(
                side='right', fill='both', expand=True, ipadx=5
            )
        return zip(start_indexes, end_indexes, self.pages)


def create_paged_canvas():
    scroll = PagedScrollFrame(root)
    scroll.pack()

    lst = tuple(range(3000))
    for start, end, parent in scroll.create_pages(len(lst)):
        for i in lst[start:end]:
            frame_ = Frame(parent)
            Label(frame_, text=str(i).zfill(4)).pack(side='left')

    scroll.pack_items()


root = Tk()
root.protocol('WM_DELETE_WINDOW', exit)

create_paged_canvas()

root.mainloop()

主要信息:
基本上这会创建分页的可滚动画布。所需要做的就是调整 create_paged_canvas() 函数中的内部循环和可迭代对象。您还可以调整每页显示多少项目(这也允许稍后配置,例如在菜单中您可以调用类似于create_paged_canvas() 的函数并将items_per_page 参数更改为其他内容(必须再次加载所有内容)但是...tkintertkinter,它非常慢,更糟糕的是它不允许直接使用线程,甚至不能谈论进程(这些东西会加快速度,但根本无法完成) ))

重要(建议):
我强烈建议在导入某些内容时不要使用通配符 (*),您应该导入您需要的内容,例如from module import Class1, func_1, var_2 等等或导入整个模块:import module 然后你也可以使用别名:import module as md 或类似的东西,关键是不要导入所有东西,除非你真的知道你在做什么;名称冲突是问题所在。

杂项:
为了获得更好的性能,最好不要创建标签,而是直接在画布上创建文本(使用其他解决方案)或使用列表框,以防您需要显示大量数据,因为它会加快速度,因为没有必须创建小部件(这也意味着您几乎只能查看数据)

如果您有任何问题,请务必提出这些问题!

【讨论】:

    【解决方案2】:

    解决办法来了:

    非常重要
    有一些奇怪的错误我不知道如何修复(至少)关于性能和东西,这使得这个解决方案非常不可用,我的意思是理论上你可以在这个上面放置更多的小部件并滚动而不是通常的方法但是取决于 CPU,它可能会工作,也可能无法正常工作。我就简单写个分页回答

    from tkinter import (Tk, Frame, Label, Scrollbar, Canvas,
                         DoubleVar, Entry, Button, StringVar, TclError)
    from tkinter.ttk import Progressbar
    
    
    class EndlessScroll(Canvas):
        def __init__(self, master, update_mode='passive', scrollbar=None, **kwargs):
            """update_mode:
                            'passive' (recommended and default): widgets have to be accessed (Text, Entry...)
                                      or using Frame, DON'T use if only single widget per line,
                                      USE Frame with this
                            'active': recommended only if single widget
                                      per line (best with Label
                                                but can use Button too)"""
            Canvas.__init__(self, master, **kwargs)
            self.master = master
            self._widget_heights = []
            self._start_index = 0
            self._widgets = []
            self.temp_frame = None
            self.__got_height = False
            self.__initialised_placement = False
            self.__height_counter = 0
            self.yscrollincrement = None
            self._update_mode = update_mode
            self.scrollbar = scrollbar
            self.__load_progress_tracker = DoubleVar(master=self.master, value=0.0)
            self.__percent_tracker = StringVar(master=self.master, value='0.0%')
            self._allow_mouse_control = False
            self.bind('<Enter>', lambda e: setattr(self, '_allow_mouse_control', True))
            self.bind('<Leave>', lambda e: setattr(self, '_allow_mouse_control', False))
            self.bind('<MouseWheel>', self.__scroll_with_mouse)
            for key, value in kwargs.items():
                setattr(self, key, value)
    
        def __scroll_with_mouse(self, event):
            if not self._allow_mouse_control:
                return
            self.yscroll('scroll', (-1 * (event.delta/120)), 'units')
    
        def set_scrollbar(self, scrollbar):
            self.scrollbar = scrollbar
    
        def init_after_widgets(self):
            self.temp_frame = Frame(self)
            self._widgets = self.winfo_children()[:-1]
            self.temp_frame.place(x=0, y=0, relwidth=1, relheight=1)
            self.update()
            Label(self.temp_frame, text='Loading...').pack(expand=True)
            Progressbar(self.temp_frame,
                        orient='horizontal',
                        variable=self.__load_progress_tracker,
                        length=self.temp_frame.winfo_width()
                               - self.temp_frame.winfo_width() // 10).pack(expand=True)
            Label(self.temp_frame, textvariable=self.__percent_tracker).pack(expand=True)
            self.after(100, self._get_widget_heights, 0)
    
        def _get_widget_heights(self, index):
            length = len(self._widgets)
            self.after(100, self.__get_widget_heights, index, self._widgets, length)
    
        def __get_widget_heights(self, index, widgets, length):
            if index >= length:
                self.__got_height = True
                self.temp_frame.place_forget()
                self._initial_placement(widgets)
                self.update_idletasks()
                self.__update()
                self.yscroll(None, 0)
                return
            id_ = self.create_window(0, 0, window=widgets[index], anchor='nw')
            self.update()
            try:
                self._widget_heights.append(widgets[index].winfo_height())
                self.delete(id_)
            except TclError:
                pass
            percent = (index + 1) * 100 / length
            self.__load_progress_tracker.set(value=percent)
            self.__percent_tracker.set(value=f'{percent: .2f}%')
            self.after(1, self.__get_widget_heights, index + 1, widgets, length)
    
        def yscroll(self, *args):
            if not self.__initialised_placement or not self.scrollbar:
                return
            type_, fraction = args[0], args[1]
            parent_height = self.winfo_height()
            if type_ == 'scroll':
                units = args[2]
                k = float(fraction)
                if k < -1.0:
                    k = -1.0
                elif k > 1.0:
                    k = 1.0
                top, bottom, *_ = self.scrollbar.get()
                length = (bottom - top) if not self.yscrollincrement else self.yscrollincrement
                if (top == 0.0 and k < 0) or (bottom == 1.0 and k > 0):
                    return
                if 0 < top < length:
                    length = top
                if 0 < (1 - bottom) < length:
                    length = 1 - bottom
                if units == 'units':
                    fraction = top + (k * length)
                elif units == 'pages':
                    return
                    # fraction = top + (k * (0.9 * parent_height) * length)
            sum_height = sum(self._widget_heights)
            scroll_height = float(fraction) * sum_height
            height_counter = 0
            for index_, height in enumerate(self._widget_heights):
                height_counter += height
                if height_counter > scroll_height:
                    self._start_index = index_
                    self.__height_counter = -(height - (height_counter - scroll_height))
                    break
            self.scrollbar.set(float(fraction), float(fraction) + parent_height / sum_height)
            if self._update_mode == 'passive':
                self.__update()
    
        def __update(self, _call_from_self=False):
            if _call_from_self and self._update_mode == 'passive':
                self.after(10, self.__update, True)
                return
            parent_height = self.winfo_height()
            height_counter = self.__height_counter
            self.delete('all')
            for height, widget in zip(self._widget_heights[self._start_index:], self._widgets[self._start_index:]):
                if height_counter > parent_height:
                    break
                self.create_window(0, height_counter, window=widget, anchor='nw')
                height_counter += height
            self.after(10, self.__update, True)
    
        def _initial_placement(self, widgets):
            parent_height = self.winfo_height()
            height_counter = 0
            for height, widget in zip(self._widget_heights, widgets):
                if height_counter > parent_height:
                    self.__initialised_placement = True
                    return
                self.create_window(0, height_counter, window=widget, anchor='nw')
                height_counter += height
    
    
    def create_scroller():
        endless_scroll = EndlessScroll(root, yscrollincrement=0.1)
        endless_scroll.pack(side='left')
    
        for i in range(100):
            frame = Frame(endless_scroll)
            Label(frame, text=str(i).zfill(4)).pack()
    
        scrollbar = Scrollbar(root, command=endless_scroll.yscroll)
        scrollbar.pack(side='right', fill='y')
    
        endless_scroll.set_scrollbar(scrollbar)
        endless_scroll.init_after_widgets()
    
    
    root = Tk()
    
    create_scroller()
    
    root.mainloop()
    

    MAIN_EDIT:
    我稍微完善了小部件,其余说明仍然有效,但我修复了以下问题,使用小部件,以便您可以看到添加的最后一个小部件(是索引问题),现在您可以看到所有这些。添加了鼠标滚动和使用滚动条的箭头滚动(仍然无法通过单击滚动条来移动它,也不知道该怎么做(还))。现在还显示百分比,减少了一些等待时间。现在,如果您在加载过程中关闭窗口,它也会捕获引发的错误。
    基本上,您只需转到 create_scroller() 函数定义并根据需要调整循环。

    重要(建议)
    我强烈建议在导入某些内容时不要使用通配符 (*),您应该导入您需要的内容,例如from module import Class1, func_1, var_2 等等或导入整个模块:import module 然后你也可以使用别名:import module as md 或类似的东西,关键是不要导入所有东西,除非你真的知道你在做什么;名称冲突是问题所在。


    这里的主要说明是,在将小部件添加到 EndlessScroll 小部件后,您必须放置 .init_after_widgets() 方法(不要打包或添加任何东西(如示例中所示)) . 请注意特殊的小部件,它也会尝试修复它以使其正常工作,而且非常重要的是它目前仅适用于拖动滚动条的滑块(也会修复它(至少尝试)。罢工>

    问题是它现在可以工作了,理论上它可以显示任意数量的小部件(尝试使用 50000 但这非常滞后,我不得不关闭它但使用 3000 它就像一个魅力),还有一件很棒的事情是窗口在“加载”小部件时是响应式的(加载对于获取小部件的高度至关重要)所以是的,您可以尝试自己改进它,但我也会自己尝试这样做,但稍后会。

    编辑1:
    例如,如果您想将更多小部件放在一起,您可以使用框架:

    for i in range(100):
        frame = Frame(endless_scroll)
        Label(frame, text=f'Entry {i}:').pack(side='left')
        Entry(frame).pack(side='right')
    

    在一定程度上解决了这个问题。请参阅下面的编辑。
    由于.__update() 循环,此特定示例的条目存在轻微问题,否则静态小部件应该完全正常工作(按钮问题较少,但可能更难按下它们,Entry 和 Text 小部件可能无法使用)

    EDIT2:
    添加了更改更新模式的选项以处理上述问题(阅读EndlessScroll 类中的文档字符串)。问题在于使用 Frame,它基本上打破了现在的“主动”模式循环(可能是因为数据量),因此您必须使用“被动”模式,但这意味着更新不太流畅(如果你不使用框架)。所以你也可以使用“活动”模式,但只有当你每行有一个小部件时(希望@TheLizzard通过制作某种自定义.grid()方法或某事来解决这个问题,在cmets中)然后它更新非常顺利。因此,目前除非确实需要在一列中有很多小部件并将它们扩展到其他列,否则我建议使用添加滚动的常用方法。当然,除非您可以在拖动滑块时稍微闪烁一下,否则其他方法不会产生该问题。

    如果你有任何问题,问他们!

    【讨论】:

    • 不错的解决方案,但我想添加对所有几何管理器的支持。我将为此添加书签并尝试考虑一个适用于所有几何管理器的答案。
    • @TheLizzard 您想在哪个部分添加此支持? EndlessScroll 小部件或放入其中的小部件? EndlessScroll 小部件已经支持所有几何管理器(至少应该支持,因为它只是继承自 Canvas)。里面的小部件?他们甚至不使用几何管理器,它们是使用画布的.create_window 方法放置的,我想你可以尝试用.place() 替换它,但是其他管理器呢?这些将需要一些其他容器。但是可以将框架放在EndlessScroll 中,然后在这些框架上使用任何几何管理器
    • 我知道你使用了.create_window。问题是,如果您想制作一个用户必须填写左侧标签和右侧条目的表单,这无济于事。我想做一个更通用的解决方案。我应该在今天结束之前有一个工作原型(除非我变得懒惰:D)。
    • @TheLizzard 嗯...您可以简单地使用框架,编辑帖子以包含您提到的表单示例,但有一个小问题需要解决:更新
    • 我认为唯一简单的解决方案不是在一列中包含 3000 个小部件,而是将其设为 4 列。这样你就不会达到画布上的 y 限制。
    猜你喜欢
    • 2019-04-15
    • 1970-01-01
    • 2013-04-17
    • 1970-01-01
    • 1970-01-01
    • 2017-03-24
    • 2015-07-13
    相关资源
    最近更新 更多