Creating a wizard in Tkinter - python

I'm creating a Wizard in Tkinter. Almost each of the steps shoud I have the same footer with the button for navigation and cancelling. How can I achieve this? Should I create a Frame? And in general, should all the steps be created as different frames?

The answer to this question is not much different than Switch between two frames in tkinter. The only significant difference is that you want a permanent set of buttons on the bottom, but there's nothing special to do there -- just create a frame with some buttons as a sibling to the widget that holds the individual pages (or steps).
I recommend creating a separate class for each wizard step which inherits from Frame. Then it's just a matter of removing the frame for the current step and showing the frame for the next step.
For example, a step might look something like this (using python 3 syntax):
class Step1(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
header = tk.Label(self, text="This is step 1", bd=2, relief="groove")
header.pack(side="top", fill="x")
<other widgets go here>
Other steps would be conceptually identical: a frame with a bunch of widgets.
Your main program or you Wizard class would either instantiate each step as needed, or instantiate them all ahead of time. Then, you could write a method that takes a step number as a parameter and adjust the UI accordingly.
For example:
class Wizard(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.current_step = None
self.steps = [Step1(self), Step2(self), Step3(self)]
self.button_frame = tk.Frame(self, bd=1, relief="raised")
self.content_frame = tk.Frame(self)
self.back_button = tk.Button(self.button_frame, text="<< Back", command=self.back)
self.next_button = tk.Button(self.button_frame, text="Next >>", command=self.next)
self.finish_button = tk.Button(self.button_frame, text="Finish", command=self.finish)
self.button_frame.pack(side="bottom", fill="x")
self.content_frame.pack(side="top", fill="both", expand=True)
self.show_step(0)
def show_step(self, step):
if self.current_step is not None:
# remove current step
current_step = self.steps[self.current_step]
current_step.pack_forget()
self.current_step = step
new_step = self.steps[step]
new_step.pack(fill="both", expand=True)
if step == 0:
# first step
self.back_button.pack_forget()
self.next_button.pack(side="right")
self.finish_button.pack_forget()
elif step == len(self.steps)-1:
# last step
self.back_button.pack(side="left")
self.next_button.pack_forget()
self.finish_button.pack(side="right")
else:
# all other steps
self.back_button.pack(side="left")
self.next_button.pack(side="right")
self.finish_button.pack_forget()
The definition of the functions next, back, and finish is very straight-forward: just call self.show_step(x) where x is the number of the step that should be shown. For example, next might look like this:
def next(self):
self.show_step(self.current_step + 1)

I recommend using one main window with the buttons and put the rest of the widgets in different labelframes that appear and disappear upon execution of different functions by the buttons

Related

python Tkinter grid method not working as should be for some reason

i am trying to get my listbox to move to the bottom of the gui but no matter how high of a row value i give it it wont budge. you can see my listbox in the Creat_Gui method in my code. im not sure why this is happpening it cant be the button because the button is in row 1 so im not sure whats causing this.
i tried using sticky='s' that didnt work i tried changing the rows multiple times didnt work. i tried using the root.rowconfigure(100,weight=1) this worked kind of but messes with thte grid which is annoying
import tkinter as tk
class Manager:
def __init__(self):
self.root=tk.Tk()
self.root.title('password_manager')
self.root.geometry('500x600')
self.create_GUI()
self.storage = {}
self.root.mainloop()
def create(self):
pass
def open_page(self):
print('openpage')
def add_new_button(self):
pass
def add_new(self):
self.app_title=tk.Label(text='test')
self.app_title.grid(row=2,column=50)
self.application_name=tk.Entry(self.root,width=20,font=('arial',18))
self.username=tk.Entry(self.root,width=20,font=('arial',18))
self.password=tk.Entry(self.root,width=20,font=('arial',18))
self.application_name.grid(row=2, column=1)
self.username.grid(row=3, column=2)
self.password.grid(row=4, column=3)
self.app_name_label = tk.Label(self.root, text='Application Name:')
self.username_label = tk.Label(self.root, text='Username:')
self.password_label = tk.Label(self.root, text='Password:')
self.app_name_label.grid(row=2, column=0)
self.username_label.grid(row=3, column=1)
self.password_label.grid(row=4, column=2)
self.password.bind('<Return>',self.hide)
def hide(self,thing):
#store user info to pass onto dictionary and hide textboxes
username=self.username.get()
password=self.password.get()
app_name=self.application_name.get()
self.application_name.grid_forget()
self.username.grid_forget()
self.password.grid_forget()
self.add_to_memory(username,password,app_name)
def add_to_memory(self,username,password,app_name):
#store username password and application name in dictionary
if app_name in self.storage.keys():
return
else:
self.storage[app_name]=(username,password)
print(self.storage)
def create_GUI(self):
#create initial interface
#self.root.columnconfigure(100, weight=1)
#self.root.rowconfigure(100, weight=1)
self.listbox=tk.Listbox(self.root,width=100)
self.listbox.grid(row=200,column=0)
self.button=tk.Button(self.root,text='add new',font=('arial',18),command=self.add_new)
self.button.grid(row=1,column=0)
Manager()
If you want to use grid, and you want a widget to be at the bottom of it's parent, you need to designate some row above that widget to take all of the extra space, forcing the widget to the bottom. Here's one example:
def create_GUI(self):
self.listbox=tk.Listbox(self.root,width=100)
self.expando_frame = tk.Frame(self.root)
self.button=tk.Button(self.root,text='add new',font=('arial',18),command=self.add_new)
self.root.grid_rowconfigure(2, weight=1)
self.button.grid(row=1,column=0)
self.expando_frame.grid(row=2, sticky="nsew")
self.listbox.grid(row=3,column=0)
With that, the listbox will be at the bottom, with extra space above it. If you want to add more widgets, you can add them to self.expando_frame rather than self.root and they will appear above the listbox.
Using frames in this way is a valuable technique. You could, for example, use three frames in root: one for the top row of buttons, one for the listbox on the bottom, and one for everything in the middle. You could then use pack on these frames and save a line of code (by virtue of not having to configure the rows). You can then use grid for widgets inside the middle frame.

Python tkinter: Allow window to be resized, but prevent it from automatically shrinking based on content?

I've been following the ttkbootstrap example for creating a collapsible frame widget. This example almost does what I need, but I've noticed the following odd behaviour:
If you run the example and do not resize the window, collapsing or expanding a frame changes the size of the entire window.
If you run the example and resize the window manually, collapsing or expanding a frame no longer changes the window size.
I'd really like to be able to use the example to build my own application, but avoid the behaviour where expanding or collapsing a frame changes the window size. I still need the window itself to be manually resizeable, so disabling window resizing altogether is not an option. Is there any way I can accomplish this?
PS: Below is a slightly modified version of the example code, which can be run without requiring the image assets that the original example uses, if you want to run the example to test it out.
"""
Author: Israel Dryer
Modified: 2021-05-03
"""
import tkinter
from tkinter import ttk
from ttkbootstrap import Style
class Application(tkinter.Tk):
def __init__(self):
super().__init__()
self.title('Collapsing Frame')
self.style = Style()
cf = CollapsingFrame(self)
cf.pack(fill='both')
# option group 1
group1 = ttk.Frame(cf, padding=10)
for x in range(5):
ttk.Checkbutton(group1, text=f'Option {x+1}').pack(fill='x')
cf.add(group1, title='Option Group 1', style='primary.TButton')
# option group 2
group2 = ttk.Frame(cf, padding=10)
for x in range(5):
ttk.Checkbutton(group2, text=f'Option {x+1}').pack(fill='x')
cf.add(group2, title='Option Group 2', style='danger.TButton')
# option group 3
group3 = ttk.Frame(cf, padding=10)
for x in range(5):
ttk.Checkbutton(group3, text=f'Option {x+1}').pack(fill='x')
cf.add(group3, title='Option Group 3', style='success.TButton')
class CollapsingFrame(ttk.Frame):
"""
A collapsible frame widget that opens and closes with a button click.
"""
ARROW_RIGHT = "\u2b9a"
ARROW_DOWN = "\u2b9b"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.columnconfigure(0, weight=1)
self.cumulative_rows = 0
def add(self, child, title="", style='primary.TButton', **kwargs):
"""Add a child to the collapsible frame
:param ttk.Frame child: the child frame to add to the widget
:param str title: the title appearing on the collapsible section header
:param str style: the ttk style to apply to the collapsible section header
"""
if child.winfo_class() != 'TFrame': # must be a frame
return
style_color = style.split('.')[0]
frm = ttk.Frame(self, style=f'{style_color}.TFrame')
frm.grid(row=self.cumulative_rows, column=0, sticky='ew')
# header title
lbl = ttk.Label(frm, text=title, style=f'{style_color}.Invert.TLabel')
if kwargs.get('textvariable'):
lbl.configure(textvariable=kwargs.get('textvariable'))
lbl.pack(side='left', fill='both', padx=10)
# header toggle button
btn = ttk.Button(frm, text=self.ARROW_DOWN, style=style, command=lambda c=child: self._toggle_open_close(child))
btn.pack(side='right')
# assign toggle button to child so that it's accesible when toggling (need to change image)
child.btn = btn
child.grid(row=self.cumulative_rows + 1, column=0, sticky='news')
# increment the row assignment
self.cumulative_rows += 2
def _toggle_open_close(self, child):
"""
Open or close the section and change the toggle button image accordingly
:param ttk.Frame child: the child element to add or remove from grid manager
"""
if child.winfo_viewable():
child.grid_remove()
child.btn.configure(text=self.ARROW_RIGHT)
else:
child.grid()
child.btn.configure(text=self.ARROW_DOWN)
if __name__ == '__main__':
Application().mainloop()
I'd really like to ... avoid the behaviour where expanding or collapsing a frame changes the window size.
You can do that by forcing the window to a specific size. Once the geometry of a window has been set -- whether by the user dragging the window or the program explicitly setting it -- it won't resize when the contents of the window resize.
For example, you can force the window to be drawn, ask tkinter for the window size, and then use the geometry method to be that same size.
For example, try adding this as the last lines in Application.__init__:
self.update()
self.geometry(self.geometry())

How can I place a widget at the very bottom of a tkinter window when positioning with .grid()?

I am aware that you cannot use different types of geometry managers within the same Tkinter window, such as .grid() and .pack(). I have a window that has been laid out using .grid() and I am now trying to add a status bar that would be snapped to the bottom of the window. The only method I have found online for this is to use .pack(side = BOTTOM), which will not work since the rest of the window uses .grid().
Is there a way that I can select the bottom of the window to place widgets from when using .grid()?
from tkinter import *
from tkinter.ttk import *
import tkinter as tk
class sample(Frame):
def __init__(self,master=None):
Frame.__init__(self, master)
self.status = StringVar()
self.status.set("Initializing")
statusbar = Label(root,textvariable = self.status,relief = SUNKEN, anchor = W)
statusbar.pack(side = BOTTOM, fill = X)
self.parent1 = Frame()
self.parent1.pack(side = TOP)
self.createwidgets()
def createwidgets(self):
Label(self.parent1,text = "Grid 1,1").grid(row = 1, column = 1)
Label(self.parent1,text = "Grid 1,2").grid(row = 1, column = 2)
Label(self.parent1,text = "Grid 2,1").grid(row = 2, column = 1)
Label(self.parent1,text = "Grid 2,2").grid(row = 2, column = 2)
if __name__ == '__main__':
root = Tk()
app = sample(master=root)
app.mainloop()
So using labels since I was kinda lazy to do other stuff, you can do frames to ensure that each section of your window can be packed/grid as required. Frames will be a useful tool for you to use when trying to arrange your widgets. Note that using a class can make things a little easier when deciding your parents. So imagine each frame is a parent and their children can be packed as required. So I would recommend drawing out your desired GUI and see how you will arrange them. Also if you want to add another frame within a frame simply do:
self.level2 = Frame(self.parent1)
You can check out additional settings in the docs
http://effbot.org/tkinterbook/frame.htm
PS: I am using a class hence the self, if you don't want to use classes then its okay to just change it to be without a class. Classes make it nicer to read though
Just give it a row argument that is larger than any other row. Then, give a weight to at least one of the rows before it.
Even better is to use frames to organize your code. Pack the scrollbar on the bottom and a frame above it. Then, use grid for everything inside the frame.
Example:
# layout of the root window
main = tk.Frame(root)
statusbar = tk.Label(root, text="this is the statusbar", anchor="w")
statusbar.pack(side="bottom", fill="x")
main.pack(side="top", fill="both", expand=True)
# layout of the main window
for row in range(1, 10):
label = tk.Label(main, text=f"R{row}")
label.grid(row=row, sticky="nsew")
main.grid_rowconfigure(row, weight=1)
...

Tkinter grid of frames gets stacked vertically instead of hotizontal

I'm surely misunderstanding something with pack and grid...
I'm making the GUI for a python application that get debug messages in multiple terminal-like consoles. Making a Tkinter GUI is new to
First of all why do I need to pack buttonframe to have it shown? Shouldn't grid be equivalent to pack?
The GUI is made of a series of buttons on top in the buttonframe and a series of vertical consoles below showing up in columns. Instead what I get is all the consoles stacked vertically.
The consoles are frames with text inside which is written by other threads in the application.
If I don't grid the consoles[idx] it doesn't change (so I assume it doesn't work). If I don't pack the consoles[idx] the GUI starts flickering.
Please note that if I instead call create_Hterms, which instead stacks vertically a bunch of horizontal consoles.
How do I correctly put my consoles as vertical columns?
import Tkinter as tikei
NCONSOLES = 4
HORZ_CONSOLE_W = 10
HORZ_CONSOLE_H = 12
# FakeConsole: one of the consoles that are spawned by MainWindow.
class FakeConsole(tikei.Frame): # from Kevin in http://stackoverflow.com/questions/19479504/how-can-i-open-two-consoles-from-a-single-script
def __init__(self, root, cust_width, cust_height, *args, **kargs):
tikei.Frame.__init__(self, root, *args, **kargs)
self.grid(row=0)
#white text on black background
self.text = tikei.Text(root, bg="black", fg="white", width=cust_width, height=cust_height)
self.text.grid(row=0)
self.text.insert(tikei.END, 'ASD')
self.text.see(tikei.END)
def clearConsole(self):
self.text.delete(1.0,tikei.END)
class MainWindow(tikei.Frame):
counter = 0
consoles = []
consoleFormat = ''
e_Vvar = ''
e_Hvar = ''
var1 = ''
ctrlCmdRecipient = ''
def __init__(self, *args, **kwargs):
tikei.Frame.__init__(self, *args, **kwargs)
self.consoleFormat = 'H'
self.cF = tikei.StringVar()
self.grid(row=0)
self.buttonframe = tikei.Frame(self)
self.buttonframe.grid(row=0)
tikei.Button(self.buttonframe, text = "Clear Consoles", command=self.clearConsoles).grid(row=0, column=0)
tikei.Button(self.buttonframe, text = "start").grid(row=0, column=1)
tikei.Button(self.buttonframe, text = "stop").grid(row=0, column=2)
self.consoleFrame = tikei.Frame(self)
self.create_Hterms()
self.consoleFrame.grid(row=1)
# create a status bar
self.statusTxt = tikei.StringVar()
self.statusTxt.set('-')
self.statusLab = tikei.Label(self.buttonframe, text="Status: ", anchor=tikei.W, height=1).grid(row=1, column=0, sticky=tikei.W)
self.statusBar = tikei.Label(self.buttonframe, textvariable=self.statusTxt, anchor=tikei.W, height=1).grid(row=1, column=1, columnspan=4, sticky=tikei.W)
def clearConsoles(self):
for i in range(NCONSOLES):
self.consoles[i].clearConsole()
def create_Hterms(self):
ctr = NCONSOLES
idx = 0
cons_w = HORZ_CONSOLE_W
cons_h = HORZ_CONSOLE_H
while ctr > 0:
self.consoles.append(FakeConsole(self.consoleFrame, cons_w, cons_h)) #
self.consoles[idx].grid(row=idx+1, column=0)
ctr -= 1
idx += 1
return idx+1
root = tikei.Tk()
mainTk = MainWindow(root)
root.wm_title("TOOLS")
mainTk.grid(row=0)
root.mainloop()
First of all why do I need to pack buttonframe to have it shown?
Shouldn't grid be equivalent to pack?
No, generally speaking you do not need to pack buttonframe to have it shown. Use pack or grid, but not both on the same widget. In this specific case, however, the use of pack masks bugs deeper in your code, and makes it seem like calling pack is required.
The problem is that you use pack for self.consoleFrame, and both self.consoleFrame and self.buttonFrame have the same parent. Because they share the same parent you must use either pack or grid for both.
The GUI is made of a series of buttons on top in the buttonframe and a
series of vertical consoles below showing up in columns. Instead what
I get is all the consoles stacked vertically.
This is because you use grid, and then you use pack. You can only use one or the other, and the last one you use "wins". Thus, you're using pack, and you're not giving it any options, so it's defaulting to packing things top to bottom.
While it's perfectly safe to use both grid and pack in the same application, it's pointless to use both on the same widget, and it's an error to use different ones for widgets that share the same parent.
The solution to the problem is to be consistent with your use of grid and pack. If you intend to use grid, make sure you use it consistently for all widgets that share the same parent.
The same holds true for pack -- use it consistently. Also, I recommend to always explicitly add the side, fill and expand arguments to pack so that it is clear what your intention is.

how to stack two widgets and switch between them?

I want to script two widgets which would be stacked and I would switch from one to another with a key. Each widget would be a Frame containing several Labels. I have the following code so far (only one Label per Frame):
import Tkinter as tk
import ttk
import datetime
def main():
# initialize root
root = tk.Tk()
# initialize widgets
dash = Dashboard(root)
notepad = Notepad(root)
# set key actions
root.bind('<F11>', root.lift)
root.bind('<F1>', dash.raiseme)
root.bind('<F2>', notepad.raiseme)
root.mainloop()
class Dashboard(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # voodoo
self.dashframe = tk.Frame(parent)
self.labone = tk.Label(self.dashframe, text="lab1", fg='black', bg='blue')
self.labone.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme dash"
self.labone.configure(text=datetime.datetime.now())
self.dashframe.lift()
class Notepad(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # also voodoo
self.noteframe = tk.Frame(parent)
self.laboneone = tk.Label(self.noteframe, text="lab11", fg='white', bg='red')
self.laboneone.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme notepad"
self.laboneone.configure(text=datetime.datetime.now())
self.noteframe.lift()
if __name__ == '__main__':
main()
Pressing F1 and F2 reach the correct routines but the only thing I get is the main window, empty. There are no errors displayed so I guess that the code runs fine (just not the way O would like to :)).
Can I achieve the switch using the skeleton above?
There are at least two big problems with your code.
First, you're creating all these new frames, but not placing them anywhere, so they will never show up anywhere. If you have a main window with nothing placed on it, of course you will just "get the main window, empty". You need to call pack or some other layout method on any widget to get it to show up on its parent. In this case, it sounds like you want to put them both in the exact same place, so grid or place is probably what you want.
Second, your Dashboard and Notepad classes are themselves Frames, but they don't do any Frame-ish stuff; instead, they each create another, sibling Frame and attach a label to that sibling. So, even if you packed the Dashboard and Notepad, they're just empty frame widgets, so that wouldn't do any good.
If you fix both of those, I think your code does what you want:
class Dashboard(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # voodoo
self.labone = tk.Label(self, text="lab1", fg='black', bg='blue')
self.labone.grid(row=0, column=0)
self.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme dash"
self.labone.configure(text=datetime.datetime.now())
self.lift()
class Notepad(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # also voodoo
self.laboneone = tk.Label(self, text="lab11", fg='white', bg='red')
self.laboneone.grid(row=0, column=0)
self.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme notepad"
self.laboneone.configure(text=datetime.datetime.now())
self.lift()
However, you might also want to set a fixed size for everything; otherwise you could end up lifting the red widget and, e.g., only covering 96% of the blue one because the current time is a bit narrower than the previous oneā€¦
The code you linked to for inspiration attempted to do this:
newFrame = tkinter.Frame(root).grid()
newFrame_name = tkinter.Label(newFrame, text="This is another frame").grid()
That won't work, because grid returns None, not the widget. But at least it calls grid; yours doesn't even do that.

Categories

Resources