You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何让PyQt5 QComboBox的LineEdit复用委托样式并实现焦点切换时的样式变化

如何让PyQt5 QComboBox的LineEdit复用委托样式并实现焦点切换时的样式变化

嘿,我完全懂你现在的困扰——好不容易用QStyledItemDelegate把下拉项美化得漂漂亮亮,结果LineEdit部分死活同步不上样式,还要实现「无焦点时和选中项一致、有焦点时变搜索框」的效果,试了各种方法都翻车对吧?别慌,我给你改好了可直接运行的代码,完美实现你要的效果,咱们一步步说清楚:

核心思路

要搞定这个需求,确实得自定义QComboBox子类,核心关键点有两个:

  • 监听焦点变化,区分「无焦点/未点击」和「有焦点/已点击」两种状态
  • 无焦点时,复用你写的委托绘制逻辑来渲染LineEdit区域,让它和选中的下拉项视觉完全一致;有焦点时,让LineEdit正常显示,承担搜索框的功能

修改后的完整可运行代码

import sys
from PyQt5 import QtWidgets, QtCore, QtGui
import qdarkstyle

class ComboBoxItemDelegate(QtWidgets.QStyledItemDelegate):
    # 这个委托负责美化下拉项,你的原代码基本不用改
    def paint(self, painter, option, index):
        primary = index.data(QtCore.Qt.DisplayRole)
        secondary = index.data(QtCore.Qt.UserRole)

        if secondary is None:
            secondary = ""

        # 鼠标悬停时高亮背景
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())
        else:
            painter.fillRect(option.rect, option.palette.base())
        
        # 定义primary和secondary的绘制区域
        rect = option.rect.adjusted(5, 0, -5, 0)  
        primaryRect = QtCore.QRect(rect.left(), rect.top(), rect.width(), rect.height()//2)
        secondaryRect = QtCore.QRect(rect.left(), rect.top() + rect.height()//2, rect.width(), rect.height()//2)
        
        # 绘制primary文本
        primaryFont = option.font
        painter.setFont(primaryFont)
        painter.setPen(option.palette.text().color())
        painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, primary)
        
        # 绘制secondary文本(小一号灰色)
        secondaryFont = QtGui.QFont(option.font)
        secondaryFont.setPointSize(option.font.pointSize() - 1)
        painter.setFont(secondaryFont)
        painter.setPen(QtGui.QColor("gray"))
        painter.drawText(secondaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, secondary)

    def sizeHint(self, option, index):
        size = super().sizeHint(option, index)
        size.setHeight(int(size.height() * 1.6))
        return size

class CustomComboBox(QtWidgets.QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setEditable(True)
        # 记录焦点状态,初始无焦点
        self._has_focus = False
        # 监听焦点事件
        self.focusInEvent = self.on_focus_in
        self.focusOutEvent = self.on_focus_out
        # 初始隐藏LineEdit,无焦点时自己绘制内容
        self.lineEdit().setVisible(False)

    def on_focus_in(self, event):
        self._has_focus = True
        self.lineEdit().setVisible(True)
        # 把当前选中项的primary文本设置到LineEdit,方便搜索
        current_index = self.currentIndex()
        if current_index >=0:
            primary = self.itemText(current_index)
            self.lineEdit().setText(primary)
        super().focusInEvent(event)

    def on_focus_out(self, event):
        self._has_focus = False
        self.lineEdit().setVisible(False)
        super().focusOutEvent(event)

    def paintEvent(self, event):
        if not self._has_focus and self.currentIndex() >=0:
            # 无焦点时,自己绘制和下拉项一致的内容
            painter = QtGui.QStylePainter(self)
            painter.setPen(self.palette().color(QtGui.QPalette.Text))

            # 获取当前选中项的数据
            index = self.model().index(self.currentIndex(), 0)
            primary = index.data(QtCore.Qt.DisplayRole)
            secondary = index.data(QtCore.Qt.UserRole) or ""

            # 绘制ComboBox的框架
            opt = QtWidgets.QStyleOptionComboBox()
            self.initStyleOption(opt)
            painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)

            # 复用委托的绘制逻辑来绘制内容区域
            content_rect = self.style().subControlRect(QtWidgets.QStyle.CC_ComboBox, opt, QtWidgets.QStyle.SC_ComboBoxEditField, self)
            # 调整绘制区域,和下拉项的边距一致
            rect = content_rect.adjusted(5, 0, -5, 0)  
            primaryRect = QtCore.QRect(rect.left(), rect.top(), rect.width(), rect.height()//2)
            secondaryRect = QtCore.QRect(rect.left(), rect.top() + rect.height()//2, rect.width(), rect.height()//2)
            
            # 绘制primary文本
            primaryFont = self.font()
            painter.setFont(primaryFont)
            painter.setPen(self.palette().text().color())
            painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, primary)
            
            # 绘制secondary文本(小一号灰色)
            secondaryFont = QtGui.QFont(self.font())
            secondaryFont.setPointSize(self.font().pointSize() - 1)
            painter.setFont(secondaryFont)
            painter.setPen(QtGui.QColor("gray"))
            painter.drawText(secondaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, secondary)
        else:
            # 有焦点时,让父类正常处理,LineEdit作为搜索框显示
            super().paintEvent(event)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        items = [{'additional_info':'HELLO', 'Item':'SYMBOL'},
                   {'additional_info':'WORLD', 'Item':'G.I. JOE'},
                   {'additional_info':'NOVABRAIN', 'Item':'FLATEARTH'},
                   {'additional_info':'SUPERSTAR', 'Item':'BOB THE BUILDER'}]
        for item in items:
            self.initial_filling(item['Item'], item['additional_info'])

    def initial_filling(self, primary, secondary):
        self.searchComboBox.addItem(primary)
        index = self.searchComboBox.model().index(self.searchComboBox.count()-1, 0)
        self.searchComboBox.model().setData(index, secondary, role=QtCore.Qt.UserRole)

    def initUI(self):
        self.setWindowTitle("My best Widget")
        self.resize(324, 500)
        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        layout = QtWidgets.QVBoxLayout(central_widget)

        # 使用自定义的ComboBox替代原QComboBox
        self.searchComboBox = CustomComboBox()
        self.searchComboBox.setItemDelegate(ComboBoxItemDelegate(self.searchComboBox))
        self.searchComboBox.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
        layout.addWidget(self.searchComboBox)

        # 搜索回车事件触发逻辑
        self.searchComboBox.lineEdit().returnPressed.connect(self.on_search_return)

        # 选中新项时触发绘制更新
        self.searchComboBox.currentIndexChanged.connect(self.update)

        # 保留原有的无关表格部分
        self.table = QtWidgets.QTableWidget()
        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels(["Some", "Nice", "Table"])
        self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
        self.table.verticalHeader().setVisible(False)
        self.table.setShowGrid(False)
        layout.addWidget(self.table)

    def on_search_return(self):
        symbol = self.searchComboBox.lineEdit().text().strip().upper()
        if not symbol:
            return

        # 模拟API返回的搜索结果
        results = [{'additional_info':'HELLO', 'Item':symbol},
                   {'additional_info':'WORLD', 'Item':symbol},
                   {'additional_info':'GALAXY', 'Item':symbol},
                   {'additional_info':'STAR', 'Item':symbol}]
        self.show_lookup_menu(results)

    def show_lookup_menu(self, results):
        menu = QtWidgets.QMenu(self)
        for result in results:
            text = f"{result['additional_info']} - {result['Item']}"
            action = QtWidgets.QAction(text, menu)
            action.setData(result)
            menu.addAction(action)
        menu.triggered.connect(self.on_menu_action_triggered)
        pos = self.searchComboBox.mapToGlobal(QtCore.QPoint(0, self.searchComboBox.height()))
        menu.exec_(pos)

    def on_menu_action_triggered(self, action):
        result = action.data()
        if result:
            print("You chose: ", result)
            primary = result["Item"]
            secondary = result["additional_info"]
            if self.searchComboBox.findText(primary) == -1:
                self.searchComboBox.addItem(primary)
                index = self.searchComboBox.model().index(self.searchComboBox.count()-1, 0)
                self.searchComboBox.model().setData(index, secondary, role=QtCore.Qt.UserRole)
            # 选中新添加的项
            self.searchComboBox.setCurrentIndex(self.searchComboBox.findText(primary))

def main():
    qt_app = QtWidgets.QApplication(sys.argv)
    qt_app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())

    window = MainWindow()
    window.show()

    sys.exit(qt_app.exec_())

if __name__ == "__main__":
    main()

关键改动点解释

  1. CustomComboBox类(核心)

    • 通过focusInEventfocusOutEvent监听焦点变化,控制LineEdit的显示/隐藏,同时记录焦点状态
    • 重写paintEvent:无焦点时先绘制ComboBox框架,再复用委托的逻辑绘制primary/secondary文本,保证和下拉项样式完全统一;有焦点时交给父类处理,让LineEdit正常作为搜索框使用
    • 焦点进入时自动把当前选中项的primary文本填入LineEdit,方便用户直接搜索
  2. MainWindow中的调整

    • 把原QComboBox替换为CustomComboBox
    • 移除了原有的updateComboBoxLineEdit方法,因为无焦点时的显示逻辑已经交给CustomComboBox处理
    • currentIndexChanged信号绑定update(),确保选中新项时无焦点状态的绘制会同步更新
  3. 保留原委托逻辑
    你的ComboBoxItemDelegate完全保留,下拉项的样式和原来一致,保证整体风格统一

现在运行代码你会看到:

  • 无焦点时,ComboBox的LineEdit区域和选中的下拉项完全一样,primary在上、secondary在下且小一号灰色
  • 点击获得焦点后,LineEdit正常显示为可输入的搜索框,回车后弹出结果菜单
  • 选择菜单中的项后,失去焦点时又会变回和下拉项一致的样式

备注:内容来源于stack exchange,提问作者Andreas M.

火山引擎 最新活动