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()
Related
In simply trying to work on organizing my code, I've found online that it seems to be best to put much of your code into classes when needed. So in doing that, I'd figure that I'll try to create a frame class with create_labels and create_buttons methods.
My goal is to be able to create 2 or more seperate frames that are similar in style (hence why I find it best to make a frame class). Then, using methods, create labels, buttons, and other widgets and allow them to move around with ease within their respective frames.
So here's my code:
import tkinter as tk
window = tk.Tk()
class MyFrame(tk.Frame):
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent)
self.parent = parent
self.layout(**kwargs)
def labels(self, text, **kwargs):
tk.Label.__init__(self, text=text)
self.layout(**kwargs)
def buttons(self, text, command, **kwargs):
tk.Button.__init__(self, text=text, command=command)
self.layout(**kwargs)
def layout(self, row=0, column=0, columnspan=None, row_weight=None, column_weight=None, color=None, sticky=None, ipadx=None, padx=None, ipady=None, pady=None):
self.grid(row=row, column=column, columnspan=columnspan, sticky=sticky, ipadx=ipadx, padx=padx, ipady=ipady, pady=pady)
self.grid_rowconfigure(row, weight=row_weight)
self.grid_columnconfigure(column, weight=column_weight)
self.config(bg=color)
frame_1 = MyFrame(window, row=0, column=0, sticky="news", color="pink")
frame_1.buttons("Btn_1/Frme_1", quit, row=0, column=0)
frame_1.buttons("Btn_2/Frme_1", quit, row=0, column=1)
frame_2 = MyFrame(window, row=1, column=0, sticky="news", color="green")
frame_2.buttons("Btn_1/Frme_2", quit, row=0, column=0)
frame_2.buttons("Btn_2/Frme_2", quit, row=0, column=1)
window.grid_columnconfigure(0, weight=1)
window.grid_columnconfigure(1, weight=1)
window.grid_rowconfigure(1, weight=1)
window.grid_rowconfigure(0, weight=1)
window.mainloop()
Now I think a problem of mine is during the __init__ method because there should be 2 frames and 2 buttons per frame. However, there's no errors which makes it harder to find out for sure of that's why only the latest buttons and frames exist. I don't even think it's a case of one frame or widget 'covering' another. I think the second frame/widgets seem to be overwriting the first frame/widgets.
Any help is appreciated.
The issue lies with your layout function. Both the frames are being grided on the row=0 and column=0, as you are not passing any specific row and column to the function. Hence, the overwriting of the frames can be seen.
Another issue (possible) in your code is that the frame_1 and frame_2 buttons do not belong to the Frame widget but to the root window
I'm pretty new to Tkinter and I build a little window with different widgets.
My Code looks like this:
import tkinter as tk
from tkinter import ttk
class Application(tk.Frame):
def __init__(self, master):
super().__init__(master)
self.master = master
self.master.geometry("800x600")
self.master.title("Tkinter Sandbox")
self.master.grid_rowconfigure(0, weight=1)
self.master.grid_columnconfigure(1, weight=1)
self._create_left_frame()
self._create_button_bar()
self._create_label_frame()
def _create_left_frame(self):
frame = tk.Frame(self.master, bg="red")
tree_view = ttk.Treeview(frame)
tree_view.column("#0", stretch=tk.NO)
tree_view.heading("#0", text="Treeview")
tree_view.pack(fill=tk.Y, expand=1)
frame.grid(row=0, column=0, rowspan=2, sticky=tk.N + tk.S)
def _create_button_bar(self):
frame = tk.Frame(self.master, bg="blue")
button_run_single = tk.Button(frame, text="Button 1")
button_run_all = tk.Button(frame, text="Button 2")
button_details = tk.Button(frame, text="Button 3")
button_run_single.grid(row=0, column=0)
button_run_all.grid(row=0, column=1, padx=(35, 35))
button_details.grid(row=0, column=2)
frame.grid(row=0, column=1, sticky=tk.N)
def _create_label_frame(self):
frame = tk.Frame(self.master, bg="blue")
name_label = tk.Label(frame, text="Label 1")
performance_label = tk.Label(frame, text="Label 2")
name_entry = tk.Entry(frame)
performance_entry = tk.Entry(frame)
name_label.grid(row=0, column=0)
name_entry.grid(row=0, column=1)
performance_label.grid(row=1, column=0)
performance_entry.grid(row=1, column=1)
frame.grid(row=1, column=1)
if __name__ == '__main__':
root = tk.Tk()
app = Application(root)
app.mainloop()
Between the three buttons and the label + entry frame is a huge space. I want the button and label + entry frame right under each other, without the huge space but the treeview should also expand vertically over the whole application window.
I think the problem might be my row and column configuration but I don't know how to solve this problem.
The way you've structured your code makes it hard to see the problem. As a good general rule of thumb, all calls to grid or pack for widgets within a single parent should be in one place. Otherwise, you create dependencies between functions that are hard to see and understand.
I recommend having each of your helper functions return the frame rather than calling grid on the frame. That way you give control to Application.__init__ for the layout of the main sections of the window.
For example:
left_frame = self._create_left_frame()
button_bar = self._create_button_bar()
label_frame = self._create_label_frame()
left_frame.pack(side="left", fill="y")
button_bar.pack(side="top", fill="x")
label_frame.pack(side="top", fill="both", expand=True)
I used pack here because it requires less code than grid for this type of layout. However, if you choose to switch to grid, or wish to add more widgets to the root window later, you only have to modify this one function rather than modify the grid calls in multiple functions.
Note: this requires that your functions each do return frame to pass the frame back to the __init__ method. You also need to remove frame.grid from each of your helper functions.
With just that simple change you end up with the button bar and label/entry combinations at the top of the section on the right. In the following screenshot I changed the background of the button_bar to green so you can see that it fills the top of the right side of the UI.
You need to change line
self.master.grid_rowconfigure(0, weight=1)
to
self.master.grid_rowconfigure(1, weight=1)
so that the second row takes all the space. Then you need to stick widgets from the label frame to its top by adding sticky parameter to the grid call in _create_label_frame:
frame.grid(row=1, column=1, sticky=tk.N)
I prefer to use the Pack Function since it gives a more open window - its easy to configure. When you use Pack() you can use labels with no text and just spaces to create a spacer, by doing this you won't run into the problem your facing.
Let's asume a simple tkinter form initially defined with a Listbox widget in selectmode=EXTENDED. I want to code a button which will add Advanced options to the form and thus force the user to select a single item of the list. This translates into changing to selectmode=SINGLE.
My code is not working. Maybe I simply cannot redefine the Listbox and I should access the parameter selectmode some other way?
class DefineMultiSelectForm(Frame):
def __init__(self, master, listtodisplay=[]):
Frame.__init__(self, master=master)
self.listtodisplay=listtodisplay
self.create_widgets()
self.pack(fill=BOTH, expand=1)
def create_widgets(self):
self.listbox = Listbox(self,selectmode=EXTENDED, width=50)
self.listbox.grid(row=0, column=0, columnspan=4, sticky=W+E+N+S)
self.advanced_butt = Button(self, text="Advanced Editing", command=self.advanced)
self.advanced_butt.grid (row=7, column=2, sticky=W+E)
self.simplifd_butt = Button(self, text="Simple Editing", command=self.simple)
self.simplifd_butt.grid (row=7, column=1, sticky=W+E)
def advanced(self):
self.listbox = Listbox(self,selectmode=SINGLE, width=50)
def simple(self):
self.listbox = Listbox(self,selectmode=EXTENDED, width=50)
Many Thanks in advance!
I’m not sure about the selectmode attitude, but for most tkinter attributes you can change it like this:
self.listbox['selectmode'] = SINGLE
# or you can do it this way
self.listbox.config(selectmode = SINGLE)
The reason why your way doesn’t work is what you have said, you are redefining the variable.
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.
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.