pythonmodelqt5pyside2qcheckbox

How to use Model-View with a QCheckBox?


How to use the model-view approach with a checkbox? The view is expected to display the model status but when the user clicks on the view, the checkbox status is actually changed before it is told by the model. For example the sequence should be:

However this doesn't happen because between steps 2 and 3 the CB state switches to checked and the view actually asks the model to switch to the other state, unchecked.

from qtpy.QtCore import QObject, Signal
from qtpy.QtWidgets import QApplication, QMainWindow, QWidget, QCheckBox, QHBoxLayout

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.model = Model()
        self.view = View(self.model)
        self.setCentralWidget(self.view)

class View(QWidget):
    user_request = Signal(bool)

    def __init__(self, model):
        super().__init__()
        self.model = model
        self.cb = QCheckBox('Click Me')
        layout = QHBoxLayout(self)
        layout.addWidget(self.cb)
        self.cb.clicked.connect(self.cb_clicked)
        self.cb.stateChanged.connect(self.cb_changed)
        self.model.updated.connect(self.update_from_model)
        self.user_request.connect(self.model.request_update)

    def cb_clicked(self):
        current_state = self.cb.isChecked()
        desired = not current_state
        print('User wants the state to be', desired)
        self.user_request.emit(desired)

    def cb_changed(self, state):
        states = ['unchecked', 'tristate', 'checked']
        print('CB has been updated to', states[state], '\n')

    def update_from_model(self, state):
        print('View aligns CB on model state', state)
        self.cb.setChecked(state)

class Model(QObject):
    updated = Signal(bool)

    def __init__(self):
        super().__init__()
        self.state = False
        self.updated.emit(self.state)

    def data(self):
        return self.state

    def request_update(self, checked):
        self.state = checked
        print('Model sets its state to', checked)
        self.updated.emit(checked)

def main():
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

if __name__ == '__main__':
    main()

Solution

  • To achieve desired dataflow you can override default QCheckBox behavior, in particular handling mouse events (keyboard event handlers needs to be overriden too).

    Here's demo in which user is allowed to change checkbox state no more than three times to demonstrate that model manages checkbox state not checkbox itself.

    from qtpy.QtCore import QObject, Signal
    from qtpy.QtWidgets import QApplication, QWidget, QCheckBox, QHBoxLayout
    
    class CheckBox(QCheckBox):
    
        def __init__(self, parent = None):
            super().__init__(parent)
            self._down = False
    
        def mousePressEvent(self, e):
            self._down = self.hitButton(e.position().toPoint())
    
        def mouseReleaseEvent(self, e):
            if self.hitButton(e.position().toPoint()) and self._down:
                self.clicked[bool].emit(not self.isChecked())
            self._down = False
        
    class Model(QObject):
    
        changed = Signal(bool)
    
        def __init__(self, state, parent = None):
            super().__init__(parent)
            self._state = state
            self._count = 0
    
        def state(self):
            return self._state
        
        def setState(self, state):
    
            if self._count >= 3:
                return
    
            if state != self._state:
                self._count += 1
                self._state = state
                self.changed.emit(state)
    
            
    class View(QWidget):
    
        def __init__(self, parent = None):
            super().__init__(parent)
    
            model = Model(True, self)
            checkBox = CheckBox('Click Me') 
    
            self.model = model
            self.checkBox = checkBox
    
            layout = QHBoxLayout(self)
            layout.addWidget(checkBox)
            
            # init checkbox state
            checkBox.setChecked(model.state())
    
            # control checkbox state with a model
            checkBox.clicked.connect(model.setState)
            model.changed.connect(checkBox.setChecked)
    
    def main():
        app = QApplication([])
        view = View()
        view.show()
        app.exec()
    
    if __name__ == '__main__':
        main()
    

    Note: there's QDataWidgetMapper exactly for that, except it sends changes to model when widget loses focus and allows widget to manage it's state until it happen, which makes it unusable with checkboxes and not fun with every other widgets if you prefer reactivity. So to make things realy neat you need to implement your own version of QDataWidgetMapper without this drawbacks, which is not an easy task.