What is the better way for two-way communication between objects? - python

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_())

Related

How to use results from list widget in another class?

I am new to object oriented programming and need assistance getting the value from a listwidget in one class to another class. I have one listwidget called selectedVariables in the MainWindow class. I want the SecondWindow class to be able to get the variables from selectedVariables so the user can select them in the second window to group the data by a certain variable. Ideally, when they click the three dots, a new window pops up with a list of the widgets, but for now I just need to understand how to get the selectedVariable list items and send it to the SecondWindow Class. The code below runs without errors, but has no reference to the selectedVariables list.
I did try layout.addWidget(MainWindow.selectedVariables, 3, 0, 1, 1) and it gave me the error :
Traceback (most recent call last):
File "S:\1WORKING\FINANCIAL ANALYST\Shawn Schreier\Python\Minimum Example.py", line 99, in
mw = MainWindow()
File "S:\1WORKING\FINANCIAL ANALYST\Shawn Schreier\Python\Minimum Example.py", line 49, in init
self.mw = SecondWindow()
File "S:\1WORKING\FINANCIAL ANALYST\Shawn Schreier\Python\Minimum Example.py", line 30, in init
layout.addWidget(MainWindow.selectedVariables, 3, 0, 1, 1)
AttributeError: type object 'MainWindow' has no attribute 'selectedVariables'
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTextEdit, QGridLayout, QHBoxLayout, QVBoxLayout, QSizePolicy, QFileDialog, QTabWidget, QCheckBox
import PyQt5.QtGui as qtg
from PyQt5.QtCore import Qt, QSettings
import inspect
from PyQt5 import QtCore
class SecondWindow(QWidget):
def __init__(self):
super().__init__()
layout = QGridLayout()
by = QLabel("By")
byVariables = QLineEdit()
byVariableList = QListWidget()
layout.addWidget(by, 1,0,1,1)
layout.addWidget(byVariables, 2, 0, 1, 1)
byButton = QPushButton("...")
layout.addWidget(byButton, 2, 1, 1, 1)
self.setLayout(layout)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# Add a title
self.setWindowTitle("GUI Querying Program")
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.initUI()
self.setButtonConnections()
self.mw = SecondWindow()
def initUI(self):
subLayouts = {}
subLayouts['LeftColumn'] = QGridLayout()
self.layout.addLayout(subLayouts['LeftColumn'],1)
# Buttons
self.buttons = {}
self.buttons['addVariable'] = QPushButton('>')
self.buttons['removeVariable'] = QPushButton('<')
self.buttons['Toolkit'] = QPushButton('Toolkit')
self.variables = QListWidget()
self.selectedVariables = QListWidget()
subLayouts['LeftColumn'].addWidget(self.variables, 7,0,4,1)
subLayouts['LeftColumn'].addWidget(self.selectedVariables, 7,1,4,1)
subLayouts['LeftColumn'].addWidget(self.buttons['addVariable'], 10,0,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['removeVariable'], 10,1,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['Toolkit'], 11,1,1,1)
names = ['apple', 'banana', 'Cherry']
self.variables.insertItems(0, names)
def setButtonConnections(self):
self.buttons['addVariable'].clicked.connect(self.add_variable)
self.buttons['removeVariable'].clicked.connect(self.remove_variable)
self.buttons['Toolkit'].clicked.connect(self.show_new_window)
def add_variable(self):
selected_elements=[item.text() for item in self.variables.selectedItems()]
variableItem = self.selectedVariables.insertItems(self.variables.count(),selected_elements)
#self.variablesSearch.clear()
def remove_variable(self):
oldVariable = self.selectedVariables.currentRow()
self.selectedVariables.takeItem(oldVariable)
def show_new_window(self):
self.mw.show()
if __name__ == "__main__":
import sys
app = QApplication([])
mw = MainWindow()
mw.show()
# Run the app
app.exec()
app.quit()
#sys.exit(app.exec())
If you want to copy items, then you should use the clone() function of QListWidgetItem.
Note that you should do the same also in add_variable: while your case is very simple, cloning an item ensures that all its properties (including custom data, like text or background color) are copied, not only its text.
class SecondWindow(QWidget):
def __init__(self):
super().__init__()
layout = QGridLayout(self)
by = QLabel("By")
byVariables = QLineEdit()
self.byVariableList = QListWidget()
byButton = QPushButton("...")
layout.addWidget(by, 0, 0)
layout.addWidget(byVariables, 1, 0)
layout.addWidget(byButton, 2, 1)
layout.addWidget(self.byVariableList, 3, 0, 1, 2)
def setItems(self, items):
self.byVariableList.clear()
for item in items:
self.byVariableList.addItem(item)
class MainWindow(QWidget):
# ...
def add_variable(self):
for item in self.variables.selectedItems():
self.selectedVariables.addItem(item.clone())
def show_new_window(self):
items = []
for i in range(self.selectedVariables.count()):
items.append(self.selectedVariables.item(i).clone())
self.mw.setItems(items)
self.mw.show()
Note that when app.exec() returns, it means that the application has been quit, calling app.quit() after that is pointless. Also: 1. rows and columns of a grid layout are always based on 0-indexes (just like almost anything in computing), so you should add items from 0, not 1; 2. the row/column span always defaults to 1, so there is no point in specifying it unless you specifically want a span greater than 1.
You can try to make the SecondWindow, class a child class. That means, making it inherit your MainWindow methods. Like this
Class SecondWindow(MainWindow):
And from what i understood, from your code the selectedVariables instance is interchangeable according to the utilized methods. So it would be wiser to define it as a instance variable in your constructor
Class MainWindow(Qwidget):
def__init__(self):
self.selectedVariables = QListWidget()
That way, it will be easier for you to inherit independently what action happens to it . Also you wont need to call the method everytime so kinda of a must.

Pass in dynamic widget object to QVariantAnimation valueChanged signal handler

I'm trying to make an invalid animation tool. I have a button that when pressed, animates the background color of a field from red to the "normal" field color. This works great but now I want to pass in any arbitrary PyQt widget object (could be QLineEdit, QComboBox, and so on). Here's what my animation handler looks like:
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
Currently it requires that the field object be already named before calling the function to change the background. I would like to be able to dynamically pass in a widget and set the stylesheet on the fly by passing in a parameter, something like this:
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
When I try to do this, the widget is able to be passed but the constant color change from WARNING_COLOR to NORMAL_COLOR doesn't work anymore. I also do not want to put this into a class since it has to be on-the-fly. My goal is being able to call a function to start the animation from anywhere instead of having to press the button. The desired goal is like this:
class VariantAnimation(QtCore.QVariantAnimation):
"""VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
def updateCurrentValue(self, value):
pass
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
def invalid_animation(widget):
return VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=lambda: invalid_animation_handler(widget))
def start_invalid_animation(animation_handler) -> None:
if animation_handler.state() == QtCore.QAbstractAnimation.Running:
animation_handler.stop()
animation_handler.start()
field = QtGui.QLineEdit()
field_animation_handler = invalid_animation(field)
# Goal is to make a generic handler to start the animation
start_invalid_animation(field_animation_handler)
Minimal working example
import sys
from PyQt4 import QtCore, QtGui
class VariantAnimation(QtCore.QVariantAnimation):
"""VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
def updateCurrentValue(self, value):
pass
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
def start_field_invalid_animation() -> None:
if field_invalid_animation.state() == QtCore.QAbstractAnimation.Running:
field_invalid_animation.stop()
field_invalid_animation.start()
NORMAL_COLOR = QtGui.QColor(25,35,45)
SUCCESSFUL_COLOR = QtGui.QColor(95,186,125)
WARNING_COLOR = QtGui.QColor(251,188,5)
ERROR_COLOR = QtGui.QColor(247,131,128)
ANIMATION_DURATION = 1500
if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
button = QtGui.QPushButton('Animate field background')
button.clicked.connect(start_field_invalid_animation)
field = QtGui.QLineEdit()
field_invalid_animation = VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=invalid_animation_handler)
mw = QtGui.QMainWindow()
layout = QtGui.QHBoxLayout()
layout.addWidget(button)
layout.addWidget(field)
window = QtGui.QWidget()
window.setLayout(layout)
mw.setCentralWidget(window)
mw.show()
sys.exit(app.exec_())
I do not understand exactly why you do not want a class but IMO is the most suitable solution. The logic is to store the callable that allows to change the property and invoke it in updateCurrentValue.
Currently I don't have PyQt4 installed so I implemented the logic with PyQt5 but I don't think it is difficult to change the imports.
import sys
from dataclasses import dataclass
from functools import partial
from typing import Callable
from PyQt5.QtCore import QAbstractAnimation, QObject, QVariant, QVariantAnimation
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QLineEdit,
QMainWindow,
QPushButton,
QWidget,
)
#dataclass
class VariantAnimation(QVariantAnimation):
widget: QWidget
callback: Callable[[QWidget, QVariant], None]
start_value: QVariant
end_value: QVariant
duration: int
parent: QObject = None
def __post_init__(self) -> None:
super().__init__()
self.setStartValue(self.start_value)
self.setEndValue(self.end_value)
self.setDuration(self.duration)
self.setParent(self.parent)
def updateCurrentValue(self, value):
if isinstance(self.widget, QWidget) and callable(self.callback):
self.callback(self.widget, value)
def invalid_animation_handler(widget: QWidget, color: QColor) -> None:
widget.setStyleSheet(f"background-color: {QColor(color).name()}")
def start_field_invalid_animation(animation: QAbstractAnimation) -> None:
if animation.state() == QAbstractAnimation.Running:
animation.stop()
animation.start()
NORMAL_COLOR = QColor(25, 35, 45)
SUCCESSFUL_COLOR = QColor(95, 186, 125)
WARNING_COLOR = QColor(251, 188, 5)
ERROR_COLOR = QColor(247, 131, 128)
ANIMATION_DURATION = 1500
if __name__ == "__main__":
app = QApplication(sys.argv)
button = QPushButton("Animate field background")
field = QLineEdit()
animation = VariantAnimation(
widget=field,
callback=invalid_animation_handler,
start_value=ERROR_COLOR,
end_value=NORMAL_COLOR,
duration=ANIMATION_DURATION,
)
button.clicked.connect(partial(start_field_invalid_animation, animation))
mw = QMainWindow()
layout = QHBoxLayout()
layout.addWidget(button)
layout.addWidget(field)
window = QWidget()
window.setLayout(layout)
mw.setCentralWidget(window)
mw.show()
sys.exit(app.exec_())

2D clickable surface in Pyside2

I'm writing a simple GUI interface for a project using PySide2.
I'm using the typical MVC design pattern, for the sake of clarity I will just post the code of my GUI (without controller and support methods ecc...)
Here's the code:
from PySide2.QtWidgets import *
from PySide2.QtWidgets import QSizePolicy
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (QPushButton, QMainWindow)
class myView(QMainWindow):
def __init__(self, parent=None):
"""View initializer."""
#Creates blank view of a given size
super().__init__()
# Set some main window's properties
self.setWindowTitle('8D.me')
self.setFixedSize(800, 500) # Block user resize of the window
self.setIcon()
self.generalLayout = QHBoxLayout() #Layout generale
self.button = QPushButton('test3',self)
self.button.setSizePolicy(
QSizePolicy.Preferred,
QSizePolicy.Expanding)
self.generalLayout.addWidget(QPushButton('test2',self),1)
self.generalLayout.addWidget(self.button,3)
# Set the central widget
self._centralWidget = QWidget(self) #creates a QWidget object to play the role of a central widget. Remember that since your GUI class inherits from QMainWindow, you need a central widget. This object will be the parent for the rest of the GUI component.
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Insert methods for creating/adding elements to the default view.
# Mehods....
def setIcon(self):
appIcon = QIcon('logo')
self.setWindowIcon(appIcon)
#Insert here the public methods called by the Controller to update the view...
My GUI right no is pretty simple and looks like this:
What I would like to do is change the test 3 button and insert a 2D clickable surface.
More in details, I would like to be able to click anywhere on this surface and get the position of the mouse click.
Basically I would like to create a 2D xy axis and retrieve the coordinates of my mouse click, something like this:
And then if I click at position (1,1) I wll print something like "You clicked at (1,1) on the axis", pretty simple.
I looked around for examples, tutorials and documentation, but I didn't find any proper tool to create what I wanted.
Is there any class inside the PySide2 package that could help me?
If you took literally that your goal is to get the X-Y plane in your image then a possible solution is to use a QGraphicsView:
import math
import sys
from PySide2.QtCore import Signal, QPointF
from PySide2.QtGui import QColor, QPainterPath
from PySide2.QtWidgets import (
QApplication,
QGraphicsScene,
QGraphicsView,
QHBoxLayout,
QMainWindow,
QPushButton,
QWidget,
)
class GraphicsScene(QGraphicsScene):
clicked = Signal(QPointF)
def drawBackground(self, painter, rect):
l = min(rect.width(), rect.height()) / 30
x_left = QPointF(rect.left(), 0)
x_right = QPointF(rect.right(), 0)
painter.drawLine(x_left, x_right)
right_triangle = QPainterPath()
right_triangle.lineTo(-0.5 * math.sqrt(3) * l, 0.5 * l)
right_triangle.lineTo(-0.5 * math.sqrt(3) * l, -0.5 * l)
right_triangle.closeSubpath()
right_triangle.translate(x_right)
painter.setBrush(QColor("black"))
painter.drawPath(right_triangle)
y_top = QPointF(0, rect.top())
y_bottom = QPointF(0, rect.bottom())
painter.drawLine(y_top, y_bottom)
top_triangle = QPainterPath()
top_triangle.lineTo(.5*l, -0.5 * math.sqrt(3) * l)
top_triangle.lineTo(-.5*l, -0.5 * math.sqrt(3) * l)
top_triangle.closeSubpath()
top_triangle.translate(y_bottom)
painter.setBrush(QColor("black"))
painter.drawPath(top_triangle)
def mousePressEvent(self, event):
sp = event.scenePos()
self.clicked.emit(sp)
super().mousePressEvent(event)
class MyView(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("8D.me")
self.setFixedSize(800, 500)
self.btn = QPushButton("test2")
self.view = QGraphicsView()
self.view.scale(1, -1)
self.scene = GraphicsScene()
self.view.setScene(self.scene)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QHBoxLayout(central_widget)
layout.addWidget(self.btn)
layout.addWidget(self.view)
self.scene.clicked.connect(self.handle_clicked)
def handle_clicked(self, p):
print("clicked", p.x(), p.y())
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyView()
w.show()
sys.exit(app.exec_())

Recursion Error on PyQt QStackedWidget when using enterEvent and leaveEvent

I am using a QStackedWidget which has its own enterEvent and leaveEvent. When I move my mouse to the QStackedWidget the enterEvent sets the current index to 1 and on the leaveEvent it sets the current index to 0 so that a different widget is shown on mouse enter and mouse leave in the area of QStackedWidget. It does what I want only if I quickly move my mouse in and out, if I place my mouse too long in the area I get RecursionError: maximum recursion depth exceeded.
Is this because the widgets are changing so fast that the internal stack can't keep up? My question is "How can I make sure this error doesn't occur? I want to display one widget as long as the mouse is over the QStackedWidget and when it is not I want to display the original widget."
The following is the code that I modified (Original Source used buttons to set the index and it is PyQt4)
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QTimeLine
from PyQt5.QtGui import *
class FaderWidget(QWidget):
def __init__(self, old_widget, new_widget):
QWidget.__init__(self, new_widget)
self.old_pixmap = QPixmap(new_widget.size())
old_widget.render(self.old_pixmap)
self.pixmap_opacity = 1.0
self.timeline = QTimeLine()
self.timeline.valueChanged.connect(self.animate)
self.timeline.finished.connect(self.close)
self.timeline.setDuration(333)
self.timeline.start()
self.resize(new_widget.size())
self.show()
def animate(self, value):
self.pixmap_opacity = 1.0 - value
self.repaint()
class StackedWidget(QStackedWidget):
def __init__(self, parent = None):
QStackedWidget.__init__(self, parent)
def setCurrentIndex(self, index):
self.fader_widget = FaderWidget(self.currentWidget(), self.widget(index))
super().setCurrentIndex(index)
def enterEvent(self,event):
self.setCurrentIndex(1)
def leaveEvent(self,event):
self.setCurrentIndex(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = QWidget()
stack = StackedWidget()
cal=QCalendarWidget()
stack.addWidget(cal)
editor = QTextEdit()
editor.setPlainText("Hello world! "*100)
stack.addWidget(editor)
layout = QGridLayout(window)
layout.addWidget(stack, 0, 0, 1, 2)
window.show()
sys.exit(app.exec_())
The recursion occurs because when you start the FaderWidget it changes focus and enterEvent is called again which creates a new FaderWidget.
The solution is to verify that the old index is different from the new index to just create the FadeWidget:
import sys
from PyQt5.QtCore import QTimeLine
from PyQt5.QtGui import QPainter, QPixmap
from PyQt5.QtWidgets import (
QApplication,
QCalendarWidget,
QGridLayout,
QStackedWidget,
QTextEdit,
QWidget,
)
class FaderWidget(QWidget):
def __init__(self, old_widget, new_widget):
QWidget.__init__(self, new_widget)
self.pixmap_opacity = 1.0
self.old_pixmap = QPixmap(new_widget.size())
old_widget.render(self.old_pixmap)
self.timeline = QTimeLine()
self.timeline.valueChanged.connect(self.animate)
self.timeline.finished.connect(self.close)
self.timeline.setDuration(333)
self.timeline.start()
self.resize(new_widget.size())
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setOpacity(self.pixmap_opacity)
painter.drawPixmap(0, 0, self.old_pixmap)
def animate(self, value):
self.pixmap_opacity = 1.0 - value
self.update()
class StackedWidget(QStackedWidget):
def setCurrentIndex(self, index):
if self.currentIndex() != index:
self.fader_widget = FaderWidget(self.currentWidget(), self.widget(index))
super().setCurrentIndex(index)
def enterEvent(self, event):
self.setCurrentIndex(1)
def leaveEvent(self, event):
self.setCurrentIndex(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = QWidget()
stack = StackedWidget()
cal = QCalendarWidget()
stack.addWidget(cal)
editor = QTextEdit()
editor.setPlainText("Hello world! " * 100)
stack.addWidget(editor)
layout = QGridLayout(window)
layout.addWidget(stack, 0, 0, 1, 2)
window.show()
sys.exit(app.exec_())

QGraphicesItem emit a signal upon hoverEnterEvent

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.

Categories