0

I'm new to using PyQt; I'm trying to setup a very simple MainWindow with 3 line edits and 1 pushbutton.

Here is the behavior I'd like to get:

When the content of the two first buttons becomes valid, the pushbutton is enabled.

When the pushbutton is clicked, the content of the second line edit is reset to an empty sting and the pushbutton is disabled.

Below is the code - the view code is generated using Qt Designer and pyuic5.

Everything seems to be fine, except when the pushbutton is clicked with the mouse - the content of the line edit appears as not erased (actually it is) and the push button looks like enabled (actually it is disabled). If a refresh of the window is triggered (ie minimize and restore), the widgets are correctly displayed. When the pushbutton is clicked using the keyboard, everything is correctly refreshed.

I can't figure out what cause this strange artifact... any help would be greatly appreciated.

[Edit]: I've just discovered it works fine only when the pushbutton is "clicked" with the enter key, if space is used the same flawed behavior is noticed... It makes me wonder what is the difference between "enter" and any other click mean?

Thank you in advance

Main python code:

from PyQt5 import QtWidgets, QtCore
import sys

import view1


class Model(QtCore.QObject):
    text1_changed = QtCore.pyqtSignal(str)
    text2_changed = QtCore.pyqtSignal(str)
    text3_changed = QtCore.pyqtSignal(str)
    canprocess_changed = QtCore.pyqtSignal(bool)

    def __init__(self):
        super().__init__()
        self._text1 = ''
        self._text2 = ''
        self._text3 = ''
        self._canprocess = False

    @property
    def text1(self):
        return self._text1

    @text1.setter
    def text1(self, value):
        self._text1 = value
        self.text1_changed.emit(value)

    @property
    def text2(self):
        return self._text2

    @text2.setter
    def text2(self, value):
        self._text2 = value
        self.text2_changed.emit(value)

    @property
    def text3(self):
        return self._text3

    @text3.setter
    def text3(self, value):
        self._text3 = value
        self.text3_changed.emit(value)

    @property
    def canprocess(self):
        return self._canprocess

    @canprocess.setter
    def canprocess(self, value):
        self._canprocess = value
        self.canprocess_changed.emit(value)


class Controller(QtCore.QObject):
    def __init__(self, model: Model):
        super().__init__()
        self._model = model
        self._text1valid = False
        self._text2valid = False

    def istext1valid(self, text) -> bool:
        return text[:3] == 'abc'

    def istext2valid(self, text) -> bool:
        return text[:3] == 'def'

    def _validity_update(self):
        self._model.canprocess = self._text1valid and self._text2valid

    @QtCore.pyqtSlot(str)
    def change_text1(self, text):
        self._text1valid = self.istext1valid(text)
        self._validity_update()
        if self._text1valid:
            self._model.text1 = text

    @QtCore.pyqtSlot(str)
    def change_text2(self, text):
        self._text2valid = self.istext2valid(text)
        self._validity_update()
        if self._text2valid:
            self._model.text2 = text

    @QtCore.pyqtSlot(str)
    def change_text3(self, text):
        self._model.text3 = text

    @QtCore.pyqtSlot()
    def process(self):
        # do real stuff
        self._text2valid = False
        self._validity_update()
        self._model.text2 = ''

    def init_view(self):
        self._text1valid = False
        self._text2valid = False
        self._validity_update()
        self._model.text1=''
        self._model.text2=''
        self._model.text3='an init value'



class MainAppWindow(QtWidgets.QMainWindow, view1.Ui_MainWindow):
    def __init__(self, model: Model, controller: Controller):
        super().__init__()
        self._model = model
        self._controller = controller
        self.setupUi(self)

    def setupUi(self, window):
        super().setupUi(self)
        self.bProceed.clicked.connect(self._controller.process)
        self.ledit1.editingFinished.connect(lambda: self._controller.change_text1(self.ledit1.text()))
        self.ledit2.editingFinished.connect(lambda: self._controller.change_text2(self.ledit2.text()))
        self.ledit3.editingFinished.connect(lambda: self._controller.change_text3(self.ledit3.text()))

        self._model.text1_changed.connect(self.ledit1.setText)
        self._model.text2_changed.connect(self.ledit2.setText)
        self._model.text3_changed.connect(self.ledit3.setText)
        self._model.canprocess_changed.connect(self.bProceed.setEnabled)

        self._controller.init_view()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    mainmodel = Model()
    maincontroller = Controller(mainmodel)
    MainWindow = MainAppWindow(mainmodel, maincontroller)
    MainWindow.show()
    sys.exit(app.exec_())

Generated UI:

from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(435, 204)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.formLayout = QtWidgets.QFormLayout(self.centralwidget)
        self.formLayout.setObjectName("formLayout")
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setObjectName("label_2")
        self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_2)
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setObjectName("label")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label)
        self.ledit1 = QtWidgets.QLineEdit(self.centralwidget)
        self.ledit1.setObjectName("ledit1")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.ledit1)
        self.ledit2 = QtWidgets.QLineEdit(self.centralwidget)
        self.ledit2.setObjectName("ledit2")
        self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.ledit2)
        self.bProceed = QtWidgets.QPushButton(self.centralwidget)
        self.bProceed.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.bProceed.setDefault(True)
        self.bProceed.setObjectName("bProceed")
        self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.bProceed)
        self.ledit3 = QtWidgets.QLineEdit(self.centralwidget)
        self.ledit3.setObjectName("ledit3")
        self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.ledit3)
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setObjectName("label_3")
        self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_3)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 435, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
        MainWindow.setTabOrder(self.ledit1, self.ledit2)
        MainWindow.setTabOrder(self.ledit2, self.bProceed)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label_2.setText(_translate("MainWindow", "Text2"))
        self.label.setText(_translate("MainWindow", "Text1"))
        self.bProceed.setText(_translate("MainWindow", "Proceed"))
        self.label_3.setText(_translate("MainWindow", "Text3"))

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Christian
  • 685
  • 1
  • 5
  • 13
  • 1
    Is there a specific reason for a structure that complex? You can achieve the same result (enabling/disabling the button) with a single function that just checks the contents of all line edits whenever *any* of them is changed, without the need to call lots of functions and connect too many signals unnecessarily. In any case, what PyQt version do you have and on what OS? – musicamante Jun 23 '20 at 13:38
  • @musicamante: the posted code is just a mockup of the real code - I've only tried to reproduce the mechanic of the real application. Os: OSX catalina PyQT: 5.15.0 Python: 3.6 – Christian Jun 24 '20 at 11:04

1 Answers1

1

It turns out that has been a know bug of PyQt5 since version v5.10 specific to Mac OSX which affects almost any widget whose content is programmatically changed.

The solution has been to subclass any widget whose content is changed and to explicitly call repaint().

As an example, here is the code I use for the pushbutton:

class OSxPushButton(QtWidgets.QPushButton):
"""
A class to correct an OSX bug affecting widgets update when attributes are
programmatically modified.
"""
def __init__(self, parent=None):
    super().__init__(parent)

def setEnabled(self, a0: bool) -> None:
    super().setEnabled(a0)
    self.repaint()
Christian
  • 685
  • 1
  • 5
  • 13
  • 1
    Hi @Christian, could you please add a link to the bug report? Also, `update()` is generally preferred over `repaint()` (see https://stackoverflow.com/questions/30728820/refreshing-a-qwidget ) – JKSH Aug 19 '20 at 01:30
  • Hi, @JKSH - I'm not sure there is a bug report tracker for pyQT. I got the answer and the solution from the mailing list. Here is the link to the corresponding archive: [https://www.riverbankcomputing.com/pipermail/pyqt/2020-June/043024.html](https://www.riverbankcomputing.com/pipermail/pyqt/2020-June/043024.html) – Christian Aug 19 '20 at 18:33
  • Had the same issue on Windows with PyQt==5.15.1. .update() and/or .repaint() are working fine for me. @JKSH – matl Oct 23 '20 at 07:58