Search code examples
pythonpyqt5python-3.10

Changing the way text is displayed for QLineEdit


I would like to change the text display of some QLineEdit's. The text should be displayed as a euro amount.

QLineEdits do not seem to be the appropriate choice for such a display. I therefore adapt the code to QDoubleSpinBox. With self.setButtonSymbols(QDoubleSpinBox.NoButtons) the graphical representation of the QDoubleSpinBoxes is identical to that of QLineEdits

Briefly so that the context is clear:

I have several QTableWidgets in which I use custom delegates such as:

class EuroDelegate(QStyledItemDelegate):
    def displayText(self, value, locale):
        if value is not None:
            value = float(value)
            return locale.toCurrencyString(value, '€', 2)
        else:
            return super().displayText(value, locale)

to display the text as a euro amount.

Some QLineEdits also contain euro amounts and I want them to "feel" as similar as possible. Therefore I changed those QLineEdits to QDoubleSpinBoxes.

As a result, some of the desired properties are quickly achieved.
striked items on the list are already implemented in the code snippet below:

  1. '€' is displayed after the amount.
  2. '€' should not be part of the LineEdit.text().
  3. The QLineEdits behave similarly to the delegates: When the QLineEdit is focused, the "€" character disappears, when it loses focus, it then reappears.
  4. Clicking/focusing the QDoubleSpinbox should select the complete content.
  5. The QDoubleSpinBox should set the "," automatically. Entries such as: "0050" should ideally be displayed directly on entry (before the 'Enter' key) as: "00,50" and after the 'Enter' key (as default with the QDoubleSpinBox) "0,50". The same would also apply to entries such as: "-1050" => "-10,50", whereby entries such as "99" + Enter should continue to produce "99,00". The box should therefore recognize that the input would exceed or fall below its "min/max range" and then automatically set the decimal point. (In my country, the separation is made using "," (=> "Euros,Cents") and the QDoubleSpinBox already respects this.)

Edit:
Further explanation to 5.): Since I may have expressed myself unclear: I would like that inputs that would fall above or below the setrange are not blocked, but that instead a comma is added if possible (check if there is already a comma?) and the input is continued instantly in the decimal point area.

A code snippet to play around with:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QDoubleSpinBox, QLineEdit, QPushButton
from PyQt5.QtCore import QTimer

class EuroDoubleSpinBox(QDoubleSpinBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setButtonSymbols(QDoubleSpinBox.NoButtons)
        self.normalSuffix = ''
        self.currencySuffix = ' €'
        self.setSuffix(self.currencySuffix)

    def focusInEvent(self, event):
        self.setSuffix(self.normalSuffix)
        QTimer.singleShot(0, self.selectAll)
        super().focusInEvent(event)

    def focusOutEvent(self, event):
        self.setSuffix(self.currencySuffix)
        super().focusOutEvent(event)


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()
        self.pushbutton = QPushButton('Print DoubleSpinBox Value')
        self.lineedit = QLineEdit() # added for focus toggling
        self.doubleSpinBox = EuroDoubleSpinBox()
        self.doubleSpinBox.setRange(-99.99, 99.99)
        self.doubleSpinBox.setSingleStep(0.01)
        layout.addWidget(self.lineedit)
        layout.addWidget(self.doubleSpinBox)
        layout.addWidget(self.pushbutton)
        self.setLayout(layout)

        self.pushbutton.clicked.connect(lambda: print(self.doubleSpinBox.cleanText()))

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.setGeometry(100, 100, 200, 100)
    window.setWindowTitle('Euro DoubleSpinBox')
    window.show()
    sys.exit(app.exec_())

I'm using python 3.10.13 and PyQt5 5.15.9


Solution

  • QAbstractSpinBox subclasses provide a validate() function that calls an internal validator, similarly to what validate() of QValidator does.

    The solution is to override that function according to your needs.

    For such a simple requirement (add the decimal point when the third digit is being typed), the following should probably suffice:

    class EuroDoubleSpinBox(QDoubleSpinBox):
        ...
        def validate(self, text, pos):
            if 2 < len(text.lstrip('-')) < 5:
                if text.isdecimal():
                    p = 2
                elif text.startswith('-') and text[1:].isdecimal() and len(text) > 3:
                    p = 3
                else:
                    return super().validate(text, pos)
                value = float('{}.{}'.format(text[:p], text[p:]))
                if self.minimum() <= value <= self.maximum():
                    return (
                        QValidator.Acceptable, 
                        text[:p] + QLocale().decimalPoint() + text[p:], 
                        pos + 1
                    )
            return super().validate(text, pos)
    

    The procedure is the following:

    1. check that the length of the typed text (without leading minus signs) is 3 or 4 characters;
    2. if the text is decimal (only characters in the 0-9 range, similar to \d? for regex), assume that the decimal point goes after the second digit;
    3. if the text begins with a minus sign and the following characters represent a decimal and the length at least 3 characters, set the position of the decimal point after the third digit;
    4. if 2 and 3 fail, return the default validation;
    5. construct the possible float value by inserting the decimal dot after the second or third position;
    6. if the value is between the minimum/maximum range of the spin box, return a QValidator.Acceptable, the text with the decimal point defined by the locale, and augment the cursor position by one;
    7. as final fall back, return the default validation;

    The above should work fine for almost any case, including pasting from keyboard. More fine tuning might be required if you also want to support pasting float values using the dot as decimal point while the system uses a different symbol (like in your case).