I'd like to allow the user to configure the appearance of the app (background color, font, font size and color, button color, etc). I've got all of the default settings saved into a config file, which the program interprets and saves into variables within a class for easy access between functions. Now, I can save the changes the user made into the config file and those changes will be reflected when the user closes and reopens the app, but I'd like the changes to be instantaneous, so I tried something like this:
import tkinter as tk
class SetColor:
def __init__(self, color):
self.color = 'green'
current = SetColor('green')
root = tk.Tk()
lbl_color = tk.Label(root, text='Choose button color')
lbl_color.grid(row=0, column=0, pady=5, padx=2)
btn_red = tk.Button(root, text='Red', bg=current.color, command=lambda:update_color('red'))
btn_red.grid(row=0, column=1, pady=5, padx=2)
btn_green = tk.Button(root, text='Green', bg=current.color, command=lambda:update_color('green'))
btn_green.grid(row=0, column=2, pady=5, padx=2)
btn_blue = tk.Button(root, text='Blue', bg=current.color, command=lambda:update_color('blue'))
btn_blue.grid(row=0, column=3, pady=5, padx=2)
def update_color(color):
current.color = color
#THIS is where I'm wondering if there's a way to refresh without individually updating each widget as I've done below
btn_red.config(bg=current.color)
btn_green.config(bg=current.color)
btn_blue.config(bg=current.color)
root.mainloop()
This does work, but in my actual app there are a loooooot more widgets that would need updating than in this^ example. So I have a feeling I'm missing something or going about this in the wrong way. Any help greatly appreciated :)
Your best bet is to store the buttons in a list and loop over that list. This way you can seperate different buttons. But if you are sure you want to change the color of every singlle button you can do: for widget in root.winfo_children():
if isinstance(widget, tk.Button):
widget.config(bg=current.color)
#Maarten's answer is perfect for the tkinter button.
There is another option using ttk.Button can be used in such scenario
create the button object with a custom style
btn_green = ttk.Button(root, text='Green', style="color.TButton", command=lambda: update_color('green'))
create style object
style = ttk.Style()
style.theme_use("default")
set the style
style.configure('color.TButton', background=current.color)
# Activate is when you mouse over the button.
style.map('color.TButton', background=[('active', current.color)])
Full example:
import tkinter as tk
from tkinter import ttk
class SetColor:
def __init__(self, color):
self.color = 'green'
def update_color(color):
current.color = color
# Let's set the style
# naming that style variable as color.TButton
# NOTE: .TButton is important, you can add any other pretix though
style.configure('color.TButton', background=current.color)
# Activate is when you mouse over the button.
style.map('color.TButton', background=[('active', current.color)])
current = SetColor('green')
root = tk.Tk()
# Create style Object
style = ttk.Style()
# Setting theme to default (built in themes can be found https://wiki.tcl-lang.org/page/List+of+ttk+Themes)
style.theme_use("default")
lbl_color = ttk.Label(root, text='Choose button color')
lbl_color.grid(row=0, column=0, pady=5, padx=2)
btn_red = ttk.Button(root, text='Red', style="color.TButton", command=lambda: update_color('red'))
btn_red.grid(row=0, column=1, pady=5, padx=2)
btn_green = ttk.Button(root, text='Green', style="color.TButton", command=lambda: update_color('green'))
btn_green.grid(row=0, column=2, pady=5, padx=2)
btn_blue = ttk.Button(root, text='Blue', style="color.TButton", command=lambda: update_color('blue'))
btn_blue.grid(row=0, column=3, pady=5, padx=2)
update_color(current.color)
root.mainloop()
There are a lot more options with ttk style to play around.
Have a look at
Python ttk Style
ttk Themes
I did this and works perfectly. I hope it works for you.
def groups1(): # This function is to place the widgets for Groups.
# Clean widgets immediately after you call the button inside
# the button function.
for widget in frameleft.winfo_children():
widget.destroy()
groups = tkinter.ttk.Label(frameleft, text='Grupos', font=
('URW Gothic', 20))
groups.place(x=20, y=30)
I'm trying to understand how tk grid layouts work since the interface isn't looking the way I thought it would. I'm trying to place a label followed by 2 buttons on the same row, and a treeview on the next row that spans beyond the width of the label and buttons. The only way to get this to look how I want is if I use a huge value for the treeview's columnspan. Here is my code:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
columnHeadings = ("Heading 1", "Heading 2")
def printMsg():
print("Ok")
frame = ttk.Frame(root).grid(row=0, column=0)
label1 = tk.Label(frame, text="Label here").grid(row=0, column=0, columnspan=1)
button1 = tk.Button(frame, text="Yes", width=2, command=printMsg).grid(row=0, column=1)
button2 = tk.Button(frame, text="No", width=2, command=printMsg).grid(row=0, column=2)
#Label and buttons too far apart
#treeview1 = ttk.Treeview(frame, columns=columnHeadings, show='headings').grid(row=1,column=0, columnspan=3)
#Right distance but that's a huge columnspan
treeview1 = ttk.Treeview(frame, columns=columnHeadings, show='headings').grid(row=1,column=0, columnspan=100)
root.mainloop()
When columnspan is 3, the first row has a lot of spaces between the label and buttons. When columnspan is 100, the label and button spacing looks a lot better but I know this isn't the correct way. What's the correct way to do this?
You have several things conspiring against you in this little program. For one, frame is set to None, so you are actually putting all these widgets in the root window rather than the frame. This is because x=y().z() always sets x to the result of z, and grid() always returns None.
Second, a good rule of thumb for grid is that you need to give at least one (and usually exactly one) row and one column to have a weight, so that tkinter knows how to allocate extra space. You also need to use the sticky option so that your widgets expand to fill the space that's been given them. You are using neither of these techniques.
Third, in my experience I think it makes it very hard to debug layout problems when your layout statements are scattered throughout your code. It's best to group them altogether so you can more easily visualize your layout.
Solving the problem with grid
You can solve your problem by giving column 3 a weight of 1, and then having your treeview span that column. This prevents the columns that your buttons are in from expanding. If you also fix the problem with frame being None, and if you use the appropriate sticky options, you can get the look you want.
Here's an example:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
columnHeadings = ("Heading 1", "Heading 2")
def printMsg():
print("Ok")
frame = tk.Frame(root)
frame.grid(row=0, column=0, sticky="nsew")
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
label1 = tk.Label(frame, text="Label here")
button1 = tk.Button(frame, text="Yes", width=2, command=printMsg)
button2 = tk.Button(frame, text="No", width=2, command=printMsg)
treeview1 = ttk.Treeview(frame, columns=columnHeadings, show='headings')
label1.grid(row=0, column=0, columnspan=1)
button1.grid(row=0, column=1)
button2.grid(row=0, column=2)
treeview1.grid(row=1,column=0, columnspan=4, sticky="nsew")
frame.grid_columnconfigure(3, weight=1)
frame.grid_rowconfigure(1, weight=1)
root.mainloop()
An alternate solution, using pack
All that being said, I think there are better solutions than to try to get everything to fit in a grid. My philosophy is to use the right tool for the job, and in this case the right tool is pack because it excels at stacking things top-to-bottom OR left-to-right (and visa versa).
In your case, you have two distinct regions in your program: a toolbar across the top, and a treeview down below. Since that's all you have, it makes sense to use pack, to place one on top of the other. So I would start by creating two frames and packing them:
toolbar = ttk.Frame(root)
treeframe = ttk.Frame(root)
toolbar.pack(side="top", fill="x")
treeframe.pack(side="bottom", fill="both", expand=True)
Now, anything you put in toolbar will not affect things in treeframe and visa versa. You are free to do whatever you want in each of those frames. As your program grows, you'll find that having distinct regions makes layout problems much easier to solve.
Since the toolbar contains buttons that are left-justified, you can use pack there too. Just make sure their parent is the toolbar frame, and you can pack them like this:
label1 = tk.Label(toolbar, ...)
button1 = tk.Button(toolbar, ...)
button2 = tk.Button(toolbar, ...)
label1.pack(side="left")
button1.pack(side="left")
button2.pack(side="left")
Finally, if you only have a treeview in the bottom (ie: no scrollbars or other widgets), you can use pack. Make sure it's parent is treeframe. You can use grid if you want, but pack is a bit more straight-forward.
treeview1.pack(fill="both", expand=True)
The nice thing about pack is you can truly put everything on one line. You don't have to take the extra step and give a row or column a weight.
Here's a complete example:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
columnHeadings = ("Heading 1", "Heading 2")
def printMsg():
print("Ok")
toolbar = ttk.Frame(root)
treeframe = ttk.Frame(root)
toolbar.pack(side="top", fill="x")
treeframe.pack(side="bottom", fill="both", expand=True)
label1 = tk.Label(toolbar, text="Label here")
button1 = tk.Button(toolbar, text="Yes", width=2, command=printMsg)
button2 = tk.Button(toolbar, text="No", width=2, command=printMsg)
label1.pack(side="left")
button1.pack(side="left")
button2.pack(side="left")
treeview1 = ttk.Treeview(treeframe, columns=columnHeadings, show='headings')
treeview1.pack(side="top", fill="both", expand=True)
root.mainloop()
I want to create tkinter grid layout 5 frames. It looked the way I wanted before replacing Frame content.
self.Frame1 = tk.Frame(self.parent, bg="grey")
with
self.Frame1 = LeftUpperWindow(self.parent)
When I maximize the window I see everything in LeftUpperWindow frame in messed up, layout incorrectly. A picture is worth a thousand words so I have this
and would like to have this
import sys
import os
import Tkinter as tk
import ttk as ttk
class LeftUpperWindow(tk.Frame):
def __init__(self, master=None):
self.parent = master
tk.Frame.__init__(self, self.parent, bg='#ffffff', borderwidth=1, relief="sunken")
self.__create_layout()
def __create_layout(self):
self.parent.grid()
for r in range(3):
self.parent.rowconfigure(r, weight=1)
for c in range(2):
self.parent.columnconfigure(c, weight=1)
self.editArea = tk.Text(self.parent, wrap=tk.NONE, undo=True,
relief=tk.SUNKEN, borderwidth=5, highlightthickness=0, insertbackground="white")
self.editArea.config(font=('courier', 10, 'normal'))
self.editArea.configure(bg="black", fg="white")
self.editArea.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
self.scrollbarY = tk.Scrollbar(self.parent, orient=tk.VERTICAL)
self.scrollbarY.grid(row=0, column=1, sticky=(tk.N, tk.S), rowspan=2)
self.scrollbarX = tk.Scrollbar(self.parent, orient=tk.HORIZONTAL)
self.scrollbarX.grid(row=1, column=0, sticky=(tk.W, tk.E))
self.status = tk.Label(self.parent, text="Status label", bd=1, relief=tk.SUNKEN, anchor=tk.W, bg='lightgray')
self.status.grid(row=2, column=0, sticky=(tk.N, tk.S, tk.W, tk.E), columnspan=2)
def __config_window(self):
pass
def close_quit(self, event=None):
self.parent.destroy()
def dummy_func(self):
print("dummy")
pass
class MainWindow(tk.Frame):
def __init__(self, master=None):
self.parent = master
tk.Frame.__init__(self, self.parent, bg='#ffffff', borderwidth=1, relief="sunken")
self.__create_layout()
self.__create_menu()
self.__config_mainWindow()
def __create_layout(self):
self.parent.grid()
for r in range(6):
self.parent.rowconfigure(r, weight=1)
for c in range(10):
self.parent.columnconfigure(c, weight=1)
self.Frame1 = LeftUpperWindow(self.parent) #tk.Frame(self.parent, bg="grey")
self.Frame1.grid(row=0, column=0, rowspan=4, columnspan=8, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame2 = tk.Frame(self.parent, bg="blue")
self.Frame2.grid(row=0, column=8, rowspan=4, columnspan=2, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame3 = tk.Frame(self.parent, bg="green")
self.Frame3.grid(row=4, column=0, rowspan=2, columnspan=5, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame4 = tk.Frame(self.parent, bg="brown")
self.Frame4.grid(row=4, column=5, rowspan=2, columnspan=5, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame5 = tk.Frame(self.parent, bg="pink")
self.Frame5.grid(row=5, column=0, rowspan=1, columnspan=10, sticky=(tk.N, tk.S, tk.W, tk.E))
def close_quit(self, event=None):
self.parent.destroy()
def __config_mainWindow(self):
self.parent.config(menu=self.menubar)
def __create_menu(self):
self.menubar = tk.Menu(self.parent)
self.filemenu = tk.Menu(self.menubar, tearoff=0)
self.filemenu.add_command(label="Open", command=self.dummy_func)
self.filemenu.add_separator()
self.filemenu.add_command(label="Exit", command=self.close_quit)
self.menubar.add_cascade(label="File", menu=self.filemenu)
helpmenu = tk.Menu(self.menubar, tearoff=0)
helpmenu.add_command(label="About...", command=self.dummy_func)
self.menubar.add_cascade(label="Help", menu=helpmenu)
def dummy_func(self):
print("dummy")
pass
#
# MAIN
#
def main():
root = tk.Tk()
root.title("Frames")
root.geometry("550x300+525+300")
root.configure(background="#808080")
root.option_add("*font", ("Courier New", 9, "normal"))
window = MainWindow(master=root)
root.protocol("WM_DELETE_WINDOW", window.close_quit)
root.mainloop()
if __name__ == '__main__':
main()
Overview
There is no quick fix for your problem. You are doing several things in your code that stack the odds against you, and which makes it really hard to debug your code. A rewrite is the best solution.
Here are things I've found that make the problem more difficult than they need to be, and which can be solved by a rewrite.
First, don't have a widget be responsible for placing itself in its parent. In your case you're going one step further by having a widget place it's parent into its grandparent. That's simply the wrong way to do it. A containing window should be responsible for layout out it's children, and nothing more
Second, put as much of your layout code together as possible. By "all" I mean "all for a given container". So, for example, instead of this:
x=Label(...)
x.grid(...)
y =Label(...)
y.grid(...)
... do it like this:
x = Label(...)
y = Label(...)
x.grid(...)
y.grid(...)
This makes it much easier to visualize your layout in the code.
Third, don't try to solve multiple layout problems at once. Solve one problem at a time. Since you posted your whole program, I assume you have a problem with the whole program and not just the upper-left widgets. When I ran your code I observed resize behavior that seemed off to me, reinforcing that belief.
Fourth, if you create a class like MainWindow that is designed to contain other widgets, all of these other widgets shild be children of the window, not children of the parent.
Fifth, as a general rule of thumb you want only a single row and single column with a container to have a non-zero weight. This is only a guideline, because there are often times when you want more than one widget to expand or contract. In the case of scrollbars and statuslabels, you typically don't want them to be given extra space. Part of the problem in your code is that you give everything a weight, so extra space is allocated to every widget.
The solution
The solution is work on one section at a time. Get it working, then move on to the next section. In your case I see the effort breaking down into four phases. First, get the main window created and laid out. Next, add the widgets that are immediate children of the main window. After that, create the container for the upper-left window. Finally, add the widgets to the upper left window. After each phase, be sure to run the code and verify that everything looks right.
The main window
The first step is to start with the main window. Get it to look right, and behave properly when you resize the main window. When I ran your code the bottom and right windows would vanish when the window became small, and the bottom frames grew when the window was large. I'm guessing that's not the correct behavior. Regardless, start with what you think is the correct behavior and make it work before adding any more widgets.
Start with the following code. Verify that when you resize the window, everything expands and contracts the way you expect. Notice that I made a couple of simple changes. The main thing to notice is that the function that creates the main window is responsible for making that window visible. I use pack since it's the only widget in the root window, and it means I can do it all in one line without having to worry about row and column weights with grid.
import sys
import os
import Tkinter as tk
import ttk as ttk
class MainWindow(tk.Frame):
def __init__(self, master=None):
self.parent = master
tk.Frame.__init__(self, self.parent, bg='bisque', borderwidth=1, relief="sunken")
self.__create_layout()
def __create_layout(self):
pass
#
# MAIN
#
def main():
root = tk.Tk()
root.title("Frames")
root.geometry("550x300+525+300")
root.configure(background="#808080")
root.option_add("*font", ("Courier New", 9, "normal"))
window = MainWindow(master=root)
window.pack(side="top", fill="both", expand=True)
root.mainloop()
if __name__ == '__main__':
main()
The widgets inside of MainWindow
Once you have the MainWindow shell behaving properly, it's time to add more widgets. It looks like you have five frames, though one of them is a custom class. For now we'll use a regular frame to keep things simple. It's good that you give each one a color, that's a great way to visualize the layout as you build up the window.
Modify __create_layout to look like the following. Note two important changes: every widget is a child of self rather than self.parent, and I've grouped all of the layout code together.
The resize behavior looks off to me, but I don't know exactly what you intend. For me it seems odd to have the green, red and pink frames resize the way they do, though maybe that's what you want. Regardless, now is the time to get it right.
def __create_layout(self):
self.Frame1 = tk.Frame(self, bg="grey")
self.Frame2 = tk.Frame(self, bg="blue")
self.Frame3 = tk.Frame(self, bg="green")
self.Frame4 = tk.Frame(self, bg="brown")
self.Frame5 = tk.Frame(self, bg="pink")
self.Frame1.grid(row=0, column=0, rowspan=4, columnspan=8, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame2.grid(row=0, column=8, rowspan=4, columnspan=2, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame3.grid(row=4, column=0, rowspan=2, columnspan=5, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame4.grid(row=4, column=5, rowspan=2, columnspan=5, sticky=(tk.N, tk.S, tk.W, tk.E))
self.Frame5.grid(row=5, column=0, rowspan=1, columnspan=10, sticky=(tk.N, tk.S, tk.W, tk.E))
for r in range(6):
self.rowconfigure(r, weight=1)
for c in range(10):
self.columnconfigure(c, weight=1)
The LeftUpperWindow widget
Next, let's define the LeftUpperWindow class, and make sure we haven't broken anything. Add a stub for the class, give the window a distinct color, and make sure the window still looks ok when it appears and when it is resized.
class LeftUpperWindow(tk.Frame):
def __init__(self, master=None):
self.parent = master
tk.Frame.__init__(self, self.parent, bg='bisque', borderwidth=1, relief="sunken")
self.__create_layout()
def __create_layout(self):
pass
Remember to modify MainWindow.__create_layout to create an instance of this class rather than frame:
self.frame1 = LeftUpperWindow(self)
The widgets in LeftUpperWindow
Finally, it's time to add the widgets in LeftUpperWindow. Like we did in MainWindow, the widgets are all children of self, and the widget creation and widget layout are done in separate groups. Also, it's usually best to only give a single row and a single column a non-zero weight. This is usually the row and column that a text or canvas widget is in.
You can do what you want, of course. Just be mindful of the fact that because you gave each row and column a weight in your original code, that contributed to all of the space between widgets.
Also, because you're giving the GUI an explicit size (via root.geometry(...)), you want to give the text widget a small size. Otherwise, the default width and height will push out the other widgets since tkinter tries to give each widget its default size. Since you're constraining the overall window size you want the text widget to be as small as possible, and then let grid expand it to fill the area it's been given.
Here's what __create_layout should look like:
def __create_layout(self):
self.editArea = tk.Text(self, wrap=tk.NONE, undo=True,
width=1, height=1,
relief=tk.SUNKEN, borderwidth=5,
highlightthickness=0, insertbackground="white")
self.editArea.config(font=('courier', 10, 'normal'))
self.editArea.configure(bg="black", fg="white")
self.scrollbarY = tk.Scrollbar(self, orient=tk.VERTICAL)
self.scrollbarX = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.status = tk.Label(self, text="Status label", bd=1, relief=tk.SUNKEN,
anchor=tk.W, bg='lightgray')
self.editArea.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
self.scrollbarY.grid(row=0, column=1, sticky=(tk.N, tk.S), rowspan=2)
self.scrollbarX.grid(row=1, column=0, sticky=(tk.W, tk.E))
self.status.grid(row=2, column=0, sticky=(tk.N, tk.S, tk.W, tk.E), columnspan=2)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
I am creating a class that derives from tkSimpleDialog.Dialog.
I have implemented the body(self, master) method, where I create some widgets (buttons, labels) and layout them using grid().
I set up the grid layout to stretch, but it never takes the full window. Furthermore, if I resize the window, the widgets will stay in place and not move.
Here's the initial layout:
And here's what happens when I resize the window:
Here's my code:
import Tkinter
import tkSimpleDialog
class Test(tkSimpleDialog.Dialog):
# ########################################
def __init__(self, parent):
tkSimpleDialog.Dialog.__init__(self, parent, "?")
# ########################################
def body(self, master):
Tkinter.Label(master, text="Hello").grid(row=0, column=0, columnspan=2, sticky="nsew")
Tkinter.Button(master, text="ONE").grid(row=1, column=0, sticky="nsew")
Tkinter.Button(master, text="TWO").grid(row=1, column=1, sticky="nsew")
master.columnconfigure(0, weight=1)
master.columnconfigure(1, weight=1)
master.rowconfigure(0, weight=1)
master.rowconfigure(1, weight=1)
self.resizable(height=True, width=True)
if __name__ == "__main__":
Test(Tkinter.Tk("test"))
The desired behavior is: buttons ONE and TWO will stretch to fill the window. Any suggestion?
Thanks!
This appears to be a bug in the Dialog class. In the code where it packs the main part of the dialog it does this:
body.pack(padx=5, pady=5)
Note that it doesn't include an expand or fill attribute, so the body isn't going to grow to fill the extra room in its parent.