如何使用QCompleter和QLineEdit实现支持模糊匹配的搜索栏
最近需要用Qt实现搜索栏,类似于浏览器的搜索栏,需要支持模糊搜索并实时显示匹配的选项。
接下任务后,迅速入门Qt. 本来准备魔改QComboBox
,但始终处理不好用户输入的焦点,最终效果并不好。后来了解到,QLineEdit
中支持QCompleter
,QCompleter
就是用来实现补全提示的。
QCompleter
支持行内补全、弹出窗口补全等多种模式;并提供了前缀匹配和子串匹配两种匹配模式,但很可惜,这两种基本的匹配模式都不是我需要的。
所谓模糊匹配,还要支持缩写(例如“中国”对应于“中华人民共和国”),因此需要额外实现QCompleter
. 为QLineEdit
设置QCompleter
后,每当输入栏的文本发生变化,QCompleter
的splitPath
方法就会被调用,其原型为:
splitPath(self, path:str) -> list[str]
其中,参数path
就是输入栏中的字符串。接下来,需要在一个待选的字符串列表中选择所有匹配项,在Qt中,字符串列表可以保存于最简单的QStrignListModel
中。
那么如何从字符串列表中筛选出匹配项呢?还需要引入一个代理类,这个类继承自QSortFilterProxyModel
. 将上述QStringListModel
传给它后,它会对每一项调用filterAcceptsRow
方法,此方法的返回值是bool
类型,表示是否接受这一项。
class FuzzyFilterProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self.split_path = ''
def SetSplitPath(self, split_path):
"""保存输入栏中的字符串, 在filterAcceptsRow检查是否模糊匹配时会用到"""
self.split_path = split_path
def filterAcceptsRow(self, source_row:int, source_parent) -> bool:
index = self.sourceModel().index(source_row, 0, source_parent)
word = self.sourceModel().data(index)
# 检查是否模糊匹配
for ch in self.split_path:
if ch not in word:
return False
return True
提示
文中代码基于PySide6实现,使用PyQt或是C++的Qt差别不大。另外,在PySide6中,自带的类方法首字母为小写,由本人自定义的类方法首字母为大写,请注意区分。
以上代码重载了filterAcceptsRow
方法,在其中实现了简单的模糊匹配功能。此外,定义了方法SetSplitPath
, 用于保存输入栏的文本。
有了这个类,就可以自定义QCompleter
了:
class FuzzyCompleter(QCompleter):
def __init__(self, parent=None):
super().__init__(parent)
self.source_model = QStringListModel([])
def splitPath(self, path: str) -> List[str]:
proxy_model = FuzzyFilterProxyModel()
proxy_model.SetSplitPath(path)
proxy_model.setSourceModel(self.source_model)
self.setModel(proxy_model)
return []
def SetSourceModel(self, source_model):
self.source_model = source_model
注释
或许你会好奇,为什么在
splitPath
中每次都需要实例化一个FuzzyFilterProxyModel
,而不将其保存为类的成员。我自己也觉得不必如此,但如果不重新设置模型,搜索提示功能无法正常工作。(做完这个就跑路,不管那么多了🤡)
之后,基于QLineEdit
实现搜索栏就是小菜一碟了:
class SearchBox(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.items = []
self.fuzzy_completer = FuzzyCompleter(self)
self.setCompleter(self.fuzzy_completer)
def SetItems(self, items:list[str]):
self.items = items
self.fuzzy_completer.SetSourceModel(QStringListModel(self.items))
由于我需要频繁修改待选的字符串列表,因此自定义了一个SetItems
方法。以上代码仅供参考,根据具体需要加以调整。
完整代码如下(代码中使用了qdarktheme(pip install pyqtdarktheme
)美化界面, 不想用的话可以删掉它):
searchbox.py
import sys
from PySide6.QtWidgets import QApplication, QLineEdit, QCompleter
from PySide6.QtCore import QStringListModel, QSortFilterProxyModel
import qdarktheme
class FuzzyFilterProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self.split_path = ''
def SetSplitPath(self, split_path):
"""保存输入栏中的字符串, 在filterAcceptsRow检查是否模糊匹配时会用到"""
self.split_path = split_path
def filterAcceptsRow(self, source_row: int, source_parent) -> bool:
index = self.sourceModel().index(source_row, 0, source_parent)
word = self.sourceModel().data(index)
# 检查是否模糊匹配
for ch in self.split_path:
if ch not in word:
return False
return True
class FuzzyCompleter(QCompleter):
def __init__(self, parent=None):
super().__init__(parent)
self.source_model = QStringListModel([])
def splitPath(self, path: str) -> list[str]:
proxy_model = FuzzyFilterProxyModel()
proxy_model.SetSplitPath(path)
proxy_model.setSourceModel(self.source_model)
self.setModel(proxy_model)
return []
def SetSourceModel(self, source_model):
self.source_model = source_model
class SearchBox(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.items = []
self.fuzzy_completer = FuzzyCompleter(self)
self.setCompleter(self.fuzzy_completer)
def SetItems(self, items:list[str]):
self.items = items
self.fuzzy_completer.SetSourceModel(QStringListModel(self.items))
def IsValidInput(self):
return self.text().strip() in self.items
if __name__ == '__main__':
app = QApplication(sys.argv)
qdarktheme.setup_theme()
exe = SearchBox()
exe.setMinimumWidth(500)
exe.SetItems(['你好', '我好', '大家好', '我们在一起', '隔壁老王', '都挺好', '好个P'])
exe.show()
sys.exit(app.exec())
最终效果如下: