3

I am really new to pyqt and this question maybe silly. Any help, I would really appreciate. I have this code I get from this page where it codes in filtering a qtablewidget. This code works perfectly fine to my desired filtering output. However I have a table and it has lot of rows, I want the menu bar used in filtering to be scrollable instead of displaying all unique contents in the row. I want to have fixed size height of the menubar.

This is the code:

import csv
import sys
from PyQt5 import QtCore

from PyQt5 import QtGui, QtWidgets


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent=parent)
        self.verticalLayout = QtWidgets.QVBoxLayout(self)
        self.table = QtWidgets.QTableWidget(self)
        self.table.setColumnCount(0)
        self.table.setRowCount(0)
        self.verticalLayout.addWidget(self.table)

        self.loadAll()
        self.horizontalHeader = self.table.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)
        self.keywords = dict([(i, []) for i in range(self.table.columnCount())])
        self.checkBoxs = []
        self.col = None

    def slotSelect(self, state):

        for checkbox in self.checkBoxs:
            checkbox.setChecked(QtCore.Qt.Checked == state)

    def on_view_horizontalHeader_sectionClicked(self, index):
        self.menu = QtWidgets.QMenu()
        self.col = index

        data_unique = []
        self.checkBoxs = []

        checkBox = QtWidgets.QCheckBox("Select all", self.menu)
        checkableAction = QtWidgets.QWidgetAction(self.menu)
        checkableAction.setDefaultWidget(checkBox)
        self.menu.addAction(checkableAction)
        checkBox.setChecked(True)
        checkBox.stateChanged.connect(self.slotSelect)

        for i in range(self.table.rowCount()):
            if not self.table.isRowHidden(i):
                item = self.table.item(i, index)
                if item.text() not in data_unique:
                    data_unique.append(item.text())
                    checkBox = QtWidgets.QCheckBox(item.text(), self.menu)
                    checkBox.setChecked(True)
                    checkableAction = QtWidgets.QWidgetAction(self.menu)
                    checkableAction.setDefaultWidget(checkBox)
                    self.menu.addAction(checkableAction)
                    self.checkBoxs.append(checkBox)

        btn = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
                                     QtCore.Qt.Horizontal, self.menu)
        btn.accepted.connect(self.menuClose)
        btn.rejected.connect(self.menu.close)
        checkableAction = QtWidgets.QWidgetAction(self.menu)
        checkableAction.setDefaultWidget(btn)
        self.menu.addAction(checkableAction)

        headerPos = self.table.mapToGlobal(self.horizontalHeader.pos())

        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(index)
        self.menu.exec_(QtCore.QPoint(posX, posY))


    def menuClose(self):
        self.keywords[self.col] = []
        for element in self.checkBoxs:
            if element.isChecked():
                self.keywords[self.col].append(element.text())
        self.filterdata()
        self.menu.close()

    def loadAll(self):
        with open("pokemon_data.csv", "r") as inpfil:
            reader = csv.reader(inpfil, delimiter=',')
            csheader = next(reader)
            ncol = len(csheader)
            data = list(reader)
            row_count = len(data)

            self.table.setRowCount(row_count)
            self.table.setColumnCount(ncol)
            self.table.setHorizontalHeaderLabels(('%s' % ', '.join(map(str, csheader))).split(","))

            for ii in range(0, row_count):
                mainins = data[ii]
                for var in range(0, ncol):
                    self.table.setItem(ii, var, QtWidgets.QTableWidgetItem(mainins[var]))

    def clearFilter(self):
        for i in range(self.table.rowCount()):
            self.table.setRowHidden(i, False)

    def filterdata(self):

        columnsShow = dict([(i, True) for i in range(self.table.rowCount())])

        for i in range(self.table.rowCount()):
            for j in range(self.table.columnCount()):
                item = self.table.item(i, j)
                if self.keywords[j]:
                    if item.text() not in self.keywords[j]:
                        columnsShow[i] = False
        for key, value in columnsShow.items():
            self.table.setRowHidden(key, not value)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())

This is currently what it looks like: enter image description here

When i try to filter by clicking the header, all my desktop window filled up because I have so much rows in my table as shown in the image below.

enter image description here

i just need a better size of the menubar.

I tried searching related queries to this but I just couldn't find that I can integrate it to this code. Please be gentle with your response as Im really new to this. Thank you so much!

1 Answers1

4

The easiest solution is to use an undocumented stylesheet property (as proposed in the unaccepted answer of this post).

    def on_view_horizontalHeader_sectionClicked(self, index):
        self.menu = QtWidgets.QMenu()
        self.menu.setStyleSheet('QMenu { menu-scrollable: true; }')
        # ...

Alternatively (if, for any reason, that behavior doesn't work as expected or its support is removed in the future) you can create a QProxyStyle subclass, implement its styleHint and return True if the given hint is SH_Menu_Scrollable.

class ProxyStyle(QtWidgets.QProxyStyle):
    def styleHint(self, hint, option, widget, data):
        if hint == self.SH_Menu_Scrollable:
            return True
        return super().styleHint(hint, option, widget, data)

# ...

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle(ProxyStyle())
    w = Widget()
    w.show()
    sys.exit(app.exec_())

If you want to keep that behavior only for that menu, you can apply the proxy style to the menu instead of doing it for the whole application:

    def on_view_horizontalHeader_sectionClicked(self, index):
        self.menu = QtWidgets.QMenu()
        self.menu.setStyle(ProxyStyle())
        # ...

By the way, you are using a menu, not a menubar. A menubar is the widget normally placed on the top of a window, containing different items, each one of it being (possibly) a menu.

UPDATE

Since the items are a lot, using a QMenu is not a good solution for various reasons. A better approach would be to use a QWidget that contains a QListWidget and the button box. To keep the behavior similar to that of a menu (it should close if a click happens outside it), you can add the Popup window flag.

class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        # ...
        self.dialog = QtWidgets.QWidget()
        self.dialog.setWindowFlags(
            self.dialog.windowFlags() | QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint)
        self.dialog.setMaximumHeight(300)
        layout = QtWidgets.QVBoxLayout(self.dialog)
        self.dialogList = QtWidgets.QListWidget()
        layout.addWidget(self.dialogList)
        self.dialogList.itemChanged.connect(self.slotSelect)

        buttonBox = QtWidgets.QDialogButtonBox(
           QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
        layout.addWidget(buttonBox)
        buttonBox.accepted.connect(self.menuClose)
        buttonBox.rejected.connect(self.dialog.hide)

    def on_view_horizontalHeader_sectionClicked(self, index):
        self.dialogList.clear()
        self.selectAllItem = QtWidgets.QListWidgetItem('Select all')
        self.selectAllItem.setCheckState(QtCore.Qt.Unchecked)
        self.dialogList.addItem(self.selectAllItem)

        self.col = index
        self.itemList = []
        data_unique = []
        for i in range(self.table.rowCount()):
            if not self.table.isRowHidden(i):
                item = self.table.item(i, index)
                if item == self.selectAllItem:
                    continue
                if item.text() not in data_unique:
                    item = QtWidgets.QListWidgetItem(item.text())
                    item.setCheckState(QtCore.Qt.Unchecked)
                    self.dialogList.addItem(item)
                    self.itemList.append(item)

        self.dialog.move(QtGui.QCursor.pos())
        self.dialog.show()

    def slotSelect(self, item):
        # temporally disconnect the signal to avoid recursion
        self.dialogList.itemChanged.disconnect(self.slotSelect)
        if item == self.selectAllItem:
            state = item.checkState()
            for i in self.itemList:
                i.setCheckState(state)
        else:
            states = [i.checkState() for i in self.itemList]
            if all(states):
                self.selectAllItem.setCheckState(QtCore.Qt.Checked)
            elif not any(states):
                self.selectAllItem.setCheckState(QtCore.Qt.Unchecked)
            else:
                self.selectAllItem.setCheckState(QtCore.Qt.PartiallyChecked)

        # reconnect the signal back again
        self.dialogList.itemChanged.connect(self.slotSelect)

    def menuClose(self):
        self.dialog.hide()
        self.keywords[self.col] = []
        for item in self.itemList:
            if item.checkState():
                self.keywords[self.col].append(item.text())
        self.filterdata()
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Great! Thank you @musicamante! just to add additional query (1) how to add a scroll bar in the left side of the menu like in excel filter? so the user can scroll fast? (2) also how to fix the size of the menu like only 300px? because it still fill and go the very down in my desktop. thank you so much! – Alvin Lorejas Jun 07 '20 at 14:39
  • 1
    To set the maximum height, just use `self.menu.setMaximumHeight(300). About the other question, that is quite more complex and it might be hard to get it right. I'd suggest you to use a popup QListView instead. – musicamante Jun 07 '20 at 14:45
  • Thank you. I actually just copy the code from the other page thats why it was set-up that way. For now, I dont really have the idea yet with the QListView and QAction. Can you possibly edit it? I dont know if thats too much to ask. I'm sorry.. Im just starting learning programming just this month. And all of this stuffs are new. Im sorry. – Alvin Lorejas Jun 07 '20 at 15:01
  • 1
    Sorry, I reread the code more carefully, now I get it. I've updated the question, (even though it doesn't really reflect the question in the title). Anyway, I'd suggest you to carefully study the documentation and try to understand the code you're using, as plain copy/paste isn't very wise without knowing what it does. – musicamante Jun 07 '20 at 15:43
  • You are such a great man!! Thank you dear! Thanks for the help :) – Alvin Lorejas Jun 08 '20 at 05:27