How to implement nested QComboBox in PyQt6?

59 views Asked by At

Is there a straightforward (without reimplementing too many members, I mean) way to use a QComboBox as QComboBox Item?

The effect I would like to achieve is a plain QComboBox showing a single value when closed, when opened it should display the normal list; some (or all) list items could be nested QComboBox which can be opened to select nested entries in a tree-like logic. When closed the QComboBox should display (and QSignal) just the selected item in the selected sub-combo.

Is this possible (without rewriting the whole widget, of course)?

I am using PyQt6, if relevant.

I am aware of possibility to use a QTreeView as QComboBox via:

...
wh = QComboBox()
tv = QTreeView()
wh.setView(tv)
wh.setModel(QStandardItemModel())
...

This seems much simpler than the pointed "solving answer", but it leads to inconsistent behavior and clipped lists (exactly as solving answer original code; @musicamante comments are completely right).

What I'm aiming at is something different, I have the following (completely unsatisfactory, see below) code:

from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt6.QtWidgets import QPushButton

class RecursiveComboBox(QPushButton):
    clicked = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.model_data = None
        self.level = parent.level + 1 if parent else 0
        self.w = QWidget()
        self.w.hide()

    def set_my_data(self, model_data: dict):
        self.model_data = model_data
        l = QVBoxLayout()
        for k, v in self.model_data.items():
            if isinstance(v, dict):
                i = RecursiveComboBox(self)
                i.setText(k)
                i.clicked.connect(self.node_clicked)
                print(f'{"  "*self.level}[{self.level} -- {self.text()}]: node_clicked conected')
                i.set_my_data(v)
            else:
                i = QPushButton(text=k, parent=self)
                i.setProperty('user_data', v)
                i.clicked.connect(self.leaf_clicked)
                print(f'{"  " * self.level}[{self.level} -- {self.text()}]: leaf_clicked conected')
            l.addWidget(i)
        self.w.setLayout(l)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton and self.rect().contains(event.pos()):
            self.setEnabled(False) # release is implicit
            self.repaint()
            self.w.show()
        else:
            super().mouseReleaseEvent(event)

    @pyqtSlot(str)
    def node_clicked(self, txt):
        print(f'node_clicked({txt}): [{self.level} -- {self.text()}]')
        self.clicked.emit(txt)
        self.w.hide()
        self.setEnabled(True)

    @pyqtSlot(bool)
    def leaf_clicked(self, _):
        child = self.sender()
        print(f'leaf_clicked({child.text()}): [{self.level} -- {self.text()}]')
        self.clicked.emit(child.text())
        self.w.hide()
        self.setEnabled(True)


if __name__ == '__main__':
    import sys
    from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout

    main_course = {
        'meat': {
            'steak': 'steak',
            'meatballs': 'meatballs',
        },
        'fish': {
            'cod': 'cod',
            'mullet': 'mullet',
            'scallops': 'scallops'
        },
        'vegetarian': {
            'salad': {
                'mixed': 'mixed',
                'cesar': 'cesar'
            },
            'flan': 'flan'
        }
    }

    app = QApplication(sys.argv)

    mw = QWidget()
    lo = QVBoxLayout()
    rc = RecursiveComboBox()
    rc.setText('Main course')
    lo.addWidget(rc)
    mw.setLayout(lo)
    rc.set_my_data(main_course)

    rc.clicked.connect(lambda s: print(f'click event for "{s}"'))

    mw.show()
    sys.exit(app.exec())

This code has the correct (for me) behavior:

  • it shows a single "button"
  • it if "button" is clicked it opens a list of sub-buttons
  • this behavior is recursive recursive
  • when clicking on a "leaf" button the whole hierarchy is closed and the single leaf is returned via pyqtSignal

OTOH this code has several issues I need help with: essentially it doesn't look like a combobox at all.

It opens unrelated windows for each nested list; this is because I don't set a parent in the constructor: self.w = QWidget() should be self.w = QWidget(self), but that leads to a clipped visualization.

There are other minor issues I think I can handle (or open separate questions for them), but the main issue is: How do I self.w.show() in the same place as self resizing it according to contents?

0

There are 0 answers