前言

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

实现过程

一个 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,以上~~

如何在pyqt中自定义SwitchButton的更多相关文章

  1. 如何在pyqt中自定义无边框窗口

    前言 之前写过很多关于无边框窗口并给窗口添加特效的博客,按照时间线罗列如下: 如何在pyqt中实现窗口磨砂效果 如何在pyqt中实现win10亚克力效果 如何在pyqt中通过调用SetWindowCo ...

  2. 如何在 pyqt 中自定义工具提示 ToolTip

    前言 Qt 自带的工具提示样式不太好看,就算加了样式表也时不时会失效,同时工具提示没有阴影,看起来就更难受了.所以本篇博客将会介绍自定义工具提示的方法,效果如下图所示: 实现过程 工具提示其实就是一个 ...

  3. 如何在pyqt中通过调用 SetWindowCompositionAttribute 实现Win10亚克力效果

    亚克力效果 在<如何在pyqt中实现窗口磨砂效果>和<如何在pyqt中实现win10亚克力效果>中,我们调用C++ dll来实现窗口效果,这种方法要求电脑上必须装有MSVC.V ...

  4. 如何在pyqt中在实现无边框窗口的同时保留Windows窗口动画效果(一)

    无边框窗体的实现思路 在pyqt中只要 self.setWindowFlags(Qt.FramelessWindowHint) 就可以实现边框的去除,但是没了标题栏也意味着窗口大小无法改变.窗口无法拖 ...

  5. 如何在pyqt中给无边框窗口添加DWM环绕阴影

    前言 在之前的博客<如何在pyqt中通过调用SetWindowCompositionAttribute实现Win10亚克力效果>中,我们实现了窗口的亚克力效果,同时也用SetWindowC ...

  6. 如何在 pyqt 中捕获并处理 Alt+F4 快捷键

    前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...

  7. 如何在pyqt中实现带动画的动态QMenu

    弹出菜单的视觉效果 QLineEdit 原生的菜单弹出效果十分生硬,而且样式很丑.所以照着Groove中单行输入框弹出菜单的样式和动画效果写了一个可以实现动态变化Item的弹出菜单,根据剪贴板的内容是 ...

  8. 6.1 如何在spring中自定义xml标签

    dubbo自定义了很多xml标签,例如<dubbo:application>,那么这些自定义标签是怎么与spring结合起来的呢?我们先看一个简单的例子. 一 编写模型类 package ...

  9. 如何在pyqt中实现窗口磨砂效果

    磨砂效果的实现思路 这两周一直在思考怎么在pyqt上实现窗口磨砂效果,网上搜了一圈,全都是 C++ 的实现方法.正好今天查python的官方文档的时候看到了 ctypes 里面的 HWND,想想倒不如 ...

随机推荐

  1. centos 各版本下载

    地址: go to http://vault.centos.org/ for packages.

  2. .NET Core 实现动态代理做AOP(面向切面编程)

    1.介绍 1.1 动态代理作用 用动态代理可以做AOP(面向切面编程),进行无入侵式实现自己的扩展业务,调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,比如:日志记录.性能统计.安全控制.事务 ...

  3. MySQL 尽量避免使用 TIMESTAMP

    MySQL 中常见的时间类型有三种DATE, DATETIME和 TIMESTAMP,其中DATE类型用于表示日期,但是不会包含时间,格式为YYYY-MM-DD,而DATETIME和TIMESTAMP ...

  4. [算法笔记-题解]问题 D: 习题4-4 三个整数求最大值

    问题 D: 习题4-4 三个整数求最大值 [命题人 : 外部导入] 时间限制 : 1.000 sec 内存限制 : 12 MB 题目描述 有3个整数a, b, c,由键盘输入,输出其中最大的数. 输入 ...

  5. G2022 次列车即将进站,前端人请抓紧时间上车

    前端发展百花放,一技未熟百技出.未知何处去下手,关注小编胜百书. 我是前端人,关注小编,分享更多前端相关知识! 平安夜一个人?圣诞一个人? 跨年还是一个人吗? 没事,关注小编,每个节日让小编陪你一起度 ...

  6. <数据结构>XDOJ332.二叉排序树的判定

    问题与解答 问题描述 给定一个二叉树,判断其是否是一个有效的二叉排序树. 假设一个二叉排序树具有如下特征: 结点的左子树只包含小于当前结点的树. 结点的右子树只包含大于当前结点的树. 所有左子树和右子 ...

  7. 【MySQL作业】外连接查询——美和易思外连接查询应用习题

    点击打开所使用到的数据库>>> 1.使用左接获取所有客户的基本信息以及订购信息,要求输出客户姓名.电话.订单 ID 和下单时间. 由于需要获取所有客户的基本信息,如果采用左连接加以实 ...

  8. 什么是NaN?它的类型是什么?如何可靠的测试一个值是否等于NaN?

    NaN属性表示"不是数字"的值.这个特殊值是由于一个操作数是非数字的(例如"abc"/4)或者因为操作的结果是非数字而无法执行的. 虽然看起来很简单,但是NaN ...

  9. lombok不支持enum类型

    今天在使用枚举时想着少写getter方法和构造方法,结果加上注解后说是只支持class类型 来自为知笔记(Wiz)

  10. linux -安装mysql,配置密码,开启远程访问

    1.安装 下载yum源的安装包 yum install https://repo.mysql.com//mysql80-community-release-el7-1.noarch.rpm 安装 yu ...