2

I want to visualize and filter data from a pandas dataframe in PyQt. I already managed to visualize the data and to open up a menu by clicking on the header. The idea is, that you can choose the elements in the menu which you want to keep in the table and filter the data that way (like in Excel).

Excel example of filtering

As in Excel I want to limit the height of the menu and make it scrollable instead. Although I set the style to QMenu { menu-scrollable: 1; } it won't allow for any scrolling if their is not enough unique values in the column.

Menu not showing any scrolling options

If there is a certain number of unique values (more than the physical screen size would allow) it allows for scrolling. In this case it shows everything correct. But if I try to scroll the menu, the menu instead moves up until it reaches the top of the screen and only then starts scrolling.

Scrollable Menu with the right height and position

After scrolling the menu moves up until it reaches the top

In the pictures above you can see, that it shows the scrollable menu, but if I am trying to scroll, the menu just moves up. Only if the menu reaches the top, it allows me to scroll through the values.

class CustomProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._filters = dict()

    @property
    def filters(self):
        return self._filters

    def setFilter(self, expresion, column):
        if expresion:
            self.filters[column] = expresion
        elif column in self.filters:
            del self.filters[column]
        self.invalidateFilter()

    def filterAcceptsRow(self, source_row, source_parent):
        for column, expresion in self.filters.items():
            text = self.sourceModel().index(source_row, column, source_parent).data()
            regex = QtCore.QRegExp(
                expresion, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp
            )
            if regex.indexIn(text) == -1:
                return False
        return True

class PandasModel(QtCore.QAbstractTableModel):
    def __init__(self, df=pandas.DataFrame(), parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent=parent)
        self._df = df.copy()

    def toDataFrame(self):
        return self._df.copy()

    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()[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.iloc[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()

class Ui(QtWidgets.QMainWindow):

    def __init__(self):
        super(Ui, self).__init__()  # Call the inherited classes __init__ method
        uic.loadUi('../test.ui', self)  # Load the .ui file

        button = self.findChild(QtWidgets.QPushButton, 'button_load')
        button.clicked.connect(self.read)

        self.view = self.findChild(QtWidgets.QTableView, 'tableView')

        self.view.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
        self.horizontalHeader = self.view.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)

        self.show()  # Show the GUI

    def read(self):
        proxy = CustomProxyModel(self)
        self.model = PandasModel(pandas.read_csv('C:/Users/Sergey/Downloads/10000 Sales Records.csv'))
        proxy.setSourceModel(self.model)
        self.view.setModel(proxy)
        self.view.resizeColumnsToContents()

    def on_view_horizontalHeader_sectionClicked(self, index):
        menu = QtWidgets.QMenu(self.view)
        signalMapper = QtCore.QSignalMapper(self)

        valuesUnique = self.model._df.iloc[:, index].unique().astype(str)

        actionAll = QtWidgets.QAction("All", self)
        # actionAll.triggered.connect(self.on_actionAll_triggered)
        menu.addAction(actionAll)
        menu.addSeparator()
        for actionNumber, actionName in enumerate(sorted(list(valuesUnique))):
            action = QtWidgets.QAction(actionName, self)
            action.setCheckable(True)
            action.setChecked(True)
            signalMapper.setMapping(action, actionNumber)
            # action.triggered.connect(signalMapper.map)
            menu.addAction(action)
        # self.signalMapper.mapped.connect(self.on_signalMapper_mapped)
        headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(index)
        posY = headerPos.y() + self.horizontalHeader.height()

        menu.setStyleSheet("QMenu { menu-scrollable: 1; }")
        menu.setMaximumHeight(155)
        menu.popup(QtCore.QPoint(posX, posY))
        menu.move(posX, posY)


app = QtWidgets.QApplication(sys.argv)  # Create an instance of QtWidgets.QApplication
window = Ui()  # Create an instance of our class
app.exec_()  # Start the application

test.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QPushButton" name="button_load">
    <property name="geometry">
     <rect>
      <x>350</x>
      <y>440</y>
      <width>75</width>
      <height>23</height>
     </rect>
    </property>
    <property name="text">
     <string>Load</string>
    </property>
   </widget>
   <widget class="QTableView" name="tableView">
    <property name="geometry">
     <rect>
      <x>80</x>
      <y>30</y>
      <width>621</width>
      <height>351</height>
     </rect>
    </property>
   </widget>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>21</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

Example data set: http://eforexcel.com/wp/wp-content/uploads/2017/07/10000-Sales-Records.zip

Sergey Mi
  • 29
  • 4

0 Answers0