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

PyQt5制作原生可折叠侧边栏:主题适配与选中边框问题问询

适配主题的PyQt5可折叠侧边栏问题及优化方案

问题描述

我正在用PyQt5制作原生风格的可展开折叠侧边栏,当前实现在WhiteSur KDE主题下运行正常,但切换到Breeze主题时遇到两个问题:

  • 设置侧边栏最小宽度时,直接用self.iconSize().width()会截断图标,在WhiteSur主题里加16能解决,但Breeze主题下这个值过宽,需要改成10左右,无法适配不同主题的内边距。
  • Breeze主题下侧边栏会显示蓝色选中边框,不符合需求。

现有实现代码

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtCore import pyqtProperty
from PyQt5.QtGui import *


class Sidebar(QListWidget):
    def __init__(self, buttons: list = None):
        super().__init__()
        
        font = QFont()
        font.setPointSize(12)
        self.setFont(font)
        self.setIconSize(QSize(35, 35))

        for button in buttons:
            self.addItem(button)
        
        self.maxWidth = self.sizeHintForColumn(0) + 10

        iconWidth = self.iconSize().width()
        self.minWidth = iconWidth + 16
        self.awidth = self.minWidth
        
        self.expandAnimation = QPropertyAnimation(self, b'awidth')
        self.expandAnimation.setEndValue(self.maxWidth)
        self.expandAnimation.setDuration(200)
        self.expandAnimation.setEasingCurve(QEasingCurve.InOutCubic)
        
        self.contractAnimation = QPropertyAnimation(self, b'awidth')
        self.contractAnimation.setEndValue(self.minWidth)
        self.contractAnimation.setDuration(200)
        self.contractAnimation.setEasingCurve(QEasingCurve.InOutCubic)

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setTextElideMode(Qt.TextElideMode.ElideNone)
        

    def leaveEvent(self, e) -> None:
        self.contractAnimation.start()
    
    def enterEvent(self, e) -> None:
        self.expandAnimation.start()


    @pyqtProperty(int)
    def awidth(self) -> int:
        return self._awidth
    
    @awidth.setter
    def awidth(self, value) -> None:
        self._awidth = value
        super().setFixedWidth(self._awidth)
    

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.buttonState = False
        self.setMinimumSize(500, 300)
        
        button1 = QListWidgetItem(QIcon.fromTheme('battery'), 'Button 1')
        button2 = QListWidgetItem(QIcon.fromTheme('computer'), 'Button 2')
        button3 = QListWidgetItem(QIcon.fromTheme('camera-web'), 'Button 3')
        self.sidebar = Sidebar([button1, button2, button3])
        self.sidebar.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Expanding)
        
        self.mainWidget = QPushButton()
        self.mainWidget.setText('0')
        font = QFont()
        font.setPointSize(20)
        font.setBold(True)
        self.mainWidget.setFont(font)
        self.mainWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.mainWidget.pressed.connect(self.buttonPressed)


        layout = QHBoxLayout()
        layout.addWidget(self.sidebar)
        layout.addWidget(self.mainWidget)
        centralWidget = QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)
        
        self.sidebar.itemSelectionChanged.connect(self.sidebarChanged)


    def sidebarChanged(self):
        selection = self.sidebar.selectedIndexes()[0].row()
        print(selection)
        
        self.mainWidget.setText(str(selection))
    

    def buttonPressed(self):
        if self.buttonState:
            self.sidebar.contractAnimation.start()
        else:
            self.sidebar.expandAnimation.start()
        
        self.buttonState = not self.buttonState
    

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

解决方案

一、获取适配主题的最小宽度

不要硬编码内边距数值,通过QStyle自动获取当前主题的布局参数:
在Sidebar的__init__方法中,替换原来的minWidth计算代码:

# 获取当前样式的布局参数
style = self.style()
# 获取列表项的左右内边距总和
itemPadding = style.pixelMetric(QStyle.PM_FocusFrameHMargin, None, self) * 2
# 最小宽度 = 图标宽度 + 左右内边距
self.minWidth = self.iconSize().width() + itemPadding

这种方式会自动适配不同主题的内边距设置,无需手动调整数值。

二、移除侧边栏选中边框

根据需求选择以下两种方法:

方法1:完全禁用选择功能

如果不需要选中逻辑,在Sidebar的__init__中添加:

self.setSelectionMode(QListWidget.NoSelection)

注意:这会导致itemSelectionChanged信号失效,可改用itemClicked信号替代点击事件。

方法2:保留点击交互但隐藏选中样式

如果需要保留点击功能但隐藏选中边框,添加样式表:

self.setStyleSheet("""
QListWidget::item:selected {
    background-color: transparent;
    border: none;
}
QListWidget::item:focus {
    outline: none;
}
""")

三、更优的原生风格侧边栏实现方案

方案1:使用QToolBar

QToolBar本身支持原生主题适配,自带可折叠、浮动特性,适合快速实现原生风格侧边栏:

class Sidebar(QToolBar):
    def __init__(self):
        super().__init__()
        self.setMovable(False)
        self.setIconSize(QSize(35, 35))
        
        # 添加侧边项
        self.addAction(QIcon.fromTheme('battery'), 'Button 1')
        self.addAction(QIcon.fromTheme('computer'), 'Button 2')
        self.addAction(QIcon.fromTheme('camera-web'), 'Button 3')
        
        # 计算适配主题的最小宽度
        style = self.style()
        itemPadding = style.pixelMetric(QStyle.PM_FocusFrameHMargin, None, self) * 2
        self.minWidth = self.iconSize().width() + itemPadding
        
        # 复用原有的展开折叠动画逻辑
        self.maxWidth = self.sizeHint().width() + 10
        self.setFixedWidth(self.minWidth)
        
        self.expandAnimation = QPropertyAnimation(self, b'fixedWidth')
        self.expandAnimation.setEndValue(self.maxWidth)
        self.expandAnimation.setDuration(200)
        self.expandAnimation.setEasingCurve(QEasingCurve.InOutCubic)
        
        self.contractAnimation = QPropertyAnimation(self, b'fixedWidth')
        self.contractAnimation.setEndValue(self.minWidth)
        self.contractAnimation.setDuration(200)
        self.contractAnimation.setEasingCurve(QEasingCurve.InOutCubic)

    def leaveEvent(self, e) -> None:
        self.contractAnimation.start()
    
    def enterEvent(self, e) -> None:
        self.expandAnimation.start()

QToolBar无需额外调整边框和内边距,完全适配系统主题。

方案2:自定义QWidget容器

如果需要高度定制,用QWidget作为容器,内部放置自定义按钮组件,通过布局管理:

class Sidebar(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0,0,0,0)
        
        # 添加自定义按钮
        btn1 = QPushButton(QIcon.fromTheme('battery'), 'Button 1')
        btn1.setIconSize(QSize(35,35))
        btn1.setStyleSheet("text-align: left; padding: 8px;")
        layout.addWidget(btn1)
        
        btn2 = QPushButton(QIcon.fromTheme('computer'), 'Button 2')
        btn2.setIconSize(QSize(35,35))
        btn2.setStyleSheet("text-align: left; padding: 8px;")
        layout.addWidget(btn2)
        
        # 复用动画逻辑
        self.minWidth = btn1.iconSize().width() + 16
        self.maxWidth = self.sizeHint().width() + 10
        self.setFixedWidth(self.minWidth)
        
        self.expandAnimation = QPropertyAnimation(self, b'fixedWidth')
        self.expandAnimation.setEndValue(self.maxWidth)
        self.expandAnimation.setDuration(200)
        self.expandAnimation.setEasingCurve(QEasingCurve.InOutCubic)
        
        self.contractAnimation = QPropertyAnimation(self, b'fixedWidth')
        self.contractAnimation.setEndValue(self.minWidth)
        self.contractAnimation.setDuration(200)
        self.contractAnimation.setEasingCurve(QEasingCurve.InOutCubic)

    def leaveEvent(self, e) -> None:
        self.contractAnimation.start()
    
    def enterEvent(self, e) -> None:
        self.expandAnimation.start()

这种方式可以完全控制每个元素的样式和交互逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 03:39:21