自定义可搜索QComboBox弹窗双击选中、悬停高亮缺失问题求助
基于PySide6的自定义可搜索QComboBox问题修复方案
问题概述
- 点击补全器弹窗选项需双击才能触发选中,仅在自定义
SubstringFilterProxyModel配合QCompleter时出现,默认补全器无此异常; - 补全器弹窗的悬停高亮效果缺失,虽可通过样式表临时规避,但存在底层逻辑问题。
相关实现代码
from PySide6.QtCore import Qt, QSortFilterProxyModel from PySide6.QtWidgets import QLineEdit, QComboBox, QApplication, QWidget, QCompleter class SubstringFilterProxyModel(QSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): model = self.sourceModel() if model is None: return False text_filter = self.filterRegularExpression().pattern().casefold() if text_filter == '': return True model_index = model.index(source_row, 0, source_parent) if not model_index.isValid(): return False model_data = model.data(model_index, Qt.DisplayRole) for substring in text_filter.split(' '): if (substring not in model_data.casefold()): return False return True def lessThan(self, left, right): left_data = self.sourceModel().data(left) right_data = self.sourceModel().data(right) if left_data is None: return False if right_data is None: return True text_filter = self.filterRegularExpression().pattern().casefold() left_index = left_data.find(text_filter) right_index = right_data.find(text_filter) return left_index < right_index class CustomQCompleter(QCompleter): def splitPath(self, path): self.model().invalidate() # Invalidates the current sorting and filtering. self.model().sort(0, Qt.AscendingOrder) return '' class SelectAllLineEdit(QLineEdit): def __init__(self, parent=None): super(SelectAllLineEdit, self).__init__(parent) self.ready_to_edit = True def mousePressEvent(self, e): super(SelectAllLineEdit, self).mousePressEvent(e) if self.ready_to_edit: self.selectAll() self.ready_to_edit = False def focusOutEvent(self, e): super(SelectAllLineEdit, self).focusOutEvent(e) self.deselect() self.ready_to_edit = True class SearchableComboBox(QComboBox): def __init__(self, parent: QWidget | None = None ) -> None: super().__init__(parent) self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) self.setEditable(True) self.setLineEdit(SelectAllLineEdit()) self.lineEdit().setEchoMode(QLineEdit.EchoMode.Normal) proxy_model = SubstringFilterProxyModel() proxy_model.setSourceModel(self.model()) self.lineEdit().textChanged.connect(proxy_model.setFilterRegularExpression) self.lineEdit().editingFinished.connect(self.editing_finished) completer = CustomQCompleter() completer.setModel(proxy_model) completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) # completer.popup().setStyleSheet("QListView::item:hover {background-color: rgb(55,134,209);}") completer.popup().clicked.connect(lambda: print("clicked")) self.setCompleter(completer) def editing_finished(self): text = self.completer().currentCompletion() index = self.findText(text) self.setCurrentIndex(index) self.clearFocus() self.activated.emit(index) if __name__ == "__main__": import sys from PySide6.QtWidgets import QApplication app = QApplication(sys.argv) app.setStyle('Fusion') comboBox = SearchableComboBox() comboBox.addItems(["Apple", "Still filter apple", "Archer", "alchemy", "Orange apple", "banana", "pineapple", "pine apple", "pine apple but with more", "pine and apple"]) comboBox.show() sys.exit(app.exec())
问题分析与修复
问题1:双击才能选中选项的修复
根源:CustomQCompleter的splitPath方法中手动调用model().invalidate()和model().sort(),导致用户点击时模型触发刷新,第一次点击实际是更新模型而非选中操作。
修复步骤:
- 移除补全器中不必要的模型刷新逻辑,让代理模型在过滤条件变化时自动处理排序;
- 重写代理模型的
setFilterRegularExpression方法,过滤条件更新后自动触发排序。
修改后的代码片段:
class CustomQCompleter(QCompleter): def splitPath(self, path): # 移除手动刷新逻辑,返回路径分割结果即可 return path.split(' ') class SubstringFilterProxyModel(QSortFilterProxyModel): # ... 保留原filterAcceptsRow、lessThan方法 ... def setFilterRegularExpression(self, pattern): super().setFilterRegularExpression(pattern) # 过滤条件变化后自动排序 self.sort(0, Qt.AscendingOrder)
问题2:悬停高亮缺失的修复
根源:代理模型未正确传递样式相关的角色数据(如背景色、前景色),导致弹窗列表项无法获取默认高亮样式。
修复步骤:
重写代理模型的data方法,将样式相关角色的请求转发给源模型,同时确保弹窗使用正确的选择模式。
修改后的代码片段:
class SubstringFilterProxyModel(QSortFilterProxyModel): # ... 保留原有方法 ... def data(self, index, role=Qt.DisplayRole): # 转发样式相关角色请求到源模型 if role in (Qt.BackgroundRole, Qt.ForegroundRole, Qt.FontRole): source_index = self.mapToSource(index) return self.sourceModel().data(source_index, role) return super().data(index, role)
在SearchableComboBox初始化中添加弹窗选择模式设置:
completer.popup().setSelectionMode(QListView.SingleSelection)
最终完整代码
from PySide6.QtCore import Qt, QSortFilterProxyModel from PySide6.QtWidgets import QLineEdit, QComboBox, QApplication, QWidget, QCompleter, QListView class SubstringFilterProxyModel(QSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): model = self.sourceModel() if model is None: return False text_filter = self.filterRegularExpression().pattern().casefold() if text_filter == '': return True model_index = model.index(source_row, 0, source_parent) if not model_index.isValid(): return False model_data = model.data(model_index, Qt.DisplayRole) for substring in text_filter.split(' '): if (substring not in model_data.casefold()): return False return True def lessThan(self, left, right): left_data = self.sourceModel().data(left) right_data = self.sourceModel().data(right) if left_data is None: return False if right_data is None: return True text_filter = self.filterRegularExpression().pattern().casefold() left_index = left_data.find(text_filter) right_index = right_data.find(text_filter) return left_index < right_index def setFilterRegularExpression(self, pattern): super().setFilterRegularExpression(pattern) self.sort(0, Qt.AscendingOrder) def data(self, index, role=Qt.DisplayRole): if role in (Qt.BackgroundRole, Qt.ForegroundRole, Qt.FontRole): source_index = self.mapToSource(index) return self.sourceModel().data(source_index, role) return super().data(index, role) class CustomQCompleter(QCompleter): def splitPath(self, path): return path.split(' ') class SelectAllLineEdit(QLineEdit): def __init__(self, parent=None): super(SelectAllLineEdit, self).__init__(parent) self.ready_to_edit = True def mousePressEvent(self, e): super(SelectAllLineEdit, self).mousePressEvent(e) if self.ready_to_edit: self.selectAll() self.ready_to_edit = False def focusOutEvent(self, e): super(SelectAllLineEdit, self).focusOutEvent(e) self.deselect() self.ready_to_edit = True class SearchableComboBox(QComboBox): def __init__(self, parent: QWidget | None = None ) -> None: super().__init__(parent) self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) self.setEditable(True) self.setLineEdit(SelectAllLineEdit()) self.lineEdit().setEchoMode(QLineEdit.EchoMode.Normal) proxy_model = SubstringFilterProxyModel() proxy_model.setSourceModel(self.model()) self.lineEdit().textChanged.connect(proxy_model.setFilterRegularExpression) self.lineEdit().editingFinished.connect(self.editing_finished) completer = CustomQCompleter() completer.setModel(proxy_model) completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) # 设置弹窗选择模式,确保高亮正常 completer.popup().setSelectionMode(QListView.SingleSelection) completer.popup().clicked.connect(lambda: print("clicked")) self.setCompleter(completer) def editing_finished(self): text = self.completer().currentCompletion() index = self.findText(text) self.setCurrentIndex(index) self.clearFocus() self.activated.emit(index) if __name__ == "__main__": import sys from PySide6.QtWidgets import QApplication app = QApplication(sys.argv) app.setStyle('Fusion') comboBox = SearchableComboBox() comboBox.addItems(["Apple", "Still filter apple", "Archer", "alchemy", "Orange apple", "banana", "pineapple", "pine apple", "pine apple but with more", "pine and apple"]) comboBox.show() sys.exit(app.exec())
内容的提问来源于stack exchange,提问作者Modo
相关产品推荐
相关产品推荐

