如何使用QCompleter和QLineEdit实现支持模糊匹配的搜索栏

最近需要用Qt实现搜索栏,类似于浏览器的搜索栏,需要支持模糊搜索并实时显示匹配的选项。

接下任务后,迅速入门Qt. 本来准备魔改QComboBox,但始终处理不好用户输入的焦点,最终效果并不好。后来了解到,QLineEdit中支持QCompleterQCompleter就是用来实现补全提示的。

QCompleter支持行内补全、弹出窗口补全等多种模式;并提供了前缀匹配和子串匹配两种匹配模式,但很可惜,这两种基本的匹配模式都不是我需要的。

 

所谓模糊匹配,还要支持缩写(例如“中国”对应于“中华人民共和国”),因此需要额外实现QCompleter. QLineEdit设置QCompleter后,每当输入栏的文本发生变化,QCompletersplitPath方法就会被调用,其原型为:

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())

最终效果如下:

posted @ 2024-07-17 13:42  overxus  阅读(517)  评论(0编辑  收藏  举报