I have a small python program to build a GUI. I'm trying to use a text widget to create an easily scrollable window that contains vertically stacked frames. A frame is created on button press and added to the bottom of the text widget. This works fine; however, I'm struggling to get these frames to stretch to fill the text box horizontally.
import Tkinter as tk
class NewEntry(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
#self.pack(fill="x", expand=True) #possible error source
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
textField = tk.Entry(self)
textField.grid(row=1, column=0, padx=2, pady=1, sticky="ew")
addButton = tk.Button(self, text="Add", cursor="arrow")
addButton.grid(row=1, column=1, padx=10, pady=2, sticky="ew")
newLabel = tk.Label(self, text="Test", bg="#5522FF")
newLabel.grid(row=0, padx=2, pady=2, sticky="ew", columnspan=2)
newLabel.columnconfigure(0, weight=1)
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent)
self.parent=parent
self.grid()
self.columnconfigure(0, weight=1)
self.grid_rowconfigure(0,weight=1)
self.text = tk.Text(self, wrap="none", bg="#AA3333")
vsb = tk.Scrollbar(orient="vertical", command=self.text.yview)
self.text.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.text.pack(fill="both", expand=True)
b = tk.Button(self, text="Button #%s" % 1, command=self.OnButtonClick)
self.text.window_create("end", window=b)
self.text.insert("end", "\n")
def OnButtonClick(self):
self.text.configure(state="normal")
panel = NewEntry(self.text, bg="#FF1111")
self.text.window_create("end", window=panel)
self.text.insert("end", "\n")
self.text.configure(state="disabled")
if __name__=="__main__":
root = tk.Tk()
root.resizable(True, True)
appinstance=MainApplication(root)
appinstance.pack(fill="both", expand=True)
root.mainloop()
I've read many different posts talking about grid_columnconfigure, fill options, sticky options, etc, but I haven't been able to get it filling properly. I am wondering if the window_create() method of the Text widget creates some sort of size limitation? It seems as though my code in NewEntry class is properly filling the space allowed by the window_create method, but I don't know how to create a "panel" to fill the width of the text box.
I am also aware of the possibility of using a canvas instead of text box (I'm wanting to maintain dynamic size and scrollability, though). I read from several different posts that a text widget is easiest if you have a simple stack of widgets, though. I will accept any recommendation, though.
The root of the problem is that a frame typically wants to fit its contents. So, when you add the label, entry widget, and button to the frame, it will shrink to fit. Any size you give to the frame will be ignored.
There are several solutions to this problem. What many people do is turn geometry propagation off (eg: self.grid_propagate(False)) but that means you have to manage both the width and height when all you really want is to control the width.
Another solution is to put something inside the panel that can be configured with an explicit width, and which will then cause the containing frame to grow to fit. For example, you can add an invisible frame in row 0 that sits behind the other widgets in row 0. When you change the width of this frame it will cause the containing frame to grow:
class NewEntry(tk.Frame):
def __init__(...):
...
# create this _before_ creating the other widgets
# so it is lowest in the stacking order
self.sizer = tk.Frame(self, height=1)
self.sizer.grid(row=0, columnspan=2)
...
With that, you can force the panel to be any width you want by setting the width of the sizer, but tkinter will continue to compute the optimum height for the widget:
self.sizer.configure(width=200)
Now you just need to set up bindings so that whenever the text widget changes size, you call this function to resize each entry.
For example, you might want to save all of the panels in a list so that you can iterate over them later.
class MainApplication(...):
def __init__(...):
...
self.panels = []
...
def OnButtonClick(...):
...
panel = NewEntry(...)
self.panels.append(panel)
...
With that, you can set up a binding that triggers whenever the window resizes:
class MainApplication(...):
def __init__(...):
...
self.text.bind("<Configure>", self.OnConfigure)
...
def OnConfigure(self, event):
for panel in self.panels:
panel.sizer.configure(width=event.width)
I wouldn't do it precisely like that since it tightly couples the implementation of the panel to the main window, but it illustrates the general technique of explicitly controlling the width of an embedded window.
Other solutions involve putting the panels inside a containing frame, and make that frame be the only widget added to the text widget. You could also use a canvas instead of a text widget since it allows you to explicitly set the width and height of embedded windows.
Related
I have a notebook that I played in grid (0;0). I want to the notebok to fill the entire screen even if its content (frames) would not fill the screen.
class App(Tk):
def __init__(self) -> None:
super().__init__()
# Widgets
self.notebook = Notebook(self) # <-- Widget I want to fill the window
self.notebook.grid(row=0, column=0)
self.control_frame = ControlFrame(self)
self.notebook.add(self.control_frame, text="Control")
class ControlFrame(Frame):
def __init__(self, master):
super().__init__(master)
self.control_bar = ControlBar(self)
self.control_bar.grid(row=0, column=0)
self.connect_btn = Button(self, text="Connect")
self.connect_btn.grid(row=1, column=0)
self.log = Text(self, width=100)
self.log.grid(row=2, column=0)
I think you need to use the sticky argument in you grid:
The width of a column (and height of a row) depends on all the widgets
contained in it. That means some widgets could be smaller than the
cells they are placed in. If so, where exactly should they be put
within their cells?
By default, if a cell is larger than the widget contained in it, the
widget will be centered within it, both horizontally and vertically.
The master's background color will display in the empty space around
the widget. In the figure below, the widget in the top right is
smaller than the cell allocated to it. The (white) background of the
master fills the rest of the cell.
The sticky option can change this default behavior. Its value is a
string of 0 or more of the compass directions nsew, specifying which
edges of the cell the widget should be "stuck" to. For example, a
value of n (north) will jam the widget up against the top side, with
any extra vertical space on the bottom; the widget will still be
centered horizontally. A value of nw (north-west) means the widget
will be stuck to the top left corner, with extra space on the bottom
and right.
So your code should look something like
self.notebook.grid(row=0, column=0, sticky='NSWE')
If you need more information check out this article
EDIT:
Thanks to #acw1668 for the hint!
I think the behaviour of the ttk.Notebook class is strange with the grid layout.
I manage to "solve" it using pack in the App() class.
Here is the code that worked for me:
class App(Tk):
def __init__(self) -> None:
super().__init__()
self.notebook = Notebook(self) # <-- Widget I want to fill the window
self.control_frame = ControlFrame(self.notebook)
self.notebook.add(self.control_frame, text="Control", sticky="NSWE")
self.notebook.pack(expand=True, fill=BOTH)
class ControlFrame(Frame):
def __init__(self, master):
super().__init__(master)
self.control_bar = ControlBar(self)
self.control_bar.grid(row=0, column=0)
self.grid(column=0, row=0, sticky="SNWE")
self.connect_btn = Button(self, text="Connect")
self.connect_btn.grid(row=1, column=0, sticky=N)
self.log = Text(self)
self.log.grid(row=2, column=0, sticky="NSWE")
self.grid_rowconfigure(0, weight=0)
self.grid_rowconfigure(1, weight=0)
self.grid_rowconfigure(2, weight=1)
self.grid_columnconfigure(0, weight=1)
Hope this solves your problem
I'm making a text game. I used an entry widget for player input, a text widget for the game's output, and put them into frames. I set the root window's geometry and the frame sizes to fit into that geometry. However, the frame sizes are smaller than expected. Specifically, my story_text_frame is shorter than expected. I have done a tutorial, and am not sure what I am missing now.
import tkinter as tk
class Game(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
self.geometry('1280x720')
self.player_input_frame = tk.Frame(self, height=20, width=625)
self.player_input_field = tk.Entry(self.player_input_frame, background='black', foreground='white', relief='flat')
self.player_input_field.grid(row=0, column=0)
self.player_input_frame.grid(row=2, column=1)
self.story_text_frame = tk.Frame(self, height=670, width=625)
self.story_text_field = tk.Text(self.story_text_frame, background='grey', foreground='white')
self.story_text_field.grid(row=0, column=0)
self.story_text_frame.grid(row=1, column=1)
To have a widget size follow the size of a master widget or a window you must specify how this is to be done with columnconfigure() and rowconfigure(). Then you must expand the widget to fill the available cell space by instructing grid() to stick to the edges; sticky='nsew'. See my example:
import tkinter as tk
class Game(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
self.geometry('800x600')
self.columnconfigure(0, weight=1) # Specify how columns and rows will
self.rowconfigure(0, weight=1) # change when window size changes.
# The game's output Text() widget
self.story_text_field = tk.Text(self)
self.story_text_field.grid(row=0, column=0, sticky='nsew',
pady=10, padx=10) # Sticky expands the widget to fill the cell
# The player input Entry() widget
self.player_input_field = tk.Entry(self)
self.player_input_field.grid(row=1, column=0, pady=(0,10))
Game().mainloop()
I have removed the Frames to make the construction clearer. Also I removed the coloring of the widgets. I introduced some padding to make the result more pleasing to the eye and changed the window size to be easier to handle on my tiny screen.
For further information on grid() I can recommend effbot's The Tkinter Grid Geometry Manager. You can also read Mike - SMT's answer to Find position of another anchor than the anchor already used which elaborates on both grid() and pack().
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).
I'm using tkinter with Python 3.4 in Windows 7.
I'm positioning in a non-absolute way (I'm not using place, I'm using grid), and therefore, the widgets should scale when I resize the window automatically. Nevertheless, that does not happen, and I'm failing to grasp the point. Here's my code:
import tkinter as tk
class App(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.config()
self.grid()
self.create_widgets()
def config(self):
self.master.title("Pykipedia Explorer")
def create_widgets(self):
self.search_label = tk.Label(self, text="Search: ")
self.search_label.grid(row=0, column=0, sticky=tk.N+tk.SW)
self.search_entry = tk.Entry(self)
self.search_entry.grid(row=0, column=0, padx=60, sticky=tk.N+tk.SW)
self.search_button = tk.Button(self, text="Explore!")
self.search_button.grid(row=0, column=0, padx=232, sticky=tk.SW)
self.content_area = tk.Text(self)
self.content_area.grid(row=1, column=0)
self.content_scroll_bar = tk.Scrollbar(self, command=self.content_area.yview)
self.content_scroll_bar.grid(row=1, column=1, sticky=tk.NW+tk.S+tk.W)
self.content_area["yscrollcommand"] = self.content_scroll_bar.set
self.quit_button = tk.Button(self, text="Quit", command=self.quit)
self.quit_button.grid(row=2, column=0, sticky=tk.SW)
def main():
app = App()
app.mainloop()
return 0
if __name__ == '__main__':
main()
Why??
Also, I've tried to use grid_columnconfigure and grid_rowconfigure just like in this answer, and it fails miserably.
You have several problems in your code that are working together to prevent the widgets from scaling (and by "widgets", I assume you mean the text widget).
First, you use grid to put the instance of App in the root window. However, you haven't set the sticky attribute, so the app won't grow and shrink. If it doesn't grow and shrink, neither will its contents.
Also, because you're using grid, you need to give row zero and column zero a positive weight so that tkinter will allocate extra space to it. However, since this is the only widget in the root window, you can use pack and solve the problem by replacing the call to grid with a call to pack:
self.pack(fill="both", expand=True)
Next, you use grid to add the text widget to the canvas. You haven't used the sticky option, so even if the space allocated to it grows, the widget will stay centered in the space. You need to use the sticky attribute to tell it to "stick" to all sides of the area it is given:
self.content_area.grid(row=1, column=0, sticky="nsew")
Finally, you haven't given any columns any "weight", which tells tkinter how to allocate extra space. When a window managed by grid resizes, any extra space is given to the rows and columns according to their weight. By default a row and column has zero weight, so it does not get any extra space.
To get the text area to grow as the window grows, you need to give column zero and row one a positive weight:
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
I have a code, which creates an UI like this.
I want those page up/down buttons to be next to Page 1 label but I couldn't managed to do that. Only way I know with pack is the side option and it is not working well.
Second thing is, that scrollbar should be in that listbox. I know, I need to create a canvas then a frame in it. Then embed both listbox and scrollbar to them but I couldn't do that either.
This is my code.
class interface(tk.Frame):
def __init__(self,den):
self.pa_nu = 0 ##page number. Both used in labeling and result slicing
self.lbl1 = tk.Label(den, text="keyword")
self.lbl2 = tk.Label(den, text="Page %d" %(self.pa_nu+1))
self.ent1 = tk.Entry(den, takefocus=True)
self.btn1 = tk.Button(den, text="Search", command=self.button1)
self.btn2 = tk.Button(den, text="Page Up", command=self.page_up)
self.btn3 = tk.Button(den, text="Page Down", command=self.page_down)
scrollbar = tk.Scrollbar(den)
scrollbar.pack(side=RIGHT, fill=Y)
self.lst1 = tk.Listbox(den, selectmode="SINGLE", width="40", yscrollcommand=scrollbar.set)
self.lst1.bind("<Double-Button-1>", self.open_folder)
scrollbar.config(command=self.lst1.yview)
self.lbl1.pack(side="top")
self.ent1.pack()
self.btn1.pack(side="top")
self.btn2.pack(side="right")
self.btn3.pack(side="left")
self.lbl2.pack(side="bottom",padx=65)
self.lst1.pack(fill=BOTH)
def button1(self):
pass #some stuff here
def page_up(self):
pass #some stuff here
def page_down(self):
pass #some stuff here
def list_fill(self,i):
pass #some stuff here
def open_folder(self,event):
pass #some stuff here
There are three geometry managers in tkinter: place (absolute position), pack (good for line of widgets, or simple layout) and grid (complex layout).
Grid is worth looking for the layout you are working on. If you keep going with pack, the usual way to achieve complex layout is to use intermediate frames. For instance, in the following picture, all widgets in frame1 are packed vertically, and horizontally in frame2.
diagram with draw.io
Regarding the scrollbar, the usual way is again to use an intermediate frame (no need for a canvas). Here a snippet (copied from http://effbot.org/zone/tkinter-scrollbar-patterns.htm#listbox)
frame = Frame(root, bd=2, relief=SUNKEN)
scrollbar = Scrollbar(frame)
scrollbar.pack(side=RIGHT, fill=Y)
listbox = Listbox(frame, bd=0, yscrollcommand=scrollbar.set)
listbox.pack()
scrollbar.config(command=listbox.yview)
frame.pack() #or others...