dying9408
dying9408

Reputation: 3

Saving updated dataframe from edited PyQt5 QTableView object

I am currently trying to load a dataframe into a PyQt QTableView to allow the re-naming of desired columns. Once the re-naming is complete, save the new dataframe as a .csv in a local folder. I cannot get the updated QTableView model to save. The workflow can be seen below.

  1. Read the .csv I would like to modify

  1. Load the dataframe into the QTableView with a Combobox in every column for the first row

  1. Be able to select different options to rename the column

  1. Select the desired name for desired columns

It would also be helpful if a certain option was selected in the Combobox (for instance, "Default"), it would make the desired column name to be the same as the original column name.

  1. Save the final dataframe as a file to a local folder

Note: Only columns that have a value in the combobox are kept in the final dataset. Columns that are specified as "Default" are kept with the original column name.

Example of the Code below:

form_class = uic.loadUiType("DataProcessing.ui")[0]

class MyWindowClass(QtWidgets.QMainWindow, form_class):
    def __init__(self, parent=None):
        super().__init__()
        self.setupUi(self)

        self.PushButtonDisplay.clicked.connect(self.IP_Data_Display)
        self.PushButtonImport.clicked.connect(self.IP_File_Import)


    def IP_Data_Display(self):
        DT_Disp = self.CBdisplay.currentText()
        data = pd.read_csv('Example.csv')
        data.loc[-1] = pd.Series([np.nan])
        data.index = data.index + 1
        data = data.sort_index()
        model = PandasModel(data)
        self.TView.setModel(model)
        for column in range(model.columnCount()):
            c = QtWidgets.QComboBox()

            c.addItems(['','Option 1','Option 2','Option 3','Option 4','Default'])
            i = self.TView.model().index(0,column)
            self.TView.setIndexWidget(i,c)

    def IP_File_Import(self):
        newModel = self.TView.model()
        data = []
        for row in range(newModel.rowCount()):
            rowRes = []
            for column in range(newModel.columnCount()):
                index = newModel.index(row, column)
                item = newModel.data(index)
                if item != '':
                rowRes.append(item)
            data.append(rowRes)
        dataFrame = pd.DataFrame(data)
        dataFrame.to_csv('Test.csv')#, index=False, header=False)


class PandasModel(QtCore.QAbstractTableModel): 
    def __init__(self, df = pd.DataFrame(), parent=None): 
        QtCore.QAbstractTableModel.__init__(self, parent=parent)
        self._df = df

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()

        if orientation == QtCore.Qt.Horizontal:
            try:
                return self._df.columns.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()
        elif orientation == QtCore.Qt.Vertical:
            try:
                return self._df.index.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()

        if not index.isValid():
            return QtCore.QVariant()

        return QtCore.QVariant(str(self._df.iloc[index.row(), index.column()]))

    def setData(self, index, value, role):
        row = self._df.index[index.row()]
        col = self._df.columns[index.column()]
        if hasattr(value, 'toPyObject'):
            value = value.toPyObject()
        else:
            dtype = self._df[col].dtype
        if dtype != object:
            value = None if value == '' else dtype.type(value)
        self._df.set_value(row, col, value)
        return True

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

    def columnCount(self, parent=QtCore.QModelIndex()): 
        return len(self._df.columns)

    def sort(self, column, order):
        colname = self._df.columns.tolist()[column]
        self.layoutAboutToBeChanged.emit()
        self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
        self._df.reset_index(inplace=True, drop=True)
        self.layoutChanged.emit()


if __name__ == '__main__':
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    else:
        print('QApplication instance already exists: %s' % str(app))
    main = MyWindowClass(None)
    main.show()

    sys.exit(app.exec_())

Upvotes: 0

Views: 2958

Answers (1)

eyllanesc
eyllanesc

Reputation: 243955

Instead of using setItemWidget it is best to create a delegate that has a permanently open editor since it has access to the QModelIndex, on the other hand a row that will have the data of the header is added. And finally use _df to get the pandas.

import sys
from PyQt5 import QtCore, QtWidgets, uic
import pandas as pd
import numpy as np

form_class = uic.loadUiType("DataProcessing.ui")[0]

class PandasModel(QtCore.QAbstractTableModel): 
    def __init__(self, df = pd.DataFrame(), parent=None): 
        QtCore.QAbstractTableModel.__init__(self, parent=parent)
        self._df = df

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()
        if orientation == QtCore.Qt.Horizontal:
            try:
                return self._df.columns.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()
        return super(PandasModel, self).headerData(section, orientation, role)

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()
        if not index.isValid():
            return QtCore.QVariant()
        if index.row() == 0:
            return QtCore.QVariant(self._df.columns.values[index.column()])
        return QtCore.QVariant(str(self._df.iloc[index.row()-1, index.column()]))

    def setData(self, index, value, role):
        if index.row() == 0:
            if isinstance(value, QtCore.QVariant):
                value = value.value()
            if hasattr(value, 'toPyObject'):
                value = value.toPyObject()
            self._df.columns.values[index.column()] = value
            self.headerDataChanged.emit(QtCore.Qt.Horizontal, index.column(), index.column())
        else:
            col = self._df.columns[index.column()]
            row = self._df.index[index.row()-1]
            if isinstance(value, QtCore.QVariant):
                value = value.value()
            if hasattr(value, 'toPyObject'):
                value = value.toPyObject()
            else:
                dtype = self._df[col].dtype
            if dtype != object:
                value = None if value == '' else dtype.type(value)
                self._df.set_value(row, col, value)
        return True

    def rowCount(self, parent=QtCore.QModelIndex()): 
        return len(self._df.index) +1 

    def columnCount(self, parent=QtCore.QModelIndex()): 
        return len(self._df.columns)

    def sort(self, column, order):
        colname = self._df.columns.tolist()[column]
        self.layoutAboutToBeChanged.emit()
        self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
        self._df.reset_index(inplace=True, drop=True)
        self.layoutChanged.emit()


class ComboBoxDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        editor = QtWidgets.QComboBox(parent)
        value = index.data()
        options = [value, 'Option 1','Option 2','Option 3','Option 4','Default']
        editor.addItems(options)
        editor.currentTextChanged.connect(self.commitAndCloseEditor)
        return editor

    @QtCore.pyqtSlot()
    def commitAndCloseEditor(self):
        editor = self.sender()
        self.commitData.emit(editor)


class MyWindowClass(QtWidgets.QMainWindow, form_class):
    def __init__(self, parent=None):
        super().__init__()
        self.setupUi(self)
        self.PushButtonDisplay.clicked.connect(self.IP_Data_Display)
        self.PushButtonImport.clicked.connect(self.IP_File_Import)
        delegate = ComboBoxDelegate(self.TView)
        self.TView.setItemDelegateForRow(0, delegate)

    def IP_Data_Display(self):
        DT_Disp = self.CBdisplay.currentText()
        data = pd.read_csv('Example.csv')
        data = data.sort_index()
        model = PandasModel(data)
        self.TView.setModel(model)
        for i in range(model.columnCount()):
            ix = model.index(0, i)
            self.TView.openPersistentEditor(ix)

    def IP_File_Import(self):
        newModel = self.TView.model()
        dataFrame = newModel._df.copy()
        dataFrame.to_csv('Test.csv') #, index=False, header=False)


if __name__ == '__main__':
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    else:
        print('QApplication instance already exists: %s' % str(app))
    main = MyWindowClass(None)
    main.show()

    sys.exit(app.exec_())

Upvotes: 1

Related Questions