table display in tkinter with scroll bars - python

I'm writing a small GUI in python3/tkinter. What I want to do is generate a window with a table of data (like a spreadsheet) and have the table be scrollable both horizontally and vertically. Right now I'm just trying to display data, so I'm using a grid of Labels. The data display works fine but I can't get the scroll bars to act correctly. Here is the relevant part of my code; the class that this is in inherits from tk.Toplevel
frame = self.frame = tk.Frame(self)
self.frame.grid(row=1, columnspan=2, padx=2, pady=2, sticky=tk.N+tk.E+tk.S+tk.W)
self.text_area = tk.Canvas(self.frame, background="black", width=400, height=500, scrollregion=(0,0,1200,800))
self.hscroll = tk.Scrollbar(self.frame, orient=tk.HORIZONTAL, command=self.text_area.xview)
self.vscroll = tk.Scrollbar(self.frame, orient=tk.VERTICAL, command=self.text_area.yview)
self.text_area['xscrollcommand'] = self.hscroll.set
self.text_area['yscrollcommand'] = self.vscroll.set
self.text_area.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.hscroll.grid(row=1, column=0, sticky=tk.E+tk.W)
self.vscroll.grid(row=0, column=1, sticky=tk.N+tk.S)
self._widgets = []
for row in range(rows):
current_row = []
for column in range(columns):
label = tk.Label(self.text_area, text="",
borderwidth=0, width=width)
label.grid(row=row, column=column, sticky="nsew", padx=1, pady=1)
current_row.append(label)
self._widgets.append(current_row)
The table displays OK and the scrollbars appear but are not functional:
Any ideas?

You have a couple of problems. First, you can't use grid to place labels in the canvas and expect them to scroll. When you scroll a canvas, only the widgets added with create_window will scroll. However, you can use grid to put the labels in a frame, and then use create_window to add the frame to the canvas. There are several examples of that technique on this site.
Second, you need to tell the canvas how much of the data in the canvas should be scrollable. You use this by setting the scrollregion attribute of the canvas. There is a method, bbox which can give you a bounding box of all of the data in the canvas. Usually it's used like this:
canvas.configure(scrollregion=canvas.bbox("all"))

An option I have used is the leave the labels static and changed the data when the scrollbar is used. This is easy for vertical scrolling but if the columns are of different widths does not work for horizontal scrolling - it would require resizing the labels.

Related

Repeated replacement of a widget with a widget of equal size - how its done?

I'm currently working on a tkinter based python gui and in my program you are able to press a button and then the first widget is getting invisible and the other one ist displayed one the same position of the first one. After that you are able to switch back and the second widget disappears and the first one will be at the same position visible and so on...
My goal is that the two widgets get the same size if you are switching the wdigets and the user will not see any bigger/smaller widgets, but I'm not able to say how I can get there - may you can help me?
My tkinter window:
self.window = tkinter.Tk()
self.window.resizable(True, True)
self.window.grid_rowconfigure(2, weight=1)
self.window.grid_columnconfigure(0, weight=2)
self.window.grid_columnconfigure(1, weight=1)
My frame for the two widgets:
contentFrame = tkinter.Frame(master=self.window, bg="#FFF", highlightthickness=0, borderwidth=0)
contentFrame.grid_rowconfigure(0, weight=1)
contentFrame.grid_columnconfigure(0, weight=1)
contentFrame.grid(column=0, row=2, padx=5, pady=5, sticky="nsew")
My first widget (red one):
canvas = tkinter.Canvas(master=contentFrame, bg="#F00", highlightthickness=0, borderwidth=0)
canvas.pack(fill="both", expand=True)
My second widget (blue one)
xmlEditor = tkinter.Text(master=contentFrame, bg="#00F", highlightthickness=0, borderwidth=0)
And whats happening when change from red to blue one:
self.window.children["!frame2"].children["!canvas"].pack_forget()
self.window.children["!frame2"].children["!text"].pack(fill="both", expand=True)
And whats happening when change from blue to red one:
self.window.children["!frame2"].children["!text"].pack_forget()
self.window.children["!frame2"].children["!canvas"].pack(fill="both", expand=True)
Look at the video here to see the problem in action :)
What I would do is use an intermediate frame which controls the size. Then, you can pack either the red or blue frame inside this frame.
Start by creating a subframe with the size you want. Lets say you want the area to be 500x500:
subframe = tk.Frame(contentFrame, width=500, height=500)
subframe.pack(fill="both", expand=True)
Next, turn geometry propagation off so that the frame won't grow or shrink to fit its children:
subframe.pack_propagate(False)
Finally, put your red and blue frames inside this subframe:
canvas = tkinter.Canvas(master=subframe, bg="#F00", highlightthickness=0, borderwidth=0)
xmlEditor = tkinter.Text(master=subframe, bg="#00F", highlightthickness=0, borderwidth=0)

Avoid the status bar (footer) from disappearing in a GUI when reducing the size of the screen

I have developed a quite large GUI using tkinter.
Everything is working great when resizing, except for the case of reducing a lot the height of the window. All the frames are placed using 'pack()'.
The basic vertical structure of the GUI is (ordered from top to bottom):
Toolbar frame
self.frame_Toolbar=Frame(self.root, bg=colorBackground)
self.frame_Toolbar.pack(side=TOP, fill=X)
Middle frame containing all the basic information
self.frame_Middle=Frame(self.root)
self.frame_Middle.pack(side=TOP, fill=BOTH, expand=YES)
Status bar
self.frame_Status=Frame(self.root, bg=colorStatus, bd=1, relief=SUNKEN)
self.frame_Status.pack(side=BOTTOM, fill=X)
The middle frame is enlarged if the window is resized, which is great since it is what I want.
The issue is that when reducing a lot the height of the window, the status bar disappears because the middle frame adapts to the internal widgets. However, I would like that both the status bar and toolbar would be maintained and the middle frame would reduce as much as needed even if hiding some widgets.
To sum up, I would like that both the toolbar and status bar have like a minimum height.
Is that possible?
Edit: I add a functional MCVE code with the same error
# Import graphical interface
from tkinter import *
# Fixed window
root_fw=Tk()
# Toolbar
frame_Toolbar=Frame(root_fw, bg='red', height=50)
frame_Toolbar.pack(side=TOP, fill=X)
# Middle
frame_Middle=Frame(root_fw, bg='blue', height=300)
frame_Middle.pack(side=TOP, fill=BOTH, expand=YES)
# Status bar
frame_Status=Frame(root_fw, bg='green', bd=1, relief=SUNKEN, height=20)
frame_Status.pack(side=BOTTOM, fill=X)
root_fw.mainloop()
Once all of the widgets are at their minimum size, when you shrink the window tkinter must start reducing the size of one or more widgets. It does this in the order that widgets appear in the stacking order. Simply put, it starts removing space from the last widget that was packed.
So, the short answer is to pack the statusbar before you pack the middle widget. Generally speaking, a GUI will have one "hero" widget that takes up most of the space; it is this window that should be packed last so that the other widgets will not be clipped when the window is resized.
My recommendation is to always separate widget creation from widget layout, and to group layout together. I find this practice makes layout problems much easier to solve. I would therefore modify your code to look like this:
from tkinter import *
root_fw=Tk()
frame_Toolbar=Frame(root_fw, bg='red', height=50)
frame_Middle=Frame(root_fw, bg='blue', height=300)
frame_Status=Frame(root_fw, bg='green', bd=1, relief=SUNKEN, height=20)
frame_Toolbar.pack(side=TOP, fill=X)
frame_Status.pack(side=BOTTOM, fill=X)
frame_Middle.pack(side=TOP, fill=BOTH, expand=YES)
root_fw.mainloop()
I suggest you to switch to the Grid layout because you can configure rows and columns. The minimum height of a row can be set with the minsize option of the rowconfigure method.
In addition, you need to configure the middle row to resize with the window by setting its weight to 1 and finally you can set a minimum size for the widow to be at least big enough to show both bars.
from tkinter import *
# Fixed window
root_fw = Tk()
# resize row 1 and column 0 with window
root_fw.rowconfigure(1, weight=1)
root_fw.columnconfigure(0, weight=1)
# set minimum height for row 0 and 2
root_fw.rowconfigure(0, minsize=50)
root_fw.rowconfigure(2, minsize=20)
# set window min size
root_fw.minsize(70, 70)
# Toolbar
frame_Toolbar = Frame(root_fw, bg='red', height=50, width=200)
frame_Toolbar.grid(row=0, sticky="ew")
# Middle
frame_Middle = Frame(root_fw, bg='blue', height=300, width=200)
frame_Middle.grid(row=1, sticky="ewsn")
# Status bar
frame_Status = Frame(root_fw, bg='green', bd=1, relief="sunken", height=20, width=200)
frame_Status.grid(row=2, sticky="ew")
root_fw.mainloop()

tkinter put scrollbar on canvas at bottom position

Question on scrollbar positions on my tkinter canvas. I have a frame with 3 canvas widgets. Courtesy to this post for the idea. I added a horizontal scrollbar and each canvas has a 50+ column 500+ row pandas dataframe. The load is not very fast but that isn't an objective.
New rows will be added to the bottom of each dataframe. This new row needs a validation. So instead of scrolling down every time, it would be great if the scrollbar / or canvas shows the bottom part.
See below the code where the 3x canvas and 3x scrollbars (x+y) are defined.
def createBox(window):
list_ = ['df1', 'df2', 'df3'] # 3 dataframes
for i in range(3):
mybox = LabelFrame(window, padx=5, pady=4)
mybox.grid(row=i, column=0)
createWindow(mybox, list_[i], i)
def createWindow(box, lt_actual, i):
canvas = Canvas(box, borderwidth=0)
frame = Frame(canvas)
vsbY = Scrollbar(box, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsbY.set, width=1200, heigh=200)
vsbY.pack(side="right", fill="y")
vsbX = Scrollbar(box, orient="horizontal", command=canvas.xview)
canvas.configure(xscrollcommand=vsbX.set, width=1200, heigh=200)
vsbX.pack(side="bottom", fill="x")
#canvas.yview_moveto(1) - no effect
#canvas.yview_moveto(1.0) - no effect
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw", tags="frame")
# be sure that we call OnFrameConfigure on the right canvas
frame.bind("<Configure>", lambda event, canvas=canvas: OnFrameConfigure(canvas))
I read on this forum and on some info (effbot) pages that i should use the moveto() / yview_moveto() command option but so far this doesn't seem to work.
Question 1. Should I put the y-scrollbar to the bottom or should i put the canvas view to the bottom.
Question 2. Can you provide some guidance on how to use the moveto or should I follow a different approach?
Thanks so much!
The yview_moveto method of the canvas is indeed the right function to use. Its argument is the fraction of the total height of the canvas that you want off-screen. So using 0 as argument shows the top of the canvas and 1, the bottom.
Reference:
Tkinter.Canvas.yview_moveto-method
Adjusts the canvas to the given Scroll offset.
Offset '0.0' is the beginning of the scrollregion, '1.0' the end.
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root)
canvas.grid(row=0, column=0)
canvas.create_oval(0, 0, 20, 20, fill='red')
canvas.create_oval(0, 800, 20, 820, fill='blue')
ys = tk.Scrollbar(root, orient='vertical', command=canvas.yview)
ys.grid(row=0, column=1, sticky='ns')
# configure scrolling
canvas.configure(yscrollcommand=ys.set, scrollregion=canvas.bbox('all'))
# show bottom of canvas
canvas.yview_moveto('1.0')
root.mainloop()
By the way, I don't see any difference between putting the y-scrollbar to the bottom or putting the canvas view to the bottom because the two are linked. But I guessed you wanted to know whether to do it using a method of the scrollbar or of the canvas, and I gave the answer above.
I have found that it is necessary to use idle_tasks before moving the scroll tab:
self.canvas.update_idletasks()
self.canvas.yview_moveto(0)

TKinter: Scrollable Canvas, Expandable Widgets, and Pack

I am creating a table using Labels laid out as a grid within a Frame. I'd like to embed this Frame in a scrollable canvas, and I'd like the Labels to expand to fill the horizontal space while maintaining vertical scrolling capability. To see what I am trying to do, here is my code:
class ScrollableTable(Frame):
def __init__(self, parent, rows=4, columns=5):
self.rows = rows
self.columns = columns
Frame.__init__(self, parent, borderwidth=0, background='green')
self.canvas = Canvas(self, borderwidth=0)
self.frame = Frame(self.canvas, borderwidth=0, background='black')
self.vsb = Scrollbar(parent, orient=VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.canvas.pack(side=LEFT, fill=X, expand=TRUE)
# This is where I fix the height I want this window to take up
self.canvas.configure(yscrollcommand=self.vsb.set, width=720, height=80)
self.height = self.canvas.winfo_reqheight()
self.width = self.canvas.winfo_reqwidth()
self._widgets = []
for row in range(rows):
current_row = []
for column in range(columns):
label = Label(self.frame, text="N/A",
borderwidth=0, width=5, anchor=W, justify=LEFT)
label.grid(row=row, column=column, sticky='NESW', padx=0.5, pady=0.5)
current_row.append(label)
self._widgets.append(current_row)
self.canvas.create_window((0,0), window=self.frame, anchor='w', tags=self.frame)
self.frame.bind("<Configure>", self.OnFrameConfigure)
self.canvas.bind("<Configure>", self.onResize)
#self.frame.pack(side=LEFT, fill=X, expand=True)
self.vsb.pack(side=RIGHT, fill=Y, expand=True)
In the above code, the 2nd to last line is commented. When it is commented out, I get a table that doesn't expand to fill the space, but the scrollbar works. However, if I uncomment that lines and use the frame.pack approach, the table fills the horizontal space, but I lose scrolling capability. I'm having trouble wrapping my head around why I'm losing the ability to scroll - any pointers?
You need to not call pack on the frame, and because of that you'll need to manage the width of the frame manually. Typically you do this by binding to the <Configure> event on the canvas. In the bound function, get the width of the canvas, and then use the itemconfigure method to set the width of the window object.

Python Tkinter - multiple listboxes and scrollbars

having issues with getting the scrollbars set correctly. The following code renders 2 listboxes, one on top of the other, and 2 scrollbars. However, scrollbar spans the entire height of both boxes, and scrollbar2 is only the 2nd listbox (lb2). I need scrollbar and lb to be the same height, and scrollbar2 and lb2 to be the same height. Here is a screenshot of what I currently have http://tinyurl.com/mxo9llb
frame = Frame(app,bd=2,relief=SUNKEN)
scrollbar = Scrollbar(frame, orient="vertical")
lb = Listbox(frame, width=30, height=10, yscrollcommand=scrollbar.set)
scrollbar.config(command=lb.yview)
scrollbar.pack(side="right", fill="y")
lb.pack(side="top",fill="both", expand=True)
scrollbar2 = Scrollbar(frame, orient="vertical")
lb2 = Listbox(frame, width=30, height=10, yscrollcommand=scrollbar2.set)
scrollbar2.config(command=lb2.yview)
scrollbar2.pack(side="right",fill="y")
lb2.pack(side="top",fill="both", expand=True)
for item in ad_members:
lb.insert(END, item)
for item in ad_members:
lb2.insert(END, item)
frame.pack(side='right',padx=15)
For such a layout, you're better off using grid rather than pack. You can't do what you want using pack unless you add some extra frames to help with the layout.
Using pack, you need to create two additional frames: one for the top half and one for the bottom half. Then, within the frame you can put each scrollbar on the right and the listbox on the left. Finally, pack the frames on top of each other.
Using grid, you would put each listbox/scrollbar pair on a different row, with the listbox in column zero and the scrollbar in column one.

Categories

Resources