What is the best method/practice for emitting a signal upon entering either a QGraphicsWidget or a QGraphicsItem ?
In my MWE I would like to trigger a call to MainWindow.update, from Square.hoverEnterEvent, whenever the user mouse(s) over an item in a QGraphicsScene. The trouble is that QGraphicsItem/Widget is not really designed to emit signals. Instead these classes are setup to handle events passed down to them from QGraphicsScene. QGraphicsScene handles the case that the user has selected an item but does not appear to handle mouse entry events, At least there is no mechanism for entryEvent to percolate up to the parent widget/window.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, qApp, QMainWindow, QGraphicsScene, QGraphicsView, QStatusBar, QGraphicsWidget, QStyle
from PyQt5.QtCore import Qt, QSizeF
class Square(QGraphicsWidget) :
"""
doc string
"""
def __init__(self,*args, name = None, **kvps) :
super().__init__(*args, **kvps)
self.radius = 5
self.name = name
self.setAcceptHoverEvents(True)
def sizeHint(self, hint, size):
size = super().sizeHint(hint, size)
print(size)
return QSizeF(50,50)
def paint(self, painter, options, widget):
self.initStyleOption(options)
ink = options.palette.highLight() if options.state == QStyle.State_Selected else options.palette.button()
painter.setBrush(ink) # ink
painter.drawRoundedRect(self.rect(), self.radius, self.radius)
def hoverEnterEvent(self, event) :
print("Enter Event")
super().hoverEnterEvent(event)
class MainWindow(QMainWindow):
def __init__(self, *args, **kvps) :
super().__init__(*args, **kvps)
# Status bar
self.stat = QStatusBar(self)
self.setStatusBar(self.stat)
self.stat.showMessage("Started")
# Widget(s)
self.data = QGraphicsScene(self)
self.view = QGraphicsView(self.data, self)
item = self.data.addItem(Square())
self.view.ensureVisible(self.data.sceneRect())
self.setCentralWidget(self.view)
# Visibility
self.showMaximized()
def update(self, widget) :
self.stat.showMessage(str(widget.name))
if __name__ == "__main__" :
# Application
app = QApplication(sys.argv)
# Scene Tests
main = MainWindow()
main.show()
# Loop
sys.exit(app.exec_())
The docs state that QGraphicsItem is not designed to emit signals, instead it is meant to respond to the events sent to it by QGraphicsScene. In contrast it seems that QGraphicsWidget is designed to do so but I'm not entirely sure where the entry point ought to be. Personally I feel QGraphicsScene should really be emitting these signals, from what I understand of the design, but am not sure where the entry point ought to be in this case either.
Currently I see the following possible solutions, with #3 being the preferred method. I was wondering if anyone else had a better strategy :
Create a QGraphicsScene subclass, let's call it Scene, to each QGraphicsItem/QGraphicsWidget and call a custom trigger/signal upon the Scene from each widget. Here I would have to subclass any item I intend on including within the scene.
Set Mainwindow up as the event filter for each item in the scene or upon the scene itself and calling MainWindow.update.
Set Mainwindow.data to be a subclass of QGraphicsScene, let's call it Scene, and let it filter it's own events emitting a hoverEntry signal. hoverEntry is then connected to MainWindow.update as necessary.
As Murphy's Law would have it Ekhumoro already provides an answer.
It seems one should subclass QGraphicsScene and add the necessary signal. this is then triggered from the QGraphicsItem/Widget. This requires that all items within a scene be sub-classed to ensure that they call the corresponding emit function but it seems must do this do this anyhow when working with the graphics scene stuff.
I'll not mark this as answered for a bit in case some one has a better suggestion.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, qApp, QMainWindow, QGraphicsScene, QGraphicsView, QStatusBar, QGraphicsWidget, QStyle, QGraphicsItem
from PyQt5.QtCore import Qt, QSizeF, pyqtSignal
class Square(QGraphicsWidget) :
"""
doc string
"""
def __init__(self,*args, name = None, **kvps) :
super().__init__(*args, **kvps)
self.radius = 5
self.name = name
self.setAcceptHoverEvents(True)
self.setFlag(self.ItemIsSelectable)
self.setFlag(self.ItemIsFocusable)
def sizeHint(self, hint, size):
size = super().sizeHint(hint, size)
print(size)
return QSizeF(50,50)
def paint(self, painter, options, widget):
self.initStyleOption(options)
ink = options.palette.highLight() if options.state == QStyle.State_Selected else options.palette.button()
painter.setBrush(ink) # ink
painter.drawRoundedRect(self.rect(), self.radius, self.radius)
def hoverEnterEvent(self, event) :
super().hoverEnterEvent(event)
self.scene().entered.emit(self)
self.update()
class GraphicsScene(QGraphicsScene) :
entered = pyqtSignal([QGraphicsItem],[QGraphicsWidget])
class MainWindow(QMainWindow):
def __init__(self, *args, **kvps) :
super().__init__(*args, **kvps)
# Status bar
self.stat = QStatusBar(self)
self.setStatusBar(self.stat)
self.stat.showMessage("Started")
# Widget(s)
self.data = GraphicsScene(self)
self.data.entered.connect(self.itemInfo)
self.data.focusItemChanged.connect(self.update)
self.view = QGraphicsView(self.data, self)
item = Square(name = "A")
item.setPos( 50,0)
self.data.addItem(item)
item = Square(name = "B")
item.setPos(-50,0)
self.data.addItem(item)
self.view.ensureVisible(self.data.sceneRect())
self.setCentralWidget(self.view)
# Visibility
self.showMaximized()
def itemInfo(self, item):
print("Here it is -> ", item)
if __name__ == "__main__" :
# Application
app = QApplication(sys.argv)
# Scene Tests
main = MainWindow()
main.show()
# Loop
sys.exit(app.exec_())
The magic lines of interest are then the QGrahicsScene subclass.
class GraphicsScene(QGraphicsScene) :
entered = pyqtSignal([QGraphicsItem],[QGraphicsWidget])
The QGraphicsWidget.hoverEnterEvent triggers the entered signal. (This is where I got stuck)
def hoverEnterEvent(self, event) :
...
self.scene().entered.emit(self)
...
And the switcheroo from self.data = QGraphicsScene(...) to self.data = GraphicsScene in the MainWindow's init function.
Related
I want to set the background in dim mode, when a QMessagebox is popped up.
Currently, I have tried to use a simple QMesssagebox, but the background shows as normal display, when it pops up.
The image for 1st page is as follow
When go to next slide is pushed, it goes to next index as follow
When going back to 1st index, the back button is pushed which pops up the messagebox as follow
However, the mainwindow seems to have no effect on its focus.
Therefore, what would I need to do to make it dimmer than the focused messagebox.
How can I do this? Any suggestions?
EDIT
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = uic.loadUi("message.ui",self)
self.notification = QMessageBox()
self.ui.next_slide.clicked.connect(self.second_index)
self.ui.go_back.clicked.connect(self.alert_msg)
self.show()
def home(self):
self.ui.stackedWidget.setCurrentIndex(0)
def second_index(self):
self.ui.stackedWidget.setCurrentIndex(1)
def alert_msg(self):
self.notification.setWindowTitle("Exiting")
self.notification.setText("Are you sure, you want to exit")
self.notification.setIcon(QMessageBox.Critical)
self.notification.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
self.back = self.notification.exec_()
if self.back == QMessageBox.Yes:
self.home()
else:
pass
if __name__ == "__main__":
app=QApplication(sys.argv)
mainwindow=MainWindow()
app.exec_()
You can create a custom widget that is a direct child of the window that has to be "dimmed", ensure that it always has the same size as that window, and just paint it with the selected color:
class Dimmer(QWidget):
def __init__(self, parent):
parent = parent.window()
super().__init__(parent)
parent.installEventFilter(self)
self.setAttribute(Qt.WA_DeleteOnClose)
self.adaptToParent()
self.show()
def adaptToParent(self):
self.setGeometry(self.parent().rect())
def eventFilter(self, obj, event):
if event.type() == event.Resize:
self.adaptToParent()
return super().eventFilter(obj, event)
def paintEvent(self, event):
qp = QPainter(self)
qp.fillRect(self.rect(), QColor(127, 127, 127, 127))
class MainWindow(QMainWindow):
# ...
def alert_msg(self):
dimmer = Dimmer(self)
# ...
self.back = self.notification.exec_()
dimmer.close()
Note that, unless you plan to reuse the "dim widget", it must be destroyed either by calling close() as done above (see the WA_DeleteOnClose flag) or using deleteLater(). Hiding it will not be enough.
I have a custom qcompleter (to match any part of the string) and a custom QStyledItemDelegate (to show different formatting on the drop down options returned by the qcompleter) applied to a QLineEdit, and they both work individually however the QStyledItemDelegate doesn't work when I apply them both.
import sys
from PySide2.QtWidgets import QApplication, QMainWindow, QLineEdit, QCompleter, QStyledItemDelegate
from PySide2.QtCore import Qt, QSortFilterProxyModel, QStringListModel
from PySide2.QtGui import QColor, QPalette
Qcompleter Item delegate:
class CompleterItemDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super(CompleterItemDelegate, self).initStyleOption(option, index)
option.backgroundBrush = QColor("red")
option.palette.setBrush(QPalette.Text, QColor("blue"))
option.displayAlignment = Qt.AlignCenter
Custom QCompleter:
class CustomQCompleter(QCompleter):
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
class InnerProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
index0 = self.sourceModel().index(sourceRow, 0, sourceParent)
searchStr = local_completion_prefix.lower()
searchStr_list = searchStr.split()
modelStr = self.sourceModel().data(index0,Qt.DisplayRole).lower()
for string in searchStr_list:
if not string in modelStr:
return False
return True
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
def splitPath(self, path):
self.local_completion_prefix = str(path)
self.updateModel()
return ""
Main:
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
model = QStringListModel()
model.setStringList(['Tom', 'Tommy Stevens', 'Steven'])
# ITEM DELEGATE ONLY - WORKS
# completer = QCompleter()
# completer.setModel(model)
# delegate = CompleterDelegate()
# completer.popup().setItemDelegate(delegate)
# QCOMPLETER DELEGATE ONLY - WORKS
# completer = CustomQCompleter(self)
# completer.setModel(model)
# ITEM DELEGATE AND QCOMPLETER DELEGATE - ITEM DELEGATE DOESNT WORK
completer = CustomQCompleter(self)
completer.setModel(model)
delegate = CompleterItemDelegate()
completer.popup().setItemDelegate(delegate)
self.lineEdit = QLineEdit()
self.lineEdit.setCompleter(completer)
self.setCentralWidget(self.lineEdit)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
p = MainWindow()
p.show()
sys.exit(app.exec_())
Is there a way to make this code work?
Is there a better way to achieve both choosing the completion rules for the qcompleter and formatting the popup results?
Setting the delegate on the popup won't work if the model is set afterwards, since setModel() also calls setPopup(), which in turn sets a new item delegate.
So, you either:
ensure that you set the delegate after setting the model on the completer;
subclass the completer and override setModel(), by calling the base implementation and then restore the delegate, or complete() by restoring the delegate before the base implementation call; note that this won't work in your case because you called the base implementation in updateModel() which will clearly ignore the override;
Moving
delegate = CompleterItemDelegate()
self.popup().setItemDelegate(delegate)
into the CustomQCompleter updateModel function solves the problem as pointed out by musicamante.
There have been similar questions asked about overriding the QCompleter popup position but i'll still not found a working solution. I simply want to move the popup down around 5px (I have some specific styling requirements)
I've tried subclassing a QListView and using that as my popup using setPopup(). I then override the showEvent and move the popup down in Y. I also do this on the resizeEvent since I believe this is triggered when items are filtered and the popup resizes. However this doesn't work.. I then used a singleshot timer to trigger the move after 1ms. This does kind of work but it seems quite inconsistent - the first time it shows is different to subsequent times or resizing.
Below is my latest attempt (trying to hack it by counting the number of popups..), hopefully someone can show me what i'm doing wrong or a better solution
import sys
import os
from PySide2 import QtCore, QtWidgets, QtGui
class QPopup(QtWidgets.QListView):
def __init__(self, parent=None):
super(QPopup, self).__init__(parent)
self.popups = 0
def offset(self):
y = 3 if self.popups < 2 else 7
print('y: {}'.format(y))
self.move(self.pos().x(), self.pos().y() + y)
self.popups += 1
def showEvent(self, event):
print('show')
# self.offset()
QtCore.QTimer.singleShot(1, self.offset)
def resizeEvent(self, event):
print('resize')
# self.offset()
QtCore.QTimer.singleShot(1, self.offset)
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MyDialog, self).__init__(parent)
self.create_widgets()
self.create_layout()
self.create_connections()
def create_widgets(self):
self.le = QtWidgets.QLineEdit('')
self.completer = QtWidgets.QCompleter(self)
self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
self.completer.setMaxVisibleItems(10)
self.completer.setFilterMode(QtCore.Qt.MatchContains)
self.completer.setPopup(QPopup())
popup = QPopup(self)
self.completer.setPopup(popup)
self.model = QtCore.QStringListModel()
self.completer.setModel(self.model)
self.le.setCompleter(self.completer)
self.completer.model().setStringList(['one','two','three'])
def create_layout(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.le)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show() # Show the UI
sys.exit(app.exec_())
One solution could be to make a subclass of QLineEdit and override keyPressEvent to display the popup with an offset:
PySide2.QtWidgets.QCompleter.complete([rect=QRect()])
For PopupCompletion and QCompletion::UnfilteredPopupCompletion modes, calling this function displays the popup displaying the current completions. By default, if rect is not specified, the popup is displayed on the bottom of the widget() . If rect is specified the popup is displayed on the left edge of the rectangle.
see doc.qt.io -> QCompleter.complete.
Complete, self-contained example
The rect is calculated based on the y-position of the cursor rect. The height of the popup window is not changed. The width is adjusted to the width of the ZLineEdit widget.
rect = QtCore.QRect(0,
self.cursorRect().y() + 4,
self.width(),
self.completer().widget().height())
Your code, slightly modified using the points mentioned above, could look like this:
import sys
from PySide2 import QtCore, QtWidgets
from PySide2.QtWidgets import QLineEdit, QDialog, QCompleter
class ZLineEdit(QLineEdit):
def __init__(self, string, parent=None):
super().__init__(string, parent)
def keyPressEvent(self, event):
super().keyPressEvent(event)
if len(self.text()) > 0:
rect = QtCore.QRect(0,
self.cursorRect().y() + 4,
self.width(),
self.completer().widget().height())
self.completer().complete(rect)
class MyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.le = ZLineEdit('')
autoList = ['one', 'two', 'three']
self.completer = QCompleter(autoList, self)
self.setup_widgets()
self.create_layout()
self.create_connections()
def setup_widgets(self):
self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
self.completer.setMaxVisibleItems(10)
self.completer.setFilterMode(QtCore.Qt.MatchContains)
self.le.setCompleter(self.completer)
def create_layout(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.le)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show()
sys.exit(app.exec_())
Test
On the left side you see the default behavior. On the right side the popup is moved down 4px:
I'm trying to get this custom frame be usable as a widget in qt-designer. I'm following this example in pyqt5 documentation. I've implemented subclasses of QPyDesignerContainerExtension, CollapsibleBoxExtensionFactory and QPyDesignerCustomWidgetPlugin. The only difference is that the example involves pages, and mine is just a custom frame. It is the only example demonstrating a container plugin, and therefore, using the class QPyDesignerContainerExtension.
All function of the widget work when used in python, it appears in qt-designer, but I'm unable to drop a widget in it. Except if I force getCurrentIndex to be 0 instead of -1 when the frame is empty. But this obviously create a segmentation fault error in qt designer as it tries to access a non existing widget.
After a bit of reverse ingeneering on how qt designer interact with multipagewidgetplugin, I have the impression that the QPyDesignerContainerExtension is more for multipage containers that for basic frames.
Is QPyDesignerContainerExtension supposed to work with basic containers, as a frame ? Or should I be looking for a bug in my code...
Thanks for any help
edit:
To refocus the question on the primary problem, here is a simple version of the widget I want to use in qt-designer
The widget:
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QWidget, QScrollArea, QSizePolicy, QVBoxLayout, QLabel
class CustomFrame(QWidget):
def __init__(self, parent=None, title='title'):
super().__init__(parent)
layout = QVBoxLayout(self)
self.setLayout(layout)
self.title = QLabel(text=title)
layout.addWidget(self.title)
self.content_area = QScrollArea()
# self.content_area.setMinimumHeight(0)
# self.content_area.setMaximumHeight(0)
self.content_area.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed
)
layout.addWidget(self.content_area)
self.content_layout = QVBoxLayout(self.content_area)
self.content_area.setLayout(self.content_layout)
def _update_animation(self):
# need to be called each time a widget is added
pass
def getTitle(self):
return self.toggle_button.text()
#pyqtSlot(str)
def setTitle(self, value):
self.toggle_button.setText(value)
def resetTitle(self):
self.toggle_button.setText("Title")
titleString = pyqtProperty(str, getTitle, setTitle, resetTitle)
A plugin:
from PyQt5.QtDesigner import QPyDesignerCustomWidgetPlugin
class MultiPageWidgetPlugin(QPyDesignerCustomWidgetPlugin):
def __init__(self, parent=None):
super(MultiPageWidgetPlugin, self).__init__(parent)
self.initialized = False
def initialize(self, formEditor):
if self.initialized:
return
self.initialized = True
def isInitialized(self):
return self.initialized
def createWidget(self, parent):
widget = CustomFrame(parent)
return widget
def name(self):
return "CustomFrame"
def group(self):
return "PyQt Examples"
def icon(self):
return QIcon()
def toolTip(self):
return ""
def whatsThis(self):
return ""
def isContainer(self):
return True
def domXml(self):
return ('<widget class="CustomFrame" name="customframe">'
' <widget class="QWidget" name="page" />'
'</widget>')
def includeFile(self):
return "customframe"
The goal is, in qt-designer, that a widget dropped to the CustomFrame is added to content_area. By setting isContainer() to true, the widget accept dropped widgets, but they are added the widget itself, not to content_area. So unless there is a way to change this behaviour, It seems to be requiered to use a QPyDesignerContainerExtension with a single page...
I am coding a GUI program now and I have two separate PyQt5 widget objects that need to communicate with each other. I have something that works now (which I have provided a simplified example of below), but I suspect there is a more robust way of doing this that I am hoping to learn about. I will summarize the functionality below, for those that would like a bit of an intro to the code.
TL;DR: Please help me find a better way to use a button click in object 1 to change a variable in object 2 that sends the coordinates of a mouse click in object 2 to object 1 where those coordinates populate two spin boxes.
This first MainWindow class is where the widget objects are defined. The two objects of interest are MainWindow.plotWidget, an instance of the MplFig class, and MainWindow.linePt1, an instance of the LineEndpoint class. Note here that I am able to pass the self.plotWidget as an argument into the LineEndpoint object, but since MainWindow.plotWidget is defined first, I cannot pass self.linePt1 as an argument there.
The functionality I have achieved with these widgets is a button in LineEndpoint (LineEndpoint.chooseBtn) that, when clicked, changes a variable in MplFig (MplFig.waitingForPt) from None to the value of ptNum which is passed as an argument of LineEndpoint (in the case of linePt1, this value is 1). MplFig has button press events tied to the method MplFig.onClick() which, is MplFig.onClick is not None, passes the coordinates of the mouse click to the two QDoubleSpinBox objects in LineEndpoint.ptXSpin and LineEndpoint.ptYSpin. To achieve this, I pass self as the parent argument when I create the MainWIndow.plotWidget object of MplFig. I set the parent as self.parent which allows me to call the LineEndpoint object as self.parent.linePt1, which from there allows me to access the spin boxes.
This seems like a round-a-bout way of doing things and I'm wondering if anybody could suggest a better way of structuring this functionality? I like the method of passing the MplFig object as an argument to the LineEndpoint class as that makes it clear from the init method in the class definition that the LineEndpoint class communicates with the MplFig class. I know I cannot have both classes depend on each other in the same way, but i would love to learn a way of doing this that still makes it clear in the code that the objects are communicating. I am still open to all suggestions though!
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_())
If you want to transmit information between objects (remember that classes are only abstractions) then you must use signals:
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_())