I have the next problem:
A created a custom widget (simple derived QWidget class). When I middle-mouse click on it - it creates another QWidget class (sort of context menu). When I release middle-mouse button - that context widget disappears. So that works. What does not work is - that context widget also has some content added, like other small widgets, icons, etc and they all have their own custom events (simple example - enterEvent and leaveEvent with prints indicating those events). But those inner widget events are not working, they are blocked while I keep middle-mouse pressed. When I release it - context widget disappears. Would like to know if there is any solution to let inner widgets'events work as expected.
Here is a minimal example where inner widget does not run mouse events as they are blocked by MainWidget event:
from PyQt5 import QtWidgets, QtGui, QtCore
class SomeSmallWidget(QtWidgets.QWidget):
def __init__(self, increment=1, globalValue=0):
super(SomeSmallWidget, self).__init__()
# UI
self.setMinimumWidth(40)
self.setMaximumWidth(40)
self.setMinimumHeight(40)
self.setMaximumHeight(40)
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
def enterEvent(self, event):
print('Entered') # NOT WORKING
super(SomeSmallWidget, self).enterEvent(event)
def leaveEvent(self, event):
print('Leaved') # NOT WORKING
super(SomeSmallWidget, self).leaveEvent(event)
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ContextWidget, self).__init__()
self.setMouseTracking(True)
# position
point = parent.rect().topLeft()
global_point = parent.mapToGlobal(point)
self.move(global_point - QtCore.QPoint(0, 0))
self.innerWidget = SomeSmallWidget() # Here is that small widget, which event does not work
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
self.mainLayout.addWidget(self.innerWidget)
class MainWidget(QtWidgets.QLineEdit):
def __init__(self, value='0'):
super(MainWidget, self).__init__()
self.setMouseTracking(True)
self.popMenu = None
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton: # while we keep MMB pressed - we see context widget
self.popMenu = ContextWidget(parent=self)
self.popMenu.show()
super(MainWidget, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
if self.popMenu:
self.popMenu = None
Events are not blocked, all mouse events are sent to widget that was under cursor when mouse button was pressed. Usually (always) it makes more sense. Imagine two buttons next to each other. Suppose user pressed one, moved cursor and released over second button. What was his intention? He probably changed his mind. If button triggers action on mouse press - this options will not be available and it's probably too soon, if button triggers action on mouse release, which button should recieve mouserelease event? If we send mouseevent to second button - that was not pressed it will trigger action that used didn't want. If we dont send mouserelease event to first button - it will stay in sunken mode. Imagine user is selecting text in lineedit, and while selecting he leaves lineedit and goes to other widgets, should they react somehow, and should focus be switched? Probably not. So there is only one active window and only one focused widget at a time and it receives keyboard and mouse input and reacts to it. Most of menus are shown after mouserelease and closes on next mouseclick, providing better user experience.
However, If you still want your widget to receive mouse events, you can achive this by translating it from ContextWidget to SomeSmallWidget like this:
class SomeSmallWidget(QtWidgets.QWidget):
...
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.blue)
def onMouseEnter(self):
print('onMouseEnter')
def onMouseLeave(self):
print('onMouseLeave')
class MainWidget(QtWidgets.QLineEdit):
...
def mouseMoveEvent(self, event):
if self.popMenu:
self.popMenu.mouseTest(event.globalPos())
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
...
self._inside = False
def mouseTest(self, p):
widget = self.innerWidget
rect = QtCore.QRect(widget.mapToGlobal(QtCore.QPoint(0,0)), widget.size())
inside = rect.contains(p)
if inside != self._inside:
if inside:
widget.onMouseEnter()
else:
widget.onMouseLeave()
self._inside = inside
Notice I added paintEvent to see the widget bounds.
Related
I have a pushButton and want to have a specific behaviour while users interact with that button.
Here is what I want:
user pressed on the button (Without release)
Then the user moved the cursor outside the button (Still the mouse is not released yet).
So, the button should take an active icon and whenever he points outside the button that icon should be changed to inactive-icon
Here is what I get:
I made my own QPushButton and overridden both (leaveEvent(), mouseReleaseEvent(), mousePressEvent()), But the problem is that after the user press and keep pressing on that button no other events are handled, So I need a way to handle other events like the leaveEvent()
Here is my own button class:
class ToggledButton(QPushButton):
def __init__(self, icon: QIcon = None, active_icon: QIcon = None):
super(ToggledButton, self).__init__()
self.active_icon= active_icon
self.inactive_icon = icon
self.setIcon(icon)
self.setCheckable(True)
self.setFlat(False)
def leaveEvent(self, event):
super(ToggledButton, self).leaveEvent(event)
self.setIcon(self.inactive_icon )
def mousePressEvent(self, event):
super(ToggledButton, self).mousePressEvent(event)
self.setIcon(self.active_icon)
def mouseReleaseEvent(self, event):
super(ToggledButton, self).mouseReleaseEvent(event)
self.setIcon(self.inactive_icon )
When a widget receives a mouse button press, it normally becomes the mouse grabber. When that happens, no widget will ever receive an enter or leave event, including that same widget.
A widget that is the mouse grabber will always receive mouse move events, so that is the function that you need to override:
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
if event.buttons() == Qt.LeftButton:
if event.pos() in self.rect():
self.setIcon(self.active_icon)
else:
self.setIcon(self.inactive_icon)
Note: you should check the button() in the mouse press event, otherwise it will change icon also when the user presses a different mouse button:
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
self.setIcon(self.active_icon)
Note the difference between buttons() (the pressed mouse buttons at the time of the event) and button(), which is the mouse button that caused the event, which is obviously meaningless for mouse move events, since the event is not caused by mouse buttons but by the movement.
I want my app to close when mouse or keyboard movement is detected, it does not detect anything, only when I close the app
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent=parent)
self.setupUi(self)
def keyPressEvent(self, event):
if event.key():
self.close()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.showMaximized()
sys.exit(app.exec_())
Input events are not propagated to the parent if a child handles them.
If a widget accepts keyboard events, they will not be received by its parent(s).
A widget receives mouse move events only if it grabs the mouse (normally, after clicking on that widget) or if it has the mouseTracking property enabled and the mouse is actually over the widget, but not if the mouse is over a child widget (or a widget that partially covers it).
Since your top level widget has children, those events might never be received, as those children will handle and/or accept them.
The solution is to install an event filter on the global application, and check for both MouseMove and KeyPress events, then eventually quit the application, being careful to do that with a delayed timer, as the application will still need to finish processing its event queue:
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent=parent)
self.setupUi(self)
QApplication.instance().installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() in (event.MouseMove, event.KeyPress):
QTimer.singleShot(0, QApplication.quit)
return True
return super().eventFilter(obj, event)
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.
QPushButton has a signal which is named clicked(), and we can catch click events through it. Is there a method or signal which catches hover and leave events?
How can I catch mouse-over button and mouse-leave button, like this:
button = QPushButton(window)
button.clicked.connect(afunction)
Note: I use python3.
You need to subclass the QPushButton class and reimplement the enterEvent and leaveEvent:
class Button(QPushButton):
def __init__(self, parent=None):
super(Button, self).__init__(parent)
# other initializations...
def enterEvent(self, QEvent):
# here the code for mouse hover
pass
def leaveEvent(self, QEvent):
# here the code for mouse leave
pass
You can then handle the event locally, or emit a signal (if other widgets needs to react on this event you could use a signal to notify the event to other widgets).
Q1: Is there a mouse single click event in wxpython. I didn't find a single click event. So I use Mouse_Down and Mouse_UP to implement that.
Q2: I also have a double click event. But double click event can emmit mouse up and down also. How can I distinguish them?
To distinguish click and double click you can use wx.Timer:
Start the timer in OnMouseDown
In OnDoubleClick event handler stop the timer
If timer wasn't stopped by OnDoubleClick you can handle single click in the timer handler.
Similar discussion in Google Groups.
Code example (needs polishing and testing of course but gives a basic idea):
import wx
TIMER_ID = 100
class Frame(wx.Frame):
def __init__(self, title):
wx.Frame.__init__(self, None, title=title, size=(350,200))
self.timer = None
self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
def OnDoubleClick(self, event):
self.timer.Stop()
print("double click")
def OnSingleClick(self, event):
print("single click")
self.timer.Stop()
def OnLeftDown(self, event):
self.timer = wx.Timer(self, TIMER_ID)
self.timer.Start(200) # 0.2 seconds delay
wx.EVT_TIMER(self, TIMER_ID, self.OnSingleClick)
app = wx.App(redirect=True)
top = Frame("Hello World")
top.Show()
app.MainLoop()