Модель PyQt для подключения к нескольким различным QTableViews?

#python #pandas #pyqt #pyqt5 #qtableview

#питон #pandas #pyqt ( пикт ) #pyqt5 #qtableview ( просмотр таблицы ) #python #pyqt #qtableview

Вопрос:

У меня есть классы, определенные в приведенном ниже коде, для отображения фрейма данных Pandas (структура данных для представления 2D-таблиц) с мультизаголовком (заголовок с несколькими уровнями), подобный этому:

введите описание изображения здесь

Как могут выглядеть данные в Excel: введите описание изображения здесь

Я делаю это с помощью двух QTableViews, одного для данных в самом фрейме данных и одного для меток с несколькими заголовками. Однако я хотел бы иметь возможность хранить фрейм данных в одной модели и подключать к нему эти несколько QTableViews. В идеале я мог бы передать дополнительный аргумент методу data() из представления, указывающий, предназначено ли представление для заголовка или тела, но я не думаю, что это возможно?

По некоторым причинам я хочу объединить их в единую модель…

  • Фрейм данных не синхронизируется между заголовком и телом. Другими словами, header.model().df is data.model().df имеет значение True для запуска, но False после delete_first_column вызова и self.df перезаписи
  • Структурно это имеет больше смысла, поскольку фрейм данных — это единый объект
  • Эта текущая структура требует дублирования кода и взаимодействия между двумя моделями, например, функция delete_first_column() должна применяться как к телу, так и к заголовку, но как есть она применима только к модели, в которой она находится.

Как я могу реорганизовать этот код, чтобы представления были подключены только к одной модели для одного фрейма данных?


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

# DataTableModel and DataTableView show the data in the rows of the DataFrame

class DataTableModel(QtCore.QAbstractTableModel):
    """
    Model for DataTableView to connect for DataFrame data
    """

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

    # Headers for DataTableView are hidden. Header data is shown in HeaderView
    def headerData(self, section, orientation, role=None):
        pass

    def columnCount(self, parent=None):
        return len(self.df.columns)

    def rowCount(self, parent=None):
        return len(self.df)

    # Returns the data from the DataFrame
    def data(self, index, role=None):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            col = index.column()
            cell = self.df.iloc[row, col]
            return str(cell)

class DataTableView(QtWidgets.QTableView):
    def __init__(self, df):
        super().__init__()

        # Create and set model
        model = DataTableModel(df)
        self.setModel(model)

        # Hide the headers. The DataFrame headers (index amp; columns) will be displayed in the DataFrameHeaderViews
        self.horizontalHeader().hide()
        self.verticalHeader().hide()

# HeaderModel and HeaderView show the header of the DataFrame, in this case a 3 level header

class HeaderModel(QtCore.QAbstractTableModel):
    def __init__(self, df):
        super().__init__()
        self.df = df

    def columnCount(self, parent=None):
        return len(self.df.columns.values)

    def rowCount(self, parent=None):
        if type(self.df.columns) == pd.MultiIndex:
            if type(self.df.columns.values[0]) == tuple:
                return len(self.df.columns.values[0])
            else:
                return 1

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.ToolTipRole:
            if type(self.df.columns) == pd.MultiIndex:
                row = index.row()
                col = index.column()
                return str(self.df.columns.values[col][row])
            else:  # Not MultiIndex
                col = index.column()
                return str(self.df.columns.values[col])

    # A simple example of some way this model might modify its data
    def delete_first_column(self):
        self.beginResetModel()
        self.df = self.df.drop(self.df.columns[0], axis=1)
        self.endResetModel()

class HeaderView(QtWidgets.QTableView):
    def __init__(self, df):
        super().__init__()
        self.setModel(HeaderModel(df))
        self.clicked.connect(self.model().delete_first_column)

        self.horizontalHeader().hide()
        self.verticalHeader().hide()

        self.setFixedHeight(115)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('Windows XP')

    tuples = [('A', 'one', 'x'), ('A', 'one', 'y'), ('A', 'two', 'x'), ('A', 'two', 'y'),
              ('B', 'one', 'x'), ('B', 'one', 'y'), ('B', 'two', 'x'), ('B', 'two', 'y')]
    columns = pd.MultiIndex.from_tuples(tuples, names=['first', 'second', 'third'])
    multidf = pd.DataFrame(np.arange(40).reshape(5,8), columns=columns[:8])

    container = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout()
    container.setLayout(layout)

    header = HeaderView(multidf)
    data = DataTableView(multidf)

    layout.addWidget(header)
    layout.addWidget(data)

    print(header.model().df is data.model().df)
    container.show()
    sys.exit(app.exec_())
  

Ответ №1:

Я не уверен, что могу однозначно ответить на этот вопрос, но я бы попытался сохранить две модели и два представления, которые у вас уже есть, затем создать новую модель и представление, которые действуют в унифицированных интерфейсах. На самом деле они были бы просто оболочками поверх ваших существующих моделей и представлений. Итак, если кто-то вызывает delete_first_column() в вашем классе-оболочке, он обрабатывает детали передачи этого как в базовое тело, так и в заголовок, поддерживая их синхронизацию.

Если вы любите приключения, вы можете использовать QTableView.setSpan(), чтобы создать вид объединенных столбцов в вашем представлении.