Search code examples
pythonpyqt5qtableviewqabstracttablemodel

update dataframe after edited in qTableView Python PyQt5


Good day,

I am trying to load a dataframe into a PyQt5 QTableView to allow edit last column with ComboBox. Once the edit is complete, print the dataframe so that I can edit it further. When I click 'print_data' I cannot get the updated QTableView model to be printed. The code can be seen below.

here is my GUI with data, the last column should be editable

import sys
from PyQt5.QtWidgets import (QWidget, QLabel, QLineEdit, QTextEdit, QGridLayout, QApplication)
import pandas as pd
import numpy as np
import PyQt5 
from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QGroupBox, QHBoxLayout, QMainWindow, QApplication, QLineEdit, QFileDialog,  QTableWidget,QTableWidgetItem, QTableView, QStyledItemDelegate
from PyQt5 import QtCore, QtGui, QtWidgets   
import os
from PyQt5.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QApplication)
from PyQt5.QtGui import QIcon
import re

def dataframe():
    lst = [['tom', 'reacher', 'True'], ['krish', 'pete', 'True'], 
           ['nick', 'wilson', 'True'], ['juli', 'williams', 'True']] 
    df = pd.DataFrame(lst, columns =['FName', 'LName', 'Student?'], dtype = float) 
    return df

class Delegate(QtWidgets.QItemDelegate):
    def __init__(self, owner, choices):
        super().__init__(owner)
        self.items = choices

    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QComboBox(parent)
        self.editor.currentIndexChanged.connect(self.commit_editor)
        self.editor.addItems(self.items)
        return self.editor    

    def paint(self, painter, option, index):
        value = index.data(QtCore.Qt.DisplayRole)
        style = QtWidgets.QApplication.style()
        opt = QtWidgets.QStyleOptionComboBox()
        opt.text = str(value)
        opt.rect = option.rect
        style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt, painter)
        QtWidgets.QItemDelegate.paint(self, painter, option, index)

    def commit_editor(self):      ####test
        editor = self.sender()
        self.commitData.emit(editor)


    def setEditorData(self, editor, index):
        value = index.data(QtCore.Qt.DisplayRole)
        num = self.items.index(value)
        editor.setCurrentIndex(num)

    def setModelData(self, editor, model, index):
        value = editor.currentText()
        model.setData(index, value, QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)


class PandasModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self._data = data

    def rowCount(self, parent=None):
        return self._data.shape[0]

    def columnCount(self, parent=None):
        return self._data.shape[1]

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

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if index.isValid():
            if role == QtCore.Qt.DisplayRole:
                return str(self._data.iloc[index.row(), index.column()])
        return None

    def headerData(self, col, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self._data.columns[col]
        return None

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

class MyWindow(QWidget):

    def __init__(self):
        super().__init__()
        self.setGeometry(300, 200 ,600, 400)
        self.setWindowTitle('Test')
        self.initUI()


    def show_data(self):
        choices = ['True', 'False']
        self.model = PandasModel(dataframe())
        self.table_data.setModel(self.model)                            
        self.table_data.setItemDelegateForColumn(2, Delegate(self,choices))
        ##make combo boxes editable with a single-click:
        for row in range(5):  
            self.table_data.openPersistentEditor(self.model.index(row, 2))

    def print_data(self):
        print(self.table_data.model()._data)

    def initUI(self):

        welcom = QLabel('Welcome to my app!')

        self.btn_print_data = QPushButton('print data')
        self.btn_print_data.clicked.connect(self.print_data)  ##test

        self.btn_show_table = QPushButton('show data')
        self.btn_show_table.clicked.connect(self.show_data)

        self.table_data = QTableView()
        #self.table_result = QTableView()

        hbox1 = QHBoxLayout()
        hbox1.addWidget(welcom)


        vbox2 = QVBoxLayout()
        vbox2.addWidget(self.btn_show_table)
        vbox2.addWidget(self.btn_print_data) ####test

        vbox3 = QVBoxLayout()
        vbox3.addWidget(self.table_data)
        #vbox3.addWidget(self.table_result)

        hbox2 = QHBoxLayout()
        hbox2.addLayout(vbox2)
        hbox2.addLayout(vbox3)

        vbox1 = QVBoxLayout()
        vbox1.addLayout(hbox1)
        vbox1.addLayout(hbox2)

        self.setLayout(vbox1)
        self.show()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MyWindow()
    sys.exit(app.exec_())

Solution

  • Firstly, I couldn't run your code, so I've changed:

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        self._data[index.row()][index.column()] = value
    

    to

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        self._data.iloc[index.row(), index.column()] = value
    

    Then I realize that every time you click the "show data" button, you are loading the the original dataframe again, so it will erase all changes.

        def show_data(self):
            ...
            self.model = PandasModel(dataframe())
    

    So I added to init "self.df = dataframe()"

        def __init__(self):
            ....
            self.df = dataframe()
    

    and then changed the show_data

        def show_data(self):
            ...
            self.model = PandasModel(self.df)
    

    I think this solves the problem, I hope this will help you as much as helped me, who came here searching for this kind of implementation of editable Pandas tables in PyQt5