PYQT5 自定义SwitchButton

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

请添加图片描述

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

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

hover
pressed
checked
而 QToolButton 正好具有这些伪状态,所以我们通过继承 QToolButton 并重写 paintEvent 来实现 Indicator。从动图中可以看到,指示器的里面滑块应该随着指示器 checked 状态的改变而改变其位置和颜色。改变位置可以使用 QTimer 来实现,而改变其颜色可以通过定义 sliderOnColor、sliderOffColor 和 sliderDisabledColor 这几个 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 的实现较为简单,只要将 Indicator 和 QLabel 添加到水平布局中即可。不过为了控制 Indicator 和 QLabel 之间的间隔,我们定义一个属性 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_())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值