1

Suppose I have the following dataframe:

df = {'Banana': {0: 1, 1: 2, 2: 5}, 'Apple': {0: 3, 1: 4, 2: 3}, 'Elderberry': {0: 5, 1: 4, 2: 1},
'Clementine': {0: 4, 1: 7, 2: 0}, 'Fig': {0: 1, 1: 9, 2: 3}}
   Banana  Apple  Elderberry  Clementine  Fig
0       1      3           5           4    1
1       2      4           4           7    9
2       5      3           1           0    3

Which is passed to QAbstractTableModel and displayed with QTableView:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import Qt
import pandas as pd


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, model):
        super().__init__()
        self.model = model
        self.table = QtWidgets.QTableView()
        self.table.setModel(self.model)
        self.setCentralWidget(self.table)


class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, section, orientation, role):
        # section is the index of the column/row.
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])

            if orientation == Qt.Vertical:
                return str(self._data.index[section])

    def data(self, index, role):

        row = index.row()
        column = index.column()
        column_name = self._data.columns[column]
        value = self._data.iloc[row, column]

        if role == Qt.DisplayRole:
            print(str(value))
            return str(value)


app = QtWidgets.QApplication(sys.argv)
df = {'Banana': {0: 1, 1: 2, 2: 5}, 'Apple': {0: 3, 1: 4, 2: 3}, 'Elderberry': {0: 5, 1: 4, 2: 1}, 'Clementine': {0: 4, 1: 7, 2: 0}, 'Fig': {0: 1, 1: 9, 2: 3}}
data_model = TableModel(df)
window1 = MainWindow(data_model)
window1.resize(800, 400)
window1.show()
sys.exit(app.exec_())

enter image description here

I now want to create a separate window using QListView, to display a list of df column names, allowing me to specify which columns I want to appear in the above QTableView. Like this:

enter image description here

If I uncheck a column in QListView, I want that column to be removed from the QTableView. Similarly if I check a column, that column should be added to the table view. The purpose of the list view is essentially to allow the user to specify which columns should appear in the table.

What is the best way of doing this? I imagine I could create a QListWidget and use signals to update the QTableView. However I am a bit reluctant to do this, as I imagine it could get very messy.

Is it therefore best practice to use QListView, so that both widgets reference the same data model, and automatically talk to each other like this?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Alan
  • 509
  • 4
  • 15

1 Answers1

4

You have to use proxy models to build a model for the listview (tranpose the original model, map the header to a single row or column and add the checkboxes) and another to filter:

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


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent=None):
        super(TableModel, self).__init__(parent)
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return str(self._data.columns[section])
            if orientation == QtCore.Qt.Vertical:
                return str(self._data.index[section])

    def data(self, index, role):
        row = index.row()
        column = index.column()
        value = self._data.iloc[row, column]
        if role == QtCore.Qt.DisplayRole:
            return str(value)


class CustomProxy(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.show_columns = set()

    def show_column(self, c):
        self.show_columns.add(c)
        self.invalidateFilter()

    def hide_column(self, c):
        if c not in self.show_columns:
            return
        self.show_columns.remove(c)
        self.invalidateFilter()

    def filterAcceptsColumn(self, source_column, source_parent):
        return source_column in self.show_columns


class HeaderProxyModel(QtCore.QIdentityProxyModel):
    checked = QtCore.pyqtSignal(int, bool)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.checks = {}

    def columnCount(self, index=QtCore.QModelIndex()):
        return 1

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            return self.headerData(index.row(), QtCore.Qt.Vertical, role)
        elif role == QtCore.Qt.CheckStateRole and index.column() == 0:
            return self.checks.get(
                QtCore.QPersistentModelIndex(index), QtCore.Qt.Unchecked
            )

    def setData(self, index, value, role=QtCore.Qt.EditRole):

        if not index.isValid():
            return False
        if role == QtCore.Qt.CheckStateRole:
            self.checks[QtCore.QPersistentModelIndex(index)] = value
            self.checked.emit(index.row(), bool(value))
            return True
        return False

    def flags(self, index):
        fl = super().flags(index)
        if index.column() == 0:
            fl |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
        return fl


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, model):
        super().__init__()
        self.model = model

        self.listview = QtWidgets.QListView()
        tranpose = QtCore.QTransposeProxyModel()
        tranpose.setSourceModel(self.model)
        header_model = HeaderProxyModel()
        header_model.setSourceModel(tranpose)
        self.listview.setModel(header_model)

        self.tableview = QtWidgets.QTableView()
        self.filter_proxy = CustomProxy()
        self.filter_proxy.setSourceModel(self.model)
        self.tableview.setModel(self.filter_proxy)

        header_model.checked.connect(self.on_checked)

        central_widget = QtWidgets.QWidget()
        hlay = QtWidgets.QHBoxLayout(central_widget)
        hlay.addWidget(self.listview)
        hlay.addWidget(self.tableview, stretch=1)
        self.setCentralWidget(central_widget)

    def on_checked(self, r, state):
        if state:
            self.filter_proxy.show_column(r)
        else:
            self.filter_proxy.hide_column(r)


if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    df = pd.DataFrame(
        {
            "Banana": {0: 1, 1: 2, 2: 5},
            "Apple": {0: 3, 1: 4, 2: 3},
            "Elderberry": {0: 5, 1: 4, 2: 1},
            "Clementine": {0: 4, 1: 7, 2: 0},
            "Fig": {0: 1, 1: 9, 2: 3},
        }
    )
    data_model = TableModel(df)
    window1 = MainWindow(data_model)
    window1.resize(800, 400)
    window1.show()
    sys.exit(app.exec_())

enter image description here

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • If I understand correctly, the proxy model sits between the model and the view, like this: data --> model --> proxy --> view, allowing the model to be configured for the view without actually changing the model. Is this correct? Your code makes sense, however I can't get my head around what's happening when a checkbox is checked or unchecked. If a column is added, does the whole table view get refreshed, or does the model view only paint the additional column? Also where in the code can I set the default state of the check boxes (currently they're all unchecked as default)? – Alan May 14 '20 at 07:51
  • I sent you an email – Alan May 19 '20 at 23:32
  • @Alan Okay, I will review it and I will reply in a few hours. – eyllanesc May 20 '20 at 22:24