I have a QGraphicsView where I do various mouse operations and while doing mouse operations I change mouse cursor to give user visual clues.
I recently put some QWidgets onto my scene which automatically creates QGraphicsProxyWidget objects. And after putting these widgets I started having problems with my mouse cursor changes. I can change mouse cursor until I hover over a QGraphicsProxyWidget item. After that, my mouse cursor changes stop taking effect.
I created a minimal example in PySide2:
import sys
from PySide2.QtGui import QMouseEvent, Qt
from PySide2.QtWidgets import QApplication, QLabel, QGraphicsView, QGraphicsScene
class CustomGraphicsView(QGraphicsView):
def mousePressEvent(self, event: QMouseEvent):
super().mousePressEvent(event)
self.setCursor(Qt.ClosedHandCursor)
event.accept()
def mouseReleaseEvent(self, event: QMouseEvent):
super().mouseReleaseEvent(event)
self.setCursor(Qt.ArrowCursor)
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
graphics_view = CustomGraphicsView()
scene = QGraphicsScene(0, 0, 1000, 1000)
graphics_view.setScene(scene)
label = QLabel("Hello World")
label_font = label.font()
label_font.setPointSize(50)
label.setFont(label_font)
label_proxy = scene.addWidget(label)
label_proxy.setPos(400, 450)
graphics_view.show()
app.exec_()
To reproduce: first, click on anywhere in the scene without hovering your mouse over the "Hello World" text. You will see mouse cursor changing into a closed hand. Then hover your mouse over "Hello World" text and try again clicking anywhere on scene. You will see that the cursor is not changing anymore.
I am not sure if this is an expected behavior or bug. What might be the reason for this behavior?
System
OS: Windows 10 20H2
Python 3.8.6 64-bit
PySide2 5.15.2
Disclaimer: Still investigating the cause of that behavior so at the time of writing this answer I will only give the solution. But it seems is that the propagation of the events is not handled correctly by the QGraphicsProxyWidget. See this bug for more information.
Instead of setting the cursor in the QGraphicsView it should be set in the viewport.
class CustomGraphicsView(QGraphicsView):
def mousePressEvent(self, event: QMouseEvent):
super().mousePressEvent(event)
self.viewport().setCursor(Qt.ClosedHandCursor)
event.accept()
def mouseReleaseEvent(self, event: QMouseEvent):
super().mouseReleaseEvent(event)
self.viewport().unsetCursor()
event.accept()
Related
I'm using pyqt5 to create some sort of an audio editor for my own needs and I need to be able to handle the mouse wheel and mouse clicks and release on the waveform to zoom in or to set a region that will be cut. Here is a shortened code that displays just the Plotwidget.
from PyQt5.QtWidgets import QApplication,QWidget, QMainWindow,QHBoxLayout
from pyqtgraph import PlotWidget
import pyqtgraph as pg
import numpy as np
import sys
class MainWidget(QMainWindow):
def __init__(self, parent=None):
super(MainWidget, self).__init__()
self.resize(400,200)
self.setMouseTracking(True)
self.graphWidget = PlotWidget()
self.graphWidget.setMouseEnabled(x=False, y=False)
data = np.random.randn(10)
self.graphWidget.plot(data)
myLayout = QHBoxLayout()
myLayout.addWidget(self.graphWidget)
centralWidget = QWidget(self)
self.setCentralWidget(centralWidget)
centralWidget.setLayout(myLayout)
def mouseReleaseEvent(self, event):
#this is not working
print("mouse released")
def mousePressEvent(self, event):
#this is working
print("mousepressed")
def wheelEvent(self,event):
#this is not working
print("wheelevent?")
def window():
app = QApplication(sys.argv)
win = MainWidget()
win.show()
sys.exit(app.exec_())
window()
I know that some of these features are already built in pyqtgraph, but some are not working well for me. For example, the vertical zoom uses the pointer as the origin instead of 0 and does not keep a vertically symmetric view as it should be for audio waveform representation. Another example is that even though I was able with proxies and eventfilters to get the mouse position, I was not able to detect the mouse release and thus could not create a rectangular section using a ROI, I could start it but never let it go of my cursor.
Just some additional info, the mouse wheel WAS working properly with a previous version of pyqt5 and pyqtgraph (not the mouse release though), but I had to install everything a new machine and it does not work anymore... I'm using PYQT5.15.6 and PyqtGraph0.12.4
I strongly suspect I have to create an event filter somewhere, but I have ran out of options on where... the Plotwidget, the plotitem inside it, nothing works...
Does someone have an idea to get access to the mouse events again on that small example?
Nevermind, I found the solution in another question here: pyqtgraph disable parent widget scrolling while mouse hovering on/is used from sub-widget
If I just create a subclass of the plotwidget first and create all the events in it, it works perfectly!
Here is the code in case it helps someone else.
from PyQt5.QtWidgets import QApplication,QWidget, QMainWindow,QHBoxLayout
from pyqtgraph import PlotWidget
import pyqtgraph as pg
import numpy as np
import sys
class MyPlotWidget(pg.PlotWidget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def wheelEvent(self,event):
#this works
print("wheel event")
def mouseReleaseEvent(self, event):
#this works
print("mouse released")
def mousePressEvent(self, event):
#this works
print("mousepressed")
class MainWidget(QMainWindow):
def __init__(self, parent=None):
super(MainWidget, self).__init__()
self.resize(400,200)
self.setMouseTracking(True)
self.graphWidget = MyPlotWidget()
self.graphWidget.setMouseEnabled(x=False, y=False)
data = np.random.randn(10)
self.graphWidget.plot(data)
myLayout = QHBoxLayout()
myLayout.addWidget(self.graphWidget)
centralWidget = QWidget(self)
self.setCentralWidget(centralWidget)
centralWidget.setLayout(myLayout)
def mouseReleaseEvent(self, event):
#this is not working
print("mouse released")
def mousePressEvent(self, event):
#this is working
print("mousepressed")
def wheelEvent(self,event):
#this is not working
print("wheelevent?")
def window():
app = QApplication(sys.argv)
win = MainWidget()
win.show()
sys.exit(app.exec_())
window()
There is not even a need for event filters or proxies...
I am trying to create a custom image viewer widget with zoom to the mouse position. So far I have managed to detect mouse scroll events, but I cannot detect a mouse hover events so I could determine a mouse position to zoom to.
I seems to me that the mouse hover event is not even occurring. I tried to print out all events, but the QHoverEvent is just not there. The only event occurring during mouse hover is QEvent::ToolTip which has the mouse position but it only occurs after the mouse hovering stops and it has quite a delay (~0.5s).
Here is the code:
import sys
from PySide6 import QtWidgets
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel
from PySide6.QtGui import QPixmap
from PySide6.QtCore import Qt
from PIL.ImageQt import ImageQt
class ImageViewer(QDialog):
def eventFilter(self, object, event):
print("Event:" + str(event))
if str(event) == '<PySide6.QtGui.QWheelEvent(Qt::NoScrollPhase, pixelDelta=QPoint(0,0), angleDelta=QPoint(0,-120))>':
print("detected zoom out")
if str(event) == '<PySide6.QtGui.QWheelEvent(Qt::NoScrollPhase, pixelDelta=QPoint(0,0), angleDelta=QPoint(0,120))>':
print("detected zoom in")
if str(event) == '<PySide6.QtCore.QEvent(QEvent::ToolTip)>':
print("detected tooltip")
return True
def __init__(self, img: ImageQt):
super().__init__()
self.setWindowTitle('Image viewer example')
self.imageWidget = QLabel()
self.imageWidget.setAlignment(Qt.AlignCenter)
self.imageWidget.setPixmap(QPixmap.fromImage(img))
self.layout = QVBoxLayout()
self.layout.addWidget(self.imageWidget)
self.setLayout(self.layout)
self.imageWidget.installEventFilter(self)
if __name__ == '__main__':
# prepare app
app = QtWidgets.QApplication(sys.argv)
# create viewer widget
imageViewer = ImageViewer(ImageQt("img.png"))
imageViewer.show()
# close app
sys.exit(app.exec())
I am able to detect the mouse scrolling, enter widget, leave, mouse button press/release, mouse move (with mouse pressed). But the mouse hover is just not there. Could someone tell me how to detect the mouse hover event (with mouse position info)?
It works for me with self.setAttribute(Qt.WidgetAttribute.WA_Hover) in the __init__() constructor.
I'm building a custom combobox, from a (subclassed) QLineEdit and QListWidget for the dropdown menu
I'm setting the window flags to QTool so that its a floating window but doesnt steal focus from the lineedit (since the user needs to be able to input text to filter the list). This works fine but the list is now completely detached from the parent widget, so I can drag the top menu bar and move it away from the list which I don't want.
Is there a way to use QTool or QTooltip but keep it parented to a widget?
One other method would be setting the window flags to QPopup, in which case the popup closes when the top menu bar is clicked so cannot be dragged away. However with QPopup it steals focus from the line edit
Below is a simple example illustrating the issue:
from PySide2 import QtCore, QtWidgets, QtGui
import sys
class LineEditClickable(QtWidgets.QLineEdit):
"""Custom QLineEdit to detect clicked, focus and key events
Signals: clicked, focusOut, arrowUp, arrowDown
"""
clicked = QtCore.Signal(QtGui.QMouseEvent)
def __init__(self, value=''):
super(LineEditClickable, self).__init__(value)
# remove border on Mac
self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 0)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
def mousePressEvent(self, event):
"""Emit clicked signal"""
self.clicked.emit(event)
super(LineEditClickable, self).mousePressEvent(event)
class popup(QtWidgets.QWidget):
def __init__(self, parent = None, widget=None):
QtWidgets.QWidget.__init__(self, parent)
layout = QtWidgets.QVBoxLayout(self)
self.list = QtWidgets.QListWidget()
layout.addWidget(self.list)
# adjust the margins or you will get an invisible, unintended border
layout.setContentsMargins(0, 0, 0, 0)
self.adjustSize()
# tag this widget as a popup
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool)
# self.setWindowFlags(QtCore.Qt.Popup)
def update(self, widget):
# calculate the botoom right point from the parents rectangle
point = widget.rect().bottomRight()
# map that point as a global position
global_point = widget.mapToGlobal(point)
# by default, a widget will be placed from its top-left corner, so
# we need to move it to the left based on the widgets width
self.move(global_point - QtCore.QPoint(self.width(), 0))
def show_popup(self, widget):
self.update(widget)
self.show()
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.le = LineEditClickable(self)
self.le.clicked.connect(self.handleOpenDialog)
self.le.move(250, 50)
self.resize(600, 200)
self.popup = popup(self, self.le)
self.popup.list.addItems(['one','two','three'])
def handleOpenDialog(self):
self.popup.show_popup(self.le)
self.popup.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())```
The basic answer to your question is to use the correct flags and focus options.
If you look at how QCompleter implements setPopup(), you'll see the following:
popup->setWindowFlag(Qt::Popup);
popup->setFocusPolicy(Qt::NoFocus);
[...]
popup->setFocusProxy(d->widget);
As you've already experienced, Tool is not a good option: while avoids stealing focus from the line edit, it also has issues with any mouse click that happens outside the UI.
If you still want to use Tool, you could update the widget position, by installing an event filter on the top level window of the line edit and intercept its move events, but it's not guaranteed that it works and totally depends on the platform you're using it. For example, on certain Linux window managers you only receive it when the mouse is released after dragging the window.
class popup(QtWidgets.QWidget):
_widget = None
_window = None
# ...
def show_popup(self, widget):
if self._window:
self._window.removeEventFilter(self)
self.update(widget)
self.show()
self._widget = widget
self._window = widget.window()
self._window.installEventFilter(self)
def hideEvent(self, event):
if self._window:
self._window.removeEventFilter(self)
def closeEvent(self, event):
if self._window:
self._window.removeEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Move:
self.update(self._widget)
return super().eventFilter(source, event)
Frankly, I'd suggest you to use what Qt already provides you without trying to reinvent the wheel. In your case, use a QCompleter and reimplement what you need for it's popup.
Note that if you want to show all items when the line edit gets focus and there's no text yet, you could change the completion mode.
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
# ...
self.textChanged.connect(self.showCompleter)
def showCompleter(self):
completer = self.completer()
if not completer:
return
if not self.text():
completer.setCompletionMode(completer.UnfilteredPopupCompletion)
else:
completer.setCompletionMode(completer.PopupCompletion)
completer.complete()
You might want to do the same also in the keyPressEvent override, after calling the base class implementation and ensuring that the popup is not yet visible.
I try to change cursor icon when the Ctrl button is pressed. The problem is, that if you press and hold down a button, the keyPressEvent method is not called. If you press two buttons at once, e.g. Ctrl+Shift, then it works as expected.
Test environment: system SUSE 12.2, python 3.6.4, pyqt5 5.11.3 (also tested pyqt5 5.9.2).
This is a minimal (not)working example:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication, QWidget
class Example(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 300, 220)
self.show()
def keyPressEvent(self, event):
print('Press')
def keyReleaseEvent(self, event):
print('Release')
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Once pressing and holding Ctrl button, the 'Press' should be printed. However, it is not until you release the button or click another one simultaneously.
I noticed different behavior for different keys. Both left and right Ctrl behave the same, also the left Shift, but the right Shift behaves as desired.
def keyPressEvent(self, event):
print('Press')
QApplication.setOverrideCursor(Qt.WaitCursor)
def keyReleaseEvent(self, event):
print('Release')
QApplication.restoreOverrideCursor()
I'm working though the pyqt5 tutorial found here Zetcode, PyQt5
As an exercise for myself I'm trying to expand on an example so that I am presented with the same dialog message box regardless of method used to close the app:
clicking the 'X' button in the title bar (works as intended)
clicking the 'Close' button (produces attribute error)
pressing the 'escape' key (works but not sure how/why)
The dialog message box is implemented in the closeEvent method, full script provided at the end.
I'm having two issues:
1. When clicking 'Close' button, instead of just quitting, I want to call closeEvent method including message box dialog.
I have replaced a line of the example code for the 'Close' push button:
btn.clicked.connect(QCoreApplication.instance().quit)
And instead am trying to call the closeEvent method which already implements the dialog I want:
btn.clicked.connect(self.closeEvent)
However when i run the script and click the 'Close' button and select the resulting 'Close' option in the dialog i get the following:
Traceback (most recent call last):
File "5-terminator.py", line 41, in closeEvent
event.accept()
AttributeError: 'bool' object has no attribute 'accept'
Aborted
Can anyone advise what I'm doing wrong and what needs to be done here?
2. When hitting the escape key somehow the message box dialog is presented and works just fine.
Ok, it's great that it works, but I'd like to know how and why the message box functionality defined in CloseEvent method is called within the keyPressEvent method.
Full script follows:
import sys
from PyQt5.QtWidgets import (
QApplication, QWidget, QToolTip, QPushButton, QMessageBox)
from PyQt5.QtCore import QCoreApplication, Qt
class Window(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn = QPushButton("Close", self)
btn.setToolTip("Close Application")
# btn.clicked.connect(QCoreApplication.instance().quit)
# instead of above button signal, try to call closeEvent method below
btn.clicked.connect(self.closeEvent)
btn.resize(btn.sizeHint())
btn.move(410, 118)
self.setGeometry(30, 450, 500, 150)
self.setWindowTitle("Terminator")
self.show()
def closeEvent(self, event):
"""Generate 'question' dialog on clicking 'X' button in title bar.
Reimplement the closeEvent() event handler to include a 'Question'
dialog with options on how to proceed - Save, Close, Cancel buttons
"""
reply = QMessageBox.question(
self, "Message",
"Are you sure you want to quit? Any unsaved work will be lost.",
QMessageBox.Save | QMessageBox.Close | QMessageBox.Cancel,
QMessageBox.Save)
if reply == QMessageBox.Close:
event.accept()
else:
event.ignore()
def keyPressEvent(self, event):
"""Close application from escape key.
results in QMessageBox dialog from closeEvent, good but how/why?
"""
if event.key() == Qt.Key_Escape:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Window()
sys.exit(app.exec_())
Hope someone can take the time to enlighten me.
Your second question answers the first question.
The reimplemented keyPressEvent method calls close(), which sends a QCloseEvent to the widget. Subsequently, the widget's closeEvent will be called with that event as its argument.
So you just need to connect the button to the widget's close() slot, and everything will work as expected:
btn.clicked.connect(self.close)
Unlike the X button your custom button does not seem to pass an close event just a bool. That's why this exercise should work for the X button but not a normal button. In any case, for your first question you might use destroy() and pass instead (of accept and ignore) just like this:
import sys
from PyQt5.QtWidgets import (
QApplication, QWidget, QToolTip, QPushButton, QMessageBox)
from PyQt5.QtCore import QCoreApplication, Qt
class Window(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn = QPushButton("Close", self)
btn.setToolTip("Close Application")
# btn.clicked.connect(QCoreApplication.instance().quit)
# instead of above button signal, try to call closeEvent method below
btn.clicked.connect(self.closeEvent)
btn.resize(btn.sizeHint())
btn.move(410, 118)
self.setGeometry(30, 450, 500, 150)
self.setWindowTitle("Terminator")
self.show()
def closeEvent(self, event):
"""Generate 'question' dialog on clicking 'X' button in title bar.
Reimplement the closeEvent() event handler to include a 'Question'
dialog with options on how to proceed - Save, Close, Cancel buttons
"""
reply = QMessageBox.question(
self, "Message",
"Are you sure you want to quit? Any unsaved work will be lost.",
QMessageBox.Save | QMessageBox.Close | QMessageBox.Cancel,
QMessageBox.Save)
if reply == QMessageBox.Close:
app.quit()
else:
pass
def keyPressEvent(self, event):
"""Close application from escape key.
results in QMessageBox dialog from closeEvent, good but how/why?
"""
if event.key() == Qt.Key_Escape:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Window()
sys.exit(app.exec_())
For your second question Qt has default behaviors depending on the Widget (Dialogs might have another, try pressing the Esc key when your Message Dialog is open just to see). When you do need to override the Esc behavior you might try this:
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
print("esc")
As you'll eventually see in ZetCode.
closeEvent()
In above right tick mark code please check the closeEvent it is same other wise it's waste of time to research.