前言
网上有很多 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_())