packing using tkinter in_ keyword doesn't work with widgets created outside of a frame subclass - python-3.x

My understanding is that the in_ keyword argument to pack/grid should allow me to specify the managing widget. I want to pack arbitrary widgets inside a Frame subclass, so I passed the widgets and packed them during intialization, but the widgets didn't appear (although space in the window appears to have been allocated...). If I create the widget internally using master which is root, there is no issue and the widgets are displayed as expected.
The following working example and its output demonstrate the issue:
import tkinter as tk
from tkinter import ttk
class ItemContainerExternal(ttk.Frame):
def __init__(self, master, input_label, input_object):
ttk.Frame.__init__(self, master)
self.label = input_label
self.label.pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
self.input_object = input_object
self.input_object.pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
def get(self):
return variable.get()
class ItemContainerInternal(ttk.Frame):
def __init__(self, master):
ttk.Frame.__init__(self, master)
ttk.Label(master, text='internal').pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
self.input_object = ttk.Entry(master)
self.input_object.pack(side=tk.LEFT, padx=5, pady=3, fill=tk.X, in_=self)
def get(self):
return variable.get()
if __name__ == '__main__':
root = tk.Tk()
inputobj = ttk.Entry(root)
inputlabel = ttk.Label(root, text='external')
ItemContainerExternal(root, inputlabel, inputobj).grid(row=0, column=0)
ItemContainerInternal(root).grid(row=1, column=0)
root.mainloop()

The problem is that you're creating the entry and label before you're creating the frame, so they have a lower stacking order. That means the frame will be on top of the entry and label and thus, obscuring them from view.
A simple fix is to call lift() on the entry and label:
class ItemContainerExternal(tk.Frame):
def __init__(self, master, input_label, input_object):
...
self.input_object.lift()
self.label.lift()

The order in which widgets get created matters. Newer widgets are "on top of" previous widgets.
Call .lower() on the Frame after you create it, assuming it's created after all the widgets that you will pack into it. If not, you'll need to either call .lower() again on the Frame after creating a new widget to go inside it, or you'll have to raise the new widget via .lift() as per Bryan's answer.

Related

How can I set the default container in a decorator class for tkinter.Frame?

I would like to create a contractible panel in a GUI, using the Python package tkinter.
My idea is to create a decorator for the tkinter.Frameclass, adding a nested frame and a "vertical button" which toggles the nested frame.
Sketch: (Edit: The gray box should say Parent of contractible panel)
I got it to toggle just fine, using the nested frame's grid_remove to hide it and then move the button to the left column (otherwise occupied by the frame).
Now I want to be able to use it like any other tkinter.Frame, but let it target the nested frame. Almost acting like a proxy for the nested frame. For example, adding a tkinter.Label (the green Child component in the sketch) to the decorator should add the label to the nested frame component (light yellow tk.Frame in the sketch) not the decorator itself (strong yellow ContractiblePanel in the sketch).
Minimal example: (omitting the toggling stuff and any "formatting"):
(Here's a published (runnable) Repl project)
import tkinter
class ContractiblePanel(tkinter.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self._panel = tkinter.Frame(self)
self._toggle = tkinter.Button(self, text='<', command=self._toggle_panel)
self.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
def _toggle_panel(self):
# ...
if __name__ == '__main__':
root = tkinter.Tk()
root.geometry('128x128')
contractible_panel = ContractiblePanel(root)
Forwarding configuration calls is just overriding the config method I guess?
class ContractiblePanel(tkinter.Frame):
# ...
def config(self, **kwargs):
self._panel.config(**kwargs)
# ...
contractible_panel.config(background='blue')
But I would like to be able to add a child component into the nested panel frame by
label_in_panel = tkinter.Label(contractible_panel, text='yadayada')
How do I get the ContractiblePanel object to act like a proxy to its member _panel, when adding child components?
What other methods/use cases should I consider? I am quite new to tkinter and thus expect the current implementation to break some common practices when developing tkinter GUIs.
This is an interesting question. Unfortunately, tkinter really isn't designed to support what you want. I think it would be less complicated to simply expose the inner frame and add widgets to it.
That being said, I'll present one possible solution. It's not implemented as a python decorator, but rather a custom class.
The difficulty is that you want the instance of the custom class to represent the outer frame in one context (for example, when packing it in your UI) and the inner frame in another context (when adding child widgets to it)
The following solution solves this by making the instance be the inner frame, and then overriding pack,place, and grid so that they operates on the outer frame. This works fine, with an important exception: you cannot use this class directly inside a notebook or embedded in a text widget or canvas.
I've used colors and borders so it's easy to see the individual components, but you can remove the colors in production code, obviously. Also, I used a label instead of a button since I created the screenshot on OSX where the background color of a button can't be changed.
import tkinter as tk
class ContractiblePanel(tk.Frame):
def __init__(self, parent, **kwargs):
self._frame = tk.Frame(parent, **kwargs)
super().__init__(self._frame, bd=2, relief="solid", bg="#EFE4B0")
self._button = tk.Label(
self._frame, text="<", bg="#00A2E8", bd=2,
relief="solid", font=("Helvetica", 20), width=4
)
self._frame.grid_rowconfigure(0, weight=1)
self._frame.grid_columnconfigure(0, weight=1)
self._button.grid(row=0, column=1, sticky="ns", padx=4, pady=4)
super().grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
self._button.bind("<1>", lambda event: self.toggle())
def collapse(self):
super().grid_remove()
self._button.configure(text=">")
def expand(self):
super().grid()
self._button.configure(text="<")
def toggle(self):
self.collapse() if self.winfo_viewable() else self.expand()
def pack(self, **kwargs):
# override to call pack in the private frame
self._frame.pack(**kwargs)
def grid(self, **kwargs):
# override to call grid in the private frame
self._frame.grid(**kwargs)
def place(self, **kwargs):
# override to call place in the private frame
self._frame.place(**kwargs)
root = tk.Tk()
root.geometry("400x300")
cp = ContractiblePanel(root, bg="yellow", bd=2, relief="raised")
cp.pack(side="left", fill="y", padx=10, pady=10)
label = tk.Label(cp, text="Child component", background="#22B14C", height=3, bd=2, relief="solid")
label.pack(side="top", expand=True, padx=20, pady=20)
root.mainloop()
First of all it is kinda gross to use this code and it's very confusing. So I'm really not sure if you really want to take this route. However, it is possible to achieve it.
The basic idea is to have a wrapper and to pretend the wrapper is the actual object you can lie with __str__ and __repr__ about what the class really is. That is not what a proxy means.
class WrapperClass:
def __init__(self, master=None, **kwargs):
self._wrapped_frame = tk.Frame(master, **kwargs)
self._panel = tk.Frame(self._wrapped_frame)
self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)
self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
return None
def _toggle_panel(self):
print('toggle')
def __str__(self):
return self._panel._w
__repr__ = __str__
You can do even more confusing things by delegate the lookup-chain to the _wrapped_frame inside the WrapperClass this enables you to call on the instance of WrapperFrame() methods like pack or every other method. It kinda works similar for inheritance with the difference that by referring to the object, you will point to different one.
I don't recommend using this code by the way.
import tkinter as tk
NONE = object()
#use an object here that there will no mistake
class WrapperClass:
def __init__(self, master=None, **kwargs):
self._wrapped_frame = tk.Frame(master, **kwargs)
self._panel = tk.Frame(self._wrapped_frame)
self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)
self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
return None
def _toggle_panel(self):
print('toggle')
def __str__(self):
return self._panel._w
__repr__ = __str__
def __getattr__(self, name):
#when wrapper class has no attr name
#delegate the lookup chain to self.frame
inreturn = getattr(self._wrapped_frame, name, NONE)
if inreturn is NONE:
super().__getattribute__(name)
return inreturn
root = tk.Tk()
wrapped_frame = WrapperClass(root, bg='red', width=200, height=200)
root.mainloop()

tkinter: how do i make the Scrollbar react to the new size of the frame

from tkinter import *
from tkinter import ttk
class Scrollbar:
def __init__(self, master):
frame = ttk.Frame(master)
frame.pack()
self.canvas = Canvas(frame, width=700, height=930)
self.scrollbar = ttk.Scrollbar(frame, orient=VERTICAL, command=self.canvas.yview)
self.main_frame = ttk.Frame(self.canvas)
self.placing_widgets()
def placing_widgets(self):
self.canvas.pack(side=LEFT, fill=Y)
self.scrollbar.pack(side=RIGHT, fill=Y)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.bind('<Configure>', lambda e: self.canvas.configure(scrollregion=self.canvas.bbox('all')))
self.canvas.create_window((0,0), window=self.main_frame, anchor='n')
Frame1(self.main_frame)
class Frame1:
def __init__(self, main_frame):
self.main_frame = main_frame
for i in range(100):
self.button = ttk.Button(self.main_frame, text=f"Button {i}", command=lambda i=i: self.destroying(i))
self.button.grid(row=i, column=0, pady=5)
def destroying(self, i):
for frame in self.main_frame.winfo_children():
frame.destroy()
Frame2(self.main_frame)
class Frame2:
def __init__(self, main_frame):
self.main_frame = main_frame
self.button = ttk.Button(self.main_frame, text='back', command=self.destroying)
self.button.pack()
def destroying(self):
for frame in self.main_frame.winfo_children():
frame.destroy()
Frame1(self.main_frame)
def main():
root = Tk()
root.config(background='black')
Scrollbar(root)
root.mainloop()
if __name__ == '__main__':
main()
I couldn't find a solution to this problem anywhere on the internet.
I have given a scrollbar to a lengthy frame but when I make the frame shorter, I don't know why the scrollbar doesn't resize to the frame's length.
Above is a short program that represents the problem I am talking about. The program has 2 different pages, one with 100 buttons and the other with one button. You can switch between pages with any buttons and see that the page with one button is scrollable which shouldn't be.
Could you please provide a solution to how I could make the scrollbar resize to the frame's length? Thank you for your time.

Adding a Scrollbar to a Tkinter Graph of data which goes off the bottom for the screen [duplicate]

I am using Python to parse entries from a log file, and display the entry contents using Tkinter and so far it's been excellent. The output is a grid of label widgets, but sometimes there are more rows than can be displayed on the screen. I'd like to add a scrollbar, which looks like it should be very easy, but I can't figure it out.
The documentation implies that only the List, Textbox, Canvas and Entry widgets support the scrollbar interface. None of these appear to be suitable for displaying a grid of widgets. It's possible to put arbitrary widgets in a Canvas widget, but you appear to have to use absolute co-ordinates, so I wouldn't be able to use the grid layout manager?
I've tried putting the widget grid into a Frame, but that doesn't seem to support the scrollbar interface, so this doesn't work:
mainframe = Frame(root, yscrollcommand=scrollbar.set)
Can anyone suggest a way round this limitation? I'd hate to have to rewrite in PyQt and increase my executable image size by so much, just to add a scrollbar!
Overview
You can only associate scrollbars with a few widgets, and the root widget and Frame aren't part of that group of widgets.
There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.
A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion option so that the scrollregion exactly matches the size of the frame.
Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_ methods. You cannot scroll items added to a canvas with pack, place, or grid. By using a frame, you can use those methods inside the frame, and then call create_window once for the frame.
Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).
For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing). The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.
Object-oriented solution
Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(self, 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 __name__ == "__main__":
root=tk.Tk()
example = Example(root)
example.pack(side="top", fill="both", expand=True)
root.mainloop()
Procedural solution
Here is a solution that doesn't use a class:
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()
Make it scrollable
Use this handy class to make the frame containing your widgets scrollable. Follow these steps:
create the frame
display it (pack, grid, etc)
make it scrollable
add widgets inside it
call the update() method
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))
Usage example
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()
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and can be used to replace a standard tk.Frame.
import tkinter as tk
class ScrollbarFrame(tk.Frame):
"""
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and
can be used to replace a standard tk.Frame
"""
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
# The Scrollbar, layout to the right
vsb = tk.Scrollbar(self, orient="vertical")
vsb.pack(side="right", fill="y")
# The Canvas which supports the Scrollbar Interface, layout to the left
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.canvas.pack(side="left", fill="both", expand=True)
# Bind the Scrollbar to the self.canvas Scrollbar Interface
self.canvas.configure(yscrollcommand=vsb.set)
vsb.configure(command=self.canvas.yview)
# The Frame to be scrolled, layout into the canvas
# All widgets to be scrolled have to use this Frame as parent
self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
self.canvas.create_window((4, 4), window=self.scrolled_frame, anchor="nw")
# Configures the scrollregion of the Canvas dynamically
self.scrolled_frame.bind("<Configure>", self.on_configure)
def on_configure(self, event):
"""Set the scroll region to encompass the scrolled frame"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
Usage:
class App(tk.Tk):
def __init__(self):
super().__init__()
sbf = ScrollbarFrame(self)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
sbf.grid(row=0, column=0, sticky='nsew')
# sbf.pack(side="top", fill="both", expand=True)
# Some data, layout into the sbf.scrolled_frame
frame = sbf.scrolled_frame
for row in range(50):
text = "%s" % row
tk.Label(frame, text=text,
width=3, borderwidth="1", relief="solid") \
.grid(row=row, column=0)
text = "this is the second column for row %s" % row
tk.Label(frame, text=text,
background=sbf.scrolled_frame.cget('bg')) \
.grid(row=row, column=1)
if __name__ == "__main__":
App().mainloop()

Accessing Children in LabelFrames using Tkinter and Python 3

I am working with tkinter and have set up a bare bones application of my project. My objective is to retrieve a value from tk.Entry() which lies within a tk.LabelFrame() (in this code referenced by the groupbox variable). The button finds the groupbox, and the code passes the compiler, too. I guess my question is: How do I access Widgets and their values in a LabelFrame?
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.master.title("Application Title")
# Introduce LabelFrame
self.groupbox = tk.LabelFrame(self, text="Parameters")
self.groupbox.grid(row=0, column=1, padx=5, pady=5)
# Test Label & Entry Widget
label = tk.Label(self.groupbox, text="label=")
label.grid(row=0, column=0, sticky="W")
entry = tk.Entry(self.groupbox)
entry.insert(0, default_value)
entry.grid(row = 0, column=1)
# Compile Button
button = tk.Button(self.groupbox, text="Compile", command=self.compile)
button.grid(row=1, column=1)
# Retrieve first Value (second Widget) from LabelFrame
def compile(self):
print(self.groupbox.entry.get(1))
if __name__ == '__main__':
figure = Application()
figure.pack()
figure.mainloop()
I am doing this because I want to perform some calculations based on the tk.Entry() values triggered by a button click which is contained in the same LabelFrame() as suggested by the code snippet above (in the original code there are a lot more widgets but that's essentially the gist of my current problem).
Change entry to self.entry.
class Application(tk.Frame):
def __init__(self, master=None):
....
self.entry = tk.Entry(self.groupbox)
self.entry.insert(0, "default_value")
self.entry.grid(row = 0, column=1)
...
# Retrieve first Value (second Widget) from LabelFrame
def compile(self):
print(self.entry.get())

Why doesn't the program wait for the function result? [duplicate]

I have a tkinter class:
class DBCreatorWin():
def closeWindow(self):
tkMessageBox.showinfo("Ilmiont SQLite Database Manager", "This window cannot be closed.\nEnter a database name and press Continue.")
def returnName(self):
dbName = self.entry.get()
self.window.destroy()
return dbName
def __init__(self):
self.window = Toplevel()
self.window.transient(tkRoot)
self.window.grab_set()
self.window.resizable(width=False, height=False)
self.window.title("Ilmiont SQLite Database Manager")
self.window.protocol("WM_DELETE_WINDOW", self.closeWindow)
self.label = Label(self.window, text="Enter the name of the database to be created: ")
self.entry = Entry(self.window, width=30)
self.button = Button(self.window, text="Continue", command=self.returnName)
self.label.grid(row=0, column=0)
self.entry.grid(row=0, column=1)
self.button.grid(row=1, column=0, columnspan=2)
I want to create an instance of this class within my main code and wait for the return value. The user types a name into the entry field and presses the Continue button. At that point, the value should be returned to where the class was originally instantiated. How do I go about this? I can't seem to make it work in a normal way and am new to tkinter.
Thanks in advance,
Ilmiont
There are a couple of ways to do this. The basic idea is to use a tkinter method to wait for a specific event before returning. Tkinter provides two methods to do just that: wait_window and wait_variable. The most common method is to open a window and then wait for it to be destroyed. Some good examples can be found on the effbot site, on a page titled Dialog Windows.
Here's a simple illustration. It's not production-ready, but illustrates the general idea. At the very least you'll want to add a grab on the dialog so that you can't interact with the main window while the dialog is open, since you said you want the dialog to be modal.
import Tkinter as tk
class MyDialog(object):
def __init__(self, parent):
self.toplevel = tk.Toplevel(parent)
self.var = tk.StringVar()
label = tk.Label(self.toplevel, text="Pick something:")
om = tk.OptionMenu(self.toplevel, self.var, "one", "two","three")
button = tk.Button(self.toplevel, text="OK", command=self.toplevel.destroy)
label.pack(side="top", fill="x")
om.pack(side="top", fill="x")
button.pack()
def show(self):
self.toplevel.deiconify()
self.toplevel.wait_window()
value = self.var.get()
return value
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.button = tk.Button(self, text="Click me!", command=self.on_click)
self.label = tk.Label(self, width=80)
self.label.pack(side="top", fill="x")
self.button.pack(pady=20)
def on_click(self):
result = MyDialog(self).show()
self.label.configure(text="your result: %s" % result)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
You can't.
The whole way the tkinter works is with callbacks. The command that you're using is the callback and you'll have to use the value inside the class. Here is an example:
def do_stuf(self):
tkMessageBox.showinfo("Foo", returnName())
....................
self.button = Button(self.window, text="Continue", command=self.do_stuff)

Resources