I have a context menu with a checkbox and a spinbox. Both work as expected, except that when I am typing a number in the spinbox, I tend to hit enter to close the menu, but that also toggles the checkbox. Does anyone know how to prevent that?
minimal example:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QFrame, QMenu, QAction, QWidgetAction, QFormLayout, QSpinBox
from PyQt5.QtCore import Qt
t = True
v = 0
class SpinAction(QWidgetAction):
def __init__(self, parent):
super(SpinAction, self).__init__(parent)
w = QWidget()
layout = QFormLayout(w)
self.spin = QSpinBox()
layout.addRow('value', self.spin)
w.setLayout(layout)
self.setDefaultWidget(w)
def _menu(position):
menu = QMenu()
test_action = QAction(text="Test", parent=menu, checkable=True)
test_action.setChecked(t)
test_action.toggled.connect(toggle_test)
spin_action = SpinAction(menu)
spin_action.spin.setValue(v)
spin_action.spin.valueChanged.connect(spin_changed)
menu.addAction(test_action)
menu.addAction(spin_action)
action = menu.exec_(w.mapToGlobal(position))
def toggle_test(val):
global t
t = val
def spin_changed(val):
global v
v = val
app = QApplication(sys.argv)
w = QFrame()
w.setContextMenuPolicy(Qt.CustomContextMenu)
w.customContextMenuRequested.connect(_menu)
w.show()
sys.exit(app.exec_())
The widget must accept focus, so a proper focus policy (StrongFocus or WheelFocus) must be set, but since it's a container you should also set the focus proxy to the spin, so that it will handle the focus for it.
In this way the keyboard navigation of the menu will also work properly by using arrows or Tab to focus the spinbox.
The action also has to be set as the active action of the menu in order to prevent the menu to accept events that may trigger other actions, and a way to achieve that is to install an event filter on the spin and check for events that should make the widget action as the active one, like FocusIn and Enter event.
Note that this results in some unintuitive results from the UX perspective: by default, hovering a normal QAction makes it the active one, but the user might want to move the mouse away from the spinbox in order to edit it without having the mouse on it, and by doing it another action could become active; in the meantime the spin should still keep the focus for obvious reasons. The result is that the other action might seem visually selected while the spinbox has focus. There is no obvious solution for this, and that's why widgets used for QWidgetActions should always be chosen with care.
class SpinAction(QWidgetAction):
FocusEvents = QtCore.QEvent.FocusIn, QtCore.QEvent.Enter
ActivateEvents = QtCore.QEvent.KeyPress, QtCore.QEvent.Wheel)
WatchedEvents = FocusEvents + ActivateEvents
def __init__(self, parent):
# ...
w.setFocusPolicy(self.spin.focusPolicy())
w.setFocusProxy(self.spin)
self.spin.installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.spin and event.type() in self.WatchedEvents:
if isinstance(self.parent(), QtWidgets.QMenu):
self.parent().setActiveAction(self)
if event.type() in self.FocusEvents:
self.spin.setFocus()
return super().eventFilter(obj, event)
Note: the obj == self.spin condition is required since QWidgetAction already installs an event filter. A more elegant solution is to create a QObject subclass intended for that purpose only, and only overriding eventFilter().
Related
I have a small GUI app made using pyqt5.
I found a strange problem while using eventFilter...
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
# RETURN
if event.key() == QtCore.Qt.Key_Return:
if source is self.userLineEdit:
self.pswLineEdit.setFocus()
elif source is self.pswLineEdit:
self.LoginButton.click()
# TAB
elif event.key() == QtCore.Qt.Key_Tab:
if source is self.userLineEdit:
self.pswLineEdit.setFocus()
return super().eventFilter(source, event)
While pressing enter key just behave normally, tab key does not.
I don't know where the problem could be. I'm going to link a video to show the exact problem as I'm not able to describe how this is not working
Link to video
I know it's pixelated (sorry) but the important thing is the behavior of the cursor
SMALL EXAMPLE
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit
from PyQt5 import QtCore
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'Hello, world!'
self.left = 10
self.top = 10
self.width = 640
self.height = 480
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.userEdit = QLineEdit(self)
self.pswEdit = QLineEdit(self)
self.userEdit.setPlaceholderText("Username")
self.pswEdit.setPlaceholderText("Password")
self.userEdit.installEventFilter(self)
self.pswEdit.installEventFilter(self)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.userEdit)
mainLayout.addWidget(self.pswEdit)
self.setLayout(mainLayout)
self.show()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.KeyPress:
# RETURN
if event.key() == QtCore.Qt.Key_Return:
if source is self.userEdit:
self.pswEdit.setFocus()
# TAB
elif event.key() == QtCore.Qt.Key_Tab:
if source is self.userEdit:
self.pswEdit.setFocus()
return super().eventFilter(source, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
If you don't filter events, they are processed by the object and eventually propagated back to the parent.
By default, QWidget will try to focus the next (or previous, for Shift+Tab) child widget in the focus chain, by calling its focusNextPrevChild(). If it can do it, it will actually set the focus on that widget, otherwise the event is ignored and propagated to the parent.
Since most widgets (including QLineEdit) don't handle the tab keys for focus changes on their own, as they don't have children, their parent will receive it, which will call focusNextPrevChild() looking for another child widget, and so on, up to the object tree, until a widget finally can handle the key, eventually changing the focus.
In your case, this is what's happening:
you check events and find that the tab key event is received by the first line edit;
you set the focus on the other line edit, the password field;
you let the event be handled anyway by the widget, since you're not ignoring or filtering it out;
the first line edit calls focusNextPrevChild() but is not able to do anything with it;
the event is propagated to the parent, which then calls its own focusNextPrevChild();
the function checks the current focused child widget, which is the password field you just focused, and finds the next, which coincidentally is the first line edit, which gets focused again;
The simple solution is to just add return True after changing the focus, so that the event doesn't get propagated to the parent causing a further focus change:
if event.key() == QtCore.Qt.Key_Tab:
if source is self.userEdit:
self.pswEdit.setFocus()
return True
Note that overriding the focus behavior is quite complex, and you have to be very careful about how focus and events are handled, especially for specific widgets that might deal with events in particular ways (studying the Qt sources is quite useful for this), otherwise you'll get unexpected behavior or even fatal recursion.
For instance, there's normally no need for an event filter for the return key, as QLineEdit already provides the returnPressed signal:
self.userEdit.returnPressed.connect(self.pswEdit.setFocus)
Qt already has a quite thorough focus management system, if you just want more control over the way the tab chain works use existing functions like setTabOrder() on the parent or top level window, and if you want to have more control over how (or if) they get it, use setFocusPolicy().
Using PyQt5 I am viewing an image in a QGraphicsView. I want to be able to zoom in/out while pressing ctrl and using the mouse wheel. I have this working, however if the image is too large, and there are scroll bars, it ignores the zoom functionality until you scroll to the top or bottom.
How can I fix this to where it does not scroll when ctrl is pressed, while allowing it to zoom in/out.
from PyQt5.QtWidgets import QFileDialog, QLineEdit, QWidget, QPushButton, QApplication, QVBoxLayout, QLabel, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene
from PyQt5.QtCore import pyqtSignal, Qt
from pdf2image import convert_from_path
from PIL import ImageQt
import sys
class step1(QWidget):
changeViewSignal = pyqtSignal()
def __init__(self, parent=None):
super(step1, self).__init__(parent)
self.name = QLineEdit(self)
self.fileBtn = QPushButton("Select file", self)
self.nextBtn = QPushButton("Next", self)
self.graphicsView = QGraphicsView()
# self.graphicsView.setFrameShadow(QFrame.Raised)
# self.graphicsView.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.graphicsView.setHorizontalScrollBarPolicy()
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.name)
self.layout.addWidget(self.fileBtn)
self.layout.addWidget(self.nextBtn)
self.layout.addWidget(self.graphicsView)
self.fileBtn.clicked.connect(self.convert_file)
def wheelEvent(self, event):
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ControlModifier:
self.graphicsView.scrollContentsBy(0,0)
x = event.angleDelta().y() / 120
if x > 0:
self.graphicsView.scale(1.05, 1.05)
elif x < 0:
self.graphicsView.scale(.95, .95)
def convert_file(self):
fname = QFileDialog.getOpenFileName(self, 'Open File', 'c:\\', "PDF Files (*.pdf)")
if len(fname[0]) > 0:
pages = convert_from_path(fname[0])
images = []
qimage = ImageQt.toqpixmap(pages[0])
item = QGraphicsPixmapItem(qimage)
scene = QGraphicsScene(self)
scene.addItem(item)
self.graphicsView.setScene(scene)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = step1()
ex.show()
sys.exit(app.exec_())
The scrolling is first handled by the QGraphicsView before it would be propagated up to the parent widget where you are reimplementing the wheelEvent. This is why the scrolling occurs according to the normal QGraphicsView behavior when it has space to scroll.
A solution is to subclass QGraphicsView and reimplement the wheelEvent there instead.
class GraphicsView(QGraphicsView):
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.scale(1.05, 1.05)
elif x < 0:
self.scale(.95, .95)
else:
super().wheelEvent(event)
Then use the subclass name here:
self.graphicsView = GraphicsView()
Besides the proper solution proposed by alec, there's also the option of using an event filter, which can be useful for UIs created in Designer without the need of using promoted widgets.
The important aspect to keep in mind is that the event filter must be installed on the view's viewport() (the widget in which the contents of the scene are actually rendered and, possibly, scrolled), because that is the widget that will receive the wheel event: input events are always sent to the widget under the mouse (or has keyboard focus)[1], and possibly propagated to their parents if the event is not handled[2].
class step1(QWidget):
def __init__(self, parent=None):
# ...
self.graphicsView.viewport().installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == event.Wheel and event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.graphicsView.scale(1.05, 1.05)
elif x < 0:
self.graphicsView.scale(.95, .95)
return True
return super().eventFilter(source, event)
Returning True means that the viewport has handled the event, and it should not be propagated to the parent; QGraphicsView is based on QAbstractScrollArea, and if a wheel event is not handled by the viewport it will call the base wheelEvent implementation of the viewport's parent (the graphics view), which by default will post the event to the scroll bars. If the filter returns True, it will avoid that propagation, thus preventing scrolling.
Note that you should not use scrollContentsBy unless you really know what you're doing; as the documentation explains: «Calling this function in order to scroll programmatically is an error, use the scroll bars instead».
[1] Mouse events are always sent to the topmost widget under the mouse, unless a modal child window is active, or there is a mouse grabber, which is a widget that has received a mouse button press event but didn't receive a mouse button release event yet, or a widget on which grabMouse() was explicitly called. Keyboard events are always sent to the widget of the active window that has current focus, or the widget on which grabKeyboard() has been explicitly called.
[2] "handled event" can be a confusing concept: it doesn't mean that the widget actually "does" something with the event, nor that it doesn't, no matter if the event then becomes accepted or ignored. A widget could "ignore" an event and still react to it in some way: for instance, you might need to notify the user that the event has been received, but let the parent manage it anyway.
I have a few toggleable QPushButtons that I need to add to all tabs of a QTabWidget. Each tab should track the current state of each toggleable QPushButton which is why I am adding the same object instead of creating new buttons. The position of the widgets doesn't really matter, as long as I have the same tbutton object on all tabs. (i.e. when tbutton is enabled on Tab 1, it should be enabled on Tabs 2 and 3).
Below is the code I'm using. Note that in this example I'm only showing one toggleable QPushButton.
import sys
from PyQt5.QtWidgets import QTabWidget, QPushButton, QApplication, QWidget, QHBoxLayout
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = QTabWidget()
tbutton = QPushButton("foo")
tbutton.setCheckable(True)
w1 = QWidget()
layout1 = QHBoxLayout(w1)
layout1.addWidget(QPushButton("bar"))
layout1.addWidget(tbutton)
w2 = QWidget()
layout2 = QHBoxLayout(w2)
layout2.addWidget(QPushButton("baz"))
layout2.addWidget(tbutton)
tabW.addTab(w1, "Tab 1")
tabW.addTab(w2, "Tab 2")
tabW.addTab(tbutton, "Tab 3")
tabW.show()
sys.exit(app.exec_())
When the app is run, only Tab 3 contains tbutton. While it is possible to always show a single button using QTabWidget's setCornerWidget method, it is more difficult to modify the layout when the corner widget added is more complicated (i.e. more toggleable buttons).
Widgets cannot be "shared" between parents and/or shown multiple times. Every time you add a widget to a new layout, then it will be removed from the previous.
While you could reparent the widget by adding it to the layout everytime the tab is changed, it's really not a practical solution, especially if complex layout systems are involved, and reparenting is normally used only in very specific cases where it's really important to use the same instance elsewhere due to its complex state (for instance, a toolbar). Since you're just using a button, there's little point in reusing the same instance.
A more logical and safe solution is to create a button for every tab and link their state using signals, then if you also need to connect the toggled signal to another function, do it to just to one of them (not all):
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = QTabWidget()
w1 = QWidget()
layout1 = QHBoxLayout(w1)
layout1.addWidget(QPushButton("bar"))
tbutton1 = QPushButton("foo", checkable=True)
layout1.addWidget(tbutton1)
w2 = QWidget()
layout2 = QHBoxLayout(w2)
layout2.addWidget(QPushButton("baz"))
tbutton2 = QPushButton("foo", checkable=True)
layout2.addWidget(tbutton2)
tbutton3 = QPushButton("foo", checkable=True)
tabW.addTab(w1, "Tab 1")
tabW.addTab(w2, "Tab 2")
tabW.addTab(tbutton3, "Tab 3")
buttons = tbutton1, tbutton2, tbutton3
def toggleButtons(state):
for button in buttons:
button.setChecked(state)
for button in buttons:
button.toggled.connect(toggleButtons)
tabW.show()
sys.exit(app.exec_())
The above indication is because the code as it's written triggers some level of recursion, as the signal will be emitted for all setChecked calls on buttons that have a different state. Since Qt is well written, all state changes emit signals only when the new state is actually different, but if you want to do things more correctly, just block the signals temporarily:
def toggleButtons(state):
for button in buttons:
blocked = button.blockSignals(True)
button.setChecked(state)
button.blockSignals(blocked)
But, the best solution is to use QSignalBlocker which does almost the same thing, but in a safer fashion:
def toggleButtons(state):
for button in buttons:
with QtCore.QSignalBlocker(button):
button.setChecked(state)
This has a catch: since the signal will then be only emitted once, for the button that has actually triggered, if you need to do something else when the state changes, you either do it in toggleButtons or you connect the function to all buttons.
If you want to separate the logic of "group toggling" from the actual function that reacts to the state change, a better solution is to use a custom signal, but you must use a class.
class TabWidget(QTabWidget):
stateChanged = QtCore.pyqtSignal(bool)
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = TabWidget()
# ...
buttons = tbutton1, tbutton2, tbutton3
def toggleButtons(state):
for button in buttons:
with QtCore.QSignalBlocker(button):
button.setChecked(state)
tabW.stateChanged.emit(state)
for button in buttons:
button.toggled.connect(toggleButtons)
def doSomething(state):
print('state changed', state)
tabW.stateChanged.connect(doSomething)
tabW.show()
sys.exit(app.exec_())
Please consider that this "procedural" flow is fine for educational purposes, but using a class and implement both UI and logic in it is usually a better practice, and the above functions should then become methods in that class so that they can act in the context of the instace.
I have a QMainWindow application that has multiple widgets (buttons, labels, etc.) inside it.
How can I get an event when the user presses ANYWHERE of the app?
I tried to customize mousePressEvent() function, but this doesn't accept the event when other widgets (buttons, labels, etc.) are pressed.
Explanation:
The handling of mouse events between the widgets goes from children to parents, that is, if the child does not accept the event (it does not use it) then it will pass the event to the parent. For example, if you press on the QPushButton, it accepts the event and the parent is not notified, unlike QLabel that does not consume it, so the event passes to the parent.
Tools:
On the other hand, there are several methods to listen to events in general, such as:
Override any method like mousePressEvent, keyPressEvent, etc or the event or customEvent method.
Use an eventFilter.
Solution:
An alternative is to apply some previous method to all the widgets but you can discard all of them for the current objective, for example:
In the first and second method it would involve detecting when a widget is added or removed (using QEvent::ChildAdded or QEvent::ChildRemoved).
In the first method it would imply override the methods that many times is impossible.
With the above, the problem is attacked on the widgets side, but there are other alternatives:
Override the notify() method of Q{Core, GUi,}Application, verify that it is a widget and that it belongs to the window, it also implies discriminating if the event has already been consumed.
Listen to the mouse event associated with the window (QWindow).
In this case the most reasonable is the second method.
import sys
from PyQt5 import QtCore, QtWidgets
class MouseObserver(QtCore.QObject):
pressed = QtCore.pyqtSignal(QtCore.QPoint)
released = QtCore.pyqtSignal(QtCore.QPoint)
moved = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, window):
super().__init__(window)
self._window = window
self.window.installEventFilter(self)
#property
def window(self):
return self._window
def eventFilter(self, obj, event):
if self.window is obj:
if event.type() == QtCore.QEvent.MouseButtonPress:
self.pressed.emit(event.pos())
elif event.type() == QtCore.QEvent.MouseMove:
self.moved.emit(event.pos())
elif event.type() == QtCore.QEvent.MouseButtonRelease:
self.released.emit(event.pos())
return super().eventFilter(obj, event)
class MainWindow(QtWidgets.QMainWindow):
pass
def main(args):
app = QtWidgets.QApplication(args)
w = MainWindow()
w.show()
mouse_observer = MouseObserver(w.window().windowHandle())
mouse_observer.pressed.connect(lambda pos: print(f"pressed: {pos}"))
mouse_observer.released.connect(lambda pos: print(f"released: {pos}"))
mouse_observer.moved.connect(lambda pos: print(f"moved: {pos}"))
app.exec_()
if __name__ == "__main__":
main(sys.argv)
I am building a PyQt application which is supposed to receive mouse right-click drags on a QGraphicsView, draw a "lasso" (a line stretching from the drag origin to the mouse position, and a circle at the mouse position), and then, on mouse release, erase the lasso graphics and display an input dialog for the next part of the app.
For some reason, when I use the mouse to click "Ok" on the input dialog, a menu artifact appears on the QGraphicsView which contained the lasso. The menu artifact is a drop-down menu line that says "(check mark) Exit". Occasionally it may be the context menu for one of my custom QGraphicsObjects as well - but for whatever reason, calling the dialog and then clicking "Ok" results in an unintended right-click-like event on the QGraphicsView.
This only seems to happen when the last step before method return is the QInputDialog - replacing it with a pass or a call to some other method does not result in the artifact. I'd be very grateful to anyone with a clue to what is causing this problem!
Here's the minimal code:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QMainWindow):
# The app main window.
def __init__(self):
super(Window, self).__init__()
# Initialize window UI
self.initUI()
def initUI(self, labelText=None):
# Set user-interface attributes.
# Set up menu-, tool-, status-bars and add associated actions.
self.toolbar = self.addToolBar('Exit')
# Create a menu item to exit the app.
exitAction = QtGui.QAction(QtGui.QIcon('icons/exit.png'), '&Exit', self)
exitAction.triggered.connect(QtGui.qApp.quit)
self.toolbar.addAction(exitAction)
# Create the main view.
self.viewNetwork = NetworkPortal()
self.viewNetwork.setMinimumWidth(800)
self.viewNetwork.setMinimumHeight(800)
self.setCentralWidget(self.viewNetwork)
self.show()
class NetworkPortal(QtGui.QGraphicsView):
# A view which allows you to see and manipulate a network of nodes.
def __init__(self):
super(NetworkPortal, self).__init__(QtGui.QGraphicsScene())
# Add the CircleThing graphic to the scene.
circleThing = CircleThing()
self.scene().addItem(circleThing)
class CircleThing(QtGui.QGraphicsEllipseItem):
# Defines the graphical object.
def __init__(self):
super(CircleThing, self).__init__(-10, -10, 20, 20)
# Set flags for the graphical object.
self.setFlags(
QtGui.QGraphicsItem.ItemIsSelectable |
QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges
)
self.dragLine = None
self.dragCircle = None
def mouseMoveEvent(self, event):
# Reimplements mouseMoveEvent to drag out a line which can, on
# mouseReleaseEvent, form a new Relationship or create a new Thing.
# If just beginning a drag,
if self.dragLine == None:
# Create a new lasso line.
self.startPosX = event.scenePos().x()
self.startPosY = event.scenePos().y()
self.dragLine = self.scene().addLine(
self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y(),
QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine)
)
# Create a new lasso circle at the location of the drag position.
self.dragCircle = QtGui.QGraphicsEllipseItem(-5, -5, 10, 10)
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
self.scene().addItem(self.dragCircle)
# If a drag is already in progress,
else:
# Move the lasso line and circle to the drag position.
self.dragLine.setLine(QtCore.QLineF(self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y()))
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
def mouseReleaseEvent(self, event):
# If the line already exists,
if self.dragLine != None:
# If the released button was the right mouse button,
if event.button() == QtCore.Qt.RightButton:
# Clean up the link-drag graphics.
self.scene().removeItem(self.dragLine)
self.dragLine = None
self.scene().removeItem(self.dragCircle)
self.dragCircle = None
# Create the related Thing.
# Display input box querying for name value.
entry, ok = QtGui.QInputDialog.getText(None, 'Enter some info: ',
'Input:', QtGui.QLineEdit.Normal, '')
return
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
newWindow = Window()
sys.exit(app.exec_())
This is a guess on my part, but I've seen this kind of weird thing before.
Some Qt widgets have default behavior on certain types of events. I've never used QGraphicsView, but often the default for a right click is to open a context-sensitive pop-up menu (usually useless, in my experience). That may be happening in your case, which would explain why you only see this when there's a right click.
You can suppress the default Qt behavior by calling event.ignore() before returning from mouseReleaseEvent.
There was not a direct answer to the cause of this bug, but using a QTimer.singleShot() to call the dialog (in combination with a QSignalMapper in order to enter parameters) is a functional workaround that separates the dialog from the method in which the bug was occurring. Thanks to #Avaris for this one.