How can I make it so when a user clicks on, the up or down arrow of a QSpinBox, the value will increase as the cursor is dragging up and the value will decrease if dragging down. I fond this function very useful for users to be able to just click and drag their cursor than to constantly click the errors. Here is reference source code for a spinner made in C# which works the way i would like it to in python. http://www.paulneale.com/tutorials/dotNet/numericUpDown/numericUpDown.htm
import sys
from PySide import QtGui, QtCore
class Wrap_Spinner( QtGui.QSpinBox ):
def __init__( self, minVal=0, maxVal=100, default=0):
super( Wrap_Spinner, self ).__init__()
self.drag_origin = None
self.setRange( minVal, maxVal )
self.setValue( default)
def get_is_dragging( self ):
# are we the widget that is also the active mouseGrabber?
return self.mouseGrabber( ) == self
### Dragging Handling Methods ################################################
def do_drag_start( self ):
# Record position
# Grab mouse
self.drag_origin = QtGui.QCursor( ).pos( )
self.grabMouse( )
def do_drag_update( self ):
# Transpose the motion into values as a delta off of the recorded click position
curPos = QtGui.QCursor( ).pos( )
offsetVal = self.drag_origin.y( ) - curPos.y( )
self.setValue( offsetVal )
print offsetVal
def do_drag_end( self ):
self.releaseMouse( )
# Restore position
# Reset drag origin value
self.drag_origin = None
### Mouse Override Methods ################################################
def mousePressEvent( self, event ):
if QtCore.Qt.LeftButton:
print 'start drag'
self.do_drag_start( )
elif self.get_is_dragging( ) and QtCore.Qt.RightButton:
# Cancel the drag
self.do_drag_end( )
else:
super( Wrap_Spinner, self ).mouseReleaseEvent( event )
def mouseMoveEvent( self, event ):
if self.get_is_dragging( ):
self.do_drag_update( )
else:
super( Wrap_Spinner, self ).mouseReleaseEvent( event )
def mouseReleaseEvent( self, event ):
if self.get_is_dragging( ) and QtCore.Qt.LeftButton:
print 'finish drag'
self.do_drag_end( )
else:
super( Wrap_Spinner, self ).mouseReleaseEvent( event )
class Example(QtGui.QWidget ):
def __init__( self):
super( Example, self ).__init__( )
self.initUI( )
def initUI( self ):
self.spinFrameCountA = Wrap_Spinner( 2, 50, 40)
self.spinB = Wrap_Spinner( 0, 100, 10)
self.positionLabel = QtGui.QLabel( 'POS:' )
grid = QtGui.QGridLayout( )
grid.setSpacing( 0 )
grid.addWidget( self.spinFrameCountA, 0, 0, 1, 1 )
grid.addWidget( self.spinB, 1, 0, 1, 1 )
grid.addWidget( self.positionLabel, 2, 0, 1, 1 )
self.setLayout( grid )
self.setGeometry( 800, 400, 200, 150 )
self.setWindowTitle( 'Max Style Spinner' )
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
self.show( )
def main( ):
app = QtGui.QApplication( sys.argv )
ex = Example( )
sys.exit( app.exec_( ) )
if __name__ == '__main__':
main()
I'm a big fan of your plugins so I'm happy I can answer this one for you! I assume you are coding a Max plug-in in pyside, because that's exactly what I was doing when I ran into the same problem (I like the Max default "scrubby" spinners too).
The solution is actually pretty simple, you just have to do it manually. I subclassed the QSpinBox and captured the mouse event, using it to calculate the y position relative to when you first start clicking on the widget. Here's the code, this is pyside2 because as of 3DS Max and Maya 2018 that's what Autodesk is using:
from PySide2 import QtWidgets, QtGui, QtCore
import MaxPlus
class SampleUI(QtWidgets.QDialog):
def __init__(self, parent=MaxPlus.GetQMaxMainWindow()):
super(SampleUI, self).__init__(parent)
self.setWindowTitle("Max-style spinner")
self.initUI()
MaxPlus.CUI.DisableAccelerators()
def initUI(self):
mainLayout = QtWidgets.QHBoxLayout()
lbl1 = QtWidgets.QLabel("Test Spinner:")
self.spinner = SuperSpinner(self)
#self.spinner = QtWidgets.QSpinBox() -- here's the old version
self.spinner.setMaximum(99999)
mainLayout.addWidget(lbl1)
mainLayout.addWidget(self.spinner)
self.setLayout(mainLayout)
def closeEvent(self, e):
MaxPlus.CUI.EnableAccelerators()
class SuperSpinner(QtWidgets.QSpinBox):
def __init__(self, parent):
super(SuperSpinner, self).__init__(parent)
self.mouseStartPosY = 0
self.startValue = 0
def mousePressEvent(self, e):
super(SuperSpinner, self).mousePressEvent(e)
self.mouseStartPosY = e.pos().y()
self.startValue = self.value()
def mouseMoveEvent(self, e):
self.setCursor(QtCore.Qt.SizeVerCursor)
multiplier = .5
valueOffset = int((self.mouseStartPosY - e.pos().y()) * multiplier)
print valueOffset
self.setValue(self.startValue + valueOffset)
def mouseReleaseEvent(self, e):
super(SuperSpinner, self).mouseReleaseEvent(e)
self.unsetCursor()
if __name__ == "__main__":
try:
ui.close()
except:
pass
ui = SampleUI()
ui.show()
This is old, but is still a top hit on Google.
I found a few possibilities online, but none were ideal. My solution was top create a new type of label that 'scrubs' a QSpinBox or QDoubleSpinBox when dragged. Here you go:
////////////////////////////////////////////////////////////////////////////////
// Label for a QSpinBox or QDoubleSpinBox (or derivatives) that scrubs the spinbox value on click-drag
//
// Notes:
// - Cursor is hidden and cursor position remains fixed during the drag
// - Holding 'Ctrl' reduces the speed of the scrub
// - Scrub multipliers are currently hardcoded - may want to make that a parameter in the future
template <typename SpinBoxT, typename ValueT>
class SpinBoxLabel : public QLabel
{
public:
SpinBoxLabel(const QString& labelText, SpinBoxT& buddy)
: QLabel(labelText)
, Buddy(&buddy)
{
setBuddy(&buddy);
}
protected:
virtual void mouseMoveEvent(QMouseEvent* event) override {
if (!(event->buttons() & Qt::LeftButton))
return QLabel::mouseMoveEvent(event);
if (!IsDragging) {
StartDragPos = QCursor::pos();
Value = double(Buddy->value());
IsDragging = true;
QApplication::setOverrideCursor(Qt::BlankCursor);
}
else {
int dragDist = QCursor::pos().x() - StartDragPos.x();
if (dragDist == 0)
return;
double dragMultiplier = .25 * Buddy->singleStep();
if (!(event->modifiers() & Qt::ControlModifier))
dragMultiplier *= 10.0;
Value += dragMultiplier * dragDist;
Buddy->setValue(ValueT(Value));
QCursor::setPos(StartDragPos);
}
}
virtual void mouseReleaseEvent(QMouseEvent* event) override {
if (!IsDragging || event->button() != Qt::LeftButton)
return QLabel::mouseReleaseEvent(event);
IsDragging = false;
QApplication::restoreOverrideCursor();
}
private:
SpinBoxT* Buddy;
bool IsDragging = false;
QPoint StartDragPos;
double Value = 0.0;
};
typedef SpinBoxLabel<QDoubleSpinBox, double> DoubleSpinBoxLabel;
typedef SpinBoxLabel<QSpinBox, int> IntSpinBoxLabel;
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:
The speed of the spinbox increment can be changed with QAbstractSpinBox.setAccelerated :
self.spinFrameCountA.setAccelerated(True)
With this enabled, the spinbox value will change more quickly the longer the mouse button is held down.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.