mapf
mapf

Reputation: 2058

Positioning of overlapping widgets

I am trying to create a display where you have a matplotlib canvas in the background, and an overlapping widget in the foreground displaying some arbitrary information about the plotted data. This in itself I have basically achieved, however, I have trouble with the alignment.

I would like the overlapping widget (in the example below the QGroupBox) to be aligned with the lower left corner of the axes, and also respond to whenever the window size is changed. The problem is that I don't know how I can change the relative position of the two overlapping widgets correctly.

I found this answer (below called method 1), which uses QAlignment, but once that is set, the QGroupBox seems irresponsive to any kind of positional changes. Maybe it is possible to add margins and change them dynamically?

The other method I found is this one (below called method 2), which uses absolute positioning, and thus doesn't change with resizing the window. Maybe this one makes more sense? But then there is some transformation and signal handling necessary to reposition the QGroupBox every time the window resizes. But somehow I didn't manage to get the transformation right.

Lastly, I also found this, dealing with anchors, but I have no idea how they work, and if they are even a thing in regular PyQt5.

import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import \
    FigureCanvasQTAgg as FigureCanvas
import matplotlib.patheffects as PathEffects

from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QDialog, QApplication, QGridLayout, QGroupBox, \
    QLabel, QLineEdit


class MainWindow(QDialog):
    def __init__(self):
        super().__init__()

        # this is just for context

        self.fig, self.ax = plt.subplots()
        self.canvas = FigureCanvas(self.fig)
        self.data = np.random.uniform(0, 1, (50, 2))
        self.artists = []
        for point in self.data:
            artist = self.ax.plot(*point, 'o', c='orange')[0]
            artist.set_pickradius = 5
            self.artists.append(artist)
        self.zoom_factor = 1.2
        self.x_press = 0
        self.y_press = 0
        self.last_artist = None
        self.cid_motion = self.canvas.mpl_connect(
            'motion_notify_event', self.motion_event
        )
        self.cid_button = self.canvas.mpl_connect(
            'button_press_event', self.pan_press
        )
        self.cid_zoom = self.canvas.mpl_connect('scroll_event', self.zoom)

        self.mainLayout = QGridLayout(self)
        self.mainLayout.addWidget(self.canvas, 0, 0)
        self.setLayout(self.mainLayout)
        self.statsBox = QGroupBox('Stats:')
        self.statsLayout = QGridLayout(self.statsBox)
        self.posLabel = QLabel('Pos:')
        self.statsLayout.addWidget(self.posLabel, 0, 0)
        self.posEdit = QLineEdit()
        self.posEdit.setReadOnly(True)
        self.posEdit.setAlignment(Qt.AlignHCenter)
        self.statsLayout.addWidget(self.posEdit, 0, 1)

        # here is what's interesting

        # method 1
        # self.mainLayout.addWidget(
        #     self.statsBox, 0, 0, Qt.AlignRight | Qt.AlignBottom
        # )

        # method 2
        self.statsBox.setParent(self)
        self.statsBox.setFixedSize(self.statsBox.sizeHint())
        self.position_statsBox()

    def resizeEvent(self, a0):
        super().resizeEvent(a0)
        self.position_statsBox()

    def position_statsBox(self):
        x, y = self.ax.get_xlim()[1], self.ax.get_ylim()[0]
        pos = QPoint(*self.ax.transData.transform((x, y)))
        self.statsBox.move(pos)

    # below here is just for context again

    def motion_event(self, event):
        if event.inaxes == self.ax and event.button == 1:
            self.pan_move(event)
        else:
            self.hover(event)

    def pan_press(self, event):
        if event.inaxes == self.ax and event.button == 1:
            self.x_press = event.xdata
            self.y_press = event.ydata

    def pan_move(self, event):
        xdata = event.xdata
        ydata = event.ydata
        cur_xlim = self.ax.get_xlim()
        cur_ylim = self.ax.get_ylim()
        dx = xdata - self.x_press
        dy = ydata - self.y_press
        new_xlim = [cur_xlim[0] - dx, cur_xlim[1] - dx]
        new_ylim = [cur_ylim[0] - dy, cur_ylim[1] - dy]

        self.ax.set_xlim(new_xlim)
        self.ax.set_ylim(new_ylim)
        self.canvas.draw_idle()

    def zoom(self, event):
        if event.inaxes == self.ax:
            xdata, ydata = event.xdata, event.ydata
            xlim = self.ax.get_xlim()
            ylim = self.ax.get_ylim()
            x_left = xdata - xlim[0]
            x_right = xlim[1] - xdata
            y_bottom = ydata - ylim[0]
            y_top = ylim[1] - ydata
            scale_factor = np.power(self.zoom_factor, -event.step)
            new_xlim = xdata-x_left*scale_factor, xdata+x_right*scale_factor
            new_ylim = ydata-y_bottom*scale_factor, ydata+y_top*scale_factor
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw_idle()

    def hover(self, event):
        ind = 0
        cont = None
        while ind in range(len(self.artists)) and not cont:
            artist = self.artists[ind]
            cont, _ = artist.contains(event)
            if cont and artist is not self.last_artist:
                if self.last_artist is not None:
                    self.last_artist.set_path_effects(
                        [PathEffects.Normal()]
                    )
                    self.last_artist = None
                artist.set_path_effects(
                    [PathEffects.withStroke(
                        linewidth=7, foreground="c", alpha=0.4
                    )]
                )
                self.last_artist = artist
                x, y = artist.get_data()
                pos = f'({x[0]:.2f}, {y[0]:.2f})'
                self.posEdit.setText(pos)
            ind += 1

        if not cont and self.last_artist:
            self.last_artist.set_path_effects([PathEffects.Normal()])
            self.last_artist = None
            self.posEdit.clear()

        self.canvas.draw_idle()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    GUI = MainWindow()
    GUI.show()
    sys.exit(app.exec_())

Upvotes: 1

Views: 361

Answers (1)

eyllanesc
eyllanesc

Reputation: 243897

The solution is to set the QGroupBox as a child of the canvas and change the position using the position of the axis bbox:

import sys
import numpy as np

from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.patheffects as PathEffects

from PyQt5.QtCore import Qt, QPoint, QTimer
from PyQt5.QtWidgets import (
    QDialog,
    QApplication,
    QGridLayout,
    QGroupBox,
    QLabel,
    QLineEdit,
)


class MainWindow(QDialog):
    def __init__(self):
        super().__init__()

        # this is just for context

        self.fig = Figure()
        self.canvas = FigureCanvas(self.fig)
        self.ax = self.canvas.figure.subplots()
        self.data = np.random.uniform(0, 1, (50, 2))
        self.artists = []
        for point in self.data:
            artist = self.ax.plot(*point, "o", c="orange")[0]
            artist.set_pickradius = 5
            self.artists.append(artist)
        self.zoom_factor = 1.2
        self.x_press = 0
        self.y_press = 0
        self.last_artist = None
        self.cid_motion = self.canvas.mpl_connect(
            "motion_notify_event", self.motion_event
        )
        self.cid_button = self.canvas.mpl_connect("button_press_event", self.pan_press)
        self.cid_zoom = self.canvas.mpl_connect("scroll_event", self.zoom)

        self.mainLayout = QGridLayout(self)
        self.mainLayout.addWidget(self.canvas, 0, 0)

        self.statsBox = QGroupBox("Stats:", self.canvas)
        self.statsLayout = QGridLayout(self.statsBox)
        self.posLabel = QLabel("Pos:")
        self.statsLayout.addWidget(self.posLabel, 0, 0)
        self.posEdit = QLineEdit()
        self.posEdit.setReadOnly(True)
        self.posEdit.setAlignment(Qt.AlignHCenter)
        self.statsLayout.addWidget(self.posEdit, 0, 1)
        self.statsBox.setFixedSize(self.statsBox.sizeHint())

        self.position_statsBox()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.position_statsBox()

    def position_statsBox(self):
        x0, y0, x1, y1 = self.ax.bbox.extents
        p = QPoint(int(x0), int(y1))
        p -= QPoint(0, self.statsBox.height())
        p += QPoint(0, 6)  # FIXME
        self.statsBox.move(p)

    def motion_event(self, event):
        if event.inaxes == self.ax and event.button == 1:
            self.pan_move(event)
        else:
            self.hover(event)

    def pan_press(self, event):
        if event.inaxes == self.ax and event.button == 1:
            self.x_press = event.xdata
            self.y_press = event.ydata

    def pan_move(self, event):
        xdata = event.xdata
        ydata = event.ydata
        cur_xlim = self.ax.get_xlim()
        cur_ylim = self.ax.get_ylim()
        dx = xdata - self.x_press
        dy = ydata - self.y_press
        new_xlim = [cur_xlim[0] - dx, cur_xlim[1] - dx]
        new_ylim = [cur_ylim[0] - dy, cur_ylim[1] - dy]

        self.ax.set_xlim(new_xlim)
        self.ax.set_ylim(new_ylim)
        self.canvas.draw_idle()

    def zoom(self, event):
        if event.inaxes == self.ax:
            xdata, ydata = event.xdata, event.ydata
            xlim = self.ax.get_xlim()
            ylim = self.ax.get_ylim()
            x_left = xdata - xlim[0]
            x_right = xlim[1] - xdata
            y_bottom = ydata - ylim[0]
            y_top = ylim[1] - ydata
            scale_factor = np.power(self.zoom_factor, -event.step)
            new_xlim = xdata - x_left * scale_factor, xdata + x_right * scale_factor
            new_ylim = ydata - y_bottom * scale_factor, ydata + y_top * scale_factor
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw_idle()

    def hover(self, event):
        ind = 0
        cont = None
        while ind in range(len(self.artists)) and not cont:
            artist = self.artists[ind]
            cont, _ = artist.contains(event)
            if cont and artist is not self.last_artist:
                if self.last_artist is not None:
                    self.last_artist.set_path_effects([PathEffects.Normal()])
                    self.last_artist = None
                artist.set_path_effects(
                    [PathEffects.withStroke(linewidth=7, foreground="c", alpha=0.4)]
                )
                self.last_artist = artist
                x, y = artist.get_data()
                pos = f"({x[0]:.2f}, {y[0]:.2f})"
                self.posEdit.setText(pos)
            ind += 1

        if not cont and self.last_artist:
            self.last_artist.set_path_effects([PathEffects.Normal()])
            self.last_artist = None
            self.posEdit.clear()

        self.canvas.draw_idle()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    GUI = MainWindow()
    GUI.show()
    sys.exit(app.exec_())

Upvotes: 2

Related Questions