0
votes

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.

1
Are you sure this is your actual code? I get the error "cannot use geometry manager pack inside ." - Bryan Oakley
I just copied and pasted to verify that I didn't actually make any changes. I am running python 2.7 if that matters. - Tarm
There are very definitely bugs in the code. It seems impossible that this exact code works for you. - Bryan Oakley
I created this in the Anaconda 2 software package - however, I ran from the command line rather than the IDE just to make sure. Not certain if that matters or not. That being said, it is definitely running for me from the command line. Does it give a line where the error is? - Tarm
It IS working for me, strangely. However, I quickly researched the error - try commenting out/removing line 8 - self.pack(fill="x", expand=True). I will edit in my post too. - Tarm

1 Answers

0
votes

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.