0

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.

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

6 Comments

Note: 1. sectionPosition() is not correct, as it reports the position from the first top/left item without considering scrolling: it should be sectionViewportPosition() instead; 2. mapToGlobal should be called against the header, not the table; 3. if the widget already exists, and it's not being reused, overwriting its reference will not delete it (as it was created with a parent), causing a memory leak: self.current_popup.deleteLater() should be called before creating the new one.
It works perfectly, thanks. @musicamante I will consider the suggestions. Thanks.
@AndrésNecochea You're welcome. Be aware that while point 2 will not change things much in most cases, points 1 and 3 are not just "optional" and slight adjustments: (1) will cause incorrect positioning whenever the view is horizontally scrolled, specifically if the horizontal header is much larger than the view and the view is scrolled near the right edge (it could even position the popup outside screen margins); (3) may seriously increase RAM usage especially if the program is kept running for a lot of time, or even cause unexpected crashes in case the popup is connected to some signals »
@AndrésNecochea » that may indirectly raise some exceptions. Right now this is not a serious issue, as you don't have a direct relation between the popup and the model, but since you're going to use it to show filters and other options that will be based on the model shape and contents, it could easily increase in complexity and memory usage. Remember that PyQt (and PySide) are Python bindings for the Qt library: deleting (even by overwriting) an object in Python doesn't mean that its Qt counterpart is, and vice versa: when you create a QObject with a parent, you should always call »
@AndrésNecochea » its deleteLater() when you don't need it anymore, unless you are completely sure that it will be deleted by Qt in other ways (for instance, if the parent is being deleted for Qt as well). While widgets don't normally use much RAM (a basic QWidget takes a few kB), and we may not care a lot with modern computers, it's still good practice to consider these aspects. Unrelated: I realize that I approved your Staging Ground post a bit superficially; your original code does work, and asking "how could I improve it" isn't the scope of SO, unless you have specific issues you »
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.