Scrollable page in Tkinter Notebook - python

For a project I need to specify a certain value for N subfiles (sets of data), and this value can either be evenly spaced (omitted here), requiring only a starting value and an increment, or unevenly spaced, which means each subfile has its own value. I've decided to use a Notebook to separate the two methods of entry.
As the number of subfiles can get into hundreds, I would need a scrollbar, and after consulting Google I've found out that to use a scrollbar in such manner I would need to use a canvas and place a frame in it with everything I would want to scroll through.
The number can vary each time, so I decided to use a dictionary, that would be iteratively filled, to contain all 'entry frames' that each contain a label, an entry field and a variable, rolled up into one custom class IterEntryField. After a class instance is created, it's packed inside one container frame. After the for loop is over, the container frame is placed on a canvas and the scrollbar is given a new scrollregion.
from tkinter import *
from tkinter.ttk import Notebook
N = 25
class IterEntryField:
def __init__(self, frame, label):
self.frame = frame
self.label = label
def pack(self):
self.valLabel = Label(self.frame, text = self.label, anchor = 'w')
self.valLabel.pack(fill = X, side = LEFT)
self.variable = StringVar()
self.variable.set('0')
self.valEntry = Entry(self.frame, textvariable = self.variable)
self.valEntry.pack(fill = X, side = RIGHT)
def notebookpopup():
zSetupWindow = Toplevel(root)
zSetupWindow.geometry('{}x{}'.format(800, 300))
notebook = Notebook(zSetupWindow)
evspace = Frame(notebook)
notebook.add(evspace, text = "Evenly spaced values")
sOverflow = Label(evspace, text = 'Ignore this')
sOverflow.pack()
uevspace = Frame(notebook)
notebook.add(uevspace, text = "Individual values")
canvas = Canvas(uevspace, width = 800, height = 400)
vsb = Scrollbar(canvas, command=canvas.yview)
canvas.config(yscrollcommand = vsb.set)
canvas.pack(side = LEFT, fill = BOTH, expand = True)
vsb.pack(side = RIGHT, fill = Y)
entryContainer = Frame(canvas)
entryContainer.pack(fill = BOTH)
frameDict = {}
for i in range(0, N):
frameDict[i] = Frame(entryContainer)
frameDict[i].pack(fill = X)
entry = IterEntryField(frameDict[i], 'Z value for subfile {}'.format(i+1))
entry.pack()
canvas.create_window(200, 0, window = entryContainer)
canvas.config(scrollregion = (0,0,100,1000))
notebook.pack(fill = X)
root = Tk()
button = Button(root, text = 'new window', command = notebookpopup)
button.pack()
root.mainloop()
I'm having three problems with this code:
The pages are incredibly short, only showing a couple lines.
I can't figure out the "proper" offset in create_window. I thought 0, 0 would place it in upper left corner of the canvas, but apparently the upper left corner of the window is taken instead. This could probably fixed by some reverse of the canvasx and canvasy methods, but I haven't been able to find any.
The entry fields and labels are cramped together instead of taking up the entire width of the canvas. This wasn't a problem when I only used the notebook page frame as the container.

Your first problem goes back to how you pack your notebook. Simply change notebook.pack(...) to below:
notebook.pack(fill="both", expand=True)
The second one can be solved by specifying the anchor position in your create_window method:
canvas.create_window(0, 0, window = entryContainer, anchor="nw")
I don't understand what the 3rd problem is - it looks exactly as expected.

Related

How can I bring a Canvas to front?

I overlapped 5 Tk.Canvas objects and each will have different images. I want to bring each canvas to front of every other canvases to draw pictures in the most-front canvas.
class window_tk():
def __init__(self,main):
self.main=main
self.canvas_org = tk.Canvas(self.main, bg='white')
self.canvas_layer1 = tk.Canvas(self.main, bg='red')
self.canvas_layer2 = tk.Canvas(self.main, bg='green')
self.canvas_layer3 = tk.Canvas(self.main, bg='blue')
self.canvas_layer4 = tk.Canvas(self.main, bg='black')
self.btn_load = tk.Button(self.main,text = "Load Image",command = self.load_ct)
self.btn_layer1 = tk.Button(self.main,text = "Draw in L1",command = self.bring_1)
self.btn_layer2 = tk.Button(self.main,text = "Draw in L2",command = self.bring_2)
self.btn_layer3 = tk.Button(self.main,text = "Draw in L3",command = self.bring_3)
self.btn_layer4 = tk.Button(self.main,text = "Draw in L4",command = self.bring_4)
def bring_1(self):
self.canvas_layer1.place(x=50,y=00)
def bring_2(self):
self.canvas_layer2.place(x=50, y=00)
def bring_3(self):
self.canvas_layer3.place(x=50, y=00)
def bring_4(self):
self.canvas_layer4.place(x=50, y=00)
I thought the canvas.place() function will bring the canvas front but it was not. Which function can I use ? Or should I unpack all other canvases ?
Since Canvas has override the .tkraise() function, you need to call TCL command directly:
self.canvas.tk.call('raise', self.canvas._w)
Please see the answer given by acw1668. The lift function doesn't work for Canvas objects. His answer is correct.
All tkinter objects, Canvas included, support the following method:
w.lift(aboveThis=None)
If the argument is None, the window containing w is moved to the top of the window stacking order. To move the window just above some Toplevel window w, pass w as an argument.
This gives you full control over which widget sits on top.
https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/universal.html
Now that I re-read that, I see that its language is slightly incorrect. "w" is any tkinter widget, "above_this" is another tkinter widget. The function places "w" above "above_this" in the stacking order.
You can use the following functions -
canvas.tag_raise(canvas_layer4) -> For bringing to front
canvas.tag_lower(canvas_layer4) -> For pushing back

Labels Added via Button Command Not Expanding Frame

I am trying to dynamically add labels to a frame contained within a canvas, for scrollbar capability. The labels are being added via a function that is called from a button. The function works fine if called on startup, the frame updates as expected and I have scrollbar capabilities. If I call the function from the button command the frame updates with the labels, but only up to the limit of the starting frame/canvas size. The additional area of the frame containing the rest of the labels won't be visible as the scrollbar isn't "activated"
I tried searching but couldn't find this question being asked before. I am new to Python and Tkinter so apologies if I'm missing something obvious.
from tkinter import *
from tkinter import ttk
root = Tk()
root.geometry("550x550")
main_frame = Frame(root)
main_frame.grid(row = 0, column = 0)
button_frame = Frame(root)
button_frame.grid(row = 1, column = 0)
image_canvas_0 = Canvas(main_frame, height = 500, width = 500)
image_canvas_0.grid(row = 0, column = 0)
image_canvas_0_scrollbar = ttk.Scrollbar(main_frame, orient = VERTICAL, command = image_canvas_0.yview)
image_canvas_0_scrollbar.grid(column = 1, row = 0, sticky = (N,S))
image_canvas_0.config(yscrollcommand = image_canvas_0_scrollbar.set)
image_canvas_0.bind('<Configure>', lambda e: image_canvas_0.configure(scrollregion = image_canvas_0.bbox("all")))
second_frame = Frame(image_canvas_0)
image_canvas_0.create_window((0,0), window = second_frame, anchor = 'nw')
def test_function(*args):
for i in range(100):
label_text = 'test' + str(i)
Label(second_frame, text = label_text).grid(row = i, column = 0)
func_button = Button(button_frame, text = 'Click Me', command = test_function)
func_button.grid(row = 0, column = 0)
#test_function()
root.mainloop()
You aren't changing the scrollregion after you've added widgets to the frame. The canvas doesn't know that there's new data that should be scrollable.
Normally this is done by adding a bind to the frame's <Configure> event, since that event fires whenever the frame changes size.
second_frame.bind("<Configure>", lambda e: image_canvas_0.configure(scrollregion = image_canvas_0.bbox("all")))
The reason your code seems to work when calling the function directly at startup is that you have a similar binding on the canvas itself, which automatically fires once the widget is actually shown on the screen. That happens after you call test_function() when mainloop first starts to run. Once the program starts, the canvas <Configure> event doesn't fire a second time.

Cannot use .place for a label frame in tkinter

I've been working on adding label frames to my window but for some reason whenever I use .place it never places the frame. Grid and pack work though. I'm trying to get the label frame right in the middle of the screen through coordinates. Heres my code (the error is somewhere in the createstock functiom):
import tkinter as tk
import yfinance
class StockWindow:
def __init__(self,master,number):
self.master = master
self.frame=tk.Frame(self.master)
self.frame.pack()
w, h = master.winfo_screenwidth(), master.winfo_screenheight()
self.master.overrideredirect(1)
self.master.geometry("%dx%d+0+0" % (w, h))
self.master.focus_set() # <-- move focus to this widget
self.master.bind("<Escape>", lambda e: e.widget.quit())
#################################################
## Labels
self.amountChanged = tk.Label(self.master,text = "$1000")
self.amountChanged.place(x=w/2,y=h/2)
self.highestChangedStock = tk.Label(self.master,text = "Amzn")
self.highestChangedStock.place(x=w/2+10,y=h/2+40)
self.lowestChangedStock = tk.Label(self.master,text = "this one")
self.stockTips = tk.Label(self.master,text = "Buy some")
self.stockTips.place(x=2,y=777)
self.marketChange = tk.Label(self.master,text = "Alot!")
self.marketChange.place(x=23,y=66)
self.stockNews = tk.Label(self.master,text = "News Here!")
self.stockNews.place(x=23,y=234)
self.stockNewds = tk.Label(self.master,text = "News Hewewere!")
self.stockNewds.place(x=300,y=300)
## Buttons
self.seeAllStocks = tk.Button(self.master,text ="do you wanna see more stocks?")
self.seeAllStocks.place(x=0,y=0)
self.goBack =tk.Button(self.master,text = "Go back",command=self.close_windows)
self.goBack.place(x=100,y=100)
self.createStock("afdhsfdhsfhsfghsgfhsdg",3,30)
#########
def createStock(self,name,pricechange,placement):
stockframe = tk.LabelFrame(self.frame,text='')
#stockframe.size(200)
stockframe.place(x=400,y=400,height=10,width=10)
#stockframe.pack(expand='yes',fill='both')
#stockframe.grid(column=5,row=5)
tempLabel = tk.Label(stockframe,text=name)
tempLabel.pack()
def close_windows(self):
self.master.destroy()
The problem is that self.frame is used as the parent of the label frame, but it has a height of 1x1 since there's nothing in it. Thus, any widget placed inside it will also be invisible.
If you want to use place to center a widget, the simplest solution is to use relative coordinates with a relative x and y coordinate of .5 (ie: 50% of the width and height of the window).
For example, you don't need to place the labelframe inside a label inside the root window. Just remove self.frame and use relative coordinates for the label frame:
stockframe = tk.LabelFrame(self.master,text='')
stockframe.place(relx=.5, rely=.5)

Grid and placing widgets : once again

Dears,
Although I want to make it simple, I'm still failing in creating a class of "windows" which by default will have a basic menu (not in code yet), a frame of 10 rows and 10 cls, and in the last cell of this frame (row = 9, col=9) a "Close" button.
Then, I could create several classes that will inherit from that one, and adding more widgets, commands, .. Well very basic
Yes but, although I gave weight to cells,..,... the button is still on the top left corner, and not the bottom right one. What did I miss when managing widgets with .grid()
Thks a lot
import tkinter as tk
from tkinter import ttk
class myWindows(tk.Tk):
def __init__(self,
pWtitle='',
pParent = '',
pIsOnTop = False,
pWidth=800,
pHeight=600,
pIsResizable=False,
pNbRows = 10,
pNbCols = 10):
tk.Tk.__init__(self)
tk.Tk.wm_title(self, pWtitle)
self.geometry('%sx%s' % (pWidth, pHeight))
if pIsResizable :
self.minsize(pWidth, pWidth)
rFrame =tk.Frame(self, borderwidth=1, relief="ridge")
#to make it simple by default, there will be a grid of 10rows and 10columns
for r in range(pNbRows) :
rFrame.grid_rowconfigure(r,weight=1)
for c in range(pNbCols) :
rFrame.grid_columnconfigure(c,weight=1)
rFrame.grid(row=0, column=0, sticky='ns')
#all windows will have a quit button on the bottom right corner (last cells)
#Need deduct 1 in the parameter as indexes start from 0
bt=ttk.Button(self, text='Close',command=self.quit)
bt.grid(row=pNbRows -1, column=pNbCols -1, sticky='se')
app = myWindows( pWtitle='MAIN')
app.mainloop()
You are configuring the weights inside of rFrame, but you are putting rFrame and the button directly in the root window. You have not configured weights for any rows or columns in the root window.
grid doesn't display rows and columns that don't contain anythng. Try, for example adding one placeholder label with empty picture (Label = (self, image = PhotoImage())) in every row and column of the grid until you populate it with real stuff. Source
http://effbot.org/tkinterbook/grid.htm
minsize= Defines the minimum size for the row. Note that if a row is
completely empty, it will not be displayed, even if this option is
set.
Finally, I came up with a solution :
import tkinter as tk
from tkinter import ttk
class myWindows(tk.Tk):
def __init__(self,
pWtitle='',
pParent = '',
pIsOnTop = False,
pWidth=800,
pHeight=600,
pIsResizable=False
):
tk.Tk.__init__(self)
tk.Tk.wm_title(self, pWtitle)
self.geometry('%sx%s' % (pWidth, pHeight))
if pIsResizable :
self.minsize(pWidth, pHeight)
#all windows will have a quit button on the bottom right corner (last cells)
#Need deduct 1 in the parameter as indexes start from 0
bt=ttk.Button(self, text='Close',command=self.quit)
bt.grid(row=1, column=0, sticky='se')
#to make it simple by default, there will be a container on top of button
rFrame =tk.Frame(self, borderwidth=1, bg = 'blue', relief="ridge")
rFrame.grid(row=0, column=0)
#give max weight to the first cell to
#make sure the container is filling up the empty space
#on top of the button(s)
self.grid_rowconfigure(0, weight = 1)
self.grid_rowconfigure(1, weight =0)
self.grid_columnconfigure(0, weight =1)
app = myWindows( pWtitle='MAIN')
app.mainloop()

Adding widgets to third page causes Frames to adjust size

I've created 3 frames (pages) for my GUI and added a couple of widgets to show a basic framework of my GUI.
I have used the .grid_propagate(0) method to stop my frames from adjusting size based on the widgets within them.
See below for code:
from Tkinter import *
# from tkFileDialog import askopenfilename
# from Function_Sheet import *
# from time import sleep
# import ttk, ttkcalendar, tkSimpleDialog, csv, pyodbc, threading
# from Queue import Queue
# import os
# cwd = os.getcwd()
class CA_GUI:
def __init__(self, master):
### Configure ###
win_colour = '#D2B48C'
master.title('CA 2.0'), master.geometry('278x289'), master.configure(background='#EEE5DE')
win1, win2, win3 = Frame(master, background=win_colour, bd=5, relief=GROOVE, pady=10, padx=20, width=260, height = 270), Frame(master, background=win_colour, bd=5, relief=GROOVE, pady=10, padx=20, width=260, height = 270), Frame(master, background=win_colour, bd=5, relief=GROOVE, pady=10, padx=20, width=260, height = 270)
### Grid Frames ###
for window in [win1,win2,win3]:
window.grid(column=0, row=0, sticky='news', pady=10, padx=10)
window.grid_propagate(0)
### Window 1 ###
self.win1_label1 = Label(win1, text = 'This is page 1!')
self.win1_label1.pack(fill = X, side = TOP)
self.win1_button1 = Button(win1, text = 'Close', command = master.quit)
self.win1_button1.pack(fill = X, side = BOTTOM)
self.win1_button2 = Button(win1, text = 'Page 2', command = lambda:self.next_page(win2))
self.win1_button2.pack(fill = X, side = BOTTOM)
### Window 2 ###
self.win2_label1 = Label(win2, text = 'This is page 2!')
self.win2_label1.pack(fill = X, side = TOP)
self.win2_button1 = Button(win2, text = 'Close', command = master.quit)
self.win2_button1.pack(fill = X, side = BOTTOM)
self.win2_button2 = Button(win2, text = 'Page 3', command = lambda:self.next_page(win3))
self.win2_button2.pack(fill = X, side = BOTTOM)
### Window 3 ###
self.win3_label1 = Label(win3, text = 'This is page 3!')
self.win3_label1.pack(fill = X, side = TOP)
win1.tkraise()
def next_page(self, window):
window.tkraise()
root = Tk()
b = CA_GUI(root)
root.mainloop()
The problem comes when I'm adding widgets to win3. If I comment out the code relating to win3, all the frames stay at their specified size and everything looks good. However, adding even a simple label widget to win3, the frames sizes seem to adjust to the size of their widgets. - This is not what I want!
P.S.
The issue does not seem to be exclusive to win3 as commenting out another frames widgets solves the re-sizing issue.
Any feedback would be appreciated!
My recommendation is to never turn off geometry propagation. It's almost always the wrong choice. Tkinter does a fantastic job of efficiently laying out widgets. Let the frame shrink (or grow) to fit the contents, and use the geometry manager to cause the frame to fit the space allotted to it.
The problem in this code is that you aren't allowing grid to allocate all of the space to the frames. You need to give at least one row and one column "weight" so that grid will allocate extra space to that row and column, forcing the frames to fill the space rather than shrink.
Change the one section of your code to look like this:
### Grid Frames ###
master.grid_rowconfigure(0,weight=1)
master.grid_columnconfigure(0,weight=1)
for window in [win1,win2,win3]:
window.grid(column=0, row=0, sticky='news', pady=10, padx=10)
This all works because you're giving an explicit size to the main window. In a sense, setting a fixed size for the window turns off the automatic re-sizing of the window based on its immediate children. With the re-sizing turned off, and the proper use of grid options, the inner frames will fill the window.
Of course, if you put widgets that are too big to fit, they will be chopped off. Such is the price you pay for using explicit sizes rather than letting tkinter grow or shrink to fit.
Inside your three windows, you are packing the widgets, not griding them. So all what you need to do is to change this line:
window.grid_propagate(0)
to:
window.pack_propagate(0)
After doing so, you will get what you expect:

Categories