前言

网上有很多 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. 【环境搭建】安装pyQt5 在pycharm报This application failed to start because no Qt platform plugin could be initialized的问题

    报错:This application failed to start because no Qt platform plugin could be initialized 解决办法: http:// ...

  2. ROC and AUC

    目录 概 TPR, FPR ROC and AUC 代码 ROC-wiki 概 AUC常常在文章中作为评价一个分类器优劣的指标, 却总是忘记其原由, 索性记上一笔. TPR, FPR 首先理解TP, ...

  3. Mysql数据库服务端的安装

    一般提到Mysql数据库的安装在工作当中是说的安装数据库管理软件的服务端,服务端的安装可以安装在Windows环境,也可以安装在Linux环境. Windows环境安装:目前安装比较流行的是5.7,增 ...

  4. 自动化集成:Pipeline整合Docker容器

    前言:该系列文章,围绕持续集成:Jenkins+Docker+K8S相关组件,实现自动化管理源码编译.打包.镜像构建.部署等操作:本篇文章主要描述流水线集成Docker用法. 一.背景描述 微服务架构 ...

  5. 编写Java程序,以继承和多态思想模拟饲养员喂养不同动物的不同行为

    返回本章节 返回作业目录 需求说明: 以继承和多态思想模拟饲养员喂养不同动物的不同行为 动物园有饲养员和动物,其中动物有老虎.马.猴子.羊.狼等. 饲养员对不同的动物有不同的喂养行为. 实现思路: 以 ...

  6. 查看电脑内存是ddr3还是ddr4

    内存不够用了 要加个内存 但是不想拆机 怎么知道自己电脑是第几代内存呢? 怎么知道频率呢? 1.运行cmd 2.输入wmic回车 3.输入memorychip回车 4.往右拉找到Speed Statu ...

  7. .net core中Grpc使用报错:Request protocol 'HTTP/1.1' is not supported.

    显然这个报错是说HTTP/1.1不支持. 首先,我们要知道,Grpc是Google开源的,跨语言的,高性能的远程过程调用框架,它是以HTTP/2作为通信协议的,所以当我启动启用一个服务作为Grpc的服 ...

  8. CSS基础 背景图片的相关属性

    属性名: background-size: 宽度 高度; 属性值 说明 数字+px 简单方便,常用 百分比 相当于盒子自身的百分比,如:百分百,就是就算是图片变形也要显示 contain 动比例缩放, ...

  9. CSS 基础 背景相关属性操作

    1.background-color:red : //设置背景颜色为红色,rgb(0,0,0)和transparent 均为透明颜色 2.background-image(可缩bgi写用tab键) 语 ...

  10. Linux根目录缺少x权限,产生的两个错误

    错误一:root用户执行systemctl命令报误 [root@node1 ~]# systemctl restart sshd * (pkttyagent:10364): WARNING *: Un ...