0

I've created a GUI in pyside2 with an embedded matplotlib canvas. I'm planning to display a lot of information on this canvas, so I need more vertical space. I figured I could place the canvas inside a QScrollArea and create a "tall" figure with some subplots below each other.

This answer comes close to what I want, but I don't want horizontal scrolling at all. Rather, I'd like my canvas to expand/shrink according to the GUI, fully occupying the available horizontal space inside the QScrollArea, while still only allowing for vertical scrolling of the figure.

The following code is a MRE without any scrollbars:

import sys

from PySide2.QtCore import Qt
from PySide2.QtWidgets import (QApplication, QHBoxLayout, QMainWindow, 
                               QPushButton, QVBoxLayout, QWidget)
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as Canvas,
                                                NavigationToolbar2QT as Navbar)
from matplotlib.pyplot import Figure


class MyApp(QMainWindow):
    def __init__(self) -> None:
        super().__init__()

        # Main Window setup
        self.showMaximized()
        self.frame = QWidget(self)
        self.setCentralWidget(self.frame)
        main_layout = QHBoxLayout()
        self.frame.setLayout(main_layout)

        # Buttons (left column)
        buttons_row = QHBoxLayout()

        self.plot_above_button = QPushButton(self, text='Plot Data Above')
        self.plot_above_button.clicked.connect(self.plot_above)
        buttons_row.addWidget(self.plot_above_button)

        self.plot_below_button = QPushButton(self, text='Plot Data Below')
        self.plot_below_button.clicked.connect(self.plot_below)
        buttons_row.addWidget(self.plot_below_button)

        main_layout.addLayout(buttons_row)

        # Plot (right column)
        right_column = QVBoxLayout()

        self.fig = Figure(facecolor='white')
        self.ax1 = self.fig.add_axes([0.1, 0.3, 0.8, 0.7])
        self.ax2 = self.fig.add_axes([0.1, 0.1, 0.8, 0.1])

        self.canvas = Canvas(self.fig)
        self.canvas.setParent(self)

        right_column.addWidget(self.canvas)
        right_column.addWidget(Navbar(self.canvas, self.frame))
        main_layout.addLayout(right_column)

    def plot_above(self):
        self.ax1.plot([1, 2], [3, 4])
        self.canvas.draw()

    def plot_below(self):
        self.ax2.plot([1, 2], [3, 4])
        self.canvas.draw()


if __name__ == '__main__':
    app = QApplication()
    gui = MyApp()
    gui.show()
    sys.exit(app.exec_())

And this is the GUI I see when I execute the code above:

enter image description here

As you can see, the canvas occupies the entire available space, vertically and horizontally. Except for the lacking vertical scrollbar, this is exactly what I want.

Now, if I try to place self.canvas inside a QScrollArea, by adding these lines to my code:

from PySide2.QtWidgets import QScrollArea  # additional import

        # ...
        self.canvas.setParent(self)  # this is old code 

        # new code starts here
        scroll_area = QScrollArea()
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.horizontalScrollBar().setEnabled(False)
        scroll_area.setWidget(self.canvas)

        right_column.addWidget(scroll_area)
        right_column.addWidget(Navbar(self.canvas, self.frame))  # this is old code
        # ...

I now end up with:

enter image description here

Instead of automatically adjusting its size, the figure just "sits" inside the QScrollArea, initialized with the default figsize value.

I also tried to manually resize the figure, by writing e.g.:

        self.fig = Figure(facecolor='white', figsize=(11, 12))

Which gives me:

enter image description here

This is very close to what I want, but also very hacky: I empirically chose the figsize (11, 12) based on how the canvas looks like on my screen, but these values could be different in another screen or if I add extra widgets to the left side. The canvas also doesn't resize along with the main window.

Is there a way to achieve what I want?

jfaccioni
  • 7,099
  • 1
  • 9
  • 25
  • Try `scroll_area.setWidgetResizable(True)` – alec Apr 17 '20 at 06:20
  • @alec thanks for your comment, I tried playing around with your suggestion but unfortunately the results were the same as my first image (i.e. no scrollbar). – jfaccioni Apr 17 '20 at 14:15

1 Answers1

0

After trying to solve this for another day, I finally figured it out. I'm writing the answer here in case anyone stumbles into this issue in the future.

We can reimplement resizeEvent of the GUI to resize the canvas width to the QScrollArea current width:

    def resizeEvent(self, e):
        self.canvas.resize(self.scroll_area.width(), self.canvas.height())
        super().resizeEvent(e)

If I call self.showMaximized() at the end of the init method, the resize will trigger automatically. This means that the canvas is properly resized when the window is first shown, and it adapts when I change the window size.

This is the outcome:

enter image description here

And the full code used to generate the GUI:

import sys

from PySide2.QtCore import Qt
from PySide2.QtWidgets import (QApplication, QHBoxLayout, QMainWindow, 
                            QPushButton, QScrollArea, QVBoxLayout, QWidget)
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as Canvas,
                                                NavigationToolbar2QT as Navbar)
from matplotlib.pyplot import Figure


class MyApp(QMainWindow):
    def __init__(self) -> None:
        super().__init__()

        # Main Window setup
        self.frame = QWidget(self)
        self.setCentralWidget(self.frame)
        main_layout = QHBoxLayout()
        self.frame.setLayout(main_layout)

        # Buttons (left column)
        buttons_row = QHBoxLayout()

        self.plot_above_button = QPushButton(self, text='Plot Data Above')
        self.plot_above_button.clicked.connect(self.plot_above)
        buttons_row.addWidget(self.plot_above_button)

        self.plot_below_button = QPushButton(self, text='Plot Data Below')
        self.plot_below_button.clicked.connect(self.plot_below)
        buttons_row.addWidget(self.plot_below_button)

        main_layout.addLayout(buttons_row)

        # Plot (right column)
        right_column = QVBoxLayout()

        self.fig = Figure(facecolor='white', figsize=(12, 12))
        self.ax1 = self.fig.add_axes([0.1, 0.3, 0.8, 0.7])
        self.ax2 = self.fig.add_axes([0.1, 0.1, 0.8, 0.1])
        self.canvas = Canvas(self.fig)
        self.canvas.setParent(self)

        self.scroll_area = QScrollArea()
        self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scroll_area.horizontalScrollBar().setEnabled(False)
        self.scroll_area.setWidget(self.canvas)

        right_column.addWidget(self.scroll_area)

        right_column.addWidget(Navbar(self.canvas, self.frame))
        main_layout.addLayout(right_column)

        self.showMaximized()

    def plot_above(self):
        self.ax1.plot([1, 2], [3, 4])
        self.canvas.draw()

    def plot_below(self):
        self.ax2.plot([1, 2], [3, 4])
        self.canvas.draw()

    def resizeEvent(self, e):
        self.canvas.resize(self.scroll_area.width(), self.canvas.height())
        super().resizeEvent(e)


if __name__ == '__main__':
    app = QApplication()
    gui = MyApp()
    gui.show()
    sys.exit(app.exec_())
jfaccioni
  • 7,099
  • 1
  • 9
  • 25