Добавление полосы прокрутки к группе виджетов в Tkinter

Я использую Python для синтаксического анализа записей из файла журнала и отображения содержимого записи с помощью Tkinter, и до сих пор это было отлично. Вывод представляет собой сетку виджетов с метками, но иногда строк больше, чем можно отобразить на экране. Я хотел бы добавить полосу прокрутки, которая выглядит так, как будто это должно быть очень легко, но я не могу понять это.

Документация подразумевает, что только виджеты List, Textbox, Canvas и Entry поддерживают интерфейс полосы прокрутки. Ни один из них не подходит для отображения сетки виджетов. Можно поместить произвольные виджеты в виджет Canvas, но вам, похоже, нужно использовать абсолютные координаты, поэтому я не смогу использовать менеджер компоновки сетки?

Я пытался поместить сетку виджетов в фрейм, но кажется, что она не поддерживает интерфейс полосы прокрутки, поэтому это не работает:

mainframe = Frame(root, yscrollcommand=scrollbar.set)

Кто-нибудь может предложить способ обойти это ограничение? Я не хотел бы, чтобы мне пришлось переписывать в PyQt и так сильно увеличивать размер моего исполняемого образа, просто чтобы добавить полосу прокрутки!

2 ответа

Решение

обзор

Вы можете связать полосы прокрутки только с несколькими виджетами, а корневой виджет и Frame не являются частью этой группы виджетов.

Наиболее распространенное решение - создать виджет холста и связать полосы прокрутки с этим виджетом. Затем в этот холст вставьте фрейм, содержащий ваши виджеты с ярлыками. Определите ширину / высоту рамки и введите ее в холст scrollregion вариант, чтобы область прокрутки точно соответствовала размеру рамки.

Рисование текстовых элементов непосредственно на холсте не очень сложное, поэтому вам может потребоваться пересмотреть этот подход, если решение "встроенный в холст" кажется слишком сложным. Поскольку вы создаете сетку, координаты каждого текстового элемента будет очень легко вычислить, особенно если каждая строка имеет одинаковую высоту (что, вероятно, так и есть, если вы используете один шрифт).

Для рисования непосредственно на холсте просто определите высоту строки используемого шрифта (и для этого есть команды). Тогда каждая координата у row*(lineheight+spacing), Координата x будет фиксированным числом на основе самого широкого элемента в каждом столбце. Если вы дадите всем тег для столбца, в котором он находится, вы можете отрегулировать координату x и ширину всех элементов в столбце с помощью одной команды.

Объектно-ориентированное решение

Вот пример решения frame-embedded-in-canvas с использованием объектно-ориентированного подхода:

import tkinter as tk

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

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

Примечание: чтобы сделать это в Python 2.x, используйте Tkinter скорее, чем tkinter в операторе импорта

Сделать прокручиваемым

Используйте этот удобный класс, чтобы сделать прокручиваемый фрейм, содержащий ваши виджеты. Следуй этим шагам:

  1. создать рамку
  2. показать его (упаковка, сетка и т. д.)
  3. сделать прокруткой
  4. добавить виджеты внутри него
  5. вызвать метод update()

import tkinter as tk
from tkinter import ttk

class Scrollable(ttk.Frame):
    """
       Make a frame scrollable with scrollbar on the right.
       After adding or removing widgets to the scrollable frame, 
       call the update() method to refresh the scrollable area.
    """

    def __init__(self, frame, width=16):

        scrollbar = tk.Scrollbar(frame, width=width)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)

        self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar.config(command=self.canvas.yview)

        self.canvas.bind('<Configure>', self.__fill_canvas)

        # base class initialization
        tk.Frame.__init__(self, frame)         

        # assign this obj (the inner frame) to the windows item of the canvas
        self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)


    def __fill_canvas(self, event):
        "Enlarge the windows item to the canvas width"

        canvas_width = event.width
        self.canvas.itemconfig(self.windows_item, width = canvas_width)        

    def update(self):
        "Update the canvas and the scrollregion"

        self.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))

Пример использования

root = tk.Tk()

header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()

ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()


scrollable_body = Scrollable(body, width=32)

for i in range(30):
    ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()

scrollable_body.update()

root.mainloop()

Расширяет класс tk.Frameдля поддержки прокручиваемого фрейма
Этот класс не зависит от прокручиваемых виджетов и может использоваться для замены стандартногоtk.Frame.


import tkinter as tk

class ScrollbarFrame(tk.Frame):
    """
    Extends class tk.Frame to support a scrollable Frame 
    This class is independent from the widgets to be scrolled and 
    can be used to replace a standard tk.Frame
    """
    def __init__(self, parent, **kwargs):
        tk.Frame.__init__(self, parent, **kwargs)

        # The Scrollbar, layout to the right
        vsb = tk.Scrollbar(self, orient="vertical")
        vsb.pack(side="right", fill="y")

        # The Canvas which supports the Scrollbar Interface, layout to the left
        self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
        self.canvas.pack(side="left", fill="both", expand=True)

        # Bind the Scrollbar to the self.canvas Scrollbar Interface
        self.canvas.configure(yscrollcommand=vsb.set)
        vsb.configure(command=self.canvas.yview)

        # The Frame to be scrolled, layout into the canvas
        # All widgets to be scrolled have to use this Frame as parent
        self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
        self.canvas.create_window((4, 4), window=self.scrolled_frame, anchor="nw")

        # Configures the scrollregion of the Canvas dynamically
        self.scrolled_frame.bind("<Configure>", self.on_configure)

    def on_configure(self, event):
        """Set the scroll region to encompass the scrolled frame"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

Применение:

class App(tk.Tk):
    def __init__(self):
        super().__init__()

        sbf = ScrollbarFrame(self)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        sbf.grid(row=0, column=0, sticky='nsew')
        # sbf.pack(side="top", fill="both", expand=True)

        # Some data, layout into the sbf.scrolled_frame
        frame = sbf.scrolled_frame
        for row in range(50):
            text = "%s" % row
            tk.Label(frame, text=text,
                     width=3, borderwidth="1", relief="solid") \
                .grid(row=row, column=0)

            text = "this is the second column for row %s" % row
            tk.Label(frame, text=text,
                     background=sbf.scrolled_frame.cget('bg')) \
                .grid(row=row, column=1)


if __name__ == "__main__":
    App().mainloop()
Другие вопросы по тегам