Search code examples
pythontkintertreeviewgrid-layout

How to grid a ttk Notebook containing two ttk Treeviews so it expands to fill its designated grid cell?


I'm having trouble laying out two same ttk.Treeview widgets using ttk.Notebook and grid layout manager. When I'm working with only one treeview (placed in a labelframe, that is placed in a frame (section), that is placed in yet another frame (main), that is placed in root), then everything works and I get the desired results:

enter image description here

But as soon as I try putting two such treeviews in a notebook, everything goes haywire, even though I don't change anything in the code, apart from adding a notebook:

enter image description here

Here's the code I prepared according to the SO guidelines to better illustrate the problem (I tried to make it as short as I could while retaining vital characteristics (like the columns layout and scrollbars) of the app it was taken out of) :

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.title("App")

mainframe = ttk.Frame(root)
mainframe.grid()
section = ttk.Frame(mainframe, padding=(0, 0, 10, 60))
section.grid(sticky="nsew")
labelframe = ttk.Labelframe(section, text="Title", padding=(5, 7, 5, 13))
labelframe.grid(sticky="nsew")

# styles
style = ttk.Style()
style.configure(
    "App.Treeview",
    font=("TkDefaultFont", 8),
    rowheight=15
)
style.configure(
    "App.Treeview.Heading",
    font=("TkDefaultFont", 8)
)


def build_tree(parent):

    # treeview
    trv = ttk.Treeview(parent)
    trv.configure(
        style="App.Treeview",
        height=19,
        columns=("#1", "#2", "#3", "#4"),
        displaycolumns="#all",
        selectmode="browse"
    )
    trv.grid(column=0, row=0, sticky="nsew")

    # treeview's scrollbars
    sbar_vertical = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=trv.yview)
    sbar_vertical.grid(column=1, row=0, sticky="ns")
    sbar_horizontal = ttk.Scrollbar(parent, orient=tk.HORIZONTAL, command=trv.xview)
    sbar_horizontal.grid(column=0, row=1, sticky="we")
    trv.configure(yscrollcommand=sbar_vertical.set, xscrollcommand=sbar_horizontal.set)

    # treeview's columns
    trv.heading("#1", text="Name_1")
    trv.heading("#2", text="Pos")
    trv.heading("#3", text="NT/R")
    trv.heading("#4", text="★")
    trv.column("#0", stretch=True, minwidth=70, width=49)
    trv.column("#1", stretch=True, minwidth=97, width=49)
    trv.column("#2", stretch=True, minwidth=35, width=49)
    trv.column("#3", stretch=True, minwidth=40, width=49, anchor=tk.E)
    trv.column("#4", stretch=True, minwidth=12, width=49)

    return trv


# tree = build_tree(labelframe)

# notebook
notebook = ttk.Notebook(labelframe)
tree1 = build_tree(notebook)
tree2 = build_tree(notebook)
notebook.add(tree1, text="One")
notebook.add(tree2, text="Two")
notebook.grid(sticky="nsew")

root.resizable(False, False)
root.mainloop()

To switch between the first result and the second, one has to only uncomment the

# tree = build_tree(labelframe)

line and comment out the # notebook section.

I tried giving the notebook height and weight option arguments in a constructor but nothing changed and also tried with adding

rowconfigure(0, weight=1)
columnconfigure(0, weight=1)

to a container on each level (mainframe, section and labelframe) and even all down to the notebook and the treeviews (even though AFAIK this should only be necessary if the window was to be resizable) - obtaining exactly the same result.

My understanding was if widgets are placed in a grid without any options passed to a grid() call, then a widget placed decides how much space it needs and the grid manager accommodates a given column's width (or a height in case of a row) to the highest value requested by the widget (of all managed in a given column or row). Then if such decided space is too big for some other widget that uses the same row/column, then it gets stretched (or no - according mainly to the sticky option). If that understanding is correct, then I don't quite understand the notebook's behavior, especially as the alone treeview seems to perfectly adhere to it.

My environment: Ubuntu 17.10, Python 3.6.3, Tkinter 8.6. In case the treeview columns' setup looks somewhat peculiar - it was derived from a comment to this question and has to be like that to make use of the horizontal scrollbar.


Solution

  • The problem is that you are trying to add the scrollbars and treeviews to the notebook with grid, and then also adding them with the add method of the notebook. You cannot mix the two. When you create a notebook, you should not directly add widgets to it via pack, place, or grid.

    Instead, each notebook tab should be a single frame. You can then put any other widgets inside that frame with pack, place, or grid. You then add this frame to the notebook with add.

    So, your first step is to modify build_tree to create and return a frame:

    def build_tree(parent):
        frame = ttk.Frame(frame)
        ...
        return frame
    

    Next, you need to make sure that the tree and scrollbars are children of this frame rather than children of the notebook:

    trv = ttk.Treeview(frame)
    sbar_vertical = ttk.Scrollbar(frame, ...)
    sbar_horizontal = ttk.Scrollbar(frame, ...)
    

    As a rule of thumb, whenever you use grid you should always give at least one row and one column a non-zero weight so that they grow and shrink to fill any extra space. So, make sure that the weights of the rows and columns of this frame are set.

    For example:

    def build_tree(parent):
        ...
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)
        ...
    

    That will now make the notebook look and behave properly with respect to the notebook.

    You similar row/column weight issues with all of the other frames, so you'll want to apply the weights to every widget which has children widgets managed by grid.