Please help anyone
You can read all details as an inline comment.
I have created three classes LeftFrame, RightFrame, DynamicWindow
In DynamicWindow I am inheriting RightFrame
Step 1
LeftFrame, in column 0 with minsize 350
Step 2
RightFrame, in column 1 with weight 1, capturing all available space
Step 3
DynamicWindow, ingeriting RightFrame , Here is main problem, Please read the code.
import tkinter as tk
from win32api import GetMonitorInfo, MonitorFromPoint
root = tk.Tk()
monitor_info = GetMonitorInfo(MonitorFromPoint((0, 0)))['Work']
root.geometry(f'{monitor_info[2]}x{monitor_info[3]}')
root.state('zoomed')
root.columnconfigure(0, minsize=350) # Width of left frame
root.columnconfigure(1, weight=1) # All available space for right frame
root.rowconfigure(0, weight=1) # Full screen height for both frame
class LeftFrame(tk.Frame):
"""
Left Frame
"""
def __init__(self, container):
super().__init__(container)
self.config(bg='red')
self.grid(row=0, column=0, sticky='nsew')
class RightFrame(tk.Frame):
"""
Right Frame:
Divided into three section head frame, middle frame and bottom frame
head frame contains button
middle frame contains dynamically changeable frame. Here I am facing problem,
frame not able to take actual size according to weight and minsize that I given.
"""
def __init__(self, container):
super().__init__(container)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, minsize=30)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, minsize=30)
self.config(bg='green')
self.grid(row=0, column=1, sticky='nsew')
self.head_frame = tk.Frame(self, bg='orange')
self.head_frame.grid(row=0, column=0, sticky='nsew')
self.head_frame.rowconfigure(0, weight=1)
self.first_screen_button = tk.Button(self.head_frame, text='First Screen')
self.first_screen_button.grid(row=0, column=0, sticky='nsew', ipadx=20)
self.second_screen_button = tk.Button(self.head_frame, text='Second Screen')
self.second_screen_button.grid(row=0, column=1, sticky='nsew', ipadx=20)
self.middle_frame = tk.Frame(self, bg='green')
self.middle_frame.grid(row=1, column=0, sticky='nsew')
self.bottom_frame = tk.Frame(self, bg='orange')
self.bottom_frame.grid(row=2, column=0, sticky='nsew')
class DynamicWindow(RightFrame):
def __init__(self, container):
super().__init__(container)
self.first_screen_button.config(command=self.first_screen)
self.second_screen_button.config(command=self.second_screen)
self.first_screen() # I am calling this here becuase on first click on first screen button
# window don't appear. You can check by comment this code.
# Can anyone please tell me why first screen not appear on first click.
def first_screen(self):
"""
First screen that I want to appear when I click on button
It has two frame
"""
for widget in self.middle_frame.winfo_children():
# Want to destroy all available widget in middle frame
widget.destroy()
self.middle_frame.columnconfigure(0, weight=1)
# Configuring size and weight but this is not working properly
self.middle_frame.columnconfigure(1, minsize=30)
self.middle_frame.rowconfigure(0, weight=1)
self.middle_frame.rowconfigure(1, weight=0)
main_chart_window = tk.Frame(self.middle_frame, bg='#4d4d4d')
main_chart_window.grid(row=0, column=0, sticky='nsew')
toolbar = tk.Frame(self.middle_frame, bg='red')
toolbar.grid(row=0, column=1, sticky='nsew')
def second_screen(self):
"""
This is not working in proper way
I am not able to reconfigure weight of middle frame
I want this window in full screen in middle frame
Here you will notice column 1 taking minsize 30, can anyone solve this
"""
for widget in self.middle_frame.winfo_children():
widget.destroy()
self.middle_frame.columnconfigure(0, weight=1)
self.middle_frame.rowconfigure(0, weight=1)
second_screen_window = tk.Frame(self.middle_frame, bg='purple')
second_screen_window.grid(row=0, column=0, sticky='nsew')
left_frame = LeftFrame(root)
dynamic_window = DynamicWindow(root)
root.mainloop()
I made it all work for you. I lost interest in fighting with your code/method so, I completely rewrote the code from scratch and devised a different method. All of the issues that you highlighted have been resolved. The structure of my code should be much easier to work with. The main issue is that you were destroying children, but you weren't destroying the column and/or row that the children were in. You basically can't. Using grid_forget() even in conjunction with destroy() or grid_remove() doesn't seem to remove the grid cell.
changes:
Every major widget has been separated into it's own class
Names have been changed to reflect the actual purpose of each widget (to the best of my ability based on your example)
We swap 'main display' widgets by removing/re-instating the entire widget ~ instead of destroying/recreating all of it's children
a lambda is used in the button command to pass the desired 'main display' to the method that does the swapping
we never use super() to instantiate a class. We specifically refer to the super by classname
all args and kwargs are maintained, so we can treat our custom widgets like their super
we only import exactly what we need (my preference)
The comments should tell you the rest, but if there is confusion, point it out to me in the comment section and I will respond with a more detailed explanation.
widgets.py
from tkinter import Frame, Button
from typing import List, Dict, Callable
from dataclasses import dataclass
#a simple "typedef" for storing menu button data
#dataclass
class MenuData_t:
func:Callable #method the command lambda will call
buttons:List[Dict] #Button(**kwargs)
targets:List[Frame] #'main display' to switch to
griddata:List[Dict] #.grid(**kwargs)
'''
this replaces your 'head_frame'
it also provides an interface to concoct all of the buttons that will swap 'main displays'
if you need other types of buttons you will need to manually create them in __init__
considerations have been made in init_displayswap_menu for existing buttons
'''
class MenuFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
def init_displayswap_menu(self, md:MenuData_t):
c = len(self.winfo_children())
for i, (b, t, g) in enumerate(zip(md.buttons, md.targets, md.griddata)):
self.__dict__[f'swap_btn{i+1}'] = Button(self, command=lambda m=t: md.func(m), **b)
self.__dict__[f'swap_btn{i+1}'].grid(row=0, column=i+c, **g)
#this replaces your "bottom_frame"
class Footer(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
#this replaces your "first_screen"
class PrimaryFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
self.chart = Frame(self, bg='#4d4d4d')
self.chart.grid(row=0, column=0, sticky='nswe')
self.toolbar = Frame(self, bg='red')
self.toolbar.grid(row=0, column=1, sticky='nswe')
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, minsize=30)
self.grid_rowconfigure(0, weight=1)
#this replaces your "second_screen"
class SecondaryFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
#this replaces your "LeftFrame"
class Sidebar(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
#this replaces your "RightFrame" AND "DynamicWindow"
class MainFrame(Frame):
def __init__(self, master, *args, **kwargs):
Frame.__init__(self, master, *args, **kwargs)
##INSTANTIATE
#menu
self.menu = MenuFrame(self, bg='orange')
self.menu.grid(row=0, column=0, sticky='nsew')
#main display
self.current = None #for storing currently used 'main display'
self.primary = PrimaryFrame(self, bg='green')
self.secondary = SecondaryFrame(self, bg='purple')
#footer
self.footer = Footer(self, bg='orange')
self.footer.grid(row=2, column=0, sticky='nsew')
##UTILIZE
#concoct main display swap menu
'''
append accordingly to the 3 lists to create more buttons that will switch frames
done this way so button creation can remain in MenuFrame but use remote data
row, column and command are managed in MenuFrame
'''
self.menu.init_displayswap_menu(MenuData_t(
self.main_display, #method the command lambda will call
[{'text':'Primary'}, #Button(**kwargs)
{'text':'Secondary'},
],
[self.primary, #'main display' to switch to
self.secondary,
],
[{'sticky':'nswe','ipadx':20}, #.grid(**kwargs)
{'sticky':'nswe','ipadx':20},
]
))
#init main display
'''
I could have called main_display directly but this illustrates 2 things
1: how to virtually click a button
2: how to access the buttons that MenuFrame created in it's __dict__
'''
self.menu.swap_btn1.invoke()
#configure grid
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, minsize=30)
self.grid_rowconfigure(1, weight=1)
self.grid_rowconfigure(2, minsize=30)
#replaces your 'first_screen' AND 'second_screen' methods
def main_display(self, frame):
if self.current is not frame: #only swap if we aren't requesting the current 'main display'
if self.current:
self.current.grid_remove() #remove current from the grid, instead of destroy
self.current = frame #set new current and add it to the grid
self.current.grid(row=1, column=0, sticky='nsew')
main.py
from win32api import GetMonitorInfo, MonitorFromPoint
from widgets import Sidebar, MainFrame
from tkinter import Tk
#This is your "root"
class Application(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
Sidebar(self, bg='red').grid(row=0, column=0, sticky='nswe')
MainFrame(self, bg='black').grid(row=0, column=1, sticky='nswe')
#configure grid
self.grid_columnconfigure(0, minsize=350)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
#kick off the entire app with proper PEP8
if __name__ == '__main__':
monitor_info = GetMonitorInfo(MonitorFromPoint((0, 0)))['Work']
app = Application()
app.title("Manish Pushpam's Bad-Ass Application")
app.geometry(f'{monitor_info[2]}x{monitor_info[3]}')
app.minsize(800, 600)
app.mainloop()
Related
I'm having an update problem in tkinter GUI, for past two days, I have searched a lot, Cant find something specific to my problem.This post Tkinter updating labels in stacked frame windows come close to my problem but not exactly. I am using classes to structure my application... The structure is given here Application structure image ( SOF not letting me embed images but link is provided )
From above structure you can see, I'm trying to make changes in DetailFrame from ListProduct Frame, now the code is reaching there and changing the values successfully but not updating the label, I'm using config method to change label... and frame background,but no luck..
I have tried StringVar as well for updating label, but nothing... Sample Code is provided below...
This application is a part of main app and for Original Code Structure Thanks to .. Bryan Oakley
class ProductWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry("600x500")
container = tk.Frame(self)
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.notebook = ttk.Notebook(container)
self.notebook.grid(row=0, column=0, sticky='nsew')
self.new_product_tab = tk.Frame(self.notebook, bg='#233223' )
self.list_product_tab = tk.Frame(self.notebook, bg='#323232')
self.edit_product_tab = tk.Frame(self.notebook, bg='#433434')
# Adding Tabs to Notebook
self.notebook.add(self.new_product_tab, text=" Add New Product ")
self.notebook.add(self.list_product_tab, text=" List All Product ")
self.notebook.add(self.edit_product_tab, text=" Edit Product ")
self.productframe = EditProductFrame(self.edit_product_tab)
self.detailframe = DetailFrame(self.productframe)
button = tk.Button(self.list_product_tab, text="Change background in Edit Form", command=self.change_method)
button.pack()
def change_method(self):
print("Trying to change the frame")
self.productframe.raise_edit_frame(DetailFrame)
self.detailframe.change_bg('green')
self.notebook.select(self.edit_product_tab)
if __name__ == "__main__":
testObj = ProductWindow()
testObj.mainloop()
In another file, I have DetailFrame below.
class EditProductFrame(tk.Frame):
def __init__(self, parent):
print("Edit product frame constructor is called...")
tk.Frame.__init__(self, parent)
self.pack(side='top', fill='both', expand=True)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# define frames and pack them in
self.frames = {}
for F in {DetailFrame, EditFrame}:
frame = F(self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky='nsew')
self.raise_edit_frame(DetailFrame)
def raise_edit_frame(self, container):
frame = self.frames[container]
frame.tkraise()
class EditFrame(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.config(bg='green')
label = tk.Label(self, text="Edit Page",)
label.pack(pady=0,padx=0)
tk.Button(self, text="Go to Detail", command=lambda:parent.raise_edit_frame(DetailFrame)).pack()
class DetailFrame(tk.Frame):
def __init__(self, parent):
print("something detail view")
tk.Frame.__init__(self, parent)
self.config(bg='blue')
self.label = tk.Label(self, text='Original Label')
self.label.pack(pady=0,padx=0)
tk.Button(self, text="Go to Edit Frame", command=lambda:parent.raise_edit_frame(EditFrame)).pack()
def change_bg(self, color):
# doesn't update the background
self.config(bg=color)
# doesn't update the Label text
self.label.config(text='Changed Label')
# print the correct changed value = 'Changed Label'
print(self.label.cget('text'))
Thanks ...
Note that you have created another instance of DetailFrame (self.detailframe) inside ProductWindow but it is not visible since no layout function is called on it. Actually there is already an instance of DetailFrame created when creating instance of EditProductFrame, so you need to call change_bg() on this instance instead:
class ProductWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry("600x500")
container = tk.Frame(self)
container.pack(side='top', fill='both', expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.notebook = ttk.Notebook(container)
self.notebook.grid(row=0, column=0, sticky='nsew')
self.new_product_tab = tk.Frame(self.notebook, bg='#233223' )
self.list_product_tab = tk.Frame(self.notebook, bg='#323232')
self.edit_product_tab = tk.Frame(self.notebook, bg='#433434')
# Adding Tabs to Notebook
self.notebook.add(self.new_product_tab, text=" Add New Product ")
self.notebook.add(self.list_product_tab, text=" List All Product ")
self.notebook.add(self.edit_product_tab, text=" Edit Product ")
# -- there is an instance of DetailFrame created inside EditProductFrame
self.productframe = EditProductFrame(self.edit_product_tab)
# -- so don't create another instance of DetailFrame
#self.detailframe = DetailFrame(self.productframe)
button = tk.Button(self.list_product_tab, text="Change background in Edit Form", command=self.change_method)
button.pack()
def change_method(self):
print("Trying to change the frame")
self.productframe.raise_edit_frame(DetailFrame)
#self.detailframe.change_bg('green')
# -- call change_bg() on the instance of DetailFrame inside EditProductFrame
self.productframe.frames[DetailFrame].change_bg('green')
self.notebook.select(self.edit_product_tab)
Another option is to make self.detailframe the reference to the instance of EditFrame inside EditProductFrame:
self.detailframe = self.productframe.frames[DetailFrame]
I want to create a window that allows entering one-to-many fields for the file transfer.
I created a Scrollable Frame and I am adding Entry-Text pairs in runtime. If I click the button for the first time, everything goes well. After the second time, nothing happens on the UI side. It works perfectly after the second click. But I saw that all pairs added successfully, just the UI did not display it. Does anybody know how to fix it?
import tkinter as tk
class VerticalScroolFrame(tk.Frame):
"""A frame with a vertical scroolbar"""
def __init__(self, master, *args, **kwargs):
main_frame = tk.Frame(master)
main_frame.grid()
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
self._canvas = tk.Canvas(main_frame)
self._canvas.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar = tk.Scrollbar(main_frame, command=self._canvas.yview)
scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W)
self._canvas.configure(yscrollcommand=scrollbar.set)
self._canvas.bind('<Configure>', lambda *_: self.on_configure())
super().__init__(self._canvas, *args, **kwargs)
self._canvas.create_window((0, 0), window=self, anchor=tk.NW)
def on_configure(self):
"""update scrollregion after starting 'mainloop'
when all widgets are in self._canvas. And also need to be triggered
whenever a widget added as a child.
"""
self._canvas.configure(scrollregion=self._canvas.bbox('all'))
class AdvancedTransfer:
"""Opens a window that allows you to enter source file list
and targets for them. One-to-many relation.
"""
def __init__(self, root):
self._scroolable_frame = VerticalScroolFrame(root)
self._entry_text_dict = {}
self._button = tk.Button(root, text="Add", command=self.add_item)
self._button.grid()
def add_item(self):
"""Add entry-text widget group"""
row = len(self._entry_text_dict)
entry = tk.Entry(self._scroolable_frame)
entry.insert(0, "row number: {0}".format(row))
entry.grid(row=row, column=0)
text = tk.Text(self._scroolable_frame)
text.grid(row=row, column=1)
self._entry_text_dict[entry] = text
self._scroolable_frame.on_configure()
def main():
root = tk.Tk()
main_frame = tk.Frame(root)
main_frame.grid()
AdvancedTransfer(main_frame)
root.mainloop()
if __name__ == "__main__":
main()
Repro steps:
Run the code below.
Click the button two times.
You should see 2 pairs but only 1 pair shown instead.
It is because you bind <Configure> event on wrong widget. You should bind on the internal frame (i.e. instance of VerticalScroolFrame) instead of canvas (self._canvas):
class VerticalScroolFrame(tk.Frame):
"""A frame with a vertical scroolbar"""
def __init__(self, master, *args, **kwargs):
main_frame = tk.Frame(master)
main_frame.grid()
main_frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
self._canvas = tk.Canvas(main_frame)
self._canvas.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar = tk.Scrollbar(main_frame, command=self._canvas.yview)
scrollbar.grid(row=0, column=1, sticky=tk.N+tk.S+tk.W)
self._canvas.configure(yscrollcommand=scrollbar.set)
#self._canvas.bind('<Configure>', lambda *_: self.on_configure())
super().__init__(self._canvas, *args, **kwargs)
self._canvas.create_window((0, 0), window=self, anchor=tk.NW)
# bind <Configure> event on itself
self.bind('<Configure>', lambda _: self.on_configure())
I just started using python and I am trying to make a simple gui that consists of 3 frames. One on the left, one on the right and one in the middle. Later I'd like to add buttons and stuff to those frames but for now that is all. I want the left frame to disappear or appear again if I press the escape key. To do this I have written the following code:
from tkinter import Tk, Button, Label, Frame
class Main:
def __init__(self):
self.root = Tk()
self.init_gui()
def init_gui(self):
self.root.title("Gui Testing")
self.root.minsize(900,600)
self.root.bind("<Escape>", self.toggle_left_menu)
self.root.grid_columnconfigure(0, minsize=200)
self.root.grid_columnconfigure(1, weight=1)
self.root.grid_columnconfigure(2, minsize=250)
self.root.grid_rowconfigure(0, weight=1)
# main 3 panels
self.left_menu_active = True
self.left_menu = Frame(self.root, bg="#333")
self.left_menu.grid(row=0, column=0, sticky="nsew")
self.center = Frame(self.root, bg="white")
self.center.grid(row=0, column=1, sticky="nsew")
self.right_menu = Frame(self.root, bg="#888")
self.right_menu.grid(row=0, column=2, sticky="nsew")
self.toggle_left_menu()
def toggle_left_menu(self, event=None):
if self.left_menu_active == True:
self.left_menu_active = False
self.root.grid_columnconfigure(0, minsize=0)
self.left_menu.grid_forget()
else:
self.left_menu_active = True
self.root.grid_columnconfigure(0, minsize=200)
self.left_menu.grid(row=0, column=0, sticky="nsew")
def start(self):
self.root.mainloop()
Main().start()
The problem is that when I press escape, nothing happens. However, when I then move the window by clicking on it and dragging it, it updates all of a sudden and it shows the window the way I want it. So the code seems to be working but the window isn't updating for some reason.
I don't know what I can do about that. I found out that it does update the grid positions of the left and the center frame, but the grid_configure() doesn't seem to update without moving the window.
Is there a way to update the frame or to achieve the frame toggling in some other way?
Edit:
The problem has been solved by adding a button to each frame. Now the frames are not empty anymore it seems works. I also edited the toggle_left_menu() function a bit. This is what I changed:
Added Buttons:
self.test_button1 = Button(self.left_menu, text="left", padx=10, pady=5)
self.test_button1.grid(row=0, column=0)
self.test_button2 = Button(self.center, text="center", padx=10, pady=5)
self.test_button2.grid(row=0, column=0)
self.test_button3 = Button(self.right_menu, text="right", padx=10, pady=5)
self.test_button3.grid(row=0, column=0)
Edited toggle_left_menu():
def toggle_left_menu(self, event=None):
if self.left_menu.winfo_viewable():
self.root.grid_columnconfigure(0, minsize=0)
self.left_menu.grid_remove()
else:
self.root.grid_columnconfigure(0, minsize=200)
self.left_menu.grid()
This worked for me, thanks!
Extending Bryan Oakley's example ~ you have to toggle the minsize, as well. As an aside, I classed out all of your gui and made Main the root. All that self.root.this and self.root.that is unnecessary this way. Also, you would have had to do this anyway, unless you intended to dump your entire gui contents into your init_gui method. If your app is large that would be a nightmare to keep track of. As an added bonus, I made the whole toggle_menu method dynamic so it can toggle either menu. You can change the key-bindings, to whatever. I used Escape then l for left_menu and Escape then r for right_menu.
from tkinter import Tk, Button, Label, Frame
class LeftMenu(Frame):
#property
def minsize(self):
return 200
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class RightMenu(Frame):
#property
def minsize(self):
return 250
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class Center(Frame):
def __init__(self, master, row=0, column=0, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid(row=row, column=column, sticky='nswe')
class Main(Tk):
def __init__(self):
Tk.__init__(self)
self.bind("<Escape><l>", self.toggle_menu)
self.bind("<Escape><r>", self.toggle_menu)
self.grid_columnconfigure(0, minsize=200)
self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure(2, minsize=250)
self.grid_rowconfigure(0, weight=1)
# main 3 panels
self.left_menu = LeftMenu(self, 0, 0, bg="#333")
self.center = Center(self, 0, 1, bg="white")
self.right_menu = RightMenu(self, 0, 2, bg="#888")
self.toggle_menu(menu=self.left_menu)
def toggle_menu(self, event=None, menu=None):
if event and event.char in 'lr':
menu = self.left_menu if event.char == 'l' else self.right_menu
if menu:
if menu.winfo_viewable():
self.grid_columnconfigure(menu.grid_info()['column'], minsize=0)
menu.grid_remove()
else:
menu.grid()
self.grid_columnconfigure(menu.grid_info()['column'], minsize=menu.minsize)
self.after_idle(self.event_generate, '<Configure>')
if __name__ == "__main__":
root = Main()
root.title("Gui Testing")
root.minsize(900,600)
root.mainloop()
Part of the problem is that minsize only affects the minimum size. If the left frame is visible and is more than zero pixels wide, setting the minsize to zero isn't going to make the frame smaller. So, one step is to remove the minsize=200 option for column 0.
Since you are using grid, the best way to hide or show a frame is to use grid_remove to remove the widget, and then grid to restore it. grid_remove will remove the widget from the window but remember all of its settings. When you subsequently call .grid(), all of the previous settings will be used.
You can also just check if the window is visible or not without having to manage a boolean flag since your function is a toggle. That simplifies the code a bit.
Also, I think there's a bug on some versions of tk (the library upon which tkinter is built) that prevents the window from refreshing in this specific type of situation. What works for me is to synthetically generate a <Configure> event on the root window.
Rolling all of that together, this version of your toggle function works for me on OSX without any other modifications to your code.
def toggle_left_menu(self, event=None):
if self.left_menu.winfo_viewable():
self.left_menu.grid_remove()
self.root.after_idle(self.root.event_generate, '<Configure>')
else:
self.left_menu.grid()
self.root.after_idle(self.root.event_generate, '<Configure>')
I'm struggling with tkraise not hiding the 'bottom' frame in my app.
I have two frames, one contains a Listbox and is packed to the left and the other will display options for each item in the listbox and is packed to the right.
My problem is that I can see the Future page when I select General and vise versa. I copied and modified it from my working main app but I don't know what I did wrong to break it for this one.
# All settings windows and forms labels are built in here
import tkinter as tk
# from main import WinSize
from tkinter import Listbox, END, ttk
class Settings(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
# create frame for listbox and parameters area
self.list_container = ttk.Frame(self, relief='sunken')
self.list_container.pack(side='left', fill='y', expand=False)
self.param_container = ttk.Frame(self)
self.param_container.pack(side='top', fill='both', expand=True)
self.options_list = Listbox(self.list_container, selectmode='single')
for choice in ['General', 'Future']:
self.options_list.insert(END, choice)
self.okbutton = ttk.Button(self.param_container, text="OK", command= self.destroy)
self.okbutton.grid_rowconfigure(0, weight=1)
self.okbutton.grid_columnconfigure(0, weight=1)
self.okbutton.grid(row=2, column=2, sticky='nsew')
# Grid layout for Settings window
self.options_list.grid(row=0, column=0, sticky='nsew')
self.list_container.grid_rowconfigure(1, weight=1)
self.list_container.grid_columnconfigure(1, weight=1)
self.param_container.grid_rowconfigure(1, weight=1)
self.param_container.grid_columnconfigure(1, weight=1)
# create empty TUPLE for future frames
self.frames = {}
# generate calls for frames
for F in (General, Future):
self.page_name = F.__name__
self.frame = F(parent=self.param_container, controller=self)
self.frames[self.page_name] = self.frame
self.options_list.select_set(0)
self.options_list.bind("<<ListboxSelect>>", self.onselect)
self.options_list.event_generate("<<ListboxSelect>>")
# grab value of listbox selection and call show_frame
def onselect(self, event):
self.widget = event.widget
self.value = self.widget.get(self.widget.curselection())
print(self.value)
self.show_frame(self.value)
# show corresponding frame based on listbox selection
def show_frame(self, page_name):
# show a frame for the given page name
self.frame = self.frames[page_name]
self.frame.grid(row=0, column=1, sticky='nsew')
self.frame.tkraise()
print("Show Frame")
class General(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.optiontitle = ttk.Label(parent, text='General')
self.optiontitle.grid(row=0, column=0, sticky='nsew')
self.dirlabel = ttk.Label(parent, text='Default Save Directory')
self.dirlabel.grid(row=1, column=1, sticky='s')
class Future(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
test1 = ttk.Label(self, text='Future')
test1.pack()
app=Settings()
app.mainloop()
I want to say it may be something to do with my grid layout but it doesn't make sense since the two 'pages' are not coupled (or supposed to be) with each other.
I solved this issue through another problem that I was able to work out with some help from others. Refer to this → Frames not stacking on top of each other in tkinter question as it has two great answers which easily allow to incorporate grid_forget() and/or pack_forget() as suggested by #jasonharper.
You aren't doing anything to actually hide the other page; you're just layering the new page on top of it, which isn't going to look right unless they occupy exactly the same screen area.
Even if they did, this still isn't a workable approach, since the widgets in the hidden page are still active. In particular, if it had any Entry or Text fields, they could still have (or gain) keyboard focus, so anything the user types might mysteriously end up in a field they can't even see at the moment.
You should call .grid_forget() on the previous page when showing the new one. Or, perhaps easier, call .grid_forget() on all pages before calling .grid() on the new one (it doesn't hurt to call this on a widget that isn't currently shown).
When implementing complex dialogs (i.e. dialogs with some 10 or more widgets, especially when arranged within multiple frames or the like), the creation requires many tkinter calls and the code can become increasingly complex (difficult to read and maintain) when it is kept within a single method. Also in general, short functions/methods are usually preferred over longer ones.
My current approach to limit method length is to encapsulate creation of all widgets that belong to a group within the dialog into one method(parent_frame, other_options) that returns the top-level widget like this:
import tkinter as tk
class Dialog:
def __init__(self, master):
self.__master = master
self.create_gui(master)
def create_gui(self, frame, title = None):
if title: frame.title(title)
group_a = self.create_group_a(frame)
group_a.grid(row=0, column=0, sticky="nsew")
group_b = self.create_group_b(frame)
group_b.grid(row=1, column=0, sticky="nsew")
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)
def create_group_a(self, frame):
inner_frame = tk.LabelFrame(frame, text="Label")
text = self.create_text_with_scrollbar(inner_frame)
text.pack(fill="both")
return inner_frame
def create_group_b(self, frame):
button = tk.Button(frame, text="Button")
return button
def create_text_with_scrollbar(self, frame):
text_frame = tk.Frame(frame)
text_frame.grid_rowconfigure(0, weight=1)
text_frame.grid_columnconfigure(0, weight=1)
text = tk.Text(text_frame)
text.grid(row=0, column=0, sticky="nsew")
scrollbar = tk.Scrollbar(text_frame, command=text.yview)
scrollbar.grid(row=0, column=1, sticky="nsew")
text['yscrollcommand'] = scrollbar.set
return text_frame
if __name__ == "__main__":
master = tk.Tk()
Dialog(master)
tk.mainloop()
Are there any specific guidelines around on code structuring in such cases? Does anybody have any advice on how to better structure such code?
What I usually do is to write a new class for every group.
Those classes inherit from Frame.
The end result will look something like this:
class MainFrame(Frame):
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
self.first_sub = FirstSubFrame(self)
self.second_sub = SecondSubFrame(self)
self.first_sub.grid()
self.second_sub.grid()
class FirstSubFrame(Frame):
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
self.possibly_another_subframe = PossibleOtherFrame(self)
self.awesome_button = tkinter.Button()
self.possibly_another_subframe.grid()
self.awesome_button.grid()
...
I hope this helps.