Модель PyQt для подключения к нескольким различным QTableViews? - PullRequest
0 голосов
/ 24 апреля 2019

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

enter image description here

Как могут выглядеть данные в Excel: enter image description here

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

Некоторые причины, по которым я хочу объединить их в одну модель ...

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

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


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 & 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 Ответ

0 голосов
/ 24 апреля 2019

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

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

...