I want to make the label item of a plot clickable, that is, I can click the bottom or left label of a plot to call a menu.
My attempt:
The most obvious way is to rewrite the LabelItem class and overwrite the mousePressEvent() function, however, I did not see any methods in docs to add a LabelItem to a PlotItem at correct position. Code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pyqtgraph as pg
import sys
class ClickableLabelItem(pg.LabelItem):
def __init__(self, text='No Data'):
super(ClickableLabelItem, self).__init__()
self.setText(text, justify='left')
def mousePressEvent(self, ev):
print('clicked')
class AbstractPlot(pg.GraphicsLayoutWidget):
def __init__(self):
super(AbstractPlot, self).__init__()
self.pl = pg.PlotItem()
self.addItem(self.pl)
left_label = ClickableLabelItem()
self.pl.addItem(left_label)
# "Clicked" is printed but the label
# appears at wrong position
if __name__ == "__main__":
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
win = QMainWindow()
plot = AbstractPlot()
win.setCentralWidget(plot)
win.show()
if (sys.flags.interactive != 1) or not hasattr(Qt.QtCore, 'PYQT_VERSION'):
QApplication.instance().exec_()
Label appears at wrong position
If you want to detect in the label associated with the axis then you can override the mousePressEvent method of GraphicsLayoutWidget and compare the item associated with the point pressed by the mouse.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
class AbstractPlot(pg.GraphicsLayoutWidget):
def __init__(self):
super(AbstractPlot, self).__init__()
self.p1 = self.addPlot(title="Plot 1")
self.p1.setLabel("left", "Value", units="V")
self.p1.setLabel("bottom", "Time", units="s")
def mousePressEvent(self, event):
super().mousePressEvent(event)
item = self.itemAt(event.pos())
if item is self.p1.getAxis("left").label:
print("clicked left")
elif item is self.p1.getAxis("bottom").label:
print("clicked bottom")
if __name__ == "__main__":
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
win = QMainWindow()
plot = AbstractPlot()
win.setCentralWidget(plot)
win.show()
if (sys.flags.interactive != 1) or not hasattr(Qt.QtCore, "PYQT_VERSION"):
QApplication.instance().exec_()
Related
I'm trying to add custom animation to QPushbutton without making a custom QPushbutton and overriding its enterEvent() and leaveEvent().
So far I've tried this,
#staticmethod
def addButtonHoverAnimation(button:QPushButton,currentPos:QPoint):
'''
Method to:
=> Add hover animation for provided button
'''
enterShift = QPropertyAnimation(button,b'pos',button)
exitShift = QPropertyAnimation(button,b'pos',button)
def enterEvent(e):
pos=button.pos()
enterShift.setStartValue(pos)
enterShift.setEndValue(QPoint(pos.x()+3,pos.y()+3))
enterShift.setDuration(100)
enterShift.start()
Effects.dropShadow(button,1,2)
def leaveEvent(e):
pos=button.pos()
exitShift.setStartValue(pos)
exitShift.setEndValue(QPoint(pos.x()-3,pos.y()-3))
exitShift.setDuration(100)
exitShift.start()
Effects.dropShadow(button)
button.enterEvent=enterEvent
button.leaveEvent=leaveEvent
But when I move the mouse very quickly in and out of the button before the animation finishes, The button starts to move wierdly towards the North-West direction.
Button Animation Using Dynamic Positions
I figured out this was due to the leaveEvent() being triggered before enterEvent() even finishes and also because the start and end values are dynamic. So, I tried providing currentPos as a static position and using it instead,
#staticmethod
def addButtonHoverAnimation(button:QPushButton,currentPos:QPoint):
'''
Method to:
=> Add hover animation for provided button
'''
enterShift = QPropertyAnimation(button,b'pos',button)
enterShift.setStartValue(currentPos)
enterShift.setEndValue(QPoint(currentPos.x()+3,currentPos.y()+3))
enterShift.setDuration(100)
exitShift = QPropertyAnimation(button,b'pos',button)
exitShift.setStartValue(QPoint(currentPos.x()-3,currentPos.y()-3))
exitShift.setEndValue(currentPos)
exitShift.setDuration(100)
def enterEvent(e):
button.setProperty(b'pos',exitShift.endValue())
enterShift.start()
Effects.dropShadow(button,1,2)
def leaveEvent(e):
exitShift.start()
Effects.dropShadow(button)
button.enterEvent=enterEvent
button.leaveEvent=leaveEvent
On running, as soon as the mouse enters the QPushbutton, it moves to the top-left of its parent widget and the animation starts working fine. I can't figure out why this is happening. But I was able to get that, it only happened when I used any static value in the animation.
Button Animation with Static Position:
Here is an example:
import sys
from PyQt5.QtCore import QEvent, QPoint, QObject, QPropertyAnimation
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
# This is the same method mentioned above
from styling import addButtonHoverAnimation
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout=QVBoxLayout()
button1 = QPushButton("Proceed1", self)
layout.addWidget(button1)
button2 = QPushButton("Proceed2", self)
layout.addWidget(button2)
self.setLayout(layout)
self.resize(640, 480)
addButtonHoverAnimation(button1)
addButtonHoverAnimation(button2)
def main():
app = QApplication(sys.argv)
view = Widget()
view.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
The problem is that probably when the state is changed from enter to leave (or vice versa) the previous animation still does not end so the position of the widget is not the initial or final position, so when starting the new animation there is a deviation that accumulates. One possible solution is to initialize the position and keep it as a reference.
On the other hand you should not do x.fooMethod = foo_callable since many can fail, in this case it is better to use an eventfilter.
import sys
from dataclasses import dataclass
from functools import cached_property
from PyQt5.QtCore import QEvent, QPoint, QObject, QPropertyAnimation
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget
#dataclass
class AnimationManager(QObject):
widget: QWidget
delta: QPoint = QPoint(3, 3)
duration: int = 100
def __post_init__(self):
super().__init__(self.widget)
self._start_value = QPoint()
self._end_value = QPoint()
self.widget.installEventFilter(self)
self.animation.setTargetObject(self.widget)
self.animation.setPropertyName(b"pos")
self.reset()
def reset(self):
self._start_value = self.widget.pos()
self._end_value = self._start_value + self.delta
self.animation.setDuration(self.duration)
#cached_property
def animation(self):
return QPropertyAnimation(self)
def eventFilter(self, obj, event):
if obj is self.widget:
if event.type() == QEvent.Enter:
self.start_enter_animation()
elif event.type() == QEvent.Leave:
self.start_leave_animation()
return super().eventFilter(obj, event)
def start_enter_animation(self):
self.animation.stop()
self.animation.setStartValue(self.widget.pos())
self.animation.setEndValue(self._end_value)
self.animation.start()
def start_leave_animation(self):
self.animation.stop()
self.animation.setStartValue(self.widget.pos())
self.animation.setEndValue(self._start_value)
self.animation.start()
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
button1 = QPushButton("Proceed1", self)
button1.move(100, 100)
button2 = QPushButton("Proceed2", self)
button2.move(200, 200)
self.resize(640, 480)
animation_manager1 = AnimationManager(widget=button1)
animation_manager2 = AnimationManager(widget=button2)
def main():
app = QApplication(sys.argv)
view = Widget()
view.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
184 / 5000
Resultados de traducción
If you are using a layout then you must reset the position since the layout does not apply the position change immediately but only when the parent widget applies the changes.
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
button1 = QPushButton("Proceed1")
button2 = QPushButton("Proceed2")
lay = QVBoxLayout(self)
lay.addWidget(button1)
lay.addWidget(button2)
self.resize(640, 480)
self.animation_manager1 = AnimationManager(widget=button1)
self.animation_manager2 = AnimationManager(widget=button2)
def resizeEvent(self, event):
super().resizeEvent(event)
self.animation_manager1.reset()
self.animation_manager2.reset()
I need to execute a method whenever the user clicks the down arrow on the combo box. I've tried the signals listed in the documentations but none of the worked.
from PyQt5.QtWidgets import *
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
self.combo = QComboBox(self)
self.combo.signal.connect(self.mymethod)
self.show()
def mymethod(self):
print('hello world')
app = QApplication(sys.argv)
win = Window()
sys.exit(app.exec_())
There is no signal that is emitted when the down arrow is pressed but you can create override the mousePressEvent method and verify that this element was pressed:
import sys
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QStyle,
QStyleOptionComboBox,
QVBoxLayout,
QWidget,
)
class ComboBox(QComboBox):
arrowClicked = pyqtSignal()
def mousePressEvent(self, event):
super().mousePressEvent(event)
opt = QStyleOptionComboBox()
self.initStyleOption(opt)
sc = self.style().hitTestComplexControl(
QStyle.CC_ComboBox, opt, event.pos(), self
)
if sc == QStyle.SC_ComboBoxArrow:
self.arrowClicked.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
self.combo = ComboBox()
self.combo.arrowClicked.connect(self.mymethod)
lay = QVBoxLayout(self)
lay.addWidget(self.combo)
lay.setAlignment(Qt.AlignTop)
def mymethod(self):
print("hello world")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
I have a QPushButton that has a QIcon set. I would like to grayscale the icon's colors until it is hovered over or selected, without writing 2 separate image files for each icon. If use QPushButton.setDisabled(True) the icon's colors do in fact turn to grayscale, so I would like this same behavior being controlled through a enterEvent. Is this at all possible?
Yes, you can do exactly what you described. Enable the button in the enterEvent and disable it in the leaveEvent if it's not checked.
import sys
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *
class Button(QPushButton):
def __init__(self):
super().__init__()
self.setCheckable(True)
self.setDisabled(True)
self.setIcon(QIcon('icon.png'))
self.setIconSize(QSize(100, 100))
def enterEvent(self, event):
self.setEnabled(True)
def leaveEvent(self, event):
if not self.isChecked():
self.setDisabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
grid = QGridLayout(w)
grid.addWidget(Button(), 0, 0, Qt.AlignCenter)
w.show()
sys.exit(app.exec_())
I used a green check mark for the icon image. Result:
You also don't need to subclass QPushButton, this will work too.
btn = QPushButton()
btn.setCheckable(True)
btn.setIcon(QIcon('icon.png'))
btn.setIconSize(QSize(100, 100))
btn.setDisabled(True)
btn.enterEvent = lambda _: btn.setEnabled(True)
btn.leaveEvent = lambda _: btn.setEnabled(btn.isChecked())
I've seen a number of replies on SO regarding this matter but not specifically to QMenu and QToolButton. Would appreciate some pointers on aligning the popup widget to the right side of the button. Here's a basic code I'm working off..
import sys
from PyQt5.QtWidgets import *
class test(QWidget):
def __init__(self):
super().__init__()
self.resize(200, 100)
layout = QHBoxLayout(self)
label = QLabel('Testing QToolButton Popup')
toolbutton = QToolButton()
toolbutton.setPopupMode(QToolButton.InstantPopup)
widget = QWidget()
widgetLayout = QHBoxLayout(widget)
widgetLabel = QLabel('Popup Text')
widgetSpinbox = QSpinBox()
widgetLayout.addWidget(widgetLabel)
widgetLayout.addWidget(widgetSpinbox)
widgetAction = QWidgetAction(toolbutton)
widgetAction.setDefaultWidget(widget)
widgetMenu = QMenu(toolbutton)
widgetMenu.addAction(widgetAction)
toolbutton.setMenu(widgetMenu)
layout.addWidget(label)
layout.addWidget(toolbutton)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = test()
win.show()
sys.exit(app.exec_())
The outcome looks like this:
The Qt developer thought the default position was correct, so if you want to modify the alignment you must move the QMenu as I show below:
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMenu,
QSpinBox,
QToolButton,
QWidgetAction,
QWidget,
)
class Menu(QMenu):
def showEvent(self, event):
if self.isVisible():
button = self.parentWidget()
if button is not None:
pos = button.mapToGlobal(button.rect().bottomRight())
self.move(pos - self.rect().topRight())
super().showEvent(event)
class Test(QWidget):
def __init__(self):
super().__init__()
self.resize(200, 100)
layout = QHBoxLayout(self)
label = QLabel("Testing QToolButton Popup")
toolbutton = QToolButton(popupMode=QToolButton.InstantPopup)
widgetLabel = QLabel("Popup Text")
widgetSpinbox = QSpinBox()
widget = QWidget()
widgetLayout = QHBoxLayout(widget)
widgetLayout.addWidget(widgetLabel)
widgetLayout.addWidget(widgetSpinbox)
widgetAction = QWidgetAction(toolbutton)
widgetAction.setDefaultWidget(widget)
widgetMenu = Menu(toolbutton)
widgetMenu.addAction(widgetAction)
toolbutton.setMenu(widgetMenu)
layout.addWidget(label)
layout.addWidget(toolbutton)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Test()
win.show()
sys.exit(app.exec_())
I recently making a stoplight program (gui) and i can't figure it out which is wrong I dont know how to include the QTimer or any function for delay to make the color change and i tried the show function ended up getting two programs i really don't know how to fix my code yet can you please help me?
import PyQt5, sys, time,os
from os import system,name
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QPoint,QTimerEvent,QTimer,Qt
from PyQt5.QtWidgets import QWidget,QApplication,QMainWindow
from PyQt5.QtGui import QPainter
class Stoplight(QMainWindow):
def __init__(self,parent = None):
QWidget.__init__(self,parent)
self.setWindowTitle("Stoplight")
self.setGeometry(500,500,250,510)
def paintEvent(self,event):
radx = 50
rady = 50
center = QPoint(125,125)
p = QPainter()
p.begin(self)
p.setBrush(Qt.white)
p.drawRect(event.rect())
p.end()
p1 = QPainter()
p1.begin(self)
p1.setBrush(Qt.red)
p1.setPen(Qt.black)
p1.drawEllipse(center,radx,rady)
p1.end()
class Stoplight1(Stoplight):
def __init__(self,parent = None):
QWidget.__init__(self,parent)
self.setWindowTitle("Stoplight")
self.setGeometry(500,500,250,510)
def paintEvent(self,event):
radx = 50
rady = 50
center = QPoint(125,125)
p = QPainter()
p.begin(self)
p.setBrush(Qt.white)
p.drawRect(event.rect())
p.end()
p1 = QPainter()
p1.begin(self)
p1.setBrush(Qt.green)
p1.setPen(Qt.black)
p1.drawEllipse(center,radx,rady)
p1.end()
if __name__ == "__main__":
application = QApplication(sys.argv)
stoplight1 = Stoplight()
stoplight2 = Stoplight1()
time.sleep(1)
stoplight1.show()
time.sleep(1)
stoplight2.show()
sys.exit(application.exec_())
Although the response of #f.wue works apparently is not correct since you should not use time.sleep() in the GUI thread since it freezes the application, for example try to resize the window while running the time.sleep().
Instead you should use a QTimer as you say, that timer must connect to a function that changes the color and call the update() method that indirectly invokes the paintEvent()event. Since you want colors to change colors cyclically, you must create a cyclic iterator.
from itertools import cycle
from PyQt5 import QtCore, QtGui, QtWidgets
class TrafficLight(QtWidgets.QMainWindow):
def __init__(self,parent = None):
super(TrafficLight, self).__init__(parent)
self.setWindowTitle("TrafficLight ")
self.traffic_light_colors = cycle([
QtGui.QColor('red'),
QtGui.QColor('yellow'),
QtGui.QColor('green')
])
self._current_color = next(self.traffic_light_colors)
timer = QtCore.QTimer(
self,
interval=2000,
timeout=self.change_color
)
timer.start()
self.resize(200, 400)
#QtCore.pyqtSlot()
def change_color(self):
self._current_color = next(self.traffic_light_colors)
self.update()
def paintEvent(self, event):
p = QtGui.QPainter(self)
p.setBrush(self._current_color)
p.setPen(QtCore.Qt.black)
p.drawEllipse(self.rect().center(), 50, 50)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = TrafficLight()
w.show()
sys.exit(app.exec_())
This would be a hint in accomplishing it. You need to define some functions or objects to handle the different locations of the cycles. Furthermore use the function self.update()to call the paintEvent and application.processEvents() to make the changes visible in the gui. You can use the code in the execution part and generate a more sophisticated function out of it.
EDIT: Try this approach with a dictionary, as it contains less code.
import PyQt5, sys, time,os
from os import system,name
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QPoint,QTimerEvent,QTimer,Qt
from PyQt5.QtWidgets import QWidget,QApplication,QMainWindow
from PyQt5.QtGui import QPainter
class Stoplight(QMainWindow):
def __init__(self,parent = None):
QWidget.__init__(self,parent)
self.setWindowTitle("Stoplight")
self.setGeometry(500,500,250,510)
self.radx = 50
self.rady = 50
self.traffic_light_dic = {"red":{"QColor":Qt.red,
"center":QPoint(125,125)},
"yellow": {"QColor": Qt.yellow,
"center": QPoint(125, 250)},
"green": {"QColor": Qt.green,
"center": QPoint(125, 375)},
}
def switch_to_color(self, color):
self.center = self.traffic_light_dic[color]["center"]
self.color = self.traffic_light_dic[color]["QColor"]
self.update()
def paintEvent(self, event):
p = QPainter()
p.begin(self)
p.setBrush(self.color)
p.setPen(Qt.black)
p.drawEllipse(self.center, self.radx,self.rady)
p.end()
if __name__ == "__main__":
application = QApplication(sys.argv)
stoplight1 = Stoplight()
stoplight1.show()
time.sleep(2)
stoplight1.switch_to_color("red")
application.processEvents()
time.sleep(2)
stoplight1.switch_to_color("yellow")
application.processEvents()
time.sleep(2)
stoplight1.switch_to_color("green")
application.processEvents()
sys.exit(application.exec_())