pythonqwidgetqtableviewpyqt6focusout

Display floating popup widget box on the header of a QTableView with Python and PyQt6


I'm trying to make a QTableView with Python and PyQt6 that display a popup message and posibly some tools, like filters, when you click in a header section.

I'm trying to replace an excel spreadsheet where I'm using autofilters and commentaries and I'm triyng to replicate in a python script.

I used copilot to generate some basic code that ilustrate my goal:

from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView, QWidget, QLabel, QVBoxLayout
from PyQt6.QtCore import Qt, QAbstractTableModel, QPoint, QTimer

class MyTableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self.data = data

    def rowCount(self, index):
        return len(self.data)

    def columnCount(self, index):
        return len(self.data[0])

    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            return self.data[index.row()][index.column()]

class FloatingFrame(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(Qt.WindowType.Tool | Qt.WindowType.FramelessWindowHint)  # Makes it a floating, frameless window
        self.setWindowState(Qt.WindowState.WindowActive)
        self.setStyleSheet("background-color: lightgray; border: 1px solid black;")
        self.setFixedSize(300, 200)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        self.active = False

    def focusOutEvent(self, event):
        QTimer.singleShot(500, self.close) # Close the window when it loses focus
        super().focusOutEvent(event)

    def close(self):
        self.active = False
        return super().close()

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        self.table = QTableView(self)
        self.table.horizontalHeader()
        self.setCentralWidget(self.table)

        data = [["Apple", "Red"], ["Banana", "Yellow"], ["Cherry", "Red"], ["Grape", "Purple"]]
        self.model = MyTableModel(data)
        self.table.setModel(self.model)

        header = self.table.horizontalHeader()
        header.sectionClicked.connect(self.show_filter_menu)
        self.floating_frame = FloatingFrame()

    def show_filter_menu(self, logicalIndex):
        geom = self.table.horizontalHeader().geometry()
        ypos = geom.bottomLeft().y()
        xpos = self.table.horizontalHeader().sectionViewportPosition(logicalIndex)
        point = QPoint()
        point.setX(xpos +15)
        point.setY(ypos)
        point = self.mapToGlobal(point)
        width = self.table.horizontalHeader().sectionSize(logicalIndex)
        height = self.table.horizontalHeader().size().height()

        if not self.floating_frame.active:
            self.floating_frame = FloatingFrame()
            self.floating_frame.active = True
            self.floating_frame.setFixedSize(width, height*3)
            self.floating_frame.move(point.x(), point.y())

            layout = QVBoxLayout()
            layout.setContentsMargins(0,0,0,0)
            label = QLabel("Extra info on Column " + str(logicalIndex))
            label.setWordWrap(True)
            label.setAlignment(Qt.AlignmentFlag.AlignLeft)
            layout.addWidget(label)

            self.floating_frame.setLayout(layout)
            self.floating_frame.show()
            self.floating_frame.setFocus()

app = QApplication([])
window = MyWindow()
window.show()
app.exec()

It works. But I think is not the correct way to make a popup message.


Solution

  • You're off to a good start, and your code does work, but you're right to question if it's the best way to implement a floating popup. Let me offer you a more robust, PyQt6-idiomatic, and scalable solution using QDialog (or QFrame) with proper modality and behavior expected of UI tooltips or popup filters.

    from PyQt6.QtWidgets import (
        QApplication, QMainWindow, QTableView, QDialog, QLabel, QVBoxLayout, QHeaderView
    )
    from PyQt6.QtCore import Qt, QAbstractTableModel, QPoint
    
    
    class MyTableModel(QAbstractTableModel):
        def __init__(self, data):
            super().__init__()
            self._data = data
    
        def rowCount(self, index):
            return len(self._data)
    
        def columnCount(self, index):
            return len(self._data[0])
    
        def data(self, index, role):
            if role == Qt.ItemDataRole.DisplayRole:
                return self._data[index.row()][index.column()]
    
    
    class HeaderPopup(QDialog):
        def __init__(self, column: int, parent=None):
            super().__init__(parent)
            self.setWindowFlags(Qt.WindowType.Popup)  # Popup closes on outside click
            self.setFixedSize(200, 100)
    
            layout = QVBoxLayout(self)
            label = QLabel(f"Filter tools for column {column}")
            layout.addWidget(label)
            self.setLayout(layout)
    
    
    class MyWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            self.table = QTableView(self)
            self.setCentralWidget(self.table)
    
            data = [["Apple", "Red"], ["Banana", "Yellow"], ["Cherry", "Red"], ["Grape", "Purple"]]
            self.model = MyTableModel(data)
            self.table.setModel(self.model)
    
            header = self.table.horizontalHeader()
            header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
            header.sectionClicked.connect(self.show_filter_popup)
    
            self.current_popup = None
    
        def show_filter_popup(self, logical_index):
            header = self.table.horizontalHeader()
            xpos = header.sectionPosition(logical_index)
            ypos = header.height()
            global_pos = self.table.mapToGlobal(QPoint(xpos, ypos))
    
            # Close previous popup if open
            if self.current_popup and self.current_popup.isVisible():
                self.current_popup.close()
    
            # Create and show new popup
            self.current_popup = HeaderPopup(column=logical_index, parent=self)
            self.current_popup.move(global_pos)
            self.current_popup.show()
    
    
    if __name__ == "__main__":
        app = QApplication([])
        window = MyWindow()
        window.resize(600, 400)
        window.show()
        app.exec()
    
    
    

    You Can Extend With: QComboBox or QLineEdit for column filtering and QPushButton to apply/clear filters.