【问题标题】:Positioning of overlapping widgets重叠小部件的定位
【发布时间】:2021-05-21 15:53:29
【问题描述】:

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

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

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

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

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

【问题讨论】:

    标签: python matplotlib pyqt pyqt5


    【解决方案1】:

    解决方法是将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_())
    

    【讨论】:

    • 哦,这很聪明。没想到。谢谢! # FIXME 指的是什么?
    • @mapf 是我必须使用那条线来校正垂直位移(至少在我的测试中我看到了),因为“6”是任何方法都不会产生的值但是从实验性的反复试验中,我认为应该从它出现的地方寻找“6”(也许在另一个环境中不是“6”,这会导致我想要纠正的问题)
    • 感谢您的解释。抱歉,在再次发表评论之前没有看到您的答案。但是,是的,这似乎很奇怪。在我的情况下,随着窗口的扩大,位移会稍微变大。不过,没有什么是不能用一点魔法来解决的。仍然想知道它来自哪里。
    • 好的,我做了一些实验,发现p = QPoint(x0, y1*1.015 - self.statsBox.height()) 几乎完全适合任何尺寸。仍然不知道是什么原因造成的。顺便提一句。使用tight_layoutconstrained_layoutsubplots_adjust 也可以完全关闭它,因此需要不同的幻数。我也不是很明白。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-12
    • 1970-01-01
    • 1970-01-01
    • 2022-09-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多