I ran into the same issue and unfortunately the solutions I found only work when you click and drag from the arrows or the spinbox's border. But most users would want to drag from the actual text field, so doing this wasn't intuitive.
Instead you can subclass a QLineEdit
to get the proper behavior. When you click it, it'll save its current value so that when the user drags it gets the mouse position's delta and applies that back onto the spinbox.
Here's a full example I'm using myself. Sorry though, it's in Maya's attribute style instead of Max's, so you click and drag the middle-mouse button to set the value. With some tweaking you can easily get it to work exactly like Max's:
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
class CustomSpinBox(QtWidgets.QLineEdit):
"""
Tries to mimic behavior from Maya's internal slider that's found in the channel box.
"""
IntSpinBox = 0
DoubleSpinBox = 1
def __init__(self, spinbox_type, value=0, parent=None):
super(CustomSpinBox, self).__init__(parent)
self.setToolTip(
"Hold and drag middle mouse button to adjust the value\n"
"(Hold CTRL or SHIFT change rate)")
if spinbox_type == CustomSpinBox.IntSpinBox:
self.setValidator(QtGui.QIntValidator(parent=self))
else:
self.setValidator(QtGui.QDoubleValidator(parent=self))
self.spinbox_type = spinbox_type
self.min = None
self.max = None
self.steps = 1
self.value_at_press = None
self.pos_at_press = None
self.setValue(value)
def wheelEvent(self, event):
super(CustomSpinBox, self).wheelEvent(event)
steps_mult = self.getStepsMultiplier(event)
if event.delta() > 0:
self.setValue(self.value() + self.steps * steps_mult)
else:
self.setValue(self.value() - self.steps * steps_mult)
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.MiddleButton:
self.value_at_press = self.value()
self.pos_at_press = event.pos()
self.setCursor(QtGui.QCursor(QtCore.Qt.SizeHorCursor))
else:
super(CustomSpinBox, self).mousePressEvent(event)
self.selectAll()
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
self.value_at_press = None
self.pos_at_press = None
self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
return
super(CustomSpinBox, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() != QtCore.Qt.MiddleButton:
return
if self.pos_at_press is None:
return
steps_mult = self.getStepsMultiplier(event)
delta = event.pos().x() - self.pos_at_press.x()
delta /= 6 # Make movement less sensitive.
delta *= self.steps * steps_mult
value = self.value_at_press + delta
self.setValue(value)
super(CustomSpinBox, self).mouseMoveEvent(event)
def getStepsMultiplier(self, event):
steps_mult = 1
if event.modifiers() == QtCore.Qt.CTRL:
steps_mult = 10
elif event.modifiers() == QtCore.Qt.SHIFT:
steps_mult = 0.1
return steps_mult
def setMinimum(self, value):
self.min = value
def setMaximum(self, value):
self.max = value
def setSteps(self, steps):
if self.spinbox_type == CustomSpinBox.IntSpinBox:
self.steps = max(steps, 1)
else:
self.steps = steps
def value(self):
if self.spinbox_type == CustomSpinBox.IntSpinBox:
return int(self.text())
else:
return float(self.text())
def setValue(self, value):
if self.min is not None:
value = max(value, self.min)
if self.max is not None:
value = min(value, self.max)
if self.spinbox_type == CustomSpinBox.IntSpinBox:
self.setText(str(int(value)))
else:
self.setText(str(float(value)))
class MyTool(QtWidgets.QWidget):
"""
Example of how to use the spinbox.
"""
def __init__(self, parent=None):
super(MyTool, self).__init__(parent)
self.setWindowTitle("Custom spinboxes")
self.resize(300, 150)
self.int_spinbox = CustomSpinBox(CustomSpinBox.IntSpinBox, parent=self)
self.int_spinbox.setMinimum(-50)
self.int_spinbox.setMaximum(100)
self.float_spinbox = CustomSpinBox(CustomSpinBox.DoubleSpinBox, parent=self)
self.float_spinbox.setSteps(0.1)
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.int_spinbox)
self.main_layout.addWidget(self.float_spinbox)
self.setLayout(self.main_layout)
# Run the tool.
global tool_instance
tool_instance = MyTool()
tool_instance.show()
I tried to make the functions match Qt's native spinBox
. I didn't need it in my case, but it would be easy to add a signal when the value changes on release. It would also be easy to take it to the next level like Houdini's sliders so that the steps rate can change depending on where the mouse is vertically. Bah, maybe for a rainy day though :).
Here's what this features right now:
- Can do both integer or double spinboxes
- Click then drag middle-mouse button to set the value
- While dragging, hold ctrl to increase the rate or hold shift to slow the rate
- You can still type in the value like normal
- You can also change the value by scrolling the mouse wheel (holding ctrl and shift changes rate)