Как удалить данные из файла Excel, отображаемого в PyQt5, и обновить его

#python-3.x #pandas #pyqt5

#python-3.x #pandas #pyqt5

Вопрос:

Мое приложение базы данных использует библиотеку Pandas. Я могу отобразить файл Excel в своем TableView, но в любое время я удаляю данные с мэйнфрейма и пытаюсь обновить TableView. Это дает мне ошибку ключа.

Я пытаюсь заставить его отображать обновленную таблицу. Я пытаюсь удалить строку, которую запрашивает пользователь. Он работает, когда он падает, потому что я вывел информацию, но сам TableView не обновляется и выдает ошибку.

 df = pd.read_excel("filename")
model = PandasModel(df)
self.tableView.setModel(model)
self.tableView.resizeColumnsToContents()
  
 def DeletePlayer(self):
        global df
        choose = self.removePlayerEdit.text()
        if(choose == '0'):
            df = df.drop([0])
            print("Player deleted")
            print(df)
  
 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()
                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.ix[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'):
            # PyQt4 gets a QVariant
            value = value.toPyObject()
        else:
            # PySide gets an unicode
            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()
  

Ответ №1:

При реализации модели вы не должны напрямую обращаться к элементу, в котором хранятся данные (dataframe), потому что, если вы измените его, модель не будет знать, что вызовет проблемы, вместо этого вы должны создать методы, которые изменяют внутренние данные, но используют такие методы, как beginRemoveRows и endRemoveColumns, которые будут уведомлять пользователямодель изменения.

 def removeColumn(self, col):
    if 0 <= col < self.columnCount():
        self.beginRemoveRows(QtCore.QModelIndex(), col, col)
        self._df.drop(
            self._df.columns[[col]], axis=1, inplace=True
        )
        self._df.reset_index(inplace=True, drop=True)
        self.endRemoveColumns()

  

Я улучшил свою первоначальную модель до следующей:

 from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
import numpy as np


class FloatDelegate(QtWidgets.QStyledItemDelegate):
    @property
    def decimals(self):
        if not hasattr(self, "_decimals"):
            self._decimals = 2
        return self._decimals

    @decimals.setter
    def decimals(self, decimals):
        self._decimals = decimals

    def createEditor(self, parent, option, index):
        DBL_MAX = 1.7976931348623157e308
        editor = QtWidgets.QDoubleSpinBox(
            parent, minimum=-DBL_MAX, maximum=DBL_MAX, decimals=self.decimals
        )
        return editor

    def setEditorData(self, editor, index):
        editor.setValue(index.data())

    def setModelData(self, editor, model, index):
        model.setData(index, editor.value(), QtCore.Qt.DisplayRole)

    def displayText(self, value, locale):
        return "{}".format(value)


class DataFrameModel(QtCore.QAbstractTableModel):
    DtypeRole = QtCore.Qt.UserRole   1000
    ValueRole = QtCore.Qt.UserRole   1001

    def __init__(self, df=pd.DataFrame(), parent=None):
        super(DataFrameModel, self).__init__(parent)
        self._dataframe = df

    def setDataFrame(self, dataframe):
        self.beginResetModel()
        self._dataframe = dataframe.copy()
        self.endResetModel()

    def dataFrame(self):
        return self._dataframe

    dataFrame = QtCore.pyqtProperty(
        pd.DataFrame, fget=dataFrame, fset=setDataFrame
    )

    @QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
    def headerData(
        self,
        section: int,
        orientation: QtCore.Qt.Orientation,
        role: int = QtCore.Qt.DisplayRole,
    ):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self._dataframe.columns[section]
            else:
                return str(self._dataframe.index[section])
        return QtCore.QVariant()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self._dataframe.index)

    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dataframe.columns.size

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid() or not (
            0 <= index.row() < self.rowCount()
            and 0 <= index.column() < self.columnCount()
        ):
            return QtCore.QVariant()
        row = self._dataframe.index[index.row()]
        col = self._dataframe.columns[index.column()]
        dt = self._dataframe[col].dtype
        val = self._dataframe.iloc[row][col]
        if role == QtCore.Qt.DisplayRole:
            return val
        elif role == DataFrameModel.ValueRole:
            return val
        if role == DataFrameModel.DtypeRole:
            return dt
        return QtCore.QVariant()

    def setData(self, index, value, role):
        row = self._dataframe.index[index.row()]
        col = self._dataframe.columns[index.column()]
        if hasattr(value, "toPyObject"):
            # PyQt4 gets a QVariant
            value = value.toPyObject()
        else:
            # PySide gets an unicode
            dtype = self._dataframe[col].dtype
            if dtype != object:
                value = None if value == "" else dtype.type(value)
        self._dataframe.at[row, col] = value
        return True

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

    def roleNames(self):
        roles = {
            QtCore.Qt.DisplayRole: b"display",
            DataFrameModel.DtypeRole: b"dtype",
            DataFrameModel.ValueRole: b"value",
        }
        return roles

    def removeRow(self, row):
        if 0 <= row < self.rowCount():
            self.beginRemoveRows(QtCore.QModelIndex(), row, row)
            self._dataframe.drop([row], inplace=True)
            self._dataframe.reset_index(inplace=True, drop=True)
            self.endRemoveRows()

    def removeColumn(self, col):
        if 0 <= col < self.columnCount():
            self.beginRemoveRows(QtCore.QModelIndex(), col, col)
            self._dataframe.drop(
                self._dataframe.columns[[col]], axis=1, inplace=True
            )
            self._dataframe.reset_index(inplace=True, drop=True)
            self.endRemoveColumns()

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


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)

        tableview = QtWidgets.QTableView()
        tableview.setSortingEnabled(True)
        delegate = FloatDelegate(tableview)
        tableview.setItemDelegate(delegate)
        delegate.decimals = 4
        self.spinbox_row = QtWidgets.QSpinBox()
        self.button_row = QtWidgets.QPushButton(
            "Delete Row", clicked=self.remove_row
        )
        self.spinbox_col = QtWidgets.QSpinBox()
        self.button_col = QtWidgets.QPushButton(
            "Delete Column", clicked=self.remove_col
        )

        df = pd.DataFrame(
            np.random.uniform(0, 100, size=(100, 4)), columns=list("ABCD")
        )
        self._model = DataFrameModel(df)

        tableview.setModel(self._model)

        grid = QtWidgets.QGridLayout(self)
        grid.addWidget(tableview, 0, 0, 1, 4)
        grid.addWidget(self.spinbox_row, 1, 0)
        grid.addWidget(self.button_row, 1, 1)
        grid.addWidget(self.spinbox_col, 1, 2)
        grid.addWidget(self.button_col, 1, 3)

        self.on_rowChanged()
        self.on_columnChanged()

        self._model.rowsInserted.connect(self.on_rowChanged)
        self._model.rowsRemoved.connect(self.on_rowChanged)
        self._model.columnsInserted.connect(self.on_columnChanged)
        self._model.columnsRemoved.connect(self.on_columnChanged)

    @QtCore.pyqtSlot()
    def on_rowChanged(self):
        self.spinbox_row.setMaximum(self._model.rowCount() - 1)

    @QtCore.pyqtSlot()
    def on_columnChanged(self):
        self.spinbox_col.setMaximum(self._model.columnCount() - 1)

    @QtCore.pyqtSlot()
    def remove_row(self):
        row = self.spinbox_row.value()
        self._model.removeRow(row)

    @QtCore.pyqtSlot()
    def remove_col(self):
        col = self.spinbox_col.value()
        self._model.removeColumn(col)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())
  

Комментарии:

1. Я надеялся, что вы ответите, потому что я использовал вашу модель. Спасибо, это работает.