Make a immediate change to model using QDataWidgetMapper

Viewed 442

I have a QTableView that has a column with True/False values. To change value in this column I use a QDataWidgetMapper checkBox.

I wish to change a value immediately after clicking a checkbox. Usually a change is made after checkbox looses focus (in other words you need to make extra click somewhere else in application).

I have tried to do it like this: checkBox.stateChanged.connect(lambda: data_mapper.submit()), but I don't like that using this method too many signals are emitted: first it emits signals that all mapped items were changed, and after checkbox looses focus - a signal that only one item was changed.

So the goal is to update model immediately after clicking a checkbox and have only one signal emitted.

Code:

from PyQt5 import QtCore, QtGui, QtWidgets
import sys

class Mainwindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()
        self.widget_1 = Widget_1(self)

        self.layout_1 = QtWidgets.QHBoxLayout()
        self.layout_1.addWidget(self.table)
        self.layout_1.addWidget(self.widget_1)

        self.setLayout(self.layout_1)

        headers = ['State', 'Prorerty_1', 'Prorerty_2']
        data = [
        [True, '1', '2'],
        [False, '3', '4']
        ]

        self.model = ListModel(data, headers)
        self.table.setModel(self.model)
        self.table.setSelectionBehavior(self.table.SelectRows)

        self.table.clicked.connect(self.set_data_mapper)

        self.model.dataChanged.connect(lambda value: print(value.row(), value.column(), value.data()))

    def set_data_mapper(self):
        position = self.table.selectionModel().selectedIndexes()[0].row()

        self.widget_1.data_mapper.setModel(self.model)
        self.widget_1.data_mapper.addMapping(self.widget_1.checkBox, 0)
        self.widget_1.data_mapper.addMapping(self.widget_1.lineEdit, 1)
        self.widget_1.data_mapper.setCurrentIndex(position)

        self.widget_1.checkBox.stateChanged.connect(lambda: self.widget_1.data_mapper.submit())

class Widget_1(QtWidgets.QWidget):

    def __init__(self, parent):
        super().__init__()
        self.parent = parent

        self.layout_1 = QtWidgets.QGridLayout()
        self.state = QtWidgets.QLabel('State')
        self.checkBox = QtWidgets.QCheckBox()
        self.property = QtWidgets.QLabel('Prop.1')
        self.lineEdit = QtWidgets.QLineEdit()

        self.layout_1.addWidget(self.state, 0, 0)
        self.layout_1.addWidget(self.checkBox, 0, 1)
        self.layout_1.addWidget(self.property, 1, 0)
        self.layout_1.addWidget(self.lineEdit, 1, 1)

        self.setLayout(self.layout_1)

        self.data_mapper = QtWidgets.QDataWidgetMapper()

class ListModel(QtCore.QAbstractTableModel):

    def __init__(self, data_list = [[]], headers = [], parent = None):
        super(ListModel, self).__init__()
        self.data_list = data_list
        self.headers = headers

    def rowCount(self, parent):
        return len(self.data_list)

    def columnCount(self, parent):
        return len(self.headers)

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            column = index.column()
            value = self.data_list[row][column]
            return value

        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            value = self.data_list[row][column]
            return value

        if role == QtCore.Qt.FontRole:
            if index.column() == 0:
                boldfont = QtGui.QFont()
                boldfont.setBold(True)
                return boldfont

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            self.data_list[row][column] = value
            self.dataChanged.emit(index, index)
            return True
        return False

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self.headers[section]

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    application = Mainwindow()
    application.show()


    sys.exit(app.exec())
2 Answers

By default the QDataWidgetMapper policy is "AutoSubmit" which updates the model if any widget loses focus, so if you want it not to be emitted when it loses focus then you must use the "ManualSubmit" policy. On the other hand, the connection must be done only once since if you do it n times then n signals will be emitted and in your case "set_data_mapper is invoked every time the button is pressed.

class Mainwindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView(
            selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
        )
        self.widget_1 = Widget_1(self)

        lay = QtWidgets.QHBoxLayout(self)
        lay.addWidget(self.table)
        lay.addWidget(self.widget_1)

        headers = ["State", "Prorerty_1", "Prorerty_2"]
        data = [[True, "1", "2"], [False, "3", "4"]]

        self.model = ListModel(data, headers)
        self.table.setModel(self.model)
        self.table.setSelectionBehavior(self.table.SelectRows)

        self.widget_1.data_mapper.setModel(self.model)
        self.widget_1.data_mapper.addMapping(self.widget_1.checkBox, 0)
        self.widget_1.data_mapper.addMapping(self.widget_1.lineEdit, 1)

        self.widget_1.data_mapper.setSubmitPolicy(
            QtWidgets.QDataWidgetMapper.ManualSubmit
        )

        self.widget_1.checkBox.stateChanged.connect(self.widget_1.data_mapper.submit)
        self.widget_1.lineEdit.textChanged.connect(self.widget_1.data_mapper.submit)

        self.table.clicked.connect(self.update_current_index)

        self.model.dataChanged.connect(
            lambda value: print(value.row(), value.column(), value.data())
        )

    def update_current_index(self, index):
        position = index.row()
        self.widget_1.data_mapper.setCurrentIndex(position)

I have figured out this problem: now only one signal is emitted immediately after change.

First, I used "ManualSubmit" policy for QDataWidgetMapper , as @eyllanesc told.

Second, I wrote my own custom function for both lineEdit and a checkBox instead data_mapper.submit().

Manual connection between mapped widgets and model is done once in 'for position in range(len(self.model.data_list))' loop.

Also, I want to pay attention that inside 'data_mapper_settings' function I used 'clicked' instead 'stateChanged' for checBox and 'textEdited' instead 'textChanged' for lineEdit to avoid emitting extra signals when other row is chosen.

Still I wonder if there's more elegant way to solve this problem

from PyQt5 import QtCore, QtGui, QtWidgets
import sys

class Mainwindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()
        self.widget_1 = Widget_1(self)

        self.layout_1 = QtWidgets.QHBoxLayout()
        self.layout_1.addWidget(self.table)
        self.layout_1.addWidget(self.widget_1)

        self.setLayout(self.layout_1)

        headers = ['State', 'Prorerty_1', 'Prorerty_2']
        data = [
        [True, '1', '2'],
        [False, '3', '4']
        ]

        self.model = ListModel(data, headers)
        self.table.setModel(self.model)
        self.table.setSelectionBehavior(self.table.SelectRows)

        self.table.clicked.connect(lambda index: self.set_data_mapper(index))

        for position in range(len(self.model.data_list)):
            self.data_mapper_settings(position)


        self.model.dataChanged.connect(lambda index: print(index.row(), index.column(), index.data()))


    def set_data_mapper(self, index):
        position = index.row()

        self.widget_1.data_mapper.setModel(self.model)
        self.widget_1.data_mapper.addMapping(self.widget_1.checkBox, 0)
        self.widget_1.data_mapper.addMapping(self.widget_1.lineEdit, 1)
        self.widget_1.data_mapper.setCurrentIndex(position)

    def data_mapper_settings(self, position):

        self.widget_1.checkBox.clicked.connect(lambda value: self.submit_checkbox(value, position))
        self.widget_1.lineEdit.textEdited.connect(lambda value: self.submit_lineEdit(value, position))

    def submit_checkbox(self, value, position):
        if position == self.table.selectionModel().selectedIndexes()[0].row():
            self.model.setData(self.model.index(position, 0), value)

    def submit_lineEdit(self, value, position):
        if position == self.table.selectionModel().selectedIndexes()[0].row():
            self.model.setData(self.model.index(position, 1), value)

class Widget_1(QtWidgets.QWidget):

    def __init__(self, parent):
        super().__init__()
        self.parent = parent

        self.layout_1 = QtWidgets.QGridLayout()
        self.state = QtWidgets.QLabel('State')
        self.checkBox = QtWidgets.QCheckBox()
        self.property = QtWidgets.QLabel('Prop.1')
        self.lineEdit = QtWidgets.QLineEdit()

        self.layout_1.addWidget(self.state, 0, 0)
        self.layout_1.addWidget(self.checkBox, 0, 1)
        self.layout_1.addWidget(self.property, 1, 0)
        self.layout_1.addWidget(self.lineEdit, 1, 1)

        self.setLayout(self.layout_1)

        self.data_mapper = QtWidgets.QDataWidgetMapper()
        self.data_mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.ManualSubmit)

class ListModel(QtCore.QAbstractTableModel):

    def __init__(self, data_list = [[]], headers = [], parent = None):
        super(ListModel, self).__init__()
        self.data_list = data_list
        self.headers = headers

    def rowCount(self, parent):
        return len(self.data_list)

    def columnCount(self, parent):
        return len(self.headers)

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            column = index.column()
            value = self.data_list[row][column]
            return value

        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            value = self.data_list[row][column]
            return value

        if role == QtCore.Qt.FontRole:
            if index.column() == 0:
                boldfont = QtGui.QFont()
                boldfont.setBold(True)
                return boldfont

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            self.data_list[row][column] = value
            self.dataChanged.emit(index, index)
            return True
        return False

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self.headers[section]

    def flags(self, index):
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsUserCheckable

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    application = Mainwindow()
    application.show()


    sys.exit(app.exec())
Related