Created
December 5, 2024 09:46
-
-
Save minimalefforttech/1f0a1854a373b6522dcf761315aa88da to your computer and use it in GitHub Desktop.
Short example showing smarter filtering in Qt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from difflib import SequenceMatcher | |
try: | |
from PySide6 import QtCore, QtGui, QtWidgets | |
except ImportError: | |
from PySide2 import QtCore, QtGui, QtWidgets | |
class FilterModel(QtCore.QSortFilterProxyModel): | |
""" A simple filter model based on sequencematcher quick_ratio. | |
ratio is 0-1 value for a match. | |
Note: This is not performant, it is an example, ideally you would look into precaching or difflib.get_close_matches() for many items | |
https://docs.python.org/3/library/difflib.html | |
""" | |
def __init__(self, ratio:float=0.6, parent=None): | |
super().__init__(parent) | |
self._filter_text = "" | |
self._ratio = ratio | |
self._show_all = False | |
self.sort(0, QtCore.Qt.AscendingOrder) | |
@QtCore.Slot(str) | |
def set_filter_text(self, text:str): | |
self._filter_text = str(text).lower() | |
self.invalidate() | |
@QtCore.Slot(float) | |
def set_ratio(self, ratio:float): | |
self._ratio = float(ratio) | |
self.invalidate() | |
@QtCore.Slot(bool) | |
def set_show_all(self, show_all:bool): | |
self._show_all = bool(show_all) | |
self.invalidate() | |
def filterAcceptsColumn(self, source_column:int, source_parent:QtCore.QModelIndex)->bool: | |
return True | |
def filterAcceptsRow(self, source_row:int, source_parent:QtCore.QModelIndex)->bool: | |
if self._ratio <= 0.0 or self._show_all or not self._filter_text: | |
# Nothing set, show everything | |
return True | |
# Case-insensitive | |
text = (self.sourceModel().index(source_row, 0, source_parent).data() or "").lower() | |
if not text: | |
return False | |
if self._filter_text in text: | |
return True | |
# First parameter is optional filtering of "junk" text like spaces, default is usually fine. | |
# ratio is more accurate but doesn't provide much in this context, real_quick_ratio is not accurate enough. | |
ratio = SequenceMatcher(None, self._filter_text, text).quick_ratio() | |
return ratio >= self._ratio | |
def lessThan(self, left:QtCore.QModelIndex, right:QtCore.QModelIndex)->bool: | |
if not self._filter_text or self._show_all: | |
return left.row() < right.row() | |
left_ratio = SequenceMatcher(None, self._filter_text, left.data().lower()).quick_ratio() if left.data() else 0.0 | |
right_ratio = SequenceMatcher(None, self._filter_text, right.data().lower()).quick_ratio() if right.data() else 0.0 | |
# Sort text ascending so ratio is flipped | |
return left_ratio > right_ratio | |
def data(self, index, role=QtCore.Qt.DisplayRole): | |
# Provides a color falloff to demonstrate the filtering | |
if not self._show_all or role != QtCore.Qt.ForegroundRole or self._ratio <= 0.0: | |
return super().data(index, role) | |
ratio = SequenceMatcher(None, self._filter_text, (index.data() or "").lower()).quick_ratio() | |
if ratio < self._ratio: | |
# Draw falloff color between 20 (no match) and 255 (full match) | |
t = ratio * (1.0/self._ratio) | |
luminance = (1 - t) * 20 + t * 255 | |
return QtGui.QBrush(QtGui.QColor(luminance, luminance, luminance)) | |
return super().data(index, role) | |
class Widget(QtWidgets.QWidget): | |
""" A Simple example widget that filters items based on some text input | |
""" | |
def __init__(self, items, parent=None): | |
super().__init__(parent) | |
self.setWindowTitle("Filter Example") | |
layout = QtWidgets.QVBoxLayout(self) | |
search_box = QtWidgets.QLineEdit() | |
search_box.setPlaceholderText("filter...") | |
layout.addWidget(search_box) | |
show_all_box = QtWidgets.QCheckBox("Show All") | |
layout.addWidget(show_all_box) | |
view = QtWidgets.QListView() | |
layout.addWidget(view) | |
model = QtCore.QStringListModel(items, parent=self) | |
filter_model = FilterModel(parent=self) | |
filter_model.setSourceModel(model) | |
view.setModel(filter_model) | |
search_box.textChanged.connect(filter_model.set_filter_text) | |
show_all_box.toggled.connect(filter_model.set_show_all) | |
items = ["January", "February", "March", "April", "May", "June", "July", "August", "October", "November", "December"] | |
widget = Widget(items) | |
widget.show() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment