最近需要用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())
最終效果如下: