Change singleStep in a QDoubleSpinBox depending on the cursor position when using arrow keys

Viewed 155

I want to make a QDoubleSpinBox controllable via the arrow keys, i.e. increase the value when pressing up and decrease it when pressing down while the box is selected. I currently do this by setting singleStep to a certain value. However, I would like to be able to change the value of singleStep depending on the cursor position when editing the field.

For example for a box with two decimals, 1.23, if the cursor | is at the first decimal 1.2|3 I want to increase or decrease by 0.1 but if the cursor is at the second decimal 1.23| I only want to increase/decrease by 0.01.

Note: Not sure if relevant, but I currently have keyboardTracking set to False to avoid signal emission while editing the value by typing it in via the number keys. The values are still emitted when using the error keys.

2 Answers

Probably the easiest way to achieve this is to make a subclass of QDoubleSpinBox and override the virtual stepBy() method. That is called every time the value is to be stepped up or down. Here is a simple example that shows how to do it for an integer spin box. For a floating point value, one just has to pin the singleStep size to the minimum decimal step size, and parse the text a bit more carefully in stepBy(), taking the decimal point into account.

class MySpinBox : public QSpinBox
{
public:
    explicit MySpinBox(QWidget *parent = nullptr)
        : QSpinBox(parent) {
        setMaximum(9999);
        setValue(5000);
    };

    void stepBy(int steps) override {
        const int cursorPos = qMax(lineEdit()->cursorPosition(), 1);
        const int textLen = lineEdit()->text().length();
        for (int i = textLen - cursorPos; i > 0; i--)
            steps *= 10;
        QAbstractSpinBox::stepBy(steps);
        lineEdit()->setCursorPosition(cursorPos);
    }
};

Following aavit's answer I did also redefine stepBy within PyQt. Since I am looking for a solution that works for floats and negative values, I had to handle some special cases and I display the + sign for positive values by redefining textFromValue. Special cases are commented in the solution below:

import sys
from PyQt5.QtWidgets import QDoubleSpinBox, QMainWindow, QApplication, QLabel


class MyQDoubleSpinBox(QDoubleSpinBox):
    def textFromValue(self, value):
        # show + sign for positive values
        text = super().textFromValue(value)
        if value >= 0:
            text = "+" + text
        return text

    def stepBy(self, steps):
        cursor_position = self.lineEdit().cursorPosition()
        # number of characters before the decimal separator including +/- sign
        n_chars_before_sep = len(str(abs(int(self.value())))) + 1
        if cursor_position == 0:
            # set the cursor right of the +/- sign
            self.lineEdit().setCursorPosition(1)
            cursor_position = self.lineEdit().cursorPosition()
        single_step = 10 ** (n_chars_before_sep - cursor_position)
        # Handle decimal separator. Step should be 0.1 if cursor is at `1.|23` or
        # `1.2|3`.
        if cursor_position >= n_chars_before_sep + 2:
            single_step = 10 * single_step
        # Change single step and perform the step
        self.setSingleStep(single_step)
        super().stepBy(steps)
        # Undo selection of the whole text.
        self.lineEdit().deselect()
        # Handle cases where the number of characters before the decimal separator
        # changes. Step size should remain the same.
        new_n_chars_before_sep = len(str(abs(int(self.value())))) + 1
        if new_n_chars_before_sep < n_chars_before_sep:
            cursor_position -= 1
        elif new_n_chars_before_sep > n_chars_before_sep:
            cursor_position += 1
        self.lineEdit().setCursorPosition(cursor_position)


class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(100, 100, 600, 400)
        self.UiComponents()
        self.show()

    def UiComponents(self):
        self.spin = MyQDoubleSpinBox(self)
        self.spin.setGeometry(100, 100, 150, 40)
        self.spin.setRange(-10000, 10000)
        self.spin.setValue(50)
        self.spin.setKeyboardTracking(False)


if __name__ == "__main__":
    App = QApplication(sys.argv)
    window = Window()
    sys.exit(App.exec())

Related