Expandable and contracting frame in Tkinter - python

Does anyone know if there is already a widget/class to handle expanding/contracting a frame based on a toggled button (checkbutton) in tkinter/ttk?
This question stems from my attempt to clean up a cluttered gui that has lots of options categorized by specific actions. I would like something along the lines of:
example found on google
However instead of just text, allow for buttons, entries, any of tkinter's widgets. If this doesn't already exist, would it be possible/useful to create a class that inherits the tkinter Frame:
import tkinter as tk
import ttk
class toggledFrame(tk.Frame):
def __init__(self):
self.show=tk.IntVar()
self.show.set(0)
self.toggleButton=tk.Checkbutton(self, command=self.toggle, variable=self.show)
self.toggleButton.pack()
self.subFrame=tk.Frame(self)
def toggle(self):
if bool(self.show.get()):
self.subFrame.pack()
else:
self.subFrame.forget()
Note: this code is untested, just presenting concept

I am actually surprised at how close I was to getting functioning code. I decided to work on it some more and have develop a simple little class to perform exactly what I wanted (comments and suggestions on the code are welcome):
import tkinter as tk
from tkinter import ttk
class ToggledFrame(tk.Frame):
def __init__(self, parent, text="", *args, **options):
tk.Frame.__init__(self, parent, *args, **options)
self.show = tk.IntVar()
self.show.set(0)
self.title_frame = ttk.Frame(self)
self.title_frame.pack(fill="x", expand=1)
ttk.Label(self.title_frame, text=text).pack(side="left", fill="x", expand=1)
self.toggle_button = ttk.Checkbutton(self.title_frame, width=2, text='+', command=self.toggle,
variable=self.show, style='Toolbutton')
self.toggle_button.pack(side="left")
self.sub_frame = tk.Frame(self, relief="sunken", borderwidth=1)
def toggle(self):
if bool(self.show.get()):
self.sub_frame.pack(fill="x", expand=1)
self.toggle_button.configure(text='-')
else:
self.sub_frame.forget()
self.toggle_button.configure(text='+')
if __name__ == "__main__":
root = tk.Tk()
t = ToggledFrame(root, text='Rotate', relief="raised", borderwidth=1)
t.pack(fill="x", expand=1, pady=2, padx=2, anchor="n")
ttk.Label(t.sub_frame, text='Rotation [deg]:').pack(side="left", fill="x", expand=1)
ttk.Entry(t.sub_frame).pack(side="left")
t2 = ToggledFrame(root, text='Resize', relief="raised", borderwidth=1)
t2.pack(fill="x", expand=1, pady=2, padx=2, anchor="n")
for i in range(10):
ttk.Label(t2.sub_frame, text='Test' + str(i)).pack()
t3 = ToggledFrame(root, text='Fooo', relief="raised", borderwidth=1)
t3.pack(fill="x", expand=1, pady=2, padx=2, anchor="n")
for i in range(10):
ttk.Label(t3.sub_frame, text='Bar' + str(i)).pack()
root.mainloop()
This code produces:

To my knowledge, Tkinter/ttk does no provide such widgets. You might mimic your example (expand/collapse label list) with a tkinter.ttk.Treeview.
It is perfectly acceptable1 to develop your own widgets, and your code seems a right start.

Related

Scrollable Tkinter GUI with shiftable pods

I am trying to make a GUI such as this with pods, each containing their own elements such as text, images and buttons.
My goal is to make it so that the so called pods can be added to the GUI window (a scrolling capable window) at any point in the code and updated in the window shifting the previous pod to the right or down to the next row if the current row is full like the image below.
I have never messed with Tkinter before so I was wondering if anyone could help me with what steps I would need to take to make such a GUI.
Implement a class that inherits from the Frame class. You can then create as many instances of this class that you want. Since you want the pods to wrap, you can use a Text widget to hold the pods since it's the only scrollable widget that natively supports wrapping.
The "pod" class might look something like this:
class Pod(tk.Frame):
def __init__(self, parent, title, subtitle, image):
super().__init__(parent, bd=2, relief="groove")
if isinstance(image, tk.PhotoImage):
self.image = image
else:
self.image = tk.PhotoImage(file=image_path)
self.title = tk.Label(self, text=title)
self.image_label = tk.Label(self, image=self.image, bd=1, relief="solid")
self.subtitle = tk.Label(self, text=subtitle)
self.b1 = tk.Button(self, text="Button 1")
self.b2 = tk.Button(self, text="Button 2")
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure((0,1), weight=1)
self.title.grid(row=0, column=0, columnspan=2, sticky="ew")
self.image_label.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=8, pady=8)
self.subtitle.grid(row=2, column=0, columnspan=2, sticky="ew")
self.b1.grid(row=3, column=0)
self.b2.grid(row=3, column=1)
You can create another class to manage these objects. If you base it on a Text widget you get the wrapping behavior for free. Though, you could also base it on a Frame or Canvas and manage the wrapping yourself.
It might look something like this:
class PodManager(tk.Text):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.configure(state="disabled", wrap="char")
self.pods = []
def add(self, pod):
self.pods.append(pod)
self.configure(state="normal")
self.window_create("end", window=pod)
self.configure(state="disabled")
To tie it all together, create one PodManager class, then pass one or more instances of Pod to its add method:
import tkinter as tk
...
root = tk.Tk()
pm = PodManager(root)
vsb = tk.Scrollbar(root, orient="vertical", command=pm.yview)
pm.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
pm.pack(side="left", fill="both", expand=True)
for i in range(10):
image = tk.PhotoImage(width=200,height=100)
pod = Pod(pm, f"Title #{i+1}", "More Text", image)
pm.add(pod)
root.mainloop()

widget is being recognized as a frame when using place on a different widget python 3 tkinter

im trying to make an application that can go to different frames but i ran into a problem that seems basic, but its new to me and dont know how to fix it. how do i move widgets on a frame using place?
when i try to use this code on about_btn:
self.about_btn.place(rely=0.5)
then the start button is covered the half by the about button, i want to know why is it doing this and how do i fix it? using higher rely value button will go covered by different frame. i want to move widgets frely using place and moving them on the specific frame. Any help and suggestions or improvement for code will be very helpful, here is my example code:
import tkinter as tk
from tkinter import *
class App():
def __init__(self, parent):
self.app = parent
self.app.geometry("300x300")
self.app.title("test application")
self.home_frame = tk.Frame(self.app)
self.category_frame = tk.Frame(self.app)
self.home_frame.pack()
self.start_btn = tk.Button(self.home_frame, text="Start")
self.start_btn.pack()
self.about_btn = tk.Button(self.home_frame, text="About")
self.about_btn.place(rely=0.5)
if __name__ == "__main__":
app1 = tk.Tk()
App_class = App(app1)
app1.resizable(False, False)
app1.mainloop()
Do not mix geometry management algorithms within a container. You are using pack for start_btn and place for about_btn both in the home_frame container. Pick one - ideally either grid or pack. Place is not usually sensible.
For example, to pack your buttons on a row at the top and right:
self.home_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.about_btn.pack(side=tk.RIGHT, anchor=tk.NE)
self.start_btn.pack(side=tk.RIGHT, anchor=tk.NE)
Using place instead:
self.home_frame.place(relwidth=1.0, relheight=1.0)
self.start_btn.place(relx=0.5, rely=0, relwidth=0.25)
self.about_btn.place(relx=0.75, rely=0, relwidth=0.25)
Frankly, learn to use grid. It will be worth it. Here is a reworked version that illustrates all three geometry managers for two buttons.
import tkinter as tk
class App():
def __init__(self, parent):
self.app = parent
self.app.geometry("300x300")
self.app.title("test application")
f1 = tk.Frame(self.app, relief=tk.GROOVE, borderwidth=2)
b1a = tk.Button(f1, text="Place A")
b1b = tk.Button(f1, text="Place B")
b1a.place(relx=0.5, rely=0, relwidth=0.25)
b1b.place(relx=0.75, rely=0, relwidth=0.25)
f2 = tk.Frame(self.app, relief=tk.GROOVE, borderwidth=2)
b2a = tk.Button(f2, text="Two A")
b2b = tk.Button(f2, text="Two B")
b2b.pack(side=tk.RIGHT, anchor=tk.NE)
b2a.pack(side=tk.RIGHT, anchor=tk.NE)
f3 = tk.Frame(self.app, relief=tk.GROOVE, borderwidth=2)
b3a = tk.Button(f3, text="Grid A")
b3b = tk.Button(f3, text="Grid B")
b3a.grid(row=0, column=0, sticky=tk.NE)
b3b.grid(row=0, column=1, sticky=tk.NE)
f3.grid_rowconfigure(0, weight=1)
f3.grid_columnconfigure(0, weight=1)
f1.grid(row=0, column=0, sticky=tk.NSEW)
f2.grid(row=1, column=0, sticky=tk.NSEW)
f3.grid(row=2, column=0, sticky=tk.NSEW)
for row in range(0,3):
parent.grid_rowconfigure(row, weight=1)
parent.grid_columnconfigure(0, weight=1)
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
#app1.resizable(False, False)
root.mainloop()

Add an image to Tkinter Entry

Using tkinter, I am trying to display an image inside the border of an entry widget.
I tried to search in Google but I came with no success, someone have an idea how to do that?
There is no feature or attribute to allow an image inside the boundary of a Entry widget. However, you can simulate it pretty easily by putting an image and an entry widget inside a frame, remove the border from the entry widget, and make sure the entry widget and frame have the same background color.
Example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent, background="gray")
frame = tk.Frame(background="white", borderwidth=1, relief="sunken",
highlightthickness=1)
frame.pack(side="top", fill="x", padx=4, pady=4)
entry = tk.Entry(frame, borderwidth=0, highlightthickness=0, background="white")
entry.image = tk.PhotoImage(data=cancelImageData)
imageLabel = tk.Label(frame, image=entry.image)
imageLabel.pack(side="right", fill="y")
entry.pack(side="left", fill="both", expand=True)
cancelImageData = '''
R0lGODlhEAAQAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr
/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMA
MzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV
/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPV
mTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYr
M2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA
/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/
mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlV
M5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq
/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyA
M8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV
/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8r
mf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+q
M/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP//
/wAAAAAAAAAAAAAAACH5BAEAAPwALAAAAAAQABAAAAiWAPcJHEiwYEFpCBMiNLhP
WjZz4CB+A5dN2sGH2TJm+7ax4kCHEOlx3EgPHEeLDc1loydwokB6G1EJlEYRHMt6
+1hW/IaSpreN+/ThzIYq5kyKGffV07ePpzSeMzl+UypU6aunMhtSdCcwI0t606A2
3PjN3VVXK2NO+/iKIzZp0xB+Q4Xt4re7te4WZSgNVV+EfhkKLhgQADs=
'''
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

Is there a way to gray out (disable) a tkinter Frame?

I want to create a GUI in tkinter with two Frames, and have the bottom Frame grayed out until some event happens.
Below is some example code:
from tkinter import *
from tkinter import ttk
def enable():
frame2.state(statespec='enabled') #Causes error
root = Tk()
#Creates top frame
frame1 = ttk.LabelFrame(root, padding=(10,10,10,10))
frame1.grid(column=0, row=0, padx=10, pady=10)
button2 = ttk.Button(frame1, text="This enables bottom frame", command=enable)
button2.pack()
#Creates bottom frame
frame2 = ttk.LabelFrame(root, padding=(10,10,10,10))
frame2.grid(column=0, row=1, padx=10, pady=10)
frame2.state(statespec='disabled') #Causes error
entry = ttk.Entry(frame2)
entry.pack()
button2 = ttk.Button(frame2, text="button")
button2.pack()
root.mainloop()
Is this possible without having to individually gray out all of the frame2's widgets?
I'm using Tkinter 8.5 and Python 3.3.
Not sure how elegant it is, but I found a solution by adding
for child in frame2.winfo_children():
child.configure(state='disable')
which loops through and disables each of frame2's children, and by changing enable() to essentially reverse this with
def enable(childList):
for child in childList:
child.configure(state='enable')
Furthermore, I removed frame2.state(statespec='disabled') as this doesn't do what I need and throws an error besides.
Here's the complete code:
from tkinter import *
from tkinter import ttk
def enable(childList):
for child in childList:
child.configure(state='enable')
root = Tk()
#Creates top frame
frame1 = ttk.LabelFrame(root, padding=(10,10,10,10))
frame1.grid(column=0, row=0, padx=10, pady=10)
button2 = ttk.Button(frame1, text="This enables bottom frame",
command=lambda: enable(frame2.winfo_children()))
button2.pack()
#Creates bottom frame
frame2 = ttk.LabelFrame(root, padding=(10,10,10,10))
frame2.grid(column=0, row=1, padx=10, pady=10)
entry = ttk.Entry(frame2)
entry.pack()
button2 = ttk.Button(frame2, text="button")
button2.pack()
for child in frame2.winfo_children():
child.configure(state='disable')
root.mainloop()
Based on #big Sharpie solution here are 2 generic functions that can disable and enable back a hierarchy of widget (frames "included"). Frame do not support the state setter.
def disableChildren(parent):
for child in parent.winfo_children():
wtype = child.winfo_class()
if wtype not in ('Frame','Labelframe','TFrame','TLabelframe'):
child.configure(state='disable')
else:
disableChildren(child)
def enableChildren(parent):
for child in parent.winfo_children():
wtype = child.winfo_class()
print (wtype)
if wtype not in ('Frame','Labelframe','TFrame','TLabelframe'):
child.configure(state='normal')
else:
enableChildren(child)
I think you can simply hide the whole frame at once.
If used grid
frame2.grid_forget()
If used pack
frame2.pack_forget()
In your case the function would be
def disable():
frame2.pack_forget()
To enable again
def enable():
frame2.pack()
grid_forget() or pack_forget() can be used for almost all tkinter widgets
this is a simple way and reduces the length of your code, I'm sure it works

scrollable listbox within a grid using tkinter

I'm new to this place and tkinter. I am stuck at making a scrollable listbox or canvas. I have tried both widgets. Within this listbox or canvas, I have several entry and label widgets. The origin point is R0,C0. I used row/columnconfigure to stretch the listbox or canvas.
In the main window, I had 4 buttons on row four to column four (0,4->4,4). I placed the scrollbar on column 5. I attempted to use the grid method. The issue I am having is making the scrollbar functional.
Note: Turning the mainframe into a class is only one of the ways I have tried. Packing the scrollbar on the right has worked, with the listbox/canvas packed on the left. However, the listbox/canvas widget that the scrollbar is commanded to does not scroll the listbox/canvas. Also, adding many entry boxes does not cause the listbox/canvas to scroll. Help please.
from tkinter import *
from tkinter.ttk import *
Style().configure("B.TFrame", relief="flat",
background="blue")
Style().configure("R.TFrame", relief="flat",
background="red")
Style().configure("R.TLabel", background="red")
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master, style="B.TFrame")
self.grid(sticky=N+S+E+W)
self.mainframe()
def mainframe(self):
top=self.winfo_toplevel()
self.menuBar = Menu(top)
top["menu"] = self.menuBar
self.subMenu = Menu(self.menuBar, tearoff=0)
self.subMenu2 = Menu(self.menuBar, tearoff=0)
self.menuBar.add_cascade(label="File", menu=self.subMenu)
self.menuBar.add_cascade(label="About", menu=self.subMenu2)
self.subMenu.add_command(label="Open")
self.subMenu.add_command(label="Save")
self.subMenu.add_command(label="Exit")
self.subMenu2.add_command(label="About")
self.subMenu2.add_command(label="Help")
self.data = Listbox (self, bg='red')
scrollbar = Scrollbar(self.data, orient=VERTICAL)
self.add = Button(self, text="")
self.remove = Button(self, text="")
self.run = Button(self, text="")
self.stop = Button(self, text="")
self.data.grid (row=0, column=0, rowspan=4, columnspan=4, sticky=N+E+S+W)
self.data.columnconfigure(1, weight=1)
self.data.columnconfigure(3, weight=1)
self.add.grid(row=4,column=0,sticky=EW)
self.remove.grid(row=4,column=1,sticky=EW)
self.run.grid(row=4,column=2,sticky=EW)
self.stop.grid(row=4,column=3,sticky=EW)
scrollbar.grid(column=5, sticky=N+S)
Without any content in the listbox, there's nothing to scroll...
This seems to work though (shortened the example a bit). See also the example at the scrollbar documentation.
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid(sticky=N+S+E+W)
self.mainframe()
def mainframe(self):
self.data = Listbox(self, bg='red')
self.scrollbar = Scrollbar(self.data, orient=VERTICAL)
self.data.config(yscrollcommand=self.scrollbar.set)
self.scrollbar.config(command=self.data.yview)
for i in range(1000):
self.data.insert(END, str(i))
self.run = Button(self, text="run")
self.stop = Button(self, text="stop")
self.data.grid(row=0, column=0, rowspan=4,
columnspan=2, sticky=N+E+S+W)
self.data.columnconfigure(0, weight=1)
self.run.grid(row=4,column=0,sticky=EW)
self.stop.grid(row=4,column=1,sticky=EW)
self.scrollbar.grid(column=2, sticky=N+S)
a = Application()
a.mainframe()
a.mainloop()
You must define the command attribute to the scrollbar, and you must supply the yscrollcommand attribute to the listbox. These two attributes work together to make something scrollable.
The yscrollcommand option tells the listbox "when you are scrolled in the Y direction, call this command. This is usually the set method of a scrollbar, so that when the user scrolls via arrow keys, the scrollbar gets updated.
The command attribute of a scorllbar says "when the user moves you, call this command". This is usually the yview or xview method of a widget, which causes the widget to change its view parameters in the Y or X direction.
In your case, after creating the widgets you would do this:
self.data.config(yscrollcommand=self.scrollbar.set)
scrollbar.config(command=self.data.yview)
This thread is old but in case somebody else falls across it as I did, it needs a few precisions.
Junuxx's answer doesnt work as is, not only because there is an indentation problem due to difficulties in seizing code here (from "self.run" which is part of the "mainframe" function) but because it seems necessary to put the listbox and the scrollbar in their own frame.
Here is a working code for Python 2 and 3 :
#!/usr/bin/env python2
try:
# for Python2
from Tkinter import *
except ImportError:
# for Python3
from tkinter import *
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid(sticky=N+S+E+W)
self.mainframe()
def mainframe(self):
frame = Frame(self)
scrollbar = Scrollbar(frame, orient=VERTICAL)
data = Listbox(frame, yscrollcommand=scrollbar.set,
bg='red')
scrollbar.config(command=data.yview)
scrollbar.pack(side=RIGHT, fill=Y)
data.pack(side=LEFT, fill=BOTH, expand=1)
for i in range(1000):
data.insert(END, str(i))
self.run = Button(self, text="run")
self.stop = Button(self, text="stop")
frame.grid(row=0, column=0, rowspan=4,
columnspan=2, sticky=N+E+S+W)
frame.columnconfigure(0, weight=1)
self.run.grid(row=4,column=0,sticky=EW)
self.stop.grid(row=4,column=1,sticky=EW)
a = Application()
a.mainframe()
a.mainloop()
You may find further information here : https://www.effbot.org/tkinterbook/listbox.htm.
Hope this helps.

Categories

Resources