3

I would like to have my scrollbar in the bottom of the frame and my text widgets filling the whole frame above the scrollbar. I found some solution about the width configuration here but when I try to replace width with height, it does not work correctly.

from tkinter import *
from tkinter import ttk

class MainView(Frame):

    def FrameHeight(self, event):
        canvas_height = event.height
        self.canvas.itemconfig(self.canvas_frame, height=canvas_height)

    def OnFrameConfigure(self, event):
        self.canvas.config(scrollregion=self.canvas.bbox("all"))

    def __init__(self, *args, **kwargs):
        Frame.__init__(self, *args, **kwargs)
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        sensorsFrame = Frame(self)
        sensorsFrame.grid(row=0, sticky="nsew")
        sensorsFrame.grid_columnconfigure(0, weight=1)
        sensorsFrame.grid_rowconfigure(0, weight=1)

        self.canvas = Canvas(sensorsFrame)
        self.sensorsStatsFrame = Frame(self.canvas)
        self.canvas.grid_rowconfigure(0, weight=1)
        self.sensorsStatsFrame.grid_rowconfigure(0, weight=1)

        myscrollbar = Scrollbar(sensorsFrame,orient=HORIZONTAL,command=self.canvas.xview)
        self.canvas.configure(xscrollcommand=myscrollbar.set)
        self.canvas.pack(fill=BOTH, expand=1)
        myscrollbar.pack(fill=X, expand=1)

        test0 = Text(self.sensorsStatsFrame, state=DISABLED)
        test1 = Text(self.sensorsStatsFrame, width=150)
        test0.grid(column=0, row=0, sticky="nsew")
        test1.grid(column=1, row=0, sticky="nsew")

        self.canvas_frame = self.canvas.create_window((0,0),window=self.sensorsStatsFrame,anchor='nw')
        self.sensorsStatsFrame.bind("<Configure>", self.OnFrameConfigure)

        #When I try to use what i found
        #self.canvas.bind('<Configure>', self.FrameHeight)

if __name__ == "__main__":
    root = Tk()
    main = MainView(root)
    main.pack(fill="both", expand=1)
    root.wm_geometry("1100x500")
    root.wm_title("MongoDB Timed Sample Generator")
    root.mainloop()
Clément
  • 1,128
  • 7
  • 21
  • `width = canvas_height` can't possibly be correct... – jasonharper Jun 19 '17 at 13:46
  • Yes, I posted a wrong version of my code. On the current one, there is no longer this mistake. However, it doesn't change anything when I switch to "height". EDIT – Clément Jun 19 '17 at 13:50
  • Just wondering. Why are you using canvas instead of just placing a text widget in a frame? – Mike - SMT Jun 19 '17 at 13:55
  • Because in my final app, there will be many inline text widgets and they can't be shown all together without scrollbar on frame. And the only solution I found to attach a scrollbar to a frame was to use a canvas. – Clément Jun 19 '17 at 14:00
  • can you elaborate on `" they can't be shown all together without scrollbar on frame"` Because I am still convinced what you are trying to do is not possible with just frames and text widgets. – Mike - SMT Jun 19 '17 at 14:09
  • @Sierra Mountain Tech Changing pack layout to grid layout for self.canvas and myscrollbar made it work. I put my code as answer of this question. If you run it I think you will understand what I wanted to do. – Clément Jun 19 '17 at 14:12
  • @SierraMountainTech I think you thought I wanted to add a scrollbar at the bottom of a text Frame. It could be good but I want to display several columns of information and I don't want to be bothered by formatting my text to be displayed in several columns. This is why I use one Text widget by "column" of information – Clément Jun 19 '17 at 14:32

2 Answers2

6

Step 1: Remove space above and below the scrollbar

The expand option determines how tkinter handles unallocated space. Extra space will be evenly allocated to all widgets where the value is 1 or True. Because it's set to 1 for the scrollbar, it is given some of the extra space, causing the padding above and below the widget.

What you want instead is for all of the space to be allocated only to the canvas. Do this by setting expand to zero on the scrollbar:

myscrollbar.pack(fill=X, expand=0)

Step 2: call a function when the canvas changes size

The next problem is that you want the inner frame to grow when the canvas changes size, so you need to bind to the <Configure> event of the canvas.

def OnCanvasConfigure(self, event):
    <code to set the size of the inner frame>
...
self.canvas.bind("<Configure>", self.OnCanvasConfigure)

Step 3: let the canvas control the size of the inner frame

You can't just change the size of the inner frame in OnCanvasConfigure, because the default behavior of a frame is to shrink to fit its contents. In this case you want the contents to expand to fit the frame rather than the frame shrink to fit the contents.

There are a couple ways you can fix this. You can turn geometry propagation off for the inner frame, which will prevent the inner widgets from changing the size of the frame. Or, you can let the canvas force the size of the frame.

The second solution is the easiest. All we have to do is use the height of the canvas for the frame height, and the sum of the widths of the inner text widgets for the frame width.

def OnCanvasConfigure(self, event):
    width = 0
    for child in self.sensorsStatsFrame.grid_slaves():
        width += child.winfo_reqwidth()

    self.canvas.itemconfigure(self.canvas_frame, width=width, height=event.height)

Step 4: fix the scrollbar

There's still one more problem to solve. If you resize the window you'll notice that tkinter will chop off the scrollbar if the window gets too small. You can solve this by removing the ability to resize the window but your users will hate that.

A better solution is to cause the text widgets to shrink before the scrollbar is chopped off. You control this by the order in which you call pack.

When there isn't enough room to fit all of the widgets, tkinter will start reducing the size of widgets, starting with the last widget added to the window. In your code the scrollbar is the last widget, but if instead you make it the canvas, the scrollbar will remain untouched and the canvas will shrink instead (which in turn causes the frame to shrink, which causes the text widgets to shrink).

myscrollbar.pack(side="bottom", fill=X, expand=0)
self.canvas.pack(fill=BOTH, expand=1)
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
1

Changing pack layout to grid layout for self.canvas and myscrollbar makes it work.

from tkinter import *
from tkinter import ttk

class MainView(Frame):

    def FrameHeight(self, event):
        canvas_height = event.height
        self.canvas.itemconfig(self.canvas_frame, height = canvas_height)

    def OnFrameConfigure(self, event):
        self.canvas.config(scrollregion=self.canvas.bbox("all"))

    def __init__(self, *args, **kwargs):
        Frame.__init__(self, *args, **kwargs)
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        sensorsFrame = Frame(self)
        sensorsFrame.grid(row=0, sticky="nsew")
        sensorsFrame.grid_rowconfigure(0, weight=1)
        sensorsFrame.grid_columnconfigure(0, weight=1)


        self.canvas = Canvas(sensorsFrame, bg="blue")
        self.sensorsStatsFrame = Frame(self.canvas)
        self.canvas.grid_rowconfigure(0, weight=1)
        self.sensorsStatsFrame.grid_rowconfigure(0, weight=1)

        myscrollbar = Scrollbar(sensorsFrame,orient=HORIZONTAL,command=self.canvas.xview)
        self.canvas.configure(xscrollcommand=myscrollbar.set)
        self.canvas.grid(row=0, sticky="nsew")
        myscrollbar.grid(row=1, sticky="nsew")

        test0 = Text(self.sensorsStatsFrame, state=DISABLED, bg="red")
        test1 = Text(self.sensorsStatsFrame, width=150)
        test0.grid(column=0, row=0, sticky="nsew")
        test1.grid(column=1, row=0, sticky="nsew")

        self.canvas_frame = self.canvas.create_window((0,0),window=self.sensorsStatsFrame,anchor='nw')
        self.sensorsStatsFrame.bind("<Configure>", self.OnFrameConfigure)
        self.canvas.bind('<Configure>', self.FrameHeight)

if __name__ == "__main__":
    root = Tk()
    main = MainView(root)
    main.pack(fill="both", expand=1)
    root.wm_geometry("1100x500")
    root.wm_title("MongoDB Timed Sample Generator")
    root.mainloop()
Clément
  • 1,128
  • 7
  • 21
  • Makes sense. You just wanted to be able to scroll all the widgets packed inside the frame. And the only way to do that was to place the frame on a canvas and scroll said canvas. Not sure why I was so confused. I guess I have not woke up yet XD – Mike - SMT Jun 19 '17 at 14:30