如何让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
相关产品推荐
相关产品推荐

