python-3.xpyqt5qtablewidgetundo-redoqdatawidgetmapper

QDataWidgetMapper; mapping TableWidget to model


I am not finding any documentation showing which widgets QDataWidgetMapper actually works for and have not found any implementation of mapping with a QTableWidget.

It definitely works for QLineEdit's and QComboBoxes, which are input widgets, but is it possible to map to a QTableWidget?

Goal is to use QUndoStack to undo/redo text change in each widget when added to the QUndostack. I want to be able to undo/redo text changes for the items in the QTableWidget as well as the QLineEdits and QComboBoxes.

from PyQt5 import QtCore, QtGui, QtWidgets
import sys

class CommandTextEdit(QtWidgets.QUndoCommand):
    def __init__(self, window, index, oldText, newText, description):
        super(CommandTextEdit,self).__init__()
        self.index = index
        self.window = window
        self.oldText = oldText
        self.newText = newText

    def redo(self):
        self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
        self.index.model().setData(self.index, self.newText, QtCore.Qt.EditRole)
        self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)

    def undo(self):
        self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
        self.index.model().setData(self.index, self.oldText, QtCore.Qt.EditRole)
        self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)

class Model(QtCore.QAbstractListModel):
    itemDataChanged = QtCore.pyqtSignal(object,object, object, object)
    def __init__(self, text = [], parent = None):
        super(Model,self).__init__(parent)
        self._text = text

    def rowCount(self,parent=QtCore.QModelIndex()):
        return len(self._text)

    def data(self,index,role):
        row = index.row()
        if role == QtCore.Qt.EditRole:
            return self._text[row]

        if role == QtCore.Qt.DisplayRole:
            value = self._text[row]
            return self._text[row]

    def model(self):
        return self

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

    def setData(self, index, value, role = QtCore.Qt.UserRole + 1):
        if index.isValid():
           if role == QtCore.Qt.EditRole:
                oldValue = self.data(index,role)
                self._text[index.row()] = value
                self.dataChanged.emit(index, index)
                if oldValue != value:
                    self.itemDataChanged.emit(index, oldValue, value, role)
                return True
        return False

class Window(QtWidgets.QMainWindow):
    def __init__(self, parent = None):
        super(Window,self).__init__(parent)
        self.setCentralWidget(QtWidgets.QWidget(self))
        self.setWindowTitle('Widget Mapping GUI')
        mainlayout = QtWidgets.QVBoxLayout()
        #Information for widgets
        items = ["","H","N","M"]
        #LineEdit1
        self.labelLineEdit1 = QtWidgets.QLabel()
        self.labelLineEdit1.setText('LineEdit1')
        self.LineEdit1 = QtWidgets.QLineEdit()
        firstBox = QtWidgets.QHBoxLayout()
        firstBox.addWidget(self.labelLineEdit1)
        firstBox.addWidget(self.LineEdit1)
        #LineEdit2
        self.labelLineEdit2 = QtWidgets.QLabel()
        self.labelLineEdit2.setText('LineEdit2')
        self.LineEdit2 = QtWidgets.QLineEdit()
        secondBox = QtWidgets.QHBoxLayout()
        secondBox.addWidget(self.labelLineEdit2)
        secondBox.addWidget(self.LineEdit2)
        #ComboBox1
        self.labelComboBox1 = QtWidgets.QLabel()
        self.labelComboBox1.setText('ComboBox1')
        self.ComboBox1 = QtWidgets.QComboBox()
        self.ComboBox1.addItems(items)

        thirdBox = QtWidgets.QHBoxLayout()
        thirdBox.addWidget(self.labelComboBox1)
        thirdBox.addWidget(self.ComboBox1)
        #ComboBox2
        self.labelComboBox2 = QtWidgets.QLabel()
        self.labelComboBox2.setText('ComboBox2')
        self.ComboBox2 = QtWidgets.QComboBox()
        self.ComboBox2.addItems(items)

        fourthBox = QtWidgets.QHBoxLayout()
        fourthBox.addWidget(self.labelComboBox2)
        fourthBox.addWidget(self.ComboBox2)
        #TableWidget
        self.TableWidget = QtWidgets.QTableWidget(5,1)
        #Set header labels
        self.TableWidget.setHorizontalHeaderLabels(["Dimensions"])
        self.TableWidget.setVerticalHeaderLabels(["A","B","C","D","E"])
        self.TableWidget.setItem(0,1,QtWidgets.QTableWidgetItem('1'))

        fifthBox = QtWidgets.QVBoxLayout()
        fifthBox.addWidget(self.TableWidget)
        #Add Layouts
        mainlayout.addLayout(firstBox)
        mainlayout.addLayout(secondBox)
        mainlayout.addLayout(thirdBox)
        mainlayout.addLayout(fourthBox)
        mainlayout.addLayout(fifthBox)
        self.centralWidget().setLayout(mainlayout)
        #Model
        self.mapper = None
        self.model = Model(['','','','',''])
        self.setModel(self.model)
        #QUndoStack
        self.undoStack = QtWidgets.QUndoStack()
        self.stackView = QtWidgets.QUndoView(self.undoStack)
        self.stackView.setWindowTitle('StackView')
        self.stackView.show()
        #Run init methods
        self.createActions()
        self.makeConnections()
        self.listViewMethod()

    def createActions(self):
        self.UndoAct = QtWidgets.QAction("Undo", self)
        self.UndoAct.setShortcut(QtGui.QKeySequence.Undo)
        self.UndoAct.triggered.connect(self.undoStack.undo)
        self.RedoAct = QtWidgets.QAction("Redo", self)
        self.RedoAct.setShortcut(QtGui.QKeySequence.Redo)
        self.RedoAct.triggered.connect(self.undoStack.redo)
        self.toolbar = self.addToolBar('Exit')
        self.toolbar.addAction(self.UndoAct)
        self.toolbar.addAction(self.RedoAct)

    def listViewMethod(self):
        self.listView = QtWidgets.QListView()
        self.listView.setModel(self.model)
        self.listView.setWindowTitle('ListView')
        self.listView.show()

    def makeConnections(self):
        self.model.itemDataChanged.connect(self.itemDataChangedSlot)

    def setModel(self, model):
        self.mapper = QtWidgets.QDataWidgetMapper(self)
        self.mapper.setOrientation(QtCore.Qt.Vertical)
        self.mapper.setModel(self.model)
        self.mapper.addMapping(self.LineEdit1, 0)
        self.mapper.addMapping(self.LineEdit2, 1)
        self.mapper.addMapping(self.ComboBox1, 2)
        self.mapper.addMapping(self.ComboBox2, 3)
        self.mapper.addMapping(self.TableWidget,4)
        self.mapper.toFirst()

    def itemDataChangedSlot(self, index, oldValue, value, role):
        if role == QtCore.Qt.EditRole:
            command = CommandTextEdit(self, index, oldValue, value, "Text changed from '{0}' to '{1}'".format(oldValue, value))
            self.undoStack.push(command)

def main():
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())



if __name__ == '__main__':
    main()


Solution

  • To map the items of a QTableWidget, I used the setCellWidget method to map each cell to the model using a custom QLineEdit class.

    Updated Code:

    from PyQt5 import QtCore, QtGui, QtWidgets
    import sys
    
    #implemented stylesheets for QLineEdit to imitate a QTableWidgetItem 
    class CustomLineEdit(QtWidgets.QLineEdit):
        def __init__(self):
            super(CustomLineEdit,self).__init__()
            self.setReadOnly(True)
            self.setFrame(False)
            self.setStyleSheet("QLineEdit { border: none; } \n" \
                               "QLineEdit::focus {background-color: #3daee9;} \n"\
                               "QLineEdit::focus::pressed {background-color: none;} \n"\
                               "QLineEdit::hover {background-color: #3daee9;}"
                                )
        def mouseDoubleClickEvent(self,event):
            self.setReadOnly(False)
            self.setStyleSheet("QLineEdit { border: none; \nbackground-color: none;} \n" \
                                )
    
        def focusOutEvent(self,event):
            self.setReadOnly(True)
            self.setStyleSheet("QLineEdit { border: none; } \n" \
                               "QLineEdit::focus {background-color: #3daee9;} \n"\
                               "QLineEdit::focus::pressed {background-color: none;} \n"\
                               "QLineEdit::hover {background-color: #3daee9;}"
                                )
    
    class CommandTextEdit(QtWidgets.QUndoCommand):
        def __init__(self, window, index, oldText, newText, description):
            super(CommandTextEdit,self).__init__()
            self.index = index
            self.window = window
            self.oldText = oldText
            self.newText = newText
    
        def redo(self):
            self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
            self.index.model().setData(self.index, self.newText, QtCore.Qt.EditRole)
            self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)
    
        def undo(self):
            self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
            self.index.model().setData(self.index, self.oldText, QtCore.Qt.EditRole)
            self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)
    
    class Model(QtCore.QAbstractListModel):
        itemDataChanged = QtCore.pyqtSignal(object,object, object, object)
        def __init__(self, text = [], parent = None):
            super(Model,self).__init__(parent)
            self._text = text
    
        def rowCount(self,parent=QtCore.QModelIndex()):
            return len(self._text)
    
        def data(self,index,role):
            row = index.row()
            if role == QtCore.Qt.EditRole:
                return self._text[row]
    
            if role == QtCore.Qt.DisplayRole:
                value = self._text[row]
                return self._text[row]
    
        def model(self):
            return self
    
        def flags(self, index):
            return QtCore.Qt.ItemIsEditable |QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
    
        def setData(self, index, value, role = QtCore.Qt.UserRole + 1):
            if index.isValid():
               if role == QtCore.Qt.EditRole:
                    oldValue = self.data(index,role)
                    self._text[index.row()] = value
                    self.dataChanged.emit(index, index)
                    if oldValue != value:
                        self.itemDataChanged.emit(index, oldValue, value, role)
                    return True
            return False
    
    class Window(QtWidgets.QMainWindow):
        def __init__(self, parent = None):
            super(Window,self).__init__(parent)
            self.setCentralWidget(QtWidgets.QWidget(self))
            self.setWindowTitle('Widget Mapping GUI')
            mainlayout = QtWidgets.QVBoxLayout()
            #Information for widgets
            items = ["","H","N","M"]
            #LineEdit1
            self.labelLineEdit1 = QtWidgets.QLabel()
            self.labelLineEdit1.setText('LineEdit1')
            self.LineEdit1 = QtWidgets.QLineEdit()
            firstBox = QtWidgets.QHBoxLayout()
            firstBox.addWidget(self.labelLineEdit1)
            firstBox.addWidget(self.LineEdit1)
            #LineEdit2
            self.labelLineEdit2 = QtWidgets.QLabel()
            self.labelLineEdit2.setText('LineEdit2')
            self.LineEdit2 = QtWidgets.QLineEdit()
            secondBox = QtWidgets.QHBoxLayout()
            secondBox.addWidget(self.labelLineEdit2)
            secondBox.addWidget(self.LineEdit2)
            #ComboBox1
            self.labelComboBox1 = QtWidgets.QLabel()
            self.labelComboBox1.setText('ComboBox1')
            self.ComboBox1 = QtWidgets.QComboBox()
            self.ComboBox1.addItems(items)
    
            thirdBox = QtWidgets.QHBoxLayout()
            thirdBox.addWidget(self.labelComboBox1)
            thirdBox.addWidget(self.ComboBox1)
            #ComboBox2
            self.labelComboBox2 = QtWidgets.QLabel()
            self.labelComboBox2.setText('ComboBox2')
            self.ComboBox2 = QtWidgets.QComboBox()
            self.ComboBox2.addItems(items)
    
            fourthBox = QtWidgets.QHBoxLayout()
            fourthBox.addWidget(self.labelComboBox2)
            fourthBox.addWidget(self.ComboBox2)
            #TableWidget
            self.TableWidget = QtWidgets.QTableWidget(5,1)
            #Set header labels
            self.TableWidget.setHorizontalHeaderLabels(["Dimensions"])
            self.TableWidget.setVerticalHeaderLabels(["A","B","C","D","E"])
            self.TableWidget.setCellWidget(0,0,CustomLineEdit())
            self.TableWidget.setCellWidget(1,0,CustomLineEdit())
            self.TableWidget.setCellWidget(2,0,CustomLineEdit())
            self.TableWidget.setCellWidget(3,0,CustomLineEdit())
            self.TableWidget.setCellWidget(4,0,CustomLineEdit())
            self.TableWidget.setItem(1,0,QtWidgets.QTableWidgetItem('1'))
            fifthBox = QtWidgets.QVBoxLayout()
            fifthBox.addWidget(self.TableWidget)
            #Add Layouts
            mainlayout.addLayout(firstBox)
            mainlayout.addLayout(secondBox)
            mainlayout.addLayout(thirdBox)
            mainlayout.addLayout(fourthBox)
            mainlayout.addLayout(fifthBox)
            self.centralWidget().setLayout(mainlayout)
            #Model
            self.mapper = None
            self.model = Model(['' for i in range(9)])
            self.setModel(self.model)
            #QUndoStack
            self.undoStack = QtWidgets.QUndoStack()
            self.stackView = QtWidgets.QUndoView(self.undoStack)
            self.stackView.setWindowTitle('StackView')
            self.stackView.show()
            #Run init methods
            self.createActions()
            self.makeConnections()
            self.listViewMethod()
    
        def createActions(self):
            self.UndoAct = QtWidgets.QAction("Undo", self)
            self.UndoAct.setShortcut(QtGui.QKeySequence.Undo)
            self.UndoAct.triggered.connect(self.undoStack.undo)
            self.RedoAct = QtWidgets.QAction("Redo", self)
            self.RedoAct.setShortcut(QtGui.QKeySequence.Redo)
            self.RedoAct.triggered.connect(self.undoStack.redo)
            self.toolbar = self.addToolBar('Exit')
            self.toolbar.addAction(self.UndoAct)
            self.toolbar.addAction(self.RedoAct)
    
    
        def listViewMethod(self):
            self.listView = QtWidgets.QListView()
            self.listView.setModel(self.model)
            self.listView.setWindowTitle('ListView')
            self.listView.show()
    
        def makeConnections(self):
            self.model.itemDataChanged.connect(self.itemDataChangedSlot)
    
        def setModel(self, model):
            self.mapper = QtWidgets.QDataWidgetMapper(self)
            self.mapper.setOrientation(QtCore.Qt.Vertical)
            self.mapper.setModel(self.model)
            self.mapper.addMapping(self.LineEdit1, 0)
            self.mapper.addMapping(self.LineEdit2, 1)
            self.mapper.addMapping(self.ComboBox1, 2)
            self.mapper.addMapping(self.ComboBox2, 3)
            self.mapper.addMapping(self.TableWidget.cellWidget(0,0), 4)
            self.mapper.addMapping(self.TableWidget.cellWidget(1,0), 5)
            self.mapper.addMapping(self.TableWidget.cellWidget(2,0), 6)
            self.mapper.addMapping(self.TableWidget.cellWidget(3,0), 7)
            self.mapper.addMapping(self.TableWidget.cellWidget(4,0), 8)
            self.mapper.toFirst()
    
        def itemDataChangedSlot(self, index, oldValue, value, role):
            if role == QtCore.Qt.EditRole:
                command = CommandTextEdit(self, index, oldValue, value, "Text changed from '{0}' to '{1}'".format(oldValue, value))
                self.undoStack.push(command)
    
    def main():
    
        app = QtWidgets.QApplication(sys.argv)
        window = Window()
        window.show()
        sys.exit(app.exec_())
    
    
    
    if __name__ == '__main__':
        main()