You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义可搜索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(),导致用户点击时模型触发刷新,第一次点击实际是更新模型而非选中操作。

修复步骤:

  1. 移除补全器中不必要的模型刷新逻辑,让代理模型在过滤条件变化时自动处理排序;
  2. 重写代理模型的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 05:45:55