web-dev-qa-db-fra.com

Ajout d'une barre de défilement à un groupe de widgets dans Tkinter

J'utilise Python pour analyser les entrées d'un fichier journal et afficher le contenu des entrées à l'aide de Tkinter et jusqu'à présent, il a été excellent. La sortie est une grille de widgets d'étiquettes, mais parfois il y a plus de lignes que ce qui peut être affiché à l'écran. Je voudrais ajouter une barre de défilement, qui semble être très facile, mais je ne peux pas le comprendre.

La documentation implique que seuls les widgets List, Textbox, Canvas et Entry prennent en charge l'interface de la barre de défilement. Aucun de ceux-ci ne semble approprié pour afficher une grille de widgets. Il est possible de mettre des widgets arbitraires dans un widget Canvas, mais vous semblez devoir utiliser des coordonnées absolues, donc je ne pourrais pas utiliser le gestionnaire de disposition de grille?

J'ai essayé de mettre la grille de widgets dans un cadre, mais cela ne semble pas prendre en charge l'interface de la barre de défilement, donc cela ne fonctionne pas:

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

Quelqu'un peut-il suggérer un moyen de contourner cette limitation? Je détesterais devoir réécrire dans PyQt et augmenter la taille de mon image exécutable de tant, juste pour ajouter une barre de défilement!

55
Simon Hibbs

Présentation

Vous ne pouvez associer des barres de défilement qu'à quelques widgets, et le widget racine et Frame ne font pas partie de ce groupe de widgets.

La solution la plus courante consiste à créer un widget de canevas et à associer les barres de défilement à ce widget. Ensuite, dans cette toile, incorporez le cadre qui contient vos widgets d'étiquette. Déterminez la largeur/hauteur du cadre et introduisez-la dans le canevas scrollregion afin que la région de défilement corresponde exactement à la taille du cadre.

Il n'est pas très difficile de dessiner les éléments de texte directement sur le canevas, vous voudrez peut-être reconsidérer cette approche si la solution frame-embedded-in-a-canvas semble trop complexe. Puisque vous créez une grille, les coordonnées de chaque élément de texte vont être très faciles à calculer, surtout si chaque ligne a la même hauteur (ce qui est probablement le cas si vous utilisez une seule police).

Pour dessiner directement sur la toile, déterminez simplement la hauteur de ligne de la police que vous utilisez (et il existe des commandes pour cela). Ensuite, chaque coordonnée y est row*(lineheight+spacing). La coordonnée x sera un nombre fixe basé sur l'élément le plus large de chaque colonne. Si vous donnez à chaque élément une balise pour la colonne dans laquelle il se trouve, vous pouvez ajuster la coordonnée x et la largeur de tous les éléments d'une colonne avec une seule commande.

Solution orientée objet

Voici un exemple de la solution frame-embedded-in-canvas, utilisant une approche orientée objet:

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 __== "__main__":
    root=tk.Tk()
    Example(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

Solution procédurale

Voici une solution qui n'utilise pas d'objets:

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

Remarque: pour que cela fonctionne dans python 2.x, utilisez Tkinter plutôt que tkinter dans l'instruction d'importation

97
Bryan Oakley

Faites-le défiler

Utilisez cette classe pratique pour faire défiler le cadre contenant vos widgets. Suivez ces étapes:

  1. créer le cadre
  2. l'afficher (pack, grille, etc.)
  3. faire défiler
  4. ajouter des widgets à l'intérieur
  5. appeler la méthode update ()

import tkinter as tk
from tkinter import ttk

class Scrollable(tk.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))

Exemple d'utilisation

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