I would like to make a spin-box which only is editable after double-clicking in the digit display area.
My attempt below disables the focus in all cases except when the increment/decrement buttons are pressed.
I want increment/decrement to perform the actions without stealing the focus.
I do want the the normal blinking cursor and edit functionality when the text area is double-clicked.
After editing, the widget should release focus when another widget is clicked, or enter is pressed.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
event_dict = {v: k for k, v in QtCore.QEvent.__dict__.items() if isinstance(v, int)}
noisy_events = [
'Paint',
'Show',
'Move',
'Resize',
'DynamicPropertyChange',
'PolishRequest',
'Polish',
'ChildPolished',
'HoverMove',
'HoverEnter',
'HoverLeave',
'ChildAdded',
'ChildRemoved',
]
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
if a1.type() == QtCore.QEvent.FocusAboutToChange:
print("intercepted focus about to change")
return True
if a1.type() == QtCore.QEvent.FocusIn:
print("intercepted focus in")
return True
if a1.type() == QtCore.QEvent.MouseButtonPress:
print("intercepted mouse press")
#return True
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
print("intercepted double click")
self.setFocus()
else:
if a1.type() in event_dict:
evt_name = event_dict[a1.type()]
if evt_name not in noisy_events:
print(evt_name)
else:
pass
#print(f"Unknown event type {a1.type()}")
return super().eventFilter(a0, a1)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QtWidgets.QWidget()
l = QtWidgets.QHBoxLayout()
l.addWidget(ClickableSpinBox())
l.addWidget(ClickableSpinBox())
l.addWidget(QtWidgets.QDoubleSpinBox())
w.setLayout(l)
w.show()
app.exec_()
Edit:
To let the mouse-scroll function and the increase/decrease buttons working
I make the QLineEdit inside of the QDoubleSpinBox to be enabled/disabled when you double click inside it or in the borders of the SpinBox. With this, you can still change the value inside it with the mouse-scroll or with the buttons. Here is your code modified:
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.lineEdit().setEnabled(False)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
## When double clicked inside the Disabled QLineEdit of
## the SpinBox, this will enable it and set the focus on it
self.lineEdit().setEnabled(True)
self.setFocus()
elif a1.type() == QtCore.QEvent.FocusOut:
## When you lose the focus, e.g. you click on other object
## this will diable the QLineEdit
self.lineEdit().setEnabled(False)
elif a1.type() == QtCore.QEvent.KeyPress:
## When you press the Enter Button (Return) or the
## Key Pad Enter (Enter) you will disable the QLineEdit
if a1.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.lineEdit().setEnabled(False)
return super().eventFilter(a0, a1)
def stepBy(self, steps):
## The reason of this is because if you click two consecutive times
## in any of the two buttons, the object will trigger the DoubleClick
## event.
self.lineEdit().setEnabled(False)
super().stepBy(steps)
self.lineEdit().deselect()
The result with the QLineEdit disabled and the buttons enabled:
To let ONLY the mouse-scroll function
You just have to remove the buttons from the code above, using setButtonSymbols().
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.lineEdit().setEnabled(False)
self.setFocusPolicy(Qt.NoFocus)
## Changing button's symbol to 2 means to "delete" the buttons
self.setButtonSymbols(2)
The result with the buttons "disabled":
Previous Answer (before the Edit)
I have a tricky solution, and it consists of Enable/Disable the customs spin boxes you created. With this, the spinboxes will be enabled (and editable) only when you double-clicked on them, and when you lose focus on them they will be disabled automatically, passing the focus to the enabled SpinBox.
The reason I did that is that when the SpinBox is enabled, the DoubleClick event will only be triggered when you double click on the borders or on the increment/decrement buttons. Disabling them will do the trick because the double click event will be triggered wherever you press inside the SpinBox.
Here is your code with my modifications: (there are comments inside te code to help you understand what I did)
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
## When you double click inside the Disabled SpinBox
## this will enable it and set the focus on it
self.setEnabled(True)
self.setFocus()
elif a1.type() == QtCore.QEvent.FocusOut:
## When you lose the focus, e.g. you click on other object
## this will disable the SpinBox
self.setEnabled(False)
elif a1.type() == QtCore.QEvent.KeyPress:
## When you press the Enter Button (Return) or the
## Key Pad Enter (Enter) you will disable the SpinBox
if a1.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.setEnabled(False)
return super().eventFilter(a0, a1)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QtWidgets.QWidget()
l = QtWidgets.QHBoxLayout()
## I store the SpinBoxes to give the disable property after
## generating its instance
sp1 = ClickableSpinBox()
sp1.setEnabled(False)
sp2 = ClickableSpinBox()
sp2.setEnabled(False)
sp3 = QtWidgets.QDoubleSpinBox()
l.addWidget(sp1)
l.addWidget(sp2)
l.addWidget(sp3)
w.setLayout(l)
w.show()
app.exec_()
And some screenshots of that code running:
The demo script below should do everything you want. I have added two extra features: (1) disabling of text selection, and (2) disabling of mouse-wheel increments on the text-box (but not the buttons). If these aren't to your taste, they can easily be adapted or removed (see the comments in the code). The implementation is otherwise very simple, since it does not rely on controlling the focus.
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ClickableSpinBox(QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setEditingDisabled(True)
self.lineEdit().installEventFilter(self)
self.editingFinished.connect(self.setEditingDisabled)
def editingDisabled(self):
return self.lineEdit().isReadOnly()
def setEditingDisabled(self, disable=True):
self.lineEdit().setReadOnly(disable)
self.setFocusPolicy(Qt.TabFocus if disable else Qt.WheelFocus)
# optional: control selection in text-box
if disable:
self.clearSelection()
self.lineEdit().selectionChanged.connect(self.clearSelection)
else:
self.lineEdit().selectionChanged.disconnect(self.clearSelection)
self.lineEdit().selectAll()
def clearSelection(self):
self.lineEdit().setSelection(0, 0)
def eventFilter(self, source, event):
if (event.type() == QEvent.MouseButtonDblClick and
source is self.lineEdit() and self.editingDisabled()):
self.setEditingDisabled(False)
self.setFocus()
return True
return super().eventFilter(source, event)
# optional: control mouse-wheel events in text-box
def wheelEvent(self, event):
if self.editingDisabled():
self.ensurePolished()
options = QStyleOptionSpinBox()
self.initStyleOption(options)
rect = self.style().subControlRect(
QStyle.CC_SpinBox, options,
QStyle.SC_SpinBoxUp, self)
if event.pos().x() <= rect.left():
return
super().wheelEvent(event)
def keyPressEvent(self, event):
if not self.editingDisabled():
super().keyPressEvent(event)
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
self.spinboxA = ClickableSpinBox()
self.spinboxB = ClickableSpinBox()
self.spinboxC = QDoubleSpinBox()
layout.addWidget(self.spinboxA)
layout.addWidget(self.spinboxB)
layout.addWidget(self.spinboxC)
self.setFocusPolicy(Qt.ClickFocus)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.setGeometry(900, 100, 200, 100)
window.show()
sys.exit(app.exec_())
Related
Grееtings аll. I am new to this site, so go easy on me.
I am building a program in python using PyQt5 for the interface. I currently have, among other things, a QListWidget (list window) into which I insert a number of QWidgets (list items) through a function during the running of the program. I have implemented an eventFilter by subclassing QObject, and I use it to differentiate between left and right click, and from that I send to the controller class to handle one or the other click accordingly.
I have made it so that when right-clicked on a list item, a context menu appears. However, I also need a context menu to appear when the list window is clicked (not on a list item). The problem which occurs is that when right-clicked on a list item, both the list item context menu and list window context menu appear. This must be because the event filter recognises the click as occurring within the list window, because it is occurring on a list item, which is within the list window. What I need is that when right-clicked on a list item, only its context menu appears, and similarly for the list window, when right-clicked outside the list items.
I have tried checking if source equals the widget where the event appeared, but it seems to recognise both widgets' events independently and if I gate the call to the handler with an if condition, one of the handlers never receives a call. I have searched around the web and this site, but I have found nothing of use. Perhaps it is due to me not being a native speaker and so not being able to phrase things correctly (you can see how clunky my phrasing is).
Below follows some code extracted from my program. Note that anything irrelevant has been cut away to make for a minimal example. For your convenience, I have also merged the GUI files into one and did the same for the control files. I have tested this minimal example and it reproduces the problem. It could not get smaller, so if you deem the code listed below too long, notify me and I can reupload it to GitHub if it is allowed to show the minimal example that way instead of putting code into the question directly.
custom_classes.py:
from PyQt5.QtCore import Qt, QEvent, QObject
class MyFilter(QObject):
def __init__(self, parent, ctrl):
super().__init__(parent)
self._parent = parent
self.ctrl = ctrl
self.parent.installEventFilter(self)
#property
def parent(self):
return self._parent
def eventFilter(self, source, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton:
self.ctrl.handle_left_click()
elif event.button() == Qt.RightButton:
self.ctrl.handle_right_click(event)
return super().eventFilter(source, event)
gui.py:
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QListWidget
from PyQt5.QtWidgets import QScrollArea
from PyQt5.QtCore import Qt
class MainFrame(QWidget):
def __init__(self):
super().__init__()
self.main_layout = QHBoxLayout()
self.setLayout(self.main_layout)
class ListItemFrame(QWidget):
def __init__(self):
super().__init__()
self.main = QHBoxLayout()
self.main.setContentsMargins(0,0,0,0)
self.name_layout = QHBoxLayout()
self.name_layout.setContentsMargins(0,0,0,0)
self.main.addLayout(self.name_layout)
self.name = QLabel("")
self.name.setMaximumHeight(20)
self.name_layout.addWidget(self.name)
self.setLayout(self.main)
class ListFrame(QListWidget):
def __init__(self):
super().__init__()
self.main = QVBoxLayout()
self.scroll_widget = QScrollArea()
self.scroll_widget.setWidgetResizable(True)
self.scroll_layout = QVBoxLayout()
self.scroll_layout.setAlignment(Qt.AlignTop)
self.scroll_layout_widget = QWidget()
self.scroll_layout_widget.setLayout(self.scroll_layout)
self.scroll_widget.setWidget(self.scroll_layout_widget)
self.main.addWidget(self.scroll_widget)
self.setLayout(self.main)
ctrl.py:
from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
class Controller:
def __init__(self, ui, app):
self.ui = ui
self.app = app
self.list_ = ListControl(self)
class ListControl:
def __init__(self, ctrl):
self.ctrl = ctrl
self.ui = ListFrame()
self.the_list = self.get_list() #list of stuff
self.item_list = [] #list of list items
self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
self.index = self.ctrl.ui.main_page.main_layout.count() - 1
self.filter = MyFilter(self.ui, self)
self.show_list()
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
one_action = menu.addAction("Something!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.ctrl.ui.close()
elif action == one_action:
self.something()
def something(self):
print("Something!")
def show_list(self):
for info in self.the_list:
item = ListItem(self, info)
self.item_list.append(item)
def get_list(self):
return [x for x in "qwertzuiopasdfghjklyxcvbnm"]
class ListItem:
def __init__(self, main, info):
self.main = main
self.info = info*10
self.ui = ListItemFrame()
self.filter = MyFilter(self.ui, self)
self.set_ui()
self.add_to_ui()
self.main.ui.scroll_layout.addWidget(self.ui)
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
item_action = menu.addAction("Hello!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.main.ctrl.ui.close()
elif action == item_action:
self.hello()
def hello(self):
print(f"Hello! I am {self.info}")
def set_ui(self):
self.ui.name.setText(self.info)
def add_to_ui(self):
self.main.ui.scroll_layout.insertWidget(
self.main.ui.scroll_layout.count() - 1, self.ui
)
main.py:
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QStackedLayout
from PyQt5.QtWidgets import QWidget
from gui import MainFrame
from ctrl import Controller
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("minimal example")
self.stacked = QStackedLayout()
self.main_page = MainFrame()
self.stacked.addWidget(self.main_page)
self.setLayout(self.stacked)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = Window()
window.show()
c = Controller(window, app)
sys.exit(app.exec())
To reiterate, the context menu appears for both the list item and the list window when a list item is right-clicked. What I need is for it to appear only for the list item if a list item is right-clicked.
Edit: seems the site bit off a part of my introduction. Readded it!
this is probably not the best way to do it but it works. You just create a global variable, for example list_element_clicked and when you click "hello" (of course not quit because you are going to exit the window and there is no point) you set that variable to True. If that variable is False, you show the ListControl context menu, and if not, you set that variable to True, so next time if you click on your ListControl it will appear, and if you click on ListItem it will not.
Finally there is an extra case, if you don't click anywhere after clicking on ListItem, nothing will happen (the ListControl is not shown and the variable is not changed) so everything will work perfectly next time.
So here is the code:
ctrl.py:
from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
list_element_clicked = False
class Controller:
def __init__(self, ui, app):
self.ui = ui
self.app = app
self.list_ = ListControl(self)
class ListControl:
def __init__(self, ctrl):
self.ctrl = ctrl
self.ui = ListFrame()
self.the_list = self.get_list() #list of stuff
self.item_list = [] #list of list items
self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
self.index = self.ctrl.ui.main_page.main_layout.count() - 1
self.filter = MyFilter(self.ui, self)
self.show_list()
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
global list_element_clicked
if(list_element_clicked == False):
self.show_options(event)
else:
list_element_clicked = False
def show_options(self, event):
menu = QMenu()
one_action = menu.addAction("Something!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.ctrl.ui.close()
elif action == one_action:
self.something()
def something(self):
print("Something!")
def show_list(self):
for info in self.the_list:
item = ListItem(self, info)
self.item_list.append(item)
def get_list(self):
return [x for x in "qwertzuiopasdfghjklyxcvbnm"]
class ListItem:
def __init__(self, main, info):
self.main = main
self.info = info*10
self.ui = ListItemFrame()
self.filter = MyFilter(self.ui, self)
self.set_ui()
self.add_to_ui()
self.main.ui.scroll_layout.addWidget(self.ui)
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
item_action = menu.addAction("Hello!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.main.ctrl.ui.close()
elif action == item_action:
global list_element_clicked
list_element_clicked = True
self.hello()
def hello(self):
print(f"Hello! I am {self.info}")
def set_ui(self):
self.ui.name.setText(self.info)
def add_to_ui(self):
self.main.ui.scroll_layout.insertWidget(
self.main.ui.scroll_layout.count() - 1, self.ui
)
Is there a signal/event I can make use of for the QMenu tear-off?
I have a QMenu subclass in which it has .setTearOffEnabled(True), but I would like to set this tear-off to always be on the top if the user clicks on the tear-off 'bar'.
I am unable to utilise QtCore.Qt.WindowStaysOnTopHint, as this will result in my menu already being in the tear-off state.
For example: if my main tool area is bigger than the tear-off, and I click on my main tool, the tear-off window will be behind it.
In the following code the clicked signal is emitted when the tear off (dotted lines) is pressed:
import sys
from PyQt5 import QtCore, QtWidgets
class Menu(QtWidgets.QMenu):
clicked = QtCore.pyqtSignal()
def mouseReleaseEvent(self, event):
if self.isTearOffEnabled():
tearRect = QtCore.QRect(
0,
0,
self.width(),
self.style().pixelMetric(
QtWidgets.QStyle.PM_MenuTearoffHeight, None, self
),
)
if tearRect.contains(event.pos()):
self.clicked.emit()
QtCore.QTimer.singleShot(0, self.after_clicked)
super(Menu, self).mouseReleaseEvent(event)
#QtCore.pyqtSlot()
def after_clicked(self):
tornPopup = None
for tl in QtWidgets.QApplication.topLevelWidgets():
if tl.metaObject().className() == "QTornOffMenu":
tornPopup = tl
break
if tornPopup is not None:
print("This is the tornPopup: ", tornPopup)
tornPopup.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow(parent=None)
menu = Menu("Menu", tearOffEnabled=True)
menu.clicked.connect(lambda: print("clicked"))
w.menuBar().addMenu(menu)
for i in range(5):
action = QtWidgets.QAction("action{}".format(i), w)
menu.addAction(action)
w.show()
sys.exit(app.exec_())
When a menu is torn off, it is hidden and Qt replaces it with a copy created from an internal subclass of QMenu. So to set the WindowStaysOnTopHint on a torn off menu, you would first need to find a way to get a reference to it. One way to do this would be to set an event-filter on the application object and watch for child events of the right type:
class MenuWatcher(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ChildAdded and
event.child().metaObject().className() == 'QTornOffMenu'):
event.child().setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
This class will operate on all torn-off menus created in the application.
However, if the event-filtering was done by the source menu class, its own torn-off menu could be identified by comparing menu-items:
class Menu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTearOffEnabled(True)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded:
child = event.child()
if (child.metaObject().className() == 'QTornOffMenu' and
all(a is b for a, b in zip(child.actions(), self.actions()))):
child.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
I made a ui for work using PyQt5 and Python3. Additionally to clicking the buttons, I want to execute specific actions by pressing specific keys on my keyboard e.g. the space bar. I used the following to do so:
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Space:
print('space')
elif key == Qt.Key_W:
print('w')
Instead of printing 'space', it presses the focused button in the ui, when I'm pressing the space bar. Just like I hit return. Although pressing the 'W'-key prints 'w' as expected.
I already searched here at stackoverflow and other where in the web as well, but I found nothing really helpfull. The best I got was this one here. But it's using PyQt4.3 and copy and paste the code to my editor just brought me some errors. I thought the approach was a good one, but I were not able to transfer this to PyQt5.
Try it:
#from PyQt4.QtCore import *
#from PyQt4.QtGui import *
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
# create objects
self.la = QLabel("Press Space or `w` in this box:")
self.le = MyLineEdit()
self.la2 = QLabel("\nLook here:")
self.le2 = QLineEdit()
self.btnSpace = QPushButton("Button1")
self.btnSpace.setEnabled(False)
self.btnSpace.clicked.connect(self.onBtnSpace)
self.btnW = QPushButton("Button2")
self.btnW.setEnabled(False)
self.btnW.clicked.connect(self.onBtnW)
# layout
layout = QVBoxLayout()
layout.addWidget(self.la)
layout.addWidget(self.le)
layout.addWidget(self.la2)
layout.addWidget(self.le2)
layout.addWidget(self.btnSpace)
layout.addWidget(self.btnW)
self.setLayout(layout)
# connections
#self.connect(self.le, SIGNAL("tabPressed"), self.update)
self.le.signalTabPressed[str].connect(self.update)
def update(self, keyPressed):
newtext = str(self.le2.text()) + str(keyPressed) #"tab pressed "
self.le2.setText(newtext)
if keyPressed == "Key Space; ":
self.btnSpace.setEnabled(True)
self.btnSpace.setText(str(keyPressed))
if keyPressed == "Key W; ":
self.btnW.setEnabled(True)
self.btnW.setText(str(keyPressed))
def onBtnSpace(self):
print("keyPressed -> btnSpace")
def onBtnW(self):
print("keyPressed -> btnW")
class MyLineEdit(QLineEdit):
signalTabPressed = pyqtSignal(str) # +++
def __init__(self, *args):
QLineEdit.__init__(self, *args)
def event(self, event):
#if (event.type()==QEvent.KeyPress) and (event.key()==Qt.Key_Tab):
if (event.type()==QEvent.KeyPress) and (event.key()==Qt.Key_Space):
self.signalTabPressed.emit("Key Space; ")
return True
if (event.type()==QEvent.KeyPress) and (event.key()==Qt.Key_W):
self.signalTabPressed.emit("Key W; ")
return True
return QLineEdit.event(self, event)
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
With S. Nick's very useful answer I were able to come to a solution that really fits my needs.
class MyPushButton(QPushButton):
def __init__(self, *args):
QPushButton.__init__(self, *args)
def event(self, event):
if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Space):
print(foo)
return True
return QPushButton.event(self, event)
So, that's it. Whenever I press the space key on my keyboard now it just prints foo and nothing else. Where print(foo) is just a place-holder for whatever I wish to do, obviously.
I've got a qTreeWidget-based interface where I double click on individual items to toggle them on and off. However, I'd like to be able to bulk-toggle them by selecting multiple objects and double clicking them, but when you double click on any item you immediately lose the multi-selection.
Does anyone know a way around this?
Many thanks for your time,
Nick
The first step would be to setup an event that fires whenever an item is double clicked, like so:
treeWidget.itemDoubleClicked.connect(onClickItem)
where onClickItem is:
def onClickItem(item):
print('Text of first column in item is ', item.text(0))
Of course you'll want to do something a bit more fancy inside onClickItem().
The selection/deselection of items is controlled by the mouse-press event, which obviously happens before the double-click is registered. So you need to "eat" the mouse-press at the appropriate moment.
This example allows double-clicking when the meta-key is pressed:
from PySide import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.tree = QtGui.QTreeWidget(self)
for text in 'One Two Three Four'.split():
parent = QtGui.QTreeWidgetItem(self.tree, [text])
for text in 'Red Blue Green'.split():
child = QtGui.QTreeWidgetItem(parent, [text])
parent.setExpanded(True)
self.tree.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
self.tree.viewport().installEventFilter(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tree)
def eventFilter(self, source, event):
if (source is self.tree.viewport() and
isinstance(event, QtGui.QMouseEvent) and
event.modifiers() == QtCore.Qt.MetaModifier):
if event.type() == QtCore.QEvent.MouseButtonDblClick:
print('meta-double-click')
return True
if event.type() == QtCore.QEvent.MouseButtonPress:
# kill selection when meta-key is also pressed
return True
return super(Window, self).eventFilter(source, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(800, 300, 300, 300)
window.show()
sys.exit(app.exec_())
I want to detect middle mouse clicks on a QTabWidget. I was expecting there to be a mouse event related signal on QWidget, but all I am seeing are methods.
Do I need to subclass the QTabWidget and then override said methods in order to do what I want, or am I missing something?
You can either install an event filter on the QTabBar (returned by QTabWidget.tabBar()) to receive and handle press and release events, or subclass QTabBar to redefine mousePressEvent and mouseReleaseEvent and replace the QTabBar of the QTabWidget with QTabWidget.setTabBar().
Example using the event filter:
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow,self).__init__()
self.tabWidget = QTabWidget(self)
self.setCentralWidget(self.tabWidget)
self.tabWidget.tabBar().installEventFilter(self)
self.tabWidget.tabBar().previousMiddleIndex = -1
def eventFilter(self, object, event):
if object == self.tabWidget.tabBar() and \
event.type() in [QEvent.MouseButtonPress,
QEvent.MouseButtonRelease] and \
event.button() == Qt.MidButton:
tabIndex = object.tabAt(event.pos())
if event.type() == QEvent.MouseButtonPress:
object.previousMiddleIndex = tabIndex
else:
if tabIndex != -1 and tabIndex == object.previousMiddleIndex:
self.onTabMiddleClick(tabIndex)
object.previousMiddleIndex = -1
return True
return False
# function called with the index of the clicked Tab
def onTabMiddleClick(self, index):
pass
Example using a QTabBar subclass:
class TabBar(QTabBar):
middleClicked = pyqtSignal(int)
def __init__(self):
super(QTabBar, self).__init__()
self.previousMiddleIndex = -1
def mousePressEvent(self, mouseEvent):
if mouseEvent.button() == Qt.MidButton:
self.previousIndex = self.tabAt(mouseEvent.pos())
QTabBar.mousePressEvent(self, mouseEvent)
def mouseReleaseEvent(self, mouseEvent):
if mouseEvent.button() == Qt.MidButton and \
self.previousIndex == self.tabAt(mouseEvent.pos()):
self.middleClicked.emit(self.previousIndex)
self.previousIndex = -1
QTabBar.mouseReleaseEvent(self, mouseEvent)
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow,self).__init__()
self.tabWidget = QTabWidget(self)
self.setCentralWidget(self.tabWidget)
self.tabBar = TabBar()
self.tabWidget.setTabBar(self.tabBar)
self.tabBar.middleClicked.connect(self.onTabMiddleClick)
# function called with the index of the clicked Tab
def onTabMiddleClick(self, index):
pass
(In case you wonder why there is so much code for such a simple task, a click is defined as a press event followed by a release event at roughly the same spot, so the index of the pressed tab has to be the same as the released tab).