首页 > 解决方案 > 对象之间双向通信的更好方法是什么?

问题描述

我现在正在编写一个 GUI 程序,我有两个需要相互通信的独立 PyQt5 小部件对象。我现在有一些可以工作的东西(我在下面提供了一个简化的示例),但我怀疑有一种更强大的方法可以做到这一点,我希望了解它。我将总结下面的功能,以供那些想要对代码进行一些介绍的人使用。

TL;DR:请帮助我找到一种更好的方法来使用对象 1 中的按钮单击来更改对象 2 中的变量,该变量将对象 2 中的鼠标单击坐标发送到对象 1,这些坐标填充两个旋转框。

第一个 MainWindow 类是定义小部件对象的地方。感兴趣的两个对象是 MainWindow.plotWidget(MplFig 类的一个实例)和 MainWindow.linePt1(LineEndpoint 类的一个实例)。请注意,我可以将 self.plotWidget 作为参数传递给 LineEndpoint 对象,但是由于首先定义了 MainWindow.plotWidget,因此我无法将 self.linePt1 作为参数传递。

我使用这些小部件实现的功能是 LineEndpoint (LineEndpoint.chooseBtn) 中的一个按钮,单击该按钮时,将 MplFig (MplFig.waitingForPt) 中的变量从 None 更改为作为 LineEndpoint 的参数传递的 ptNum 的值(在linePt1 的情况下,该值为 1)。MplFig 具有与方法 MplFig.onClick() 相关联的按钮按下事件,即 MplFig.onClick 不是 None,将鼠标单击的坐标传递给 LineEndpoint.ptXSpin 和 LineEndpoint.ptYSpin 中的两个 QDoubleSpinBox 对象。为了实现这一点,我在创建 MplFig 的 MainWINdow.plotWidget 对象时将 self 作为父参数传递。我将父级设置为 self.parent,这允许我将 LineEndpoint 对象称为 self.parent.linePt1,从那里我可以访问旋转框。

这似乎是一种循环的做事方式,我想知道是否有人可以建议一种更好的方式来构建这个功能?我喜欢将 MplFig 对象作为参数传递给 LineEndpoint 类的方法,因为从类定义中的init方法可以清楚地看出 LineEndpoint 类与 MplFig 类进行通信。我知道我不能让两个类以相同的方式相互依赖,但我很想学习一种这样做的方法,它仍然可以在代码中清楚地表明对象正在通信。不过,我仍然愿意接受所有建议!

from PyQt5.QtWidgets import (
    QMainWindow, QApplication, QLabel, QLineEdit, QPushButton, QFileDialog, 
    QWidget, QHBoxLayout, QVBoxLayout, QMessageBox, QListWidget, 
    QAbstractItemView, QDoubleSpinBox
)
from PyQt5.QtCore import Qt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
)
import sys  # need sys to pass argv to QApplication


class MplFig(FigureCanvasQTAgg):

    def __init__(self, parent):
        self.fig = Figure()
        super().__init__(self.fig)
        self.parent = parent
        self.waitingForPt = None
        self.fig.canvas.mpl_connect('button_press_event', self.onClick)
        self.ax = self.figure.add_subplot(111)

    def onClick(self, e):
        if self.waitingForPt is not None:
            if self.waitingForPt == 1:
                lineObj = self.parent.linePt1
               
            roundX = round(e.xdata, lineObj.ptPrec)
            roundY = round(e.ydata, lineObj.ptPrec)
            print(f'x{self.waitingForPt}: {roundX}, '
                f'y{self.waitingForPt}: {roundY}'
            )
            lineObj.ptXSpin.setValue(roundX)
            lineObj.ptYSpin.setValue(roundY)
            lineObj.chooseBtn.setStyleSheet(
                'background-color: light gray'
            )
            self.waitingForPt = None

class LineEndpoint(QWidget):

    def __init__(self, parent, mplObject, ptNum, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.parent = parent
        self.mpl = mplObject
        self.layout = QVBoxLayout()

        row0Layout = QHBoxLayout()
        ptXLabel = QLabel(f'X{ptNum}:')
        row0Layout.addWidget(ptXLabel)
        ptMin = 0
        ptMax = 1000
        ptStep = 1
        self.ptPrec = 2
        self.ptXSpin = QDoubleSpinBox()
        self.ptXSpin.setSingleStep(ptStep)
        self.ptXSpin.setMinimum(ptMin)
        self.ptXSpin.setMaximum(ptMax)
        self.ptXSpin.setDecimals(self.ptPrec)
        row0Layout.addWidget(self.ptXSpin)
        ptYLabel = QLabel(f'Y{ptNum}:')
        row0Layout.addWidget(ptYLabel)
        self.ptYSpin = QDoubleSpinBox()
        self.ptYSpin.setMinimum(ptMin)
        self.ptYSpin.setMaximum(ptMax)
        self.ptYSpin.setSingleStep(ptStep)
        self.ptYSpin.setDecimals(self.ptPrec)
        row0Layout.addWidget(self.ptYSpin)
        self.layout.addLayout(row0Layout)

        row1Layout = QHBoxLayout()
        self.chooseBtn = QPushButton('Choose on Plot')
        self.chooseBtn.clicked.connect(lambda: self.chooseBtnClicked(ptNum))
        row1Layout.addWidget(self.chooseBtn)
        self.layout.addLayout(row1Layout)


    def chooseBtnClicked(self, endpointNum):
        print(f'Choosing point {endpointNum}...')
        self.chooseBtn.setStyleSheet('background-color: red')
        self.mpl.waitingForPt = endpointNum

class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setLayouts()

    def setLayouts(self):
        self.sideBySideLayout = QHBoxLayout()

        self.plotWidget = MplFig(self)
        self.sideBySideLayout.addWidget(self.plotWidget)

        self.linePt1 = LineEndpoint(self, self.plotWidget, 1)
        self.sideBySideLayout.addLayout(self.linePt1.layout)

        mainContainer = QWidget()
        mainContainer.setLayout(self.sideBySideLayout)
        self.setCentralWidget(mainContainer)

QApp = QApplication(sys.argv)
win = MainWindow()
win.show()

sys.exit(QApp.exec_())

标签: pythonmatplotlibpyqtpyqt5

解决方案


如果你想在对象之间传输信息(记住类只是抽象),那么你必须使用信号:

import sys

from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
    QApplication,
    QDoubleSpinBox,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QPushButton,
    QWidget,
)

from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg


class MplFig(FigureCanvasQTAgg):
    clicked = pyqtSignal(float, float)

    def __init__(self, parent=None):
        super().__init__(Figure())
        self.setParent(parent)
        self.figure.canvas.mpl_connect("button_press_event", self.onClick)
        self.ax = self.figure.add_subplot(111)

    def onClick(self, e):
        self.clicked.emit(e.xdata, e.ydata)


class LineEndpoint(QWidget):
    def __init__(self, ptNum, parent=None):
        super().__init__(parent)
        ptMin = 0
        ptMax = 1000
        ptStep = 1
        ptPrec = 2

        self.ptXSpin = QDoubleSpinBox(
            singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
        )
        self.ptYSpin = QDoubleSpinBox(
            singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
        )
        self.chooseBtn = QPushButton("Choose on Plot", checkable=True)
        self.chooseBtn.setStyleSheet(
            """
            QPushButton{
                background-color: light gray
            } 
            QPushButton:checked{
                background-color: red
            }"""
        )

        lay = QGridLayout(self)
        lay.addWidget(QLabel(f"X{ptNum}"), 0, 0)
        lay.addWidget(self.ptXSpin, 0, 1)
        lay.addWidget(QLabel(f"Y{ptNum}"), 0, 2)
        lay.addWidget(self.ptYSpin, 0, 3)
        lay.addWidget(self.chooseBtn, 1, 0, 1, 4)
        lay.setRowStretch(lay.rowCount(), 1)

    @pyqtSlot(float, float)
    def update_point(self, x, y):
        if self.chooseBtn.isChecked():
            self.ptXSpin.setValue(x)
            self.ptYSpin.setValue(y)
            self.chooseBtn.setChecked(False)


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setLayouts()

    def setLayouts(self):
        self.plotWidget = MplFig()
        self.linePt1 = LineEndpoint(1)

        self.plotWidget.clicked.connect(self.linePt1.update_point)

        mainContainer = QWidget()
        lay = QHBoxLayout(mainContainer)
        lay.addWidget(self.plotWidget)
        lay.addWidget(self.linePt1)

        self.setCentralWidget(mainContainer)


QApp = QApplication(sys.argv)
win = MainWindow()
win.show()

sys.exit(QApp.exec_())

推荐阅读