正在加载中...

如何在pyqt中自定义SwitchButton

前言

网上有很多 SwitchButton 的实现方式,大部分是通过重写 paintEvent() 来实现的,感觉灵活性不是很好。所以希望实现一个可以联合使用 qss 来更换样式的 SwitchButton。仿照 Fluent Design 中样式,最终实现效果如下(动图中没有展示按钮禁用时的样式):

SwitchButton

实现过程

一个 SwitchButton 可以拆分为左边的指示器 Indicator 和右边的标签 label,由一个 QHBoxLayout 组织起来。由于 Indicator 比较复杂,所以先来实现 Indicator

指示器的实现

为了充分利用 qss,一个指示器应该具有以下几种基本的伪状态:

  • hover
  • pressed
  • checked

QToolButton 正好具有这些伪状态,所以我们通过继承 QToolButton 并重写 paintEvent 来实现 Indicator。从动图中可以看到,指示器的里面滑块应该随着指示器 checked 状态的改变而改变其位置和颜色。改变位置可以使用 QTimer 来实现,而改变其颜色可以通过定义 sliderOnColorsliderOffColorsliderDisabledColor 这几个 pyqtProperty 并搭配 qss 来实现。下面是代码:

class Indicator(QToolButton):
    """ 指示器 """

    checkedChanged = pyqtSignal(bool)

    def __init__(self, parent):
        super().__init__(parent=parent)
        self.setCheckable(True)
        super().setChecked(False)
        self.resize(50, 26)
        self.__sliderOnColor = QColor(Qt.white)
        self.__sliderOffColor = QColor(Qt.black)
        self.__sliderDisabledColor = QColor(QColor(155, 154, 153))
        self.timer = QTimer(self)
        self.padding = self.height()//4
        self.sliderX = self.padding
        self.sliderRadius = (self.height()-2*self.padding)//2
        self.sliderEndX = self.width()-2*self.sliderRadius
        self.sliderStep = self.width()/50
        self.timer.timeout.connect(self.__updateSliderPos)

    def __updateSliderPos(self):
        """ 更新滑块位置 """
        if self.isChecked():
            if self.sliderX+self.sliderStep < self.sliderEndX:
                self.sliderX += self.sliderStep
            else:
                self.sliderX = self.sliderEndX
                self.timer.stop()
        else:
            if self.sliderX-self.sliderStep > self.sliderEndX:
                self.sliderX -= self.sliderStep
            else:
                self.sliderX = self.sliderEndX
                self.timer.stop()

        self.style().polish(self)

    def setChecked(self, isChecked: bool):
        """ 设置选中状态 """
        if isChecked == self.isChecked():
            return
        super().setChecked(isChecked)
        self.sliderEndX = self.width()-2*self.sliderRadius - \
            self.padding if self.isChecked() else self.padding
        self.timer.start(5)

    def mouseReleaseEvent(self, e):
        """ 鼠标点击更新选中状态 """
        super().mouseReleaseEvent(e)
        self.sliderEndX = self.width()-2*self.sliderRadius - \
            self.padding if self.isChecked() else self.padding
        self.timer.start(5)
        self.checkedChanged.emit(self.isChecked())

    def resizeEvent(self, e):
        self.padding = self.height()//4
        self.sliderRadius = (self.height()-2*self.padding)//2
        self.sliderStep = self.width()/50
        self.sliderEndX = self.width()-2*self.sliderRadius - \
            self.padding if self.isChecked() else self.padding
        self.update()

    def paintEvent(self, e):
        """ 绘制指示器 """
        super().paintEvent(e)  # 背景和边框由 qss 指定
        painter = QPainter(self)
        painter.setRenderHints(QPainter.Antialiasing)
        painter.setPen(Qt.NoPen)
        if self.isEnabled():
            color = self.sliderOnColor if self.isChecked() else self.sliderOffColor
        else:
            color = self.sliderDisabledColor
        painter.setBrush(color)
        painter.drawEllipse(self.sliderX, self.padding,
                            self.sliderRadius*2, self.sliderRadius*2)

    def getSliderOnColor(self):
        return self.__sliderOnColor

    def setSliderOnColor(self, color: QColor):
        self.__sliderOnColor = color
        self.update()

    def getSliderOffColor(self):
        return self.__sliderOffColor

    def setSliderOffColor(self, color: QColor):
        self.__sliderOffColor = color
        self.update()

    def getSliderDisabledColor(self):
        return self.__sliderDisabledColor

    def setSliderDisabledColor(self, color: QColor):
        self.__sliderDisabledColor = color
        self.update()

    sliderOnColor = pyqtProperty(QColor, getSliderOnColor, setSliderOnColor)
    sliderOffColor = pyqtProperty(QColor, getSliderOffColor, setSliderOffColor)
    sliderDisabledColor = pyqtProperty(
        QColor, getSliderDisabledColor, setSliderDisabledColor)

开关按钮的实现

SwitchButton 的实现较为简单,只要将 IndicatorQLabel 添加到水平布局中即可。不过为了控制 IndicatorQLabel 之间的间隔,我们定义一个属性 spacing,这样就可以搭配 qss 来使用。

class SwitchButton(QWidget):

    checkedChanged = pyqtSignal(bool)

    def __init__(self, text='关', parent=None):
        super().__init__(parent=parent)
        self.text = text
        self.__spacing = 15
        self.hBox = QHBoxLayout(self)
        self.indicator = Indicator(self)
        self.label = QLabel(text, self)
        self.__initWidget()

    def __initWidget(self):
        """ 初始化小部件 """
        # 设置布局
        self.hBox.addWidget(self.indicator)
        self.hBox.addWidget(self.label)
        self.hBox.setSpacing(self.__spacing)
        self.hBox.setAlignment(Qt.AlignLeft)
        self.setAttribute(Qt.WA_StyledBackground)
        self.hBox.setContentsMargins(0, 0, 0, 0)
        # 设置默认样式
        with open('resource/switch_button.qss', encoding='utf-8') as f:
            self.setStyleSheet(f.read())
        # 信号连接到槽
        self.indicator.checkedChanged.connect(self.checkedChanged)

    def isChecked(self):
        return self.indicator.isChecked()

    def setChecked(self, isChecked: bool):
        """ 设置选中状态 """
        self.indicator.setChecked(isChecked)

    def toggleChecked(self):
        """ 切换选中状态 """
        self.indicator.setChecked(not self.indicator.isChecked())

    def setText(self, text: str):
        self.text = text
        self.label.setText(text)
        self.adjustSize()

    def getSpacing(self):
        return self.__spacing

    def setSpacing(self, spacing: int):
        self.__spacing = spacing
        self.hBox.setSpacing(spacing)
        self.update()

    spacing = pyqtProperty(int, getSpacing, setSpacing)

样式表

动图中的样式由下面的样式表给出:

QWidget{
    background-color: white;
}

SwitchButton {
    qproperty-spacing: 15;
}

SwitchButton > QLabel {
    color: black;
    font: 18px 'Microsoft YaHei';
}


Indicator {
    height: 22px;
    width: 50px;
    qproperty-sliderOnColor: white;
    qproperty-sliderOffColor: black;
    qproperty-sliderDisabledColor: rgb(155, 154, 153);
    border-radius: 13px;
}


Indicator:!checked {
    background-color: transparent;
    border: 1px solid rgb(102, 102, 102);
}

Indicator:!checked:hover {
    border: 1px solid rgb(51, 51, 51);
    background-color: transparent;
}

Indicator:!checked:pressed {
    border: 1px solid rgb(0, 0, 0);
    background-color: rgb(153, 153, 153);
}

Indicator:checked {
    border: 1px solid rgb(0, 153, 188);
    background-color: rgb(0, 153, 188);
}

Indicator:checked:hover {
    border: 1px solid rgb(72, 210, 242);
    background-color: rgb(72, 210, 242);
}

Indicator:checked:pressed {
    border: 1px solid rgb(0, 107, 131);
    background-color: rgb(0, 107, 131);
}

Indicator:disabled{
    border: 1px solid rgb(194, 194, 191);
    background-color: rgb(194, 194, 191);
}

测试

下面是测试代码:

# coding:utf-8
import sys
from PyQt5.QtWidgets import QApplication, QWidget

from switch_button import SwitchButton


class Window(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.resize(200, 100)
        self.switchButton = SwitchButton(parent=self)
        self.switchButton.move(60, 30)
        self.switchButton.checkedChanged.connect(self.onCheckedChanged)
        with open('resource/switch_button.qss', encoding='utf-8') as f:
            self.setStyleSheet(f.read())

    def onCheckedChanged(self, isChecked: bool):
        """ 开关按钮选中状态改变的槽函数 """
        text = '开' if isChecked else '关'
        self.switchButton.setText(text)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec_())

后记

开关按钮的实现方式已经介绍完毕,更多关于 Fluent Design 的自定义小部件可以参见 PyQt-Fluent-Widgets,以上~~

posted @ 2021-09-07 11:09  之一Yo  阅读(1104)  评论(0编辑  收藏  举报