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.
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.