Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Sub-classing QlineEdit



  • A while ago - in another thread - @JKSH suggested I could subclass QLineEdit for a specific purpose.

    I decided to try, but I can't quite get it to work. The code is posted below.
    In this case I don't use the model and mapper from Qt. I run a SQL query and use the value property to get my data into the object. That works.

    But when I type into the widget, the value does not change. The text property does change, but not value. This I don't understand.

    How does my typing change the object? To me, it looks like my class definition is ignored, and QLineEdit.setText() is used.

    from PyQt5 import QtWidgets,QtGui, QtCore
    import locale
    from PyQt5.QtCore import pyqtProperty
    #*******************************************************************************
    #
    #   A subclass of QLineEdit which can be used to display decimal numbers only.
    #   The numbers are displayed according to locale and always with a
    #   fixed number of decimals
    #   A validator is in place to ensure that only valid flots are entered by User
    #
    #*******************************************************************************
    class DblEdit (QtWidgets.QLineEdit):
        def __init__(self, parent):
            super().__init__(parent)
            self._decChar = QtCore.QLocale().decimalPoint()
            self._decimals = 2
            self._minimum = 0
            self._maximum = 9999
            self._dblVal = QtGui.QDoubleValidator(self)
            self._dblVal.setNotation(QtGui.QDoubleValidator.StandardNotation)
            self._dblVal.setRange(self._minimum, self._maximum, self._decimals)
            self.setValidator(self._dblVal)
            self._value = 0.0
    
    #-------------------------------------------------------------------------------
    #   Private version of setText
    #-------------------------------------------------------------------------------
        def _setText(self, text):
            super().setText(text)
    
    #-------------------------------------------------------------------------------
    #   Private method to pad text with 0 after decimals if necessary
    #-------------------------------------------------------------------------------
        def _fillDecimals(self, lacks):
            if lacks <=  0:
                return
            else:
                self._setText(self.text() + '0')
                self._fillDecimals(lacks-1)
    
    #-------------------------------------------------------------------------------
    #   setText not to be used
    #-------------------------------------------------------------------------------
        def setText(self, text):
            raise Exception('Method setText not available. Set value property')
    
    #-------------------------------------------------------------------------------
    #   getter for _value
    #-------------------------------------------------------------------------------
        @pyqtProperty(float,user=True)
        def value(self):
            print ('You are in value')
            return self._value
    
    #-------------------------------------------------------------------------------
    #   Setter for value should be used instead of setText .
    #   Supply a float as argument
    #-------------------------------------------------------------------------------
        @value.setter
        def value(self, d):
            if str(d).strip(' ')  == '' or d == None:
                self._value= None
                self._setText = ''
            else:
                self._value = round(float(d), self._decimals)
                self._setText(locale.str(self._value).strip())
                #find position of decimal characer
                pos = self.text().find(self._decChar,0,len(self.text()))
    
                if pos == -1:
                     self._setText(self.text()+self._decChar)
                     lacksDecimals = self._decimals
                else:
                    lacksDecimals = self._decimals - ( len(self.text())-(pos+1))
                self._fillDecimals(lacksDecimals)
            print ('You are in setValue', self.text(), self.value)
    
    #-------------------------------------------------------------------------------
    #   Method to specify how many decimals are wanted
    #-------------------------------------------------------------------------------
        def setDecimals(self, i):
            self._decimals = int(i)
    
    #-------------------------------------------------------------------------------
    #   Method to set range of values for validator
    #-------------------------------------------------------------------------------
        def setRange(self, min, max):
            self._minimum = min
            self._maximum = max
    
    #-------------------------------------------------------------------------------
    #  some getters
    #-------------------------------------------------------------------------------
        def decimals(self):
            return self._decimals
    
        def minimum(self):
            return self._minimum
    
        def maximum(self):
            return self._maximum
    


  • @bkvldr
    I'm not sure, so please take my remark with a pinch of salt, but: internal Qt code will feel free to call QLineEdit::setText(). I am then unsure whether it will resolve to the base QLineEdit::setText(), or whether it will call your override which will then raise [I would put a print in there, just in case it's getting called form somewhere with exceptions trapped], but either way that does not sound your desired behaviour? I don't think you can afford to try to prevent setText() being called on your subclassed widget.

    if str(d).strip(' ') == '' or d == None: You're supposed to write that with d is None. Probably not relevant to your question. Why you choose to first str() it and then test for None I don't know, to me it would be cleaner to reverse those tests, but up to you :)


  • Banned

    Okay let us start with a much more basic MUC (Minimal Usable Code) since you did not even present a MUC or MRE I gutted your class reducing it to the bare minimum in order to play with it a bit -- had to role back to its parent class QWidget to locate some of the actual events that were responsive from here you can build up to what you are trying to achieve. Take this bare bones MUC and muck around with it look up QLineEdit and QWidget to determine what aspects of it might give you access to what you are wanting so that you can play with them some more

    from PyQt5.QtCore      import pyqtSlot
    from PyQt5.QtGui       import QDoubleValidator
    from PyQt5.QtWidgets   import QApplication, QWidget, QLineEdit, QVBoxLayout
    
    class DblEdit (QLineEdit):
        def __init__(self, Id):
          # First do not use super( ) unless you are fully aware of what 3 major issues 
          # you must code for in conjunction with using it and the actual rare issue 
          # that it is meant to solve. Note this rare issue is rare and unless you are 
          # doing some complicated inheritance you will most likely never run into that
          # issue -- however the 3 major issues super() creates by using it you are much 
          # more likely to run into than the rare issue its meant to resolve
          #
          # Further this implementation of super() was incorrect
          #  super().__init__(parent)
            QLineEdit.__init__(self)
            self.Id = Id
            self.Decimals = 2
            self.Minimum = 0
            self.Maximum = 9999
    
            self.DblVal = QDoubleValidator(self)
            self.DblVal.setNotation(QDoubleValidator.StandardNotation)
            self.DblVal.setRange(self.Minimum, self.Maximum, self.Decimals)
            self.setValidator(self.DblVal)
    
        @pyqtSlot(object)
        def keyPressEvent(self, event):
            print('Captured Key Press Event in',self.Id,') ',event.text())
            QLineEdit.keyPressEvent(self, event)
    
    class MainWindow(QWidget):
        def __init__(self):
            QWidget.__init__(self)
            self.setWindowTitle('Muck With It')
            Top=300; Left=700; Width=250; Hight=75
            self.setGeometry(Left, Top, Width, Hight)
    
            self.DblOne = DblEdit(1)
            self.DblTwo = DblEdit(2)
    
            VBox = QVBoxLayout()
            VBox.addWidget(self.DblOne)
            VBox.addWidget(self.DblTwo)
            VBox.addStretch(1)
            
            self.setLayout(VBox)
    
    if __name__ == "__main__":
        MainEventHandler = QApplication([])
    
        application = MainWindow()
        application.show()
        
        MainEventHandler.exec()
    
      # If anyone wants more extensive free help I run an online lab-like classroom-like 
      # message server feel free and drop by you will not be able to post until I clear 
      # you as a student as this prevents spammers so if interested here is the invite
      # https://discord.gg/3D8huKC
    


  • @Denni-0

    I finally got some time to play with this. In keyPressEvent, I set self._value. This keeps self.text() and self._value in sync as I type. I am still not quite comfortable, because I don't know how my typing enters into my class.
    If you look at my original post, you'll see that it cannot be through the DblEdit.setText() .

    from PyQt5.QtCore      import pyqtSlot
    from PyQt5.QtGui       import QDoubleValidator
    from PyQt5.QtWidgets   import QApplication, QWidget, QLineEdit, QVBoxLayout
    import locale
    class DblEdit (QLineEdit):
        def __init__(self, Id):
            QLineEdit.__init__(self)
            self.Id = Id
            self.Decimals = 2
            self.Minimum = 0
            self.Maximum = 9999
            self._value = 0.0
            self.DblVal = QDoubleValidator(self)
            self.DblVal.setNotation(QDoubleValidator.StandardNotation)
            self.DblVal.setRange(self.Minimum, self.Maximum, self.Decimals)
            self.setValidator(self.DblVal)
    
        @pyqtSlot(object)
        def keyPressEvent(self, event):
            print('Captured Key Press Event in',self.Id,') ',event.text())
            QLineEdit.keyPressEvent(self, event)
            if self.text() == '':
                self._value = 0.0
            else:
                self._value = locale.atof(self.text())
    
    class MainWindow(QWidget):
        def __init__(self):
            QWidget.__init__(self)
            self.setWindowTitle('Muck With It')
            Top=300; Left=700; Width=250; Hight=75
            self.setGeometry(Left, Top, Width, Hight)
    
            self.DblOne = DblEdit(1)
            self.DblTwo = DblEdit(2)
    
            VBox = QVBoxLayout()
            VBox.addWidget(self.DblOne)
            VBox.addWidget(self.DblTwo)
            VBox.addStretch(1)
    
            self.setLayout(VBox)
    
    if __name__ == "__main__":
        MainEventHandler = QApplication([])
        locale.setlocale(locale.LC_ALL, '')
        application = MainWindow()
        application.show()
        MainEventHandler.exec()
    

  • Banned

    @bkvldr I am not sure what you are asking in your last post could you elaborate on that?



  • @Denni-0
    Is setting self._value in the keyPressEvent bade practise in any way?


  • Banned

    Okay @bkvldr if what you are asking is -- Is there anything problematic with doing the following

        @pyqtSlot(object)
        def keyPressEvent(self, event):
            print('Captured Key Press Event in',self.Id,') ',event.text())
            QLineEdit.keyPressEvent(self, event)
            if self.text() == '':
                self._value = 0.0
            else:
                self._value = locale.atof(self.text())
    

    Absolutely not -- in fact that is pretty much the only way to do that. Style wise I would ask why you start your variables with an _ as that tends to denote its supposed to be private to the class variable which does not exit in Python so doing it is fairly pointless -- but again its purely style if its your preferred naming convention go for it

    By the way though you probably ought to do it this way instead

        @pyqtSlot(object)
        def keyPressEvent(self, event):
            print('Captured Key Press Event in',self.Id,') ',event.text())
            if self.text() == '':
                self._value = 0.0
            else:
                self._value = locale.atof(self.text())
            QLineEdit.keyPressEvent(self, event)
    

    Because normally you want to do the stuff you want to do before passing it on to the parent class to be finalized


Log in to reply