79794551

Date: 2025-10-20 00:45:49
Score: 1
Natty:
Report link

@musicamante Ok thanks for steering me away from QComboBox. Got a working solution "starting from scratch" and it's probably quite a bit easier to code up than the QComboBox contortions - and the two issues in the original question are resolved. Will do some more styling and sizing and such, but:

enter image description here

The code:

# heavily adapted from Google AI Overview: 'pyqt popup under button'

from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QTreeView,QMainWindow,QPushButton,QDialog
)
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import QModelIndex,Qt,QPoint

class MyPopup(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent=parent

        # Create the TreeView for the dropdown popup
        self.tree_view = QTreeView(self)
        self.tree_view.setHeaderHidden(True)  # Hide the header to look like a simple tree
        self.tree_view.setSelectionMode(QTreeView.SingleSelection)
        self.tree_view.setEditTriggers(QTreeView.NoEditTriggers)
        self.tree_view.setExpandsOnDoubleClick(False)
        self.tree_view.setAnimated(True)

        self.tree_view.setFixedHeight(300)

        # Create a model for the tree view
        self.model = QStandardItemModel()
        self.tree_view.setModel(self.model)

        self.tree_view.entered.connect(self.enteredCB)
        self.tree_view.clicked.connect(self.clickedCB)
        self.tree_view.expanded.connect(self.expandedCB)
        self.tree_view.collapsed.connect(self.collapsedCB)

        self.setWindowTitle("Popup Dialog")
        self.setWindowFlags(Qt.Popup)
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0,0,0,0)
        layout.addWidget(self.tree_view)
        self.setLayout(layout)
        self.tree_view.setMouseTracking(True)

    def enteredCB(self,i):
        print('entered')
        self.setFullLabel(i)

    def expandedCB(self,i):
        print('expanded')
        self.collapseOthers(i)

    def collapsedCB(self,i):
        print('collapsed')

    def clickedCB(self,i):
        print('clicked')
        self.setFullLabel(i)
        self.close() # close the popup

    def setFullLabel(self,i):
        # Get the full hierarchy path for display
        current_index = i
        path_list = [self.model.data(i)]
        while current_index.parent().isValid():
            parent_index = current_index.parent()
            parent_text = self.model.data(parent_index)
            path_list.insert(0, parent_text)
            current_index = parent_index
        # Join path with a separator and set the text
        self.parent.button.setText(' > '.join(path_list))

    def populate(self, data):
        """Populates the tree model from a dictionary."""
        self.model.clear()
        for key, children in data.items():
            parent_item = QStandardItem(key)
            for child_text in children:
                child_item = QStandardItem(child_text)
                parent_item.appendRow(child_item)
            self.model.appendRow(parent_item)
        for r in range(self.model.rowCount()):
            for c in range(self.model.columnCount()):
                txt=self.model.item(r,c).text()
                print(f'row {r} col {c} : {txt}')

    def collapseOthers(self,expandedIndex):
        QApplication.processEvents()
        print('collapse_others called: expandedIndex='+str(expandedIndex))
        def _collapse_recursive(parent_index: QModelIndex,sp='  '):
            for row in range(self.model.rowCount(parent_index)):
                index = self.model.index(row, 0, parent_index)
                item=self.model.itemFromIndex(index)
                txt=item.text()
                print(sp+f'checking r={row} col=0 : {txt}')
                if index.isValid() and index!=expandedIndex:
                    print(sp+'  collapsing')
                    self.tree_view.collapse(index)
                    # self.tree_view.setExpanded(index,False)
                    
                    # Recursively process children
                    if self.model.hasChildren(index):
                        _collapse_recursive(index,sp+'  ')

        # Start the recursion from the invisible root item
        _collapse_recursive(QModelIndex())
        QApplication.processEvents()

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Main Window")
        self.setGeometry(100, 100, 400, 300)

        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)

        self.button = QPushButton("Show Popup", self)
        self.button.clicked.connect(self.show_popup_under_button)
        layout.addWidget(self.button)

        self.popup = MyPopup(self)

    def show_popup_under_button(self):
        # Get the global position of the button's top-left corner
        button_pos = self.button.mapToGlobal(QPoint(0, 0))

        # Calculate the desired position for the popup
        popup_x = button_pos.x()
        popup_y = button_pos.y() + self.button.height()

        # Sample hierarchical data
        data = {
            "Fruits": ["Apple", "Banana", "Orange"],
            "Vegetables": ["Carrot", "Broccoli", "Spinach"],
            "Dairy": ["Milk", "Cheese", "Yogurt"]
        }
        
        self.popup.populate(data)

        self.popup.move(popup_x, popup_y)
        self.popup.exec_() # Show as a modal dialog

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec_()
Reasons:
  • Blacklisted phrase (0.5): thanks
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @musicamante
  • Self-answer (0.5):
  • Low reputation (0.5):
Posted by: Tom Grundy