Как сделать прокручиваемый QMenu и в то же время зафиксировать его положение? - PullRequest
0 голосов
/ 10 марта 2020

Я хочу визуализировать и фильтровать данные из pandas кадра данных в PyQt. Мне уже удалось визуализировать данные и открыть меню, нажав на заголовок. Идея состоит в том, что вы можете выбрать элементы в меню, которые вы хотите сохранить в таблице, и таким образом отфильтровать данные (как в Excel).

Пример фильтрации в Excel

Как и в Excel, я хочу ограничить высоту меню и сделать ее прокручиваемой. Несмотря на то, что я установил стиль на QMenu { menu-scrollable: 1; }, он не разрешит прокрутку, если в столбце недостаточно уникальных значений.

В меню не отображаются параметры прокрутки

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

Прокручиваемое меню с нужной высотой и положением

После прокрутки меню перемещается вверх, пока не достигнет вершины.

На рисунках выше вы можете видеть, что оно показывает прокручиваемое меню, но если Я пытаюсь прокрутить, меню просто движется вверх. Только если меню достигает вершины, оно позволяет мне прокручивать значения.

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>

Пример набора данных: http://eforexcel.com/wp/wp-content/uploads/2017/07/10000-Sales-Records.zip

...