Tabelle mit Scrollbar erstellen

Fragen zu Tkinter.
Antworten
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo

Ich habe vor eine Tabelle zu erstellen, die aus mehreren Spalten und Zeilen besteht. Ich habe ein Mockup-Bild angehängt. Bild
Die Tabelle habe ich schon. Ich habe eine Funktion create_table(rows, columns, startrow, startcolumn) geschrieben, die im wesentlichen in Schleifen eine Tabelle aus Entry-Felder erstellt. Ich benötige 3 Tabellen nebeneinandner, wie im Bild zu sehen. Das funktioniert. Nun die Herausforderung. Ich möchte 20 Zeilen sichtbar machen und einen Scrollbar daneben, falls mehr als 20 Zeilen notwendig sind. Ich dachte daran die Tabellen aus 30 Zeilen zu erstellen, und die letzten 10 Zeilen scrollbar zu machen. Dazu habe ich nichts gefunden.

Ich dachte auch schon an ein Frame-Widget, um die Tabellen als Objekt zusammenzufassen und das Frame scrollbar zu machen.

Dann dachte ich es wäre besser keine Entry-Felder sondern Tabellen aus Listboxen zu erstellen. Da bekomme ich das besser mit den Scrollbars hin, da habe ich viel dazu gefunden. Aber die Listbox sieht nun mal nicht wie eine Tabelle aus, z.B. müssten schon mal die Zeilen durch gridlines separiert werden, sonst verliert man bei einer großen Tabelle die Zugehörigkeit.
Zusammengefasst: Ich hätte gerne eine Tabelle aus drei verschiedenen Spalten und Zeilen (siehe Bild), wobei weitere Zeilen scrollbar sein sollen, aber ich habe keine Idee, wie ich es hinbekommen soll.

Es muss möglich sein, dass weiß ich. Kann mir jemand helfen? Vielen Dank!

Grüße,
Tuvok
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

LtTuvok hat geschrieben:Ich dachte auch schon an ein Frame-Widget, um die Tabellen als Objekt zusammenzufassen und das Frame scrollbar zu machen.
Also, den Frame kann man nicht scrollen. Aber richtig ist: die Entries in einen Frame. Dann den Frame in einen Canvas und zwar in einem Window mit Koordinaten x=0,y=0. Neben dem Canvas eine Scrollbar und Scrollbar und Canvas entsprechend verbinden. Den Canvas kann man nämlich scrollen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

LtTuvok hat geschrieben:Hallo
Zusammengefasst: Ich hätte gerne eine Tabelle aus drei verschiedenen Spalten und Zeilen (siehe Bild), wobei weitere Zeilen scrollbar sein sollen, aber ich habe keine Idee, wie ich es hinbekommen soll.

Es muss möglich sein, dass weiß ich. Kann mir jemand helfen? Vielen Dank!

Grüße,
Tuvok
Das wäre ein Beispiel. Brauchst nur Deine Tabelle einfügen und die Höhe entsprechend setzen:

Code: Alles auswählen

# -*- coding: utf-8 -*-

try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk

SCROLLHEIGHT = 200 # pixel

class Application(tk.Tk):

    def __init__(self,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.scroll_tabelle = ScrollTabelle(self)
        self.scroll_tabelle.pack()

class ScrollTabelle(tk.LabelFrame):

    def __init__(self,master,**kwargs):
        tk.LabelFrame.__init__(self,master,**kwargs)
        self.config(text='scroll_tabelle')
        # widget definitions ===================================
        canvas = Canvas_1(self)
        canvas.grid(row=0, sticky='nesw', column=1)
        scrollbar = tk.Scrollbar(self)
        scrollbar.grid(row=0, sticky='ns')

        canvas.config(yscrollcommand=scrollbar.set)
        scrollbar.config(command=canvas.yview)


class Canvas_1(tk.Canvas):

    def __init__(self,master,**kwargs):
        tk.Canvas.__init__(self,master,**kwargs)
        # widget definitions ===================================
        self.frame = Frame_1(self)
        coords = (0,0)
        item = self.create_window(*coords)
        self.itemconfig(item,anchor = 'nw',window = self.frame)


class Frame_1(tk.Frame):

    def __init__(self,master,**kwargs):
        tk.Frame.__init__(self,master,**kwargs)
        # widget definitions ===================================

        self.canvas = self.master
        self.canvas.bind('<Configure>',self.canvas_configure)
        self.bind('<Configure>',self.frame_configure)

        # Daten zum Anzeigen === dafür dann Tabelle reisetzen
        tk.Label(self,text='Config des Canvas',font='Helvetica 30').grid(row=0,columnspan=2)

        maxwidth=0
        for value in self.master.config().values():
            maxwidth = max(maxwidth,len(str(value)))
        print(maxwidth)
            

        row = 1
        for key,value in self.master.config().items():
            tk.Label(self,text=key).grid(row=row,sticky='e')
            entry = tk.Entry(self,width=maxwidth)
            entry.grid(row=row,column=1)
            entry.insert(0,value)
            row +=1
        # ====================================


    # was man auch mit dem Canvas tut, er muß sich immer an die Breite des Frame anpassen - an die tatsächliche Breite
    def canvas_configure(self,event=None):
        if self.canvas['width'] != self.winfo_width():
            self.canvas['width'] = self.winfo_width()

    # hier ist der layoutmanager noch nicht fertig, daher den canvas an die vorraussichtliche Größe des frames anpassen
    def frame_configure(self,event=None):
        # Scroll region des canvas so groß wie man für den Frame braucht
        self.canvas.config(scrollregion="0 0 %s %s" % (self.winfo_reqwidth(), self.winfo_reqheight()))
        # breite des canvas so breit wie man für den frame braucht
        self.canvas.config(width=self.winfo_reqwidth())
        # wenn die benötigte frame höhe größer als SCROLLHEIGHT pixel ist, dann canvas höhe auf SCROLLHEIGHT pixel beschränken
        if self.winfo_reqheight() > SCROLLHEIGHT:
            self.canvas.config(height=SCROLLHEIGHT)
        # sonst canvas höhe so hoch wie man für den frame braucht
        else:
            self.canvas.config(height=self.winfo_reqheight())
            
if __name__ == '__main__':
    Application().mainloop()
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo Alfons

Danke! Das werde ich ausprobieren.

Viele Grüße, Tuvok
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo Alfons

Danke nochmal für deinen Code. Ich habe versucht ihn nachzuvollziehen und für meine Zwecke zu vereinfachen. Insbesondere das Konzept mit .create_window war mir neu. Das war lange Zeit der Knackpunkt bis ich ihn zu verstanden glaubte. Mich wundert es immer noch ein bisschen, dass window sogar die Darstellung (z.B. .pack() oder grid()) ersetzt. Denn dies brauche ich für Entry nicht mehr, wenn sich window um das Entry stülpt. Dann muss nur noch das Window dargestellt werden.
Was hältst du von dem Code?

Code: Alles auswählen

import tkinter as tk
 
root = tk.Tk()

frame=tk.Frame(root,width=300,height=300)
frame.grid(row=0,column=0)
 
canvas = tk.Canvas(frame, bg='gray90', width=300, height=300, scrollregion=(0,0,0,1200))
canvas.grid(column=1, row=0) 

vbar=tk.Scrollbar(frame,orient="vertical") #scrollbar
vbar.grid(column=0, row=0, ipady=125)
vbar.config(command=canvas.yview)
 
canvas.config(yscrollcommand=vbar.set)

row=column=0
entry_names=[]
row_column_names=[]

rows=50
columns=3
specific_name="test"
Tk_instance=canvas
field_width=10

for row in range(rows):

	temp_coordinates_row_list=[]
	
	for column in range(columns):
		element_name=specific_name+"row"+str(row)+"column"+str(column) #this ElementName is to identify the StringVar()
		element_name=tk.StringVar()
		temp_coordinates_row_list.append(element_name) 
		eintrag=tk.Entry(Tk_instance, textvariable=element_name, width=field_width) #the variable "Eintrag" is only needed for the grid once
		row_coordinate=5+20*row
		column_coordinate=5+field_width*column*6.5
		canvas.create_window(column_coordinate, row_coordinate, window=eintrag, anchor="nw")
		
	row_column_names.append(temp_coordinates_row_list)
row_column_names[0][0].set("Hello")
print(row_column_names[0][0].get())
	
root.mainloop()
Viele Grüße und Danke nochmal,
Tuvok
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

@LtTuvok: auf oberster Ebene sollten keine Anweisungen stehen, statt dessen solltest Du alles in Funktionen schreiben. Eingerückt wird immer mit 4 Leerzeichen pro Ebene (nicht 3).

Zeile 17: row und column werden nicht gebraucht.
Zeile 18: entry_names wird nicht verwendet.
Zeile 23/24: wird nicht gebraucht.
Zeile 32: wird nicht verwendet.
Zeile 35: der Kommentar macht irgendwie keinen Sinn.
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo Sirius3

Danke für die Hinweise. Ich werde sie berücksichtigen.
Hast Du ansonsten noch inhaltliche Kommentare zur Strukture mit Frame, Canvas, Scrollbar und .create_window?

Was ich mich als nächsten Schritt Frage ist, wie es möglich ist mit einem Scrollbar zwei Canvas zu scrollen. Wie ich gesehen habe wird durch

Code: Alles auswählen

vbar.config(command=canvas.yview)
Die Scroll-Funktionalität auf das canvas gesetzt. Ich kann nicht ein canvas1 und ein canvas2 definieren (das kann ich schon noch, aber) und dann mein eines vbar sowohl dem canvas1 UND dem canvas2 zuordnen.

Eine Idee wäre die Scrollposition auszulesen und einen zweiten nicht sichtbaren Scrollbar zu übertragen, aber wie dies geht und ob es nicht einen besseren Weg gibt (wahrscheinlich) weiß ich nicht.
Ich hätte gerne verschiedene Canvas mit potentiell verschiedener Hintergrundfarben und jeweils ähnlicher Tabellen, die aber alle über den gleichen Scrollbar gesteuert werden können.

Viele Grüße,
Tuvok
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das sollte so klappen:

Code: Alles auswählen

def multi_yview(canvases, *args):
     for c in canvases:
            c.yview(*args)


command=functools.partial(multi_yview, [canvas1, canvas2])
Das ist jetzt nur ein Ausschnitt, auf dem iPad tippt es sich mistig ;)
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo deets

Danke für den Hinweis. functiontools kannte ich noch nicht. Ich habe mal versucht einen möglichst einfachen Code zu schreiben (und ein paar Hinweise von Sirius3 zu berücksichtigen). Siehe unten
Ich weiß aber noch nicht genau wie ich den Befehl richtig in meinen Code einbaue.

Viele Grüße,
Tuvok

Code: Alles auswählen

import tkinter as tk
import functools as ft

def multi_yview(canvases, *args):
	 for c in canvases:
			c.yview(*args)
	
root = tk.Tk()

frame1=tk.Frame(root,width=220,height=300)
frame1.grid(row=0,column=0)

frame2=tk.Frame(root,width=120,height=300)
frame2.grid(row=0,column=1)
 
canvas1 = tk.Canvas(frame1, bg='gray90', width=220, height=300, scrollregion=(0,0,0,500))
canvas1.grid(column=1, row=0) 

canvas2 = tk.Canvas(frame2, bg='gray90', width=120, height=300, scrollregion=(0,0,0,500))
canvas2.grid(column=0, row=0) 

vbar=tk.Scrollbar(frame1,orient="vertical")
vbar.grid(column=0, row=0, ipady=125)
vbar.config(command=canvas1.yview)
 
canvas1.config(yscrollcommand=vbar.set)
canvas2.config(yscrollcommand=vbar.set)

command=ft.partial(multi_yview, [canvas1, canvas2])



row_column_names1=[]
Tk_instance1=canvas1

rows=50
columns=3
Tk_instance=canvas1
field_width1=10

for row in range(rows):

	temp_coordinates_row_list=[]
	
	for column in range(columns):
		element_name=tk.StringVar()
		temp_coordinates_row_list.append(element_name) 
		eintrag=tk.Entry(Tk_instance1, textvariable=element_name, width=field_width1) #the variable "Eintrag" is only needed for the grid once
		row_coordinate=5+20*row
		column_coordinate=5+field_width1*column*6.5
		canvas1.create_window(column_coordinate, row_coordinate, window=eintrag, anchor="nw")
		
	row_column_names1.append(temp_coordinates_row_list)


row_column_names2=[]
Tk_instance2=canvas2
field_width2=5

for row in range(rows):

	temp_coordinates_row_list=[]
	
	for column in range(columns):
		element_name=tk.StringVar()
		temp_coordinates_row_list.append(element_name) 
		eintrag=tk.Entry(Tk_instance2, textvariable=element_name, width=field_width2) #the variable "Eintrag" is only needed for the grid once
		row_coordinate=5+20*row
		column_coordinate=5+field_width2*column*6.5
		canvas2.create_window(column_coordinate, row_coordinate, window=eintrag, anchor="nw")
		
	row_column_names2.append(temp_coordinates_row_list)
	
row_column_names1[0][0].set("Hello")
#print(row_column_names1[0][0].get())
	
root.mainloop()
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das command musst du schon übergeben an configure, statt der canvas.yview.
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

@LtTuvok: warum benennst Du canvasX in Tk_instanceX um? Statt Copy-Paste schreibt man beim Programmieren eine Funktion, die man für jedes Canvas aufrufen kann. Wie eigentlich alles ab Zeile 8 in eine Funktion gehört.

Code: Alles auswählen

import tkinter as tk
from functools import partial

def multi_yview(canvases, *args):
    for c in canvases:
         c.yview(*args)

def generate_table(root, vbar, width, field_width, num_rows=50, num_columns=3):
    canvas = tk.Canvas(root, bg='gray90', width=width, height=300, scrollregion=(0,0,0,500))
    canvas.config(yscrollcommand=vbar.set)

    rows = []
    for row in range(num_rows):
        cells = []
        for column in range(num_columns):
            contents = tk.StringVar()
            cells.append(contents)
            eintrag = tk.Entry(canvas, textvariable=contents, width=field_width)
            row_coordinate = 5 + row * 20
            column_coordinate = 5 + field_width * column * 6.5
            canvas.create_window(column_coordinate, row_coordinate, window=eintrag, anchor="nw")
        rows.append(cells)
    return canvas, rows


def main():
    root = tk.Tk()

    vbar = tk.Scrollbar(root, orient="vertical")
    vbar.grid(row=0, column=0, ipady=125)

    canvas1, rows1 = generate_table(root, vbar, width=220, field_width=10)
    canvas1.grid(row=0, column=1)

    canvas2, rows2 = generate_table(root, vbar, width=120, field_width=5)
    canvas2.grid(row=0, column=2)

    command = partial(multi_yview, [canvas1, canvas2])
    vbar.config(command=command)

    root.mainloop()
    
if __name__ == '__main__':
    main()
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo Sirius3

Danke für die Anmerkung. Da ich noch am Anfang meines Python-Programmierlernens bin, bin ich immer dankbar für jedes Kommentar, vor allem was Konventionen, Best Practice u.ä. angeht.
Du hast natürlich recht. Genau das was Du geschrieben hast mache ich auch in meinem eigentlichen Programm. Das war nur auf die schnelle zusammengebastelt und für mich war es in dem Moment einfacher. Da ich in meinem eigentlichen Code noch wesentlich mehr Tabellen habe, mache ich das dann über eine Funktion.

Viele Grüße,
Tuvok
LtTuvok
User
Beiträge: 18
Registriert: Freitag 11. August 2017, 09:53

Hallo

Nebenbei bemerkt, eine etwas unschöne Sache ist, dass die Tabelle bzw. das jeweilige ganz unten und ganz oben stehende Entry-Feld über den Canvas hinausschaut. Ich habe schon versucht ein drittes Canvas unten in die nächste Row zu setzen und mit columnspan über die beiden Tabellen "zu kleistern", aber da bleibt ja auch noch der kleine Rahmen (den ich an sich ganz nett finde) übrig, wohinein die Entry-Felder überstehen.

Hat jemand dazu eine Idee, wie man das eventuell wegbekommt?

Viele Grüße,
Tuvok
Antworten