前言

网上有很多 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. 【LeetCode】102. Binary Tree Level Order Traversal 二叉树的层序遍历 (Python&C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 BFS DFS 日期 题目地址:https://lee ...

  2. 【剑指Offer】连续子数组的最大和 解题报告(Python)

    [剑指Offer]连续子数组的最大和 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews ...

  3. 手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏01游戏窗口

    项目源码 项目源码 游戏配置信息类 Config.java 没什么解释的. package config; public class Config { public final static Stri ...

  4. spring事务失效的12种场景

    一 事务不生效 1.访问权限问题 java的访问权限主要有四种:private<default<protected<public. 把有某些事务方法,定义了错误的访问权限,就会导致事 ...

  5. rabbitmq集群和镜像队列

    Rabbitmq集群和镜像队列 1引言 1.1编写目的 2 原理和使用 2.1镜像队列原理 2.1.1 原理 默认的一个rabbitmq中的queue是在一个node上的,至于在那个node上取决于c ...

  6. vue路由history模式刷新404问题解决方案

    更改router 的base // biz是二级目录,路由文件改成 const router = new VueRouter({ mode: 'history', // base: process.e ...

  7. docker学习:dockefile解析

    是什么 DockerFile 是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本 构建三部曲 编写Dockerfile文件 docker build docker run 文件什么样 ...

  8. exit hook

    之前经常改 malloc_hook , realloc_hook,free_hook 为 one_gadget 来 get shell ,最近看到一种利用是改 exit hook(winmt师傅告诉我 ...

  9. PowerShell【变量篇】

    PS C:\Users\Administrator> $str='这是一个变量' PS C:\Users\Administrator> $str 这是一个变量 PS C:\Users\Ad ...

  10. spring security 继承 WebSecurityConfigurerAdapter 的重写方法configure() 参数 HttpSecurity 常用方法及说明

    HttpSecurity 常用方法及说明 方法 说明 openidLogin() 用于基于 OpenId 的验证 headers() 将安全标头添加到响应 cors() 配置跨域资源共享( CORS ...