首页 > 解决方案 > 重叠小部件的定位

问题描述

我正在尝试创建一个显示,其中您matplotlib在后台有一个画布,在前台有一个重叠的小部件,显示有关绘制数据的一些任意信息。这本身我基本上已经实现了,但是,我在对齐方面遇到了麻烦。

我希望重叠的小部件(在下面的示例中QGroupBox)与 的左下角对齐axes,并且在窗口大小更改时也响应。问题是我不知道如何正确更改两个重叠小部件的相对位置。

我找到了这个答案(下面称为方法 1),它使用QAlignment,但是一旦设置,QGroupBox似乎对任何类型的位置变化都没有反应。也许可以添加边距并动态更改它们?

我发现的另一种方法是这个(下面称为方法 2),它使用绝对定位,因此不会随着调整窗口大小而改变。也许这个更有意义?但是,QGroupBox每次调整窗口大小时,都需要进行一些转换和信号处理来重新定位。但不知何故,我没有设法正确地进行转换。

最后,我还发现了这个,处理锚点,但我不知道它们是如何工作的,也不知道它们是否是常规 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_())

标签: pythonmatplotlibpyqtpyqt5

解决方案


解决方案是将 QGroupBox 设置为画布的子级,并使用轴 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_())

推荐阅读