Skip to content

Instantly share code, notes, and snippets.

@oglops
Last active March 16, 2024 13:00
Show Gist options
  • Save oglops/1fa1b7c3a9d93ac6ff189cb2a556b64d to your computer and use it in GitHub Desktop.
Save oglops/1fa1b7c3a9d93ac6ff189cb2a556b64d to your computer and use it in GitHub Desktop.
filtered qtableview widgets
import sys
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
# https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
elif role == Qt.TextAlignmentRole:
return Qt.AlignCenter
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
data = [
[4, 9, 2],
[1, 0, 0],
[3, 5, 0],
[3, 3, 2],
[7, 8, 9],
]
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
self.resize(330, 190)
self.table.setItemDelegateForColumn(2, StyledItemDelegateTriangle())
self.table.setHorizontalHeader(HeaderView(Qt.Horizontal))
# https://stackoverflow.com/questions/25489640/implement-paintsection-for-qheaderview-delivered-class
class HeaderView(QtWidgets.QHeaderView):
def paintSection(self, painter, rect, index):
painter.save()
super(HeaderView, self).paintSection(painter, rect, index)
if index in self.__filtered:
# painter.drawRect(rect.adjusted(2, 2, -2, -2))
polygon = QtGui.QPolygon(3)
polygon[0] = QtCore.QPoint(rect.x() + 5, rect.y())
polygon[1] = QtCore.QPoint(rect.x(), rect.y())
polygon[2] = QtCore.QPoint(rect.x(), rect.y() + 5)
painter.restore()
painter.setBrush(QtGui.QBrush(QtGui.QColor(QtCore.Qt.darkGreen)))
painter.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.darkGreen)))
painter.drawPolygon(polygon)
# https://stackoverflow.com/questions/19308940/how-to-draw-a-triangle-in-qtableview-cell-corner-to-show-the-model-data-can-be-s
class StyledItemDelegateTriangle(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(StyledItemDelegateTriangle, self).__init__(parent)
def paint(self, painter, option, index):
super(StyledItemDelegateTriangle, self).paint(painter, option, index)
polygon = QtGui.QPolygon(3)
polygon[0] = QtCore.QPoint(option.rect.x() + 5, option.rect.y())
polygon[1] = QtCore.QPoint(option.rect.x(), option.rect.y())
polygon[2] = QtCore.QPoint(option.rect.x(), option.rect.y() + 5)
painter.save()
painter.setRenderHint(painter.Antialiasing)
painter.setBrush(QtGui.QBrush(QtGui.QColor(QtCore.Qt.darkGreen)))
painter.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.darkGreen)))
painter.drawPolygon(polygon)
painter.restore()
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
import sys
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
word_file = "/usr/share/dict/words"
WORDS = open(word_file).read().splitlines()
import random
from functools import partial
class MultiListView(QtWidgets.QListView):
def __init__(self, parent=None):
super(MultiListView, self).__init__(parent)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Space:
selected = self.selectedIndexes()
# skip the last one becaues it would be toggled again
for idx in selected[:-1]:
idx = self.model().mapToSource(idx)
item = self.model().sourceModel().itemFromIndex(idx)
curState = item.checkState()
state = (
QtCore.Qt.Unchecked
if curState == QtCore.Qt.Checked
else QtCore.Qt.Checked
)
item.setCheckState(state)
return super(MultiListView, self).keyPressEvent(event)
# https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/
class FilteredListView(QtWidgets.QWidget):
def __init__(self, parent=None):
super(FilteredListView, self).__init__(parent)
layout = QtWidgets.QVBoxLayout()
filterLayout = QtWidgets.QHBoxLayout()
self.filter = QtWidgets.QLineEdit()
self.filter.setToolTip("regex")
self.regexCheckbox = QtWidgets.QCheckBox(".*")
self.listView = MultiListView()
filterLayout.addWidget(self.filter)
filterLayout.addWidget(self.regexCheckbox)
layout.addLayout(filterLayout)
layout.addWidget(self.listView)
self.setLayout(layout)
class InnerProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
self.__filterStr = ""
self.__regexMode = False
return super(InnerProxyModel, self).__init__(*args, **kwargs)
def filterAcceptsRow(self, sourceRow, sourceParent):
if not self.__filterStr:
return True
index = self.sourceModel().index(sourceRow, 0, sourceParent)
modelStr = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
if not self.__regexMode:
regex = QtCore.QRegExp(
"*%s*" % self.__filterStr, QtCore.Qt.CaseInsensitive
)
regex.setPatternSyntax(QtCore.QRegExp.Wildcard)
return regex.exactMatch(modelStr)
else:
regex = QtCore.QRegExp(self.__filterStr, QtCore.Qt.CaseInsensitive)
return regex.indexIn(modelStr) >= 0
def setRegexMode(self, mode):
self.__regexMode = bool(mode)
def updateFilterStr(self, string):
self.__filterStr = string
self.invalidateFilter()
self.model = QtGui.QStandardItemModel(self)
self.proxy = InnerProxyModel()
self.proxy.setSourceModel(self.model)
self.listView.setModel(self.proxy)
self.filter.textChanged.connect(partial(self.proxy.updateFilterStr))
self.regexCheckbox.toggled.connect(partial(self.proxy.setRegexMode))
def populate(self, data):
for d in data:
item = QtGui.QStandardItem(d)
item.setCheckable(True)
self.model.appendRow(item)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.filterWidget = FilteredListView()
data = [random.choice(WORDS) for x in range(10)]
self.filterWidget.populate(data)
self.setCentralWidget(self.filterWidget)
self.resize(330, 190)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
from logging import Filter
import sys
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
word_file = "/usr/share/dict/words"
WORDS = open(word_file).read().splitlines()
import random
from functools import partial
HEADERS = ["Name", "Category", "Tags"]
class FilteredHeaderView(QtWidgets.QHeaderView):
def __init__(self, orientation=QtCore.Qt.Horizontal, parent=None):
super(FilteredHeaderView, self).__init__(orientation, parent)
self.__filtered = set()
self.setSectionsClickable(True)
# self.setHighlightSections(True)
def paintSection(self, painter, rect, index):
painter.save()
super(FilteredHeaderView, self).paintSection(painter, rect, index)
if index in self.__filtered:
# painter.drawRect(rect.adjusted(2, 2, -2, -2))
polygon = QtGui.QPolygon(3)
polygon[0] = QtCore.QPoint(rect.x() + 5, rect.y())
polygon[1] = QtCore.QPoint(rect.x(), rect.y())
polygon[2] = QtCore.QPoint(rect.x(), rect.y() + 5)
painter.restore()
painter.setBrush(QtGui.QBrush(QtGui.QColor(QtCore.Qt.darkGreen)))
painter.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.darkGreen)))
painter.drawPolygon(polygon)
def setFilteredColumn(self, column, clear=False):
if clear:
self.__filtered.remove(column)
else:
self.__filtered.add(column)
class PersistentMenu(QtWidgets.QMenu):
def mouseReleaseEvent(self, event):
action = self.activeAction()
if action and action.isEnabled():
action.setEnabled(False)
super(PersistentMenu, self).mouseReleaseEvent(event)
action.setEnabled(True)
action.trigger()
else:
super(PersistentMenu, self).mouseReleaseEvent(event)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
elif role == Qt.TextAlignmentRole:
return int(Qt.AlignCenter | Qt.AlignVCenter)
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return HEADERS[section]
return super(TableModel, self).headerData(section, orientation, role)
class MultiListView(QtWidgets.QListView):
def __init__(self, parent=None):
super(MultiListView, self).__init__(parent)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Space:
selected = self.selectedIndexes()
# skip the last one becaues it would be toggled again
for idx in selected[:-1]:
idx = self.model().mapToSource(idx)
item = self.model().sourceModel().itemFromIndex(idx)
curState = item.checkState()
state = (
QtCore.Qt.Unchecked
if curState == QtCore.Qt.Checked
else QtCore.Qt.Checked
)
item.setCheckState(state)
return super(MultiListView, self).keyPressEvent(event)
# https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/
class FilteredListView(QtWidgets.QWidget):
filterChanged = QtCore.Signal(set)
def __init__(self, parent=None):
super(FilteredListView, self).__init__(parent)
layout = QtWidgets.QVBoxLayout()
filterLayout = QtWidgets.QHBoxLayout()
self.filter = QtWidgets.QLineEdit()
self.filter.setToolTip("regex")
self.regexCheckbox = QtWidgets.QCheckBox(".*")
self.listView = MultiListView()
filterLayout.addWidget(self.filter)
filterLayout.addWidget(self.regexCheckbox)
layout.addLayout(filterLayout)
layout.addWidget(self.listView)
self.setLayout(layout)
# store the tags choosen, choosing all tags ends up with empty filters
self.__filters = set()
self.__data = []
class InnerProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
self.__filterStr = ""
self.__regexMode = False
return super(InnerProxyModel, self).__init__(*args, **kwargs)
def filterAcceptsRow(self, sourceRow, sourceParent):
if not self.__filterStr:
return True
index = self.sourceModel().index(sourceRow, 0, sourceParent)
modelStr = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
if not self.__regexMode:
regex = QtCore.QRegExp(
"*%s*" % self.__filterStr, QtCore.Qt.CaseInsensitive
)
regex.setPatternSyntax(QtCore.QRegExp.Wildcard)
return regex.exactMatch(modelStr)
else:
regex = QtCore.QRegExp(self.__filterStr, QtCore.Qt.CaseInsensitive)
return regex.indexIn(modelStr) >= 0
def setRegexMode(self, mode):
self.__regexMode = bool(mode)
def updateFilterStr(self, string):
self.__filterStr = string
self.invalidateFilter()
self.model = QtGui.QStandardItemModel(self)
self.proxy = InnerProxyModel()
self.proxy.setSourceModel(self.model)
self.listView.setModel(self.proxy)
self.filter.textChanged.connect(partial(self.proxy.updateFilterStr))
self.regexCheckbox.toggled.connect(partial(self.proxy.setRegexMode))
self.model.dataChanged.connect(partial(self.__dataChanged))
def populate(self, data):
self.__data = data
for d in sorted(data, key=lambda x: x.lower()):
item = QtGui.QStandardItem(d)
item.setCheckable(True)
self.model.appendRow(item)
def __dataChanged(self, top_left, bottom_right, roles):
if QtCore.Qt.CheckStateRole in roles:
value = top_left.data()
checked = bool(top_left.data(role=QtCore.Qt.CheckStateRole))
if checked:
self.__filters.add(value)
else:
self.__filters.remove(value)
if len(self.__filters) == len(self.__data):
# not filtering
filters = []
else:
filters = self.__filters
self.filterChanged.emit(filters)
def restoreFilters(self, filters):
checked = []
for r in range(self.model.rowCount()):
item = self.model.item(r)
if item.text() in filters:
item.setCheckState(QtCore.Qt.Checked)
checked.append(r)
# checked tags on top
if checked:
for i, r in enumerate(checked):
itemList = self.model.takeRow(r)
self.model.insertRow(i, itemList)
def clearFilters(self):
for r in range(self.model.rowCount()):
item = self.model.item(r)
item.setCheckState(QtCore.Qt.Unchecked)
class FilteredTableView(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
super(FilteredTableView, self).__init__(**kwargs)
self.filterHeaderView = FilteredHeaderView(QtCore.Qt.Horizontal)
self.setHorizontalHeader(self.filterHeaderView)
header = self.horizontalHeader()
header.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
header.customContextMenuRequested.connect(self.build_menu)
self.setSortingEnabled(True)
self.__filteredColumns = set()
self.__filtering = dict()
def enableFilter(self, column):
self.__filteredColumns.add(column)
def build_menu(self, pos):
col = self.horizontalHeader().logicalIndexAt(pos)
if col not in self.__filteredColumns:
return
menu = PersistentMenu()
if self.__filtering.get(col):
clearAction = menu.addAction("Clear Filters")
menu.addSeparator()
action = QtWidgets.QWidgetAction(menu)
filterWidget = FilteredListView()
# populate filterWidget based on all rows in current column
# data = [random.choice(WORDS) for x in range(10)]
colData = set()
proxy = self.model().sourceModel()
for r in range(proxy.rowCount()):
value = proxy.data(proxy.index(r, col))
colData.update(value.split())
filterWidget.populate(colData)
if self.__filtering.get(col):
filterWidget.restoreFilters(self.__filtering.get(col))
action.setDefaultWidget(filterWidget)
menu.addAction(action)
filterWidget.filterChanged.connect(partial(self.__updateFilters, col))
filterWidget.filterChanged.connect(partial(self.model().updateFilters, col))
if self.__filtering.get(col):
clearAction.triggered.connect(
partial(self.__clearFilters, filterWidget, col)
)
menu.exec_(self.horizontalHeader().viewport().mapToGlobal(pos))
def __updateFilters(self, column, filters):
self.__filtering[column] = filters
self.filterHeaderView.setFilteredColumn(column, clear=not bool(filters))
# force instant repaint
self.filterHeaderView.viewport().update()
def __clearFilters(self, filterWidget, column):
filterWidget.clearFilters()
# self.filterHeaderView.setFilteredColumn(column, clear=True)
class MultiColumnProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
self.__filtering = {}
return super(MultiColumnProxyModel, self).__init__(*args, **kwargs)
def filterAcceptsRow(self, sourceRow, sourceParent):
if not self.__filtering:
return True
for col, filters in self.__filtering.items():
if not filters:
continue
index = self.sourceModel().index(sourceRow, col, sourceParent)
modelStr = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# any of the match in filters would keep this row for current column
for f in filters:
if f == modelStr:
break
else:
return False
return True
def updateFilters(self, col, filters):
self.__filtering[col] = filters
self.invalidateFilter()
# https://stackoverflow.com/questions/65054604/pyqt5-qsortfilterproxymodel-sort-column-by-appropriate-data-type-int-float-da
def lessThan(self, left, right):
leftData = self.sourceModel().data(left)
rightData = self.sourceModel().data(right)
if leftData is None:
return True
elif rightData is None:
return False
elif type(leftData) != type(rightData):
# don't want to sort at all in these cases, False is just a copout ...
# should warn user
return False
return leftData.lower() < rightData.lower()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.table = FilteredTableView()
data = [[random.choice(WORDS) for y in range(3)] for x in range(10)]
self.model = TableModel(data)
self.proxy = MultiColumnProxyModel()
self.proxy.setSourceModel(self.model)
self.table.setModel(self.proxy)
for col in (1, 2):
self.table.enableFilter(col)
self.setCentralWidget(self.table)
self.resize(340, 340)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
from PySide2 import QtWidgets, QtCore
import sys
# https://stackoverflow.com/questions/2050462/prevent-a-qmenu-from-closing-when-one-of-its-qaction-is-triggered
class PersistentMenu(QtWidgets.QMenu):
def mouseReleaseEvent(self, event):
action = self.activeAction()
if action and action.isEnabled():
action.setEnabled(False)
super(PersistentMenu, self).mouseReleaseEvent(event)
action.setEnabled(True)
action.trigger()
else:
super(PersistentMenu, self).mouseReleaseEvent(event)
def build_menu(pos):
menu = PersistentMenu()
for i in range(10):
act = menu.addAction(f"option {i}")
act.setCheckable(True)
menu.exec_(button.mapToGlobal(pos))
app = QtWidgets.QApplication(sys.argv)
button = QtWidgets.QPushButton("Push Me")
button.show()
button.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
button.customContextMenuRequested.connect(build_menu)
app.exec_()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment