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.
Related
When making a program for a game database, I want to have the main menu to have multiple buttons leading to multiple topics. The place function isn't working for me. Here is my code:
windowFU = tk.Tk()
windowFU.title("MHFU Database")
windowFU.geometry("255x200+300+180")
frame = tk.Frame(master = windowFU, width = 255, height = 200)
frame.pack()
icon = tk.PhotoImage(file = "images/icon.png")
windowFU.iconphoto(False, icon)
welcome = tk.Label(
master = frame,
text = "What would you like to view?",
width = 30,
height = 2
)
searchEntry = tk.Entry(
master = frame,
width = 30
)
buttonMonstersFU = tk.Button(
master = frame,
text = "Monsters",
width = 12,
height = 2
)
# Here is the place function
buttonMonstersFU.place(x = 100, y = 100)
welcome.pack()
searchEntry.pack()
buttonMonstersFU.pack()
searchEntry.bind('<Return>', getEntry)
windowFU.mainloop()
Note: Currently I just have the place function set to x = 100, y = 100 to test, it is not my final location.
Here is an image of what I get:
Result
What should I do?
You call buttonMonstersFU.pack() a few lines after you call buttonMonsersFU.place(...). Only one of pack, place, or grid can be responsible for a widget. When you call pack, any work done by place will be thrown away.
If you want to use place, then remove the line that calls buttonMonstersFU.pack().
Here is the include file
import tkinter as tk
from tkinter import ttk
class CollapsiblePane(ttk.Frame):
"""
-----USAGE-----
collapsiblePane = CollapsiblePane(parent,
expanded_text =[string],
collapsed_text =[string])
collapsiblePane.pack()
button = Button(collapsiblePane.frame).pack()
"""
def __init__(self, parent, expanded_text ="Collapse <<",
collapsed_text ="Expand >>"):
ttk.Frame.__init__(self, parent)
# These are the class variable
# see a underscore in expanded_text and _collapsed_text
# this means these are private to class
self.parent = parent
self._expanded_text = expanded_text
self._collapsed_text = collapsed_text
# Here weight implies that it can grow it's
# size if extra space is available
# default weight is 0
self.columnconfigure(1, weight = 1)
# Tkinter variable storing integer value
self._variable = tk.IntVar()
# Checkbutton is created but will behave as Button
# cause in style, Button is passed
# main reason to do this is Button do not support
# variable option but checkbutton do
self._button.config( width=1, height=1, borderwidth = 0)
self._button.grid(row = 0, column = 0)
# This wil create a seperator
# A separator is a line, we can also set thickness
self._separator = ttk.Separator(self, orient ="horizontal")
self._separator.grid(row = 0, column = 1, sticky ="we")
self.frame = ttk.Frame(self)
# This will call activate function of class
self._activate()
def toggle(self):
"""Switches the label frame to the opposite state."""
self._variable = not self._variable
def _activate(self):
if not self._variable:
# As soon as button is pressed it removes this widget
# but is not destroyed means can be displayed again
self.frame.grid_forget()
# This will change the text of the checkbutton
self.toggle()
elif self._variable:
# increasing the frame area so new widgets
# could reside in this container
self.frame.grid(row = 1, column = 0, columnspan = 1)
self.toggle()
Here is the program that utilizes it.
# Importing tkinter and ttk modules
from tkinter import *
from tkinter.ttk import *
# Importing Collapsible Pane class that we have
# created in separate file
from collapsiblepane import CollapsiblePane as cp
# Making root window or parent window
root = Tk()
root.geometry('200x200')
# Creating Object of Collapsible Pane Container
# If we do not pass these strings in
# parameter the the defalt strings will appear
# on button that were, expand >>, collapse <<
cpane = cp(root, 'Expanded', 'Collapsed')
cpane.grid(row = 0, column = 0)
# Button and checkbutton, these will
# appear in collapsible pane container
b1 = Button(cpane.frame, text ="GFG").grid(
row = 1, column = 2, pady = 10)
cb1 = Checkbutton(cpane.frame, text ="GFG").grid(
row = 2, column = 3, pady = 10)
mainloop()
And here's my error.
TclError: unknown option "-borderwidth"
The error comes from this line
self._button.config( width=1, height=1, borderwidth = 0)
Which I've also tried as
self._button = ttk.Button(self,command = self._activate, width=1, height=1, borderwidth = 0)
With the same error.
I've removed the borderwidth, and then it throws the same error with width/height. I've tested the width/height/borderwidth in other instances and it works, I'm just not sure why it won't work here.
When using ttk widgets, you have to keep in mind that many of the parameters you would set separately for each tkinter widget are mostly managed by styles when it comes to ttk widgets.
If you look up the ttk.Button widget options (here), you can easily find out which parameters can and can not be set.
In your case, you'd have to create a style for the button.
s = ttk.Style()
s.configure("TButton", borderwidth=0)
If you use this code, you won't have to separately declare that your button is using that style, however every other (ttk) button will automatically use this style too. If you want to set a style for just one button, you'd have to do something like this:
s = ttk.Style()
s.configure("b1.TButton", borderwidth=0) #you can rename b1 to anything you want
and then
b1 = Button(cpane.frame, text ="GFG", style="b1.TButton").grid(
row = 1, column = 2, pady = 10)
You can also read up on styles by yourself here.
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.
I am really new to coding and python and this could be just a lack of basic knowledge, but every time I run the code it displays the Buttons and Labels in the default top left position. Does anyone know where the mistake is or how to fix this issue?
I initially tried this with .grid(row = 1, column = 5) but this also didn't work.
def swap(frame):
frame.tkraise()
for frame in (f1, f2, f3):
frame.grid(row = 0, column = 0)
# ---------------------------------------1
# This is just the first frame but I think this is scaleable
name1 = Label(f1, text = "Frame 1")
name1.pack(side = BOTTOM)
switchbutton1 = Button(f1, text = "Next Frame", command = lambda: swap(f2))
switchbutton1.pack(side = BOTTOM)
I don't get any error message but it only displays in the default position.
I expect that I can put the buttons with .grid() or .pack() in a custom position.
I want to present several questions, one after another. The first question is shown as I like, with the cursor set in the entry field. Then I destroy the window and call the function again to create a new window. This time the window is not shown in the front and therefore I first have to click on the screen in order to have the cursor set to the entry field. Also the escape key does not work until I click on the screen to bring the window to the top. I'd be very happy for your help!
Thank you in advance!
Here's my code:
from Tkinter import *
def text_input_restricted(fn,question, nr_letters, limit, len_min, len_max,keys, justify):
class MyApp():
def validate(root, S):
return all(c in keys for c in S)
def __init__(self, q= None):
#save response after "next"-button has been clicked
def okClicked():
lines = e.get()
if len_min < len(lines) < len_max:
lines = unicode(lines).encode('utf-8')
datFile = open(fn, "a")
datFile.write(" '%s'"%(lines))
datFile.close()
self.root.destroy()
self.root = Tk()
vcmd = (self.root.register(self.validate), '%S')
#quit if escape-key has been pressed
self.root.bind('<Escape>', lambda q: quit())
#colors
color = '#%02x%02x%02x' % (200, 200, 200)
self.root.configure(bg=color)
#set window size to screen size
RWidth=MAXX
RHeight=MAXY
self.root.geometry(("%dx%d")%(RWidth,RHeight))
#remove buttons (cross, minimize, maximize)
self.root.overrideredirect(1)
#remove title
self.root.title("")
#item
labelWidget = Label(self.root,text=question, font=("Arial", int(0.02*MAXX)), bd=5, bg=color, justify="center")
labelWidget.place(x=0, y=RHeight/40,width=RWidth)
#"next"-button
ok_width = RWidth/15
ok_height = RWidth/15
okWidget = Button(self.root, text= "next", command = okClicked, font=("Arial",int(0.015*MAXX)), bd=5, justify="center")
okWidget.place(x=RWidth/2-ok_width/2,y=13*RHeight/40, width=ok_width,height=ok_height)
def callback(sv):
c = sv.get()[0:limit]
sv.set(c)
sv = StringVar()
width=nr_letters * int(0.02*MAXX)*1.3
sv.trace("w", lambda name, index, mode, sv=sv: callback(sv))
e = Entry(self.root, textvariable=sv,font=("Arial", int(0.02*MAXX)),justify=justify,validate="key", validatecommand=vcmd)
e.place(x=RWidth/2-width/2, y=9*RHeight/40, width=width)
#show cursor
e.focus_set()
self.root.mainloop()
MyApp()
MAXX=1366
MAXY=768
fn = "D:/test.dat"
text_input_restricted(fn = fn, question=u"f for female, m for male", nr_letters=1, limit =1, len_min =0, len_max=2, keys = 'fm', justify="center")
text_input_restricted(fn = fn, question="How old are you?", nr_letters=2,limit=2, len_min = 1, len_max = 3, keys = '1234567890',justify="center")
In Tk you use the raise command to bring a window to the front of the Z-order. However, raise is a keyword in Python so this has been renamed to lift. Provided your application is still the foreground application you can call the lift() method on a toplevel widget. If the application is not the foreground application then this will raise the window but only above other windows from the same application. On Windows this causes the taskbar icon for your application to start flashing.
You might do better to destroy the contents of the toplevel and replace them. Or even better - create a number of frames holding each 'page' of your application and toggle the visibility of each frame by packing and pack_forgetting (or grid and grid forget). This will avoid loosing the focus completely - you can just set the focus onto the first widget of each frame as you make it visible.