0

I have written an application that allows users to select a directory and load the info in the directory. The user can then select which aspects of the files to display in a figure. The figure is placed in a Tkinter-matplotlib canvas which is within a canvas window that the user can scroll. The issue I am having is that the frame (canvas_frame in StartPage) containing the scrollable frame doesn't take the allotted space in the Tkinter window.

The code below replicates the issue and the the image is what the application looks like. Most of the code for the scrollable frame was taken from ex. 1 and ex. 2. An image of the application is here:

https://i.stack.imgur.com/jle7H.jpg

from tkinter import Tk, Frame, Canvas
from tkinter.ttk import Scrollbar
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as FigCanvas
from matplotlib.figure import Figure

class Scrollable(Frame):    
    def __init__(self, frame, fig, width=16):

        # Base class initialization
        Frame.__init__(self, frame)

        # Instance variable for tkinter canvas
        self.tk_cnv = Canvas(frame, highlightthickness=0)
        self.tk_cnv.pack(side='left', fill='both', expand=True)

        # Instance variable for the scroll-bar
        v_scroll = Scrollbar(frame, width=width)
        v_scroll.pack(side="right", fill="y", expand=False)
        v_scroll.config(command=self.tk_cnv.yview)
        v_scroll.activate(" ")

        # Instance variable for the matplotlib canvas
        self.mpl_cnv = FigCanvas(fig, frame)
        self.cnv_widget = self.mpl_cnv.get_tk_widget()

        self.cnv_widget.config(yscrollcommand=v_scroll.set)
        self.cnv_widget.bind("<Configure>", self.__fill_canvas)

        # Assign frame generated by the class to the canvas
        # and create a scrollable window for it.
        self.windows_item = \
            self.tk_cnv.create_window((0, 900), window=self.cnv_widget, anchor='e',
                                      tag='self.canvas')

        self.tk_cnv.config(scrollregion=self.tk_cnv.bbox("all"))

    def __fill_canvas(self, event):
        # Enlarge the windows item to the canvas width
        canvas_width = event.width
        canvas_height = event.height
        self.tk_cnv.itemconfig(self.windows_item, width=canvas_width,
                               height=canvas_height)

class StartPage(Frame):
    """ Tkinter based class for single frame upon which widgets
    such as buttons, check-buttons, and entry are used as a
    simple graphical user interface.
    """
    LARGE_FONT = ("Veranda", 12)

    def __init__(self, parent, controller):
        Frame.__init__(self, parent)

        # Instance variables with page/window info of current frame
        self.window = parent

        # Instance variable for third row of widgets
        self.canvas_frame = Frame(self.window, relief="sunken")
        self.canvas_frame.grid(row=0, column=0, pady=5, sticky="news")

        # Instance variables for the figure
        self.plot_fig = Figure(figsize=[14.0, 18.0])

        # Instance variable for the frame with scrolling functionality
        self.canvas_body = Scrollable(self.canvas_frame, self.plot_fig)
        self.canvas = self.canvas_body.mpl_cnv

        self.canvas_setup()

    def canvas_setup(self):
        self.canvas_frame.grid_rowconfigure(2, weight=1)
        self.canvas_frame.grid_columnconfigure(0, weight=1)

class MainContainer(Tk):
    """ Tkinter based class used to generate a single window
    and contain a single frame. The frame contains multiple
    widgets for user choice and visualization.
    """

    def __init__(self, *args, **kwargs):
        Tk.__init__(self, *args, **kwargs)

        Tk.wm_title(self, "Sequence Viewer")

        Tk.wm_resizable(self, width=True, height=True)

        container = Frame(self)
        container.grid_configure(row=0, column=0, sticky="nsew")
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}

        frame = StartPage(container, self)

        self.frames[StartPage] = frame

        self.show_frame(StartPage)

        self.center_window()

    def show_frame(self, frame_to_add):
        frame = self.frames[frame_to_add]
        frame.tkraise()

    def center_window(self):
        w = 1100
        h = 900
        sw = self.winfo_screenwidth()
        sh = self.winfo_screenheight()
        x = (sw - w) / 2
        y = (sh - h) / 2

        self.geometry('%dx%d+%d+%d' % (w, h, x, y))

if __name__ == "__main__":
    app = MainContainer()
    app.mainloop()
Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
Nana Owusu
  • 96
  • 7
  • This `FigCanvas(fig, frame)` should read `FigCanvas(fig, self.tk_cnv)`. The widget to scroll have to be the client of the `Canvas`. – stovfl Mar 31 '20 at 09:23
  • Thank you for pointing that out, however, it still doesn't resolve my issue. – Nana Owusu Apr 01 '20 at 00:09
  • Maybe this answer [how-to-get-a-matplotlib-figure-to-scroll-resize-properly-in-a-tkinter-gui](https://stackoverflow.com/questions/13197469) – stovfl Apr 02 '20 at 08:24
  • That answer is among those I looked at when writing the scrollable canvas portion of my application (ex. 2 in the initial post). I have tried implementing the scrollable canvas as a function rather than a class but I get the same problem. – Nana Owusu Apr 05 '20 at 03:20
  • Verify the return value from: `self.tk_cnv.bbox("all")` – stovfl Apr 05 '20 at 10:41
  • @sftovfl, I have done as you suggested. I found that the dimensions of tk_cnv is (283, 198) and canvas_frame, (1087, 198). Reported are width and height, respectively, for each widget. The canvas_frame containing just the mpl_cnv, the dimension of the frame is (1400, 1800), matching that of the figure. I would like the canvas frame to fit the rest of the Tk window but still have the size of the figure be as it is. – Nana Owusu Apr 05 '20 at 22:49
  • ***tk_cnv is (283, 198) and canvas_frame, (1087, 198)***: Should be equal, as you use `fill='both', expand=True`. ***mpl_cnv, the dimension of the frame is (1400, 1800)***: This should be `scrollregion=` – stovfl Apr 06 '20 at 07:40

1 Answers1

0

The major cause of the issue was the use of the initial Frame object from the MainContainer class to instantiate the Frame in which the Scrollable class is contained. The object of the MainContainer is what should have been passed, this is because it inherits from the Tk class.

Secondly, the mixture of window managers was causing problems. Because of this I switched to pack exclusively. The solution is below.

from tkinter import Tk, Frame, Canvas
from tkinter.ttk import Scrollbar
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as FigCanvas
from matplotlib.figure import Figure

class Scrollable(Frame):
    def __init__(self, frame, fig, width=16):

        # Base class initialization
        Frame.__init__(self, frame)

        # Instance variable for tkinter canvas
        self.tk_cnv = Canvas(frame, highlightthickness=0)
        self.tk_cnv.pack(side="left", anchor="nw", fill="both",
                         expand=True)

        # Instance variable for the scroll-bar
        v_scroll = Scrollbar(frame)
        v_scroll.pack(side="right", fill="y", expand=False)
        v_scroll.config(command=self.tk_cnv.yview, width=width)
        v_scroll.activate("slider")

        # Instance variable for the matplotlib canvas
        self.mpl_cnv = FigCanvas(fig, frame)
        self.cnv_widget = self.mpl_cnv.get_tk_widget()

        self.tk_cnv.config(yscrollcommand=v_scroll.set)
        self.tk_cnv.bind("<Configure>", self.__fill_canvas)

        # Assign frame generated by the class to the canvas
        # and create a scrollable window for it.
        self.windows_item = \
            self.tk_cnv.create_window((0, 900), window=self.cnv_widget, anchor='e',
                                      tag='self.canvas')

        self.tk_cnv.config(scrollregion=self.tk_cnv.bbox("all"))

    def __fill_canvas(self, event):
        # Enlarge the windows item to the canvas width
        canvas_width = event.width
        canvas_height = event.height * 2.825
        self.tk_cnv.itemconfig(self.windows_item, width=canvas_width,
                               height=canvas_height)


class StartPage(Frame):
    """ Tkinter based class for single frame upon which widgets
    such as buttons, check-buttons, and entry are used as a
    simple graphical user interface.
    """
    LARGE_FONT = ("Veranda", 12)

    def __init__(self, parent, controller):
        Frame.__init__(self, parent)

        self.controller = controller

        # Instance variable for third row of widgets
        self.canvas_frame = Frame(self.controller, relief="sunken")
        self.canvas_frame.pack(side="top", anchor="nw", fill="both",
                               expand=True)

        # Instance variables for the figure
        self.plot_fig = Figure(figsize=[14.0, 18.0])

        # Instance variable for the frame with scrolling functionality
        self.canvas_body = Scrollable(self.canvas_frame, self.plot_fig)
        self.canvas = self.canvas_body.mpl_cnv

        # Instance variable for third row of widgets
        self.control_frame = Frame(self.controller, relief="sunken")
        self.control_frame.pack(side="right", anchor="ne", fill="y",
                                expand=True)


class MainContainer(Tk):
    """ Tkinter based class used to generate a single window
    and contain a single frame. The frame contains multiple
    widgets for user choice and visualization.
    """

    def __init__(self, *args, **kwargs):
        Tk.__init__(self, *args, **kwargs)

        Tk.wm_title(self, "Sequence Viewer")

        Tk.wm_resizable(self, width=True, height=True)

        container = Frame(self)
        container.pack_configure(side="top", anchor="nw", fill="both")

        self.frames = {}

        frame = StartPage(container, self)

        self.frames[StartPage] = frame

        self.show_frame(StartPage)

        self.center_window()

    def show_frame(self, frame_to_add):
        frame = self.frames[frame_to_add]
        frame.tkraise()

    def center_window(self):
        w = 1100
        h = 900
        sw = self.winfo_screenwidth()
        sh = self.winfo_screenheight()
        x = (sw - w) / 2
        y = (sh - h) / 2

        self.geometry('%dx%d+%d+%d' % (w, h, x, y))

if __name__ == "__main__":
    app = MainContainer()
    app.mainloop()
Nana Owusu
  • 96
  • 7