PyQt5 QTableViewにpandasを表示するためのQAbstractItemModelを作成する

Share on:

PyQt5でQTableViewにpandasのDataFrameを表示するためのQAbstractItemModelを作ってみました。

表示するデータの取得

QAbstractItemModelのデータを返す関数はdef data(self, index, role=QtCore.Qt.DisplayRole)です。この関数はindexで取得するデータの位置を指定して、その位置のデータを返します。そこでindexの行、列番号をindex.row()とindex.column()で取得して、それらの番号でDataFrameの特定の場所のデータをDataFrame.iat[row, column]で取得して、data関数の返り値とします。

1def data(self, index, role=QtCore.Qt.DisplayRole):
2    if not index.isValid():
3        return QtCore.QVariant()
4    if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
5        return self.df.iat[index.row(), index.column()]
6    return QtCore.QVariant()

データのセット

QAbstractItemModelのデータを返す関数はdef setData(self, index, value, role=QtCore.Qt.EditRole)です。この関数はindexで取得するデータの位置を指定して、その位置にデータvalueをセットします。データのセットにはデータの表示と同じDataFrame.iatを使用します。

1def setData(self, index, value, role=QtCore.Qt.EditRole):
2    if role == QtCore.Qt.EditRole:
3        self.df.iat[index.row(), index.column()] = value
4        return True
5    return False

ヘッダーの表示

ヘッダーの方向が縦か横のどちらかを示すorientationによってヘッドーの表示内容を変えます。縦の場合はDataFrameのindexを表示して、横の場合はDataFrameのcolumnsを表示します。

1def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
2    if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
3        return self.df.columns[section]
4    if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
5        return self.df.index[section]

表のサイズ

表の縦横のサイズはそれぞれ、def columnCount(self, parent=QtCore.QModelIndex())とdef rowCount(self, parent=QtCore.QModelIndex())という関数です。これらはDataFrame.shapeから取得した値をそのまま返す関数としました。

1def columnCount(self, parent=QtCore.QModelIndex()):
2    return self.df.shape[1]
3def rowCount(self, parent=QtCore.QModelIndex()):
4    return self.df.shape[0]

列の挿入

列の挿入にはdef insertColumns(self, column, count, parent=QtCore.QModelIndex())を使用します。挿入を行う前にbeginInsertColumnsを実行して、挿入完了後にendInsertColumnsを実行する必要があります。挿入完了前にendInsertColumnsを実行したりするとエラーがが発生します。挿入位置はcolumnからcountの個数挿入します。そこでDataFrame.ilocで列を指定してcolumnの位置でDataFrameを左側と右側にスライスします。その中間に挿入されるDataFrameを、indexを既存のデータフレームから流用して、columnsを挿入する個数分の数として作成します。そして、pandas.concatでスライスした左側、作成した中間、スライスした右側を連結して新しいDataFrameを作成することで列の挿入を実装します。

1def insertColumns(self, column, count, parent=QtCore.QModelIndex()):
2    self.beginInsertColumns(parent, column, column + count - 1)
3    columns = [ str(self.columnCount()+i+1) for i in range(count) ]
4    left = self.df.iloc[:, 0:column]
5    mid = pd.DataFrame(index=self.df.index, columns=columns)
6    right = self.df.iloc[:, column+count-1:self.columnCount()]
7    self.df = pd.concat( [left, mid, right], axis=1 )
8    
9    self.endInsertColumns()

行の挿入

行の挿入も列の挿入と同じ要領です。挿入位置rowでDataFrameをスライスして、新たに作成したDataFrameとそれらを連結して新しいDataFrameを作成することで行の挿入を実装します。

1def insertRows(self, row, count, parent=QtCore.QModelIndex()):
2    self.beginInsertRows(parent, row, row + count - 1)
3    indexes = [ str(self.rowCount()+i) for i in range(count) ]
4    left = self.df[0:row]
5    mid = pd.DataFrame(index=indexes, columns=self.df.columns)
6    right = self.df[row+count-1:self.rowCount()]
7    self.df = pd.concat([left, mid, right])
8    self.endInsertRows()

列の削除

列の削除は、列の挿入の処理から中間に挿入するDataFrameを作成する処理を除いた処理です。

1def removeColumns(self, column, count, parent=QtCore.QModelIndex()):
2    self.beginRemoveColumns(parent, column, column + count - 1)
3    left = self.df.iloc[:, 0:column]
4    right = self.df.iloc[:, column+count:self.columnCount()]
5    self.df = pd.concat( [left, right], axis=1 )
6    self.endRemoveColumns()

行の削除

行の削除も列の削除と同様の処理です。

1def removeRows(self, row, count, parent=QtCore.QModelIndex()):
2    self.beginRemoveRows(parent, row, row + count - 1)
3    
4    left = self.df.iloc[0:row]
5    right = self.df.iloc[row+count:self.rowCount()]
6    self.df = pd.concat( [left, right], axis=0 )
7    self.endRemoveRows()

ソースコード

  1import pandas as pd
  2from PyQt5 import QtWidgets, QtCore
  3class PandasModel(QtCore.QAbstractItemModel):
  4    def __init__(self, parent=None, data_frame=None):
  5        super(PandasModel, self).__init__(parent)
  6        self.df = data_frame
  7    def columnCount(self, parent=QtCore.QModelIndex()):
  8        return self.df.shape[1]
  9    def data(self, index, role=QtCore.Qt.DisplayRole):
 10        if not index.isValid():
 11            return QtCore.QVariant()
 12        if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
 13            return self.df.iat[index.row(), index.column()]
 14        return QtCore.QVariant()
 15    def flags(self, index):
 16        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
 17    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
 18        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
 19            return self.df.columns[section]
 20        if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
 21            return self.df.index[section]
 22    def index(self, row, column, parent=QtCore.QModelIndex()):
 23        if not self.hasIndex(row, column, parent):
 24            return QtCore.QModelIndex()
 25        return self.createIndex(row, column, QtCore.QModelIndex())
 26    def insertColumns(self, column, count, parent=QtCore.QModelIndex()):
 27        self.beginInsertColumns(parent, column, column + count - 1)
 28        columns = [ str(self.columnCount()+i+1) for i in range(count) ]
 29        left = self.df.iloc[:, 0:column]
 30        mid = pd.DataFrame(index=self.df.index, columns=columns)
 31        right = self.df.iloc[:, column+count-1:self.columnCount()]
 32        self.df = pd.concat( [left, mid, right], axis=1 )
 33        
 34        self.endInsertColumns()
 35    def insertRows(self, row, count, parent=QtCore.QModelIndex()):
 36        self.beginInsertRows(parent, row, row + count - 1)
 37        indexes = [ str(self.rowCount()+i) for i in range(count) ]
 38        left = self.df[0:row]
 39        mid = pd.DataFrame(index=indexes, columns=self.df.columns)
 40        right = self.df[row+count-1:self.rowCount()]
 41        self.df = pd.concat([left, mid, right])
 42        self.endInsertRows()
 43    def removeColumns(self, column, count, parent=QtCore.QModelIndex()):
 44        self.beginRemoveColumns(parent, column, column + count - 1)
 45        left = self.df.iloc[:, 0:column]
 46        right = self.df.iloc[:, column+count:self.columnCount()]
 47        self.df = pd.concat( [left, right], axis=1 )
 48        self.endRemoveColumns()
 49    def removeRows(self, row, count, parent=QtCore.QModelIndex()):
 50        self.beginRemoveRows(parent, row, row + count - 1)
 51        
 52        left = self.df.iloc[0:row]
 53        right = self.df.iloc[row+count:self.rowCount()]
 54        self.df = pd.concat( [left, right], axis=0 )
 55        self.endRemoveRows()
 56        
 57    def rowCount(self, parent=QtCore.QModelIndex()):
 58        return self.df.shape[0]
 59    def setData(self, index, value, role=QtCore.Qt.EditRole):
 60        if role == QtCore.Qt.EditRole:
 61            self.df.iat[index.row(), index.column()] = value
 62            return True
 63        return False
 64class Delegate(QtWidgets.QStyledItemDelegate):
 65    def __init__(self, parent=None, setModelDataEvent=None):
 66        super(Delegate, self).__init__(parent)
 67        self.setModelDataEvent = setModelDataEvent
 68 
 69    def createEditor(self, parent, option, index):
 70        index.model().data(index, QtCore.Qt.DisplayRole)
 71        return QtWidgets.QLineEdit(parent)
 72 
 73    def setEditorData(self, editor, index):
 74        value = index.model().data(index, QtCore.Qt.DisplayRole)
 75        editor.setText(str(value))
 76        
 77    def setModelData(self, editor, model, index):
 78        model.setData(index, editor.text())
 79        if not self.setModelDataEvent is None:
 80            self.setModelDataEvent()
 81def main():
 82    
 83    def insert_row():
 84        if len(tableView.selectedIndexes()) == 0:
 85            model.insertRows(model.rowCount(), 1)
 86        else:
 87            rows = list( set([ index.row() for index in tableView.selectedIndexes() ]) )
 88            for row in rows[::-1]:
 89                model.insertRows(row, 1)
 90    def delete_row():
 91        rows = list( set([ index.row() for index in tableView.selectedIndexes() ]) )
 92        for row in rows[::-1]:
 93            model.removeRows(row, 1)
 94    def insert_column():
 95        if len(tableView.selectedIndexes()) == 0:
 96            model.insertColumns(model.columnCount(), 1)
 97        else:
 98            columns = list( set([ index.column() for index in tableView.selectedIndexes() ]) )
 99            for column in columns[::-1]:
100                model.insertColumns(column, 1)
101    def delete_column():
102        columns = list( set([ index.column() for index in tableView.selectedIndexes() ]) )
103        for column in columns[::-1]:
104            model.removeColumns(column, 1)
105    def exec_context_menu(point):
106        menu = QtWidgets.QMenu(tableView)
107        menu.addAction('Insert row', insert_row)
108        menu.addAction('Delete row', delete_row)
109        menu.addAction('Insert column', insert_column)
110        menu.addAction('Delete column', delete_column)
111        menu.exec( tableView.focusWidget().mapToGlobal(point) )
112    
113    import sys
114    app = QtWidgets.QApplication(sys.argv)
115    window = QtWidgets.QMainWindow()
116    window.resize(600, 600)
117    centralwidget = QtWidgets.QWidget(window)
118    verticalLayout = QtWidgets.QVBoxLayout(centralwidget)
119    df = pd.DataFrame(
120        {
121            '1': ['A1', 'A2', 'A3'],
122            '2': ['B1', 'B2', 'B3'],
123            '3': ['C1', 'C2', 'C3']
124        }, index=['0', '1', '2']
125    )
126    
127    tableView = QtWidgets.QTableView(centralwidget)
128    model = PandasModel(tableView, df)
129    tableView.setModel(model)
130    tableView.setItemDelegate(Delegate())
131    tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
132    tableView.customContextMenuRequested.connect(exec_context_menu)
133    verticalLayout.addWidget(tableView)
134    window.setCentralWidget(centralwidget)
135    window.show()
136    app.exec()
137if __name__ == '__main__':
138    main()

関連記事