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

如何让QLineEdit的Action在鼠标悬停时变色并切换光标?

解决QLineEdit Action悬停变色与光标切换的最优方案

针对你的需求,这里提供两种实用方案,不需要复杂的事件重写,兼顾简洁性和效果:

方案一:用QToolButton替代Action(推荐,样式更易控制)

QLineEdit自带的Action样式控制有限,换成嵌入QToolButton到输入框尾部,能直接用样式表实现hover变色,还能一键设置光标样式:

class ModernPasswordField(ModernField):
    def __init__(self):
        super().__init__('Password')
        self.entry_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)

        self.setup_leading_icon()
        self.setup_trailing_buttons()

    def setup_leading_icon(self):
        # 不可点击的前置图标,用禁用的ToolButton实现
        lock_btn = QtWidgets.QToolButton(self.entry_field)
        lock_btn.setIcon(QtGui.QIcon('resources/test/lock.svg'))
        lock_btn.setDisabled(True)
        lock_btn.setStyleSheet("QToolButton:disabled { border: none; background: transparent; }")
        # 调整输入框左边距,让图标显示出来
        self.entry_field.setTextMargins(lock_btn.width() + 8, 0, 0, 0)
        # 居中放置图标
        lock_btn.move(8, (self.entry_field.height() - lock_btn.height())//2)

    def setup_trailing_buttons(self):
        # 通用按钮样式:hover时用滤镜变色,无背景边框
        btn_style = """
            QToolButton {
                border: none;
                background: transparent;
                padding: 4px;
            }
            QToolButton:hover {
                filter: invert(20%) sepia(70%) saturate(300%) hue-rotate(180deg);
            }
        """

        # 复制按钮
        copy_btn = QtWidgets.QToolButton(self.entry_field)
        copy_btn.setIcon(QtGui.QIcon('resources/test/copy.svg'))
        copy_btn.setCursor(QtCore.Qt.PointingHandCursor)
        copy_btn.setStyleSheet(btn_style)
        copy_btn.clicked.connect(self.on_copy_clicked)

        # 显示/隐藏密码按钮
        hidden_btn = QtWidgets.QToolButton(self.entry_field)
        hidden_btn.setIcon(QtGui.QIcon('resources/used/invisible_eye_white.svg'))
        hidden_btn.setCursor(QtCore.Qt.PointingHandCursor)
        hidden_btn.setStyleSheet(btn_style)
        hidden_btn.clicked.connect(self.on_hidden_clicked)

        # 生成密码按钮
        generate_btn = QtWidgets.QToolButton(self.entry_field)
        generate_btn.setIcon(QtGui.QIcon('resources/test/refresh.svg'))
        generate_btn.setCursor(QtCore.Qt.PointingHandCursor)
        generate_btn.setStyleSheet(btn_style)
        generate_btn.clicked.connect(self.on_generate_clicked)

        # 计算尾部按钮总宽度,调整输入框右边距
        btn_width = copy_btn.width()
        spacing = 4
        total_trailing_width = btn_width * 3 + spacing * 2
        self.entry_field.setTextMargins(
            self.entry_field.textMargins().left(),
            0,
            total_trailing_width + 8,
            0
        )

        # 初始定位按钮(从右往左排列)
        self._position_trailing_buttons(copy_btn, hidden_btn, generate_btn, btn_width, spacing)
        # 监听输入框大小变化,实时调整按钮位置
        self.entry_field.installEventFilter(self)

    def _position_trailing_buttons(self, copy_btn, hidden_btn, generate_btn, btn_width, spacing):
        """统一设置尾部按钮的位置"""
        right_offset = 8
        generate_btn.move(
            self.entry_field.width() - right_offset - btn_width,
            (self.entry_field.height() - generate_btn.height())//2
        )
        hidden_btn.move(
            self.entry_field.width() - right_offset - btn_width*2 - spacing,
            (self.entry_field.height() - hidden_btn.height())//2
        )
        copy_btn.move(
            self.entry_field.width() - right_offset - btn_width*3 - spacing*2,
            (self.entry_field.height() - copy_btn.height())//2
        )

    def eventFilter(self, obj, event):
        if obj == self.entry_field and event.type() == QtCore.QEvent.Resize:
            # 输入框大小变化时重新定位按钮
            btn_width = self.findChild(QtWidgets.QToolButton).width()
            spacing = 4
            copy_btn = [btn for btn in self.entry_field.children() if btn.icon().name() == 'copy.svg'][0]
            hidden_btn = [btn for btn in self.entry_field.children() if btn.icon().name() == 'invisible_eye_white.svg'][0]
            generate_btn = [btn for btn in self.entry_field.children() if btn.icon().name() == 'refresh.svg'][0]
            self._position_trailing_buttons(copy_btn, hidden_btn, generate_btn, btn_width, spacing)
        return super().eventFilter(obj, event)

    def on_copy_clicked(self):
        QtWidgets.QApplication.clipboard().setText(self.entry_field.text())

    def on_hidden_clicked(self):
        btn = self.sender()
        if self.entry_field.echoMode() == QtWidgets.QLineEdit.Password:
            self.entry_field.setEchoMode(QtWidgets.QLineEdit.Normal)
            btn.setIcon(QtGui.QIcon('resources/used/visible_eye.svg'))
        else:
            self.entry_field.setEchoMode(QtWidgets.QLineEdit.Password)
            btn.setIcon(QtGui.QIcon('resources/used/invisible_eye_white.svg'))

    def on_generate_clicked(self):
        # 这里写生成密码的逻辑
        pass

方案二:给QIcon添加多状态(保留原Action结构)

如果不想替换成按钮,可以给QIcon添加Normal(默认)和Active(hover)状态的图标,再通过事件过滤控制光标:

def svg_to_colored_pixmap(svg_path, hex_color):
    """将SVG转换为指定颜色的QPixmap"""
    with open(svg_path, 'r') as f:
        svg_content = f.read()
    # 替换SVG中的白色(假设原SVG用白色)为目标颜色,可根据实际情况调整
    svg_content = svg_content.replace('#ffffff', hex_color)
    
    renderer = QtGui.QSvgRenderer(QtCore.QByteArray(svg_content.encode()))
    pixmap = QtGui.QPixmap(renderer.defaultSize())
    pixmap.fill(QtCore.Qt.transparent)
    painter = QtGui.QPainter(pixmap)
    renderer.render(painter)
    painter.end()
    return pixmap

class ModernPasswordField(ModernField):
    def __init__(self):
        super().__init__('Password')
        self.entry_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)

        # 不可点击的前置图标
        lock_icon = QtGui.QIcon('resources/test/lock.svg')
        self.entry_field.addAction(lock_icon, QtWidgets.QLineEdit.LeadingPosition)

        # 初始化带hover状态的后置Action
        self.setup_trailing_actions()
        # 安装事件过滤器处理光标切换
        self.entry_field.installEventFilter(self)

    def setup_trailing_actions(self):
        # 复制Action:默认灰色,hover蓝色
        copy_normal = svg_to_colored_pixmap('resources/test/copy.svg', '#888888')
        copy_hover = svg_to_colored_pixmap('resources/test/copy.svg', '#2196F3')
        copy_icon = QtGui.QIcon()
        copy_icon.addPixmap(copy_normal, QtGui.QIcon.Normal)
        copy_icon.addPixmap(copy_hover, QtGui.QIcon.Active)
        self.copy_action = self.entry_field.addAction(copy_icon, QtWidgets.QLineEdit.TrailingPosition)
        self.copy_action.triggered.connect(self.on_copy_clicked)

        # 显示/隐藏密码Action
        hidden_normal = svg_to_colored_pixmap('resources/used/invisible_eye_white.svg', '#888888')
        hidden_hover = svg_to_colored_pixmap('resources/used/invisible_eye_white.svg', '#2196F3')
        hidden_icon = QtGui.QIcon()
        hidden_icon.addPixmap(hidden_normal, QtGui.QIcon.Normal)
        hidden_icon.addPixmap(hidden_hover, QtGui.QIcon.Active)
        self.hidden_action = self.entry_field.addAction(hidden_icon, QtWidgets.QLineEdit.TrailingPosition)
        self.hidden_action.triggered.connect(self.on_hidden_clicked)

        # 生成密码Action
        generate_normal = svg_to_colored_pixmap('resources/test/refresh.svg', '#888888')
        generate_hover = svg_to_colored_pixmap('resources/test/refresh.svg', '#2196F3')
        generate_icon = QtGui.QIcon()
        generate_icon.addPixmap(generate_normal, QtGui.QIcon.Normal)
        generate_icon.addPixmap(generate_hover, QtGui.QIcon.Active)
        self.generate_action = self.entry_field.addAction(generate_icon, QtWidgets.QLineEdit.TrailingPosition)
        self.generate_action.triggered.connect(self.on_generate_clicked)

    def eventFilter(self, obj, event):
        if obj == self.entry_field:
            if event.type() == QtCore.QEvent.MouseMove:
                # 检查鼠标是否在某个可点击Action的区域内
                pos = event.pos()
                clickable_actions = [self.copy_action, self.hidden_action, self.generate_action]
                for action in clickable_actions:
                    if self.entry_field.actionGeometry(action).contains(pos):
                        self.entry_field.setCursor(QtCore.Qt.PointingHandCursor)
                        break
                else:
                    self.entry_field.setCursor(QtCore.Qt.IBeamCursor)
            elif event.type() == QtCore.QEvent.Leave:
                self.entry_field.setCursor(QtCore.Qt.IBeamCursor)
        return super().eventFilter(obj, event)

    def on_copy_clicked(self):
        QtWidgets.QApplication.clipboard().setText(self.entry_field.text())

    def on_hidden_clicked(self):
        if self.entry_field.echoMode() == QtWidgets.QLineEdit.Password:
            self.entry_field.setEchoMode(QtWidgets.QLineEdit.Normal)
            # 切换为可见图标(带hover状态)
            visible_normal = svg_to_colored_pixmap('resources/used/visible_eye.svg', '#888888')
            visible_hover = svg_to_colored_pixmap('resources/used/visible_eye.svg', '#2196F3')
            visible_icon = QtGui.QIcon()
            visible_icon.addPixmap(visible_normal, QtGui.QIcon.Normal)
            visible_icon.addPixmap(visible_hover, QtGui.QIcon.Active)
            self.hidden_action.setIcon(visible_icon)
        else:
            self.entry_field.setEchoMode(QtWidgets.QLineEdit.Password)
            # 切换回不可见图标
            hidden_normal = svg_to_colored_pixmap('resources/used/invisible_eye_white.svg', '#888888')
            hidden_hover = svg_to_colored_pixmap('resources/used/invisible_eye_white.svg', '#2196F3')
            hidden_icon = QtGui.QIcon()
            hidden_icon.addPixmap(hidden_normal, QtGui.QIcon.Normal)
            hidden_icon.addPixmap(hidden_hover, QtGui.QIcon.Active)
            self.hidden_action.setIcon(hidden_icon)

    def on_generate_clicked(self):
        # 生成密码逻辑
        pass

方案对比

  • 方案一:样式控制自由度高,hover效果和光标设置无需额外代码,适合需要精细样式定制的场景;唯一需要处理的是按钮位置随输入框大小变化的问题。
  • 方案二:保留了QLineEdit原生Action的结构,不需要额外添加控件,但需要手动处理SVG颜色转换,光标切换依赖事件过滤。

内容的提问来源于stack exchange,提问作者Rikjimue

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 16:05:55