Reputation: 903
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_()
Upvotes: 1
Views: 1362
Reputation: 120578
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_())
Upvotes: 2
Reputation: 864
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":
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:
Upvotes: 2