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

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

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

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

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

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

Всего 2 ответа


обзор

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

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

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

Для рисования прямо на холсте, просто определите высоту линии шрифта, который вы используете (и для этого есть команды). Тогда каждая координата y является 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()

Есть идеи?

10000