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:
unchecked
and CB state is unchecked
checked
(but not directly)checked
checked
checked
, doing what is done usually by the user click when not using the Model-View concept.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()
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.