PyQt 实现炫酷锁屏效果源码分享
- 一、源码分享
-
- 1、效果展示
- 2、源码分享
-
- 2.1、lock_screen.py
- 2.2、main.py
- 2.3、相关包安装
- 2.4、完整工程下载
- 二、实现原理
-
- 1、概述
- 2、使用场景
- 3、如何创建和使用 QPainter
- 4、 QPainter 的核心功能详述
- 5、常见绘制操作示例
- 6、QPainter 支持的所有方法列表
- 7、高级用法和技巧
- 8、常见问题解答
- 9、总结
一、源码分享
1、效果展示

2、源码分享
2.1、lock_screen.py
import math
from PyQt6.QtCore import Qt, QTimer, QRectF, QRect, QEvent, QPointF
from PyQt6.QtGui import QPainter, QPaintEvent, QImage, QLinearGradient, QPen, QColor, QMouseEvent
from PyQt6.QtWidgets import QWidget
class LockScreen(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# 透明背景 + 双缓冲防拖影核心属性
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, False)
# 门位置
self.doorLeftX = 0
self.doorRightX = 0
self.ledOpacity = 1.0
# 基础常量
self.dialSize = 250
self.indictorSize = self.dialSize + 50
self.startAngle = 95
self.endAngle = 265
self.currentAngle = self.startAngle
self.rectDial = QRectF()
self.deltaDegrees = 0.0
self.recordAngle = 0.0
# 动画参数:左右门独立速度,右门更快抵消长距离
self.speedLeft = 7
self.speedRight = 8
self.ledStep = 0.02
self.ledForward = False
self.isAnimDoor = False
self.targetLeftX = 0
self.targetRightX = 0
# 防重复解锁标记
self._unlockTriggered = False
self._mousePressed = False
# 图片路径
self.imageDial = QImage("image/dial-frame.png")
self.imageLines = QImage("image/lines.png")
self.imageNotch = QImage("image/notch.png")
self.imageLed = QImage("image/led-dark.png")
# 线条缓存,减少频繁创建图片拖影
self._lineCache = None
self._cacheAngle = –1000
# 全局主定时器 10ms
self.mainTimer = QTimer(self)
self.mainTimer.setInterval(10)
self.mainTimer.timeout.connect(self._tick)
self.mainTimer.start()
self.setVisible(False)
self.installEventFilter(self)
def normalize_angle(self, angle: float):
return math.fmod(angle + 360.0, 360.0)
def clamp(self, val, min_v, max_v):
return max(min_v, min(val, max_v))
# 窗口大小改变响应,修复缩放门错位 + 保留关门动画
def resizeEvent(self, event):
super().resizeEvent(event)
w = self.width()
if w <= 0:
return
# 锁屏静止状态(无动画、未解锁):仅更新目标,不直接覆盖doorX
if self.isVisible() and not self._unlockTriggered and not self.isAnimDoor:
self.targetLeftX = –w / 2 + 30
self.targetRightX = w / 2 – self.indictorSize / 2
# 仅完全静止时同步当前门位置,不破坏动画
self.doorLeftX = self.targetLeftX
self.doorRightX = self.targetRightX
# 正在执行解锁动画,更新飞出目标坐标
elif self.isAnimDoor:
self.targetLeftX = –w – 10
self.targetRightX = w
self.update()
# 定时器每一帧更新动画
def _tick(self):
w = self.width()
h = self.height()
if w <= 0 or h <= 0:
return
# LED明暗呼吸
if self.ledForward:
self.ledOpacity += self.ledStep
if self.ledOpacity >= 1.0:
self.ledOpacity = 1.0
self.ledForward = False
else:
self.ledOpacity -= self.ledStep
if self.ledOpacity <= 0.0:
self.ledOpacity = 0.0
self.ledForward = True
self.update()
# 门平滑移动插值
if self.isAnimDoor:
changed = False
# 左门移动逻辑
if abs(self.doorLeftX – self.targetLeftX) > self.speedLeft:
if self.doorLeftX < self.targetLeftX:
self.doorLeftX += self.speedLeft
else:
self.doorLeftX -= self.speedLeft
changed = True
else:
self.doorLeftX = self.targetLeftX
# 右门移动逻辑(独立更高速度)
if abs(self.doorRightX – self.targetRightX) > self.speedRight:
if self.doorRightX < self.targetRightX:
self.doorRightX += self.speedRight
else:
self.doorRightX -= self.speedRight
changed = True
else:
self.doorRightX = self.targetRightX
# 门动画结束逻辑
if not changed:
self.isAnimDoor = False
if self.doorLeftX <= –w:
self.setVisible(False)
self.currentAngle = self.startAngle
self._unlockTriggered = False
self.update()
# 上锁解锁对外接口
def lockScreen(self, is_lock: bool):
w = self.width()
if w <= 0:
return
if is_lock:
self._unlockTriggered = False
self.setVisible(True)
self.raise_()
self.targetLeftX = –w / 2 + 30
self.targetRightX = w / 2 – self.indictorSize / 2
# 只设置目标,不强行覆盖当前门坐标,保留动画
self.isAnimDoor = True
else:
self.targetLeftX = –w – 10
self.targetRightX = w
self.isAnimDoor = True
def paintEvent(self, event: QPaintEvent):
painter = QPainter(self)
painter.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
w = self.width()
h = self.height()
if w <= 0 or h <= 0:
return
borderWidth = 6
# 初始化门默认位置
if self.doorLeftX == 0:
self.doorLeftX = –w – 10
if self.doorRightX == 0:
self.doorRightX = w
# 绘制左门
penDoor = QPen(QColor("#000000"), 6, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.MiterJoin)
painter.setPen(penDoor)
gradLeft = QLinearGradient(w / 2, 0, w / 2, h)
gradLeft.setColorAt(0, QColor("#34373F"))
gradLeft.setColorAt(1, QColor("#1D2026"))
painter.setBrush(gradLeft)
rectLeft = QRect(self.doorLeftX, borderWidth // 2, w, h – borderWidth)
painter.drawRoundedRect(rectLeft, 40, 40)
# 右侧外圈椭圆
gradRight = QLinearGradient(w / 2, 0, w / 2, h)
gradRight.setColorAt(0, QColor("#2e3037"))
gradRight.setColorAt(1, QColor("#1D2026"))
rectRoundBorder = QRect(int(self.doorRightX + 15), int((h – (self.dialSize + 20)) / 2), self.dialSize + 20, self.dialSize + 20)
painter.setBrush(gradRight)
painter.drawEllipse(rectRoundBorder)
# 绘制右门
rectRight = QRect(int(self.doorRightX + self.indictorSize / 2), borderWidth // 2, w, h – borderWidth)
painter.setBrush(gradLeft)
painter.drawRoundedRect(rectRight, 40, 40)
# 蓝色弧形指示线
rectIndicator = QRectF(self.doorRightX, (h – self.indictorSize) / 2, self.indictorSize, self.indictorSize)
penBlue = QPen(QColor("#76AAF5"), 4, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.MiterJoin)
painter.setPen(penBlue)
arcStart = –self.startAngle * 16
arcSpan = –(self.currentAngle – self.startAngle) * 16
painter.drawArc(rectIndicator, arcStart, arcSpan)
# 中间竖遮罩
painter.save()
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(gradRight)
maskRect = QRect(int(rectRoundBorder.center().x() – 10), rectRoundBorder.y() + 3, 20, rectRoundBorder.height() – 6)
painter.drawRoundedRect(maskRect, 5, 5)
painter.restore()
# 转盘底座,图片缺失绘制圆形兜底
self.rectDial = QRectF(self.doorRightX + 25, (h – self.dialSize) / 2, self.dialSize, self.dialSize)
if self.imageDial.isNull():
painter.setBrush(QColor("#252830"))
painter.drawEllipse(self.rectDial)
else:
painter.drawImage(self.rectDial, self.imageDial)
# 旋转指示线条(缓存复用,消除拖影)
painter.save()
sizeLine = int(self.dialSize * 1.1)
painter.setOpacity(0.3)
rectLine = QRect(int(self.doorRightX + (self.indictorSize – sizeLine) / 2), int((h – sizeLine) / 2), sizeLine, sizeLine)
lineW = self.imageLines.width()
lineH = self.imageLines.height()
imgW = math.hypot(lineW, lineH)
currentRotate = self.currentAngle – self.startAngle
if self._lineCache is None or not math.isclose(self._cacheAngle, currentRotate, abs_tol=0.1):
self._lineCache = QImage(int(imgW), int(imgW), QImage.Format.Format_ARGB32_Premultiplied)
self._lineCache.fill(Qt.GlobalColor.transparent)
pCache = QPainter(self._lineCache)
pCache.translate(imgW / 2, imgW / 2)
pCache.rotate(currentRotate)
if not self.imageLines.isNull():
pCache.drawImage(QRect(–lineW // 2, –lineH // 2, lineW, lineH), self.imageLines)
pCache.end()
self._cacheAngle = currentRotate
painter.drawImage(rectLine, self._lineCache)
painter.setOpacity(1.0)
painter.restore()
# 缺口图片
centerX = self.rectDial.x() + self.rectDial.width() / 2
centerY = self.rectDial.y() + self.rectDial.height() / 2
circleR = self.rectDial.width() / 2 – 50
rad = math.radians(self.currentAngle)
notchX = centerX + circleR * math.cos(rad) – 32
notchY = centerY + circleR * math.sin(rad) – 32
rectNotch = QRectF(notchX, notchY, 64, 64)
if not self.imageNotch.isNull():
painter.drawImage(rectNotch, self.imageNotch)
# LED呼吸灯
if math.isclose(self.currentAngle, self.startAngle, abs_tol=0.1):
ledX = rectIndicator.x() + rectIndicator.width() / 2 – 30
ledY = rectIndicator.y() + rectIndicator.height() – 20
rectLed = QRect(int(ledX), int(ledY), self.imageLed.width(), self.imageLed.height())
painter.setOpacity(self.ledOpacity)
if not self.imageLed.isNull():
painter.drawImage(rectLed, self.imageLed)
# 鼠标事件过滤 修复坐标、实时旋转无延迟
def eventFilter(self, obj, event):
if not self._unlockTriggered:
if event.type() == QEvent.Type.MouseButtonPress:
if isinstance(event, QMouseEvent):
mouse_evt = event
if mouse_evt.button() == Qt.MouseButton.LeftButton:
pos = mouse_evt.pos()
pt = QPointF(pos)
if self.rectDial.contains(pt):
self._mousePressed = True
# 按下瞬间锁定角度基准
cx = self.rectDial.center().x()
cy = self.rectDial.center().y()
dx = pt.x() – cx
dy = pt.y() – cy
angleRad = math.atan2(dy, dx)
newAngle = math.degrees(angleRad)
newAngle = self.normalize_angle(newAngle)
self.deltaDegrees = newAngle
self.recordAngle = self.currentAngle
elif event.type() == QEvent.Type.MouseButtonRelease:
if isinstance(event, QMouseEvent):
mouse_evt = event
if mouse_evt.button() == Qt.MouseButton.LeftButton:
self._mousePressed = False
elif event.type() == QEvent.Type.MouseMove:
if self._mousePressed and isinstance(event, QMouseEvent):
mouse_evt = event
pos = mouse_evt.pos()
pt = QPointF(pos)
cx = self.rectDial.center().x()
cy = self.rectDial.center().y()
dx = pt.x() – cx
dy = pt.y() – cy
angleRad = math.atan2(dy, dx)
newAngle = math.degrees(angleRad)
newAngle = self.normalize_angle(newAngle)
# 实时更新旋转角度
self.currentAngle = self.clamp(
self.recordAngle + (newAngle – self.deltaDegrees),
self.startAngle,
self.endAngle
)
self.update()
# 拖拽到终点仅触发一次解锁
if self.currentAngle >= self.endAngle and not self._unlockTriggered:
self._unlockTriggered = True
self.lockScreen(False)
return super().eventFilter(obj, event)
2.2、main.py
import sys
from PyQt6.QtWidgets import QApplication
from PyQt6.QtWidgets import QMainWindow, QPushButton, QWidget, QVBoxLayout
from PyQt6.QtCore import Qt
from lock_screen import LockScreen
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("透明锁屏窗口")
self.resize(900, 700)
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.btnLock = QPushButton("点击上锁")
self.btnLock.setFixedSize(200,80)
layout.addWidget(self.btnLock)
self.lockScreen = LockScreen(self)
self.resizeEvent(None)
self.btnLock.clicked.connect(self.onBtnClick)
def onBtnClick(self):
self.lockScreen.lockScreen(True)
def keyPressEvent(self, e):
if e.key() == Qt.Key.Key_F1:
self.lockScreen.lockScreen(False)
super().keyPressEvent(e)
def resizeEvent(self, event):
self.lockScreen.setGeometry(self.rect())
super().resizeEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
2.3、相关包安装

2.4、完整工程下载
博客顶部下载
二、实现原理
主要通过QPainter绘制来完成。
1、概述
QPainter 是 PyQt6 中的一个核心图形绘制类,属于 PyQt6 的绘图模块。它提供了在绘图设备(如窗口部件、图像等)上绘制各种图形元素的功能。QPainter 支持绘制点、线、形状、文本和位图等,并通过使用画笔(pen)、画刷(brush)和变换(transformations)来控制绘制样式。理解 QPainter 对于开发图形用户界面(GUI)应用程序至关重要,尤其在实现自定义绘图效果时。
2、使用场景
QPainter 通常用于:
- 在 QWidget 的 paintEvent() 方法中进行自定义绘制。
- 操作 QImage 对象以绘制图像。
- 在打印设备上生成文档输出。
以下内容将逐步介绍 QPainter 的用法、功能以及所有支持的方法。
3、如何创建和使用 QPainter
在使用 QPainter 之前,需要将其绑定到一个绘图设备(如 QWidget 或 QImage)。通常,需要在对象的 paintEvent() 方法中进行绘制。下面是一个基本示例:
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QPainter, QPen, QColor
from PyQt6.QtCore import Qt
class CustomWidget(QWidget):
def paintEvent(self, event):
painter = QPainter(self) # 绑定到当前widget
painter.setPen(QPen(Qt.GlobalColor.red, 2)) # 设置画笔样式
painter.drawLine(10, 10, 100, 100) # 绘制一条线
app = QApplication([])
window = CustomWidget()
window.show()
app.exec()
在这个例子中:
4、 QPainter 的核心功能详述
QPainter 通过多种状态和配置来控制绘制效果。以下是关键概念的解释:
绘图设备(Paint Device) QPainter 必须绑定到一个支持绘制的设备上,比如 QWidget、QImage 或 QPixmap。在绘制前使用 begin(device) 方法启动,结束后使用 end() 方法关闭。
画笔(Pen) 画笔用于绘制轮廓线条,如线条颜色、宽度和样式。使用 QPen 类设置:
- 颜色:例如 QColor("blue")。
- 宽度:整数,表示像素宽度。
- 样式:如 Qt.PenStyle.SolidLine。
画刷(Brush) 画刷用于填充形状的内部区域,使用 QBrush 类设置:
- 填充模式:如 Qt.BrushStyle.SolidPattern。
- 颜色:可以是预定义颜色或自定义颜色。
变换(Transformations) QPainter 支持坐标系变换,如旋转、缩放和平移:
- translate():移动坐标系。
- rotate():旋转图形。
- scale():缩放尺寸。
- 保存和恢复状态使用 save() 和 restore()。
坐标系 默认坐标系的原点在设备的左上角。可以通过变换操作改变坐标位置。例如,平移后绘制:
- painter.translate(50, 50) 移动坐标起点。
- 后续绘制操作相对于新原点进行。
抗锯齿(Antialiasing) 为提高绘图质量,可以启用抗锯齿:
- painter.setRenderHint(QPainter.RenderHint.Antialiasing)。
- 这会减少锯齿效果,使线条更平滑。
绘制路径(QPainterPath) 复杂形状可以用 QPainterPath 定义,然后调用 drawPath() 渲染。
5、常见绘制操作示例
以下列出一些基本绘制操作的例子:
| 绘制矩形 | painter.drawRect(10, 10, 100, 100) |
| 绘制椭圆 | painter.drawEllipse(50, 50, 100, 50) |
| 绘制文本 | painter.drawText(20, 20, "Hello, PyQt6!") |
| 绘制点 | painter.drawPoint(10, 10) |
| 绘制直线 | painter.drawLine(0, 0, 200, 200) |
在代码中操作:
# 假设在 paintEvent 中
def paintEvent(self, event):
painter = QPainter(self)
# 设置画笔以绘制红色线
painter.setPen(QPen(QColor("red")))
painter.drawRect(10, 10, 200, 100) # 绘制矩形
# 绘制文本
painter.setPen(QPen(QColor("blue")))
painter.drawText(30, 70, "Example Text")
6、QPainter 支持的所有方法列表
QPainter 提供了丰富的方法集来支持各种绘图操作。以下是所有公开方法的表格,基于 PyQt6 的标准文档(注意:检查最新文档以确保完整性)。
| begin() | 在指定的设备上开始绘制。必须在使用其他方法前调用。 |
| end() | 结束绘制过程,释放资源。 |
| setPen(QPen) | 设置画笔用于绘制轮廓。参数为 QPen 对象。 |
| setBrush(QBrush) | 设置画刷用于填充形状。参数为 QBrush 对象。 |
| setFont(QFont) | 设置文本绘制时的字体。参数为 QFont 对象。 |
| setTransform(QTransform) | 设置坐标变换。参数为 QTransform 对象。 |
| resetTransform() | 重置所有坐标变换到默认状态。 |
| translate(qreal, qreal) | 平移坐标系到指定偏移量。 |
| scale(qreal, qreal) | 缩放坐标系。 |
| rotate(qreal) | 旋转坐标系角度。 |
| save() | 保存当前绘图状态(如画笔、画刷和变换)。 |
| restore() | 恢复到之前通过 save() 保存的状态。 |
| drawPoint(QPoint) | 绘制一个点。参数可为 QPoint 或坐标元组。 |
| drawPoints(QPolygon) | 绘制一组点。参数为 QPolygon 对象。 |
| drawLine(QLine) | 绘制一条线段。参数可为 QLine 对象或两个点。 |
| drawLines(QLineF) | 绘制多条线段。参数为列表或 QLineF 对象。 |
| drawRect(QRect) | 绘制矩形边界。参数可为 QRect 对象或坐标值和大小。 |
| drawEllipse(QRect) | 绘制椭圆时需给定边界矩形参数。也可用 QPoint 和半径。 |
| drawRoundedRect(QRect) | 绘制圆角矩形。参数为尺寸和圆角半径。 |
| drawPolygon(QPolygon) | 绘制多边形边界(填充)。参数为 QPolygon 对象。 |
| drawPath(QPainterPath) | 绘制自定义路径。参数为 QPainterPath 对象。 |
| drawText(QPoint, QString) | 在指定位置绘制文本字符串。参数可以是位置和字符串。 |
| drawTextItem(QPoint, QTextItem) | 绘制文本项。参数为位置和文本项对象。 |
| drawPixmap(QPoint, QPixmap) | 在指定位置绘制像素图。参数为位置和 QPixmap 对象。 |
| drawImage(QPoint, QImage) | 在指定位置绘制图像。参数为位置和 QImage 对象。 |
| setRenderHint(RenderHint) | 设置渲染提示,如开启抗锯齿(RenderHint.Antialiasing)。 |
| backgroundMode() | 返回当前背景模式。 |
| clipPath() | 获取当前裁剪路径(通过 setClipPath() 设置)。 |
| transform() | 返回当前变换矩阵(QTransform 对象)。 |
| isActive() | 检查是否处于激活状态(如在 begin() 之后)。 |
| worldTransform() | 获取世界空间中的变换矩阵(QTransform 对象)。 |
| combinedTransform() | 获取视口和世界变换的组合矩阵。 |
| viewport() | 返回视口设置。 |
| updateClipRegion() | 强制重新计算容器边界区域。 |
| drawStaticText() | 绘制静态文本对象以优化大批量文本渲染。 |
| setClipRect(QRect) | 设置裁剪矩形区域,后续绘制只在此区域内有效。 |
| setClipPath(QPainterPath) | 设置裁剪路径限制绘制区域。 |
| setCompositionMode() | 设置复合模式,定义如何混合颜色(颜色合成)。 |
说明:
- 此表格覆盖了 QPainter 的大部分常用方法。实际方法可能因 PyQt6 版本而略有差异。
- 绝大多数方法接受多种重载形式,例如 drawRect() 支持 QRect 或单独的坐标参数。
- 数学表达式中如变换缩放比例通常为数值类型(qreal 在 Python 中对应浮点类型),例如缩放因子可以是
k
k
k,满足k
>
0
k > 0
k>0。 - 在代码中使用这些方法时,建议参考 PyQt6 官方文档或源代码注释以获取特定类型的处理。
- 绘图性能推荐使用状态管理(如 save() 和 restore())以避免资源泄漏。
7、高级用法和技巧
- 资源管理:始终确保在结束后调用 end() 或在上下文管理器中使用(如 with 语句)。
- 路径绘制:创建复杂的自定义形状:path = QPainterPath()
path.moveTo(0, 0)
path.lineTo(100, 100)
path.addEllipse(50, 50, 50, 50)
painter.drawPath(path) - 抗锯齿设置:在主循环中启用抗锯齿可提升视觉质量:painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
8、常见问题解答
- QPainter 如何处理内存? 通常在绘图设备的上下文(如 paintEvent)中创建,结束时自动释放不存资源问题。
- 如何绘制透明效果? 使用复合模式设置:painter.setCompositionMode(QPainter.CompositionMode.SourceOver)。
9、总结
QPainter 是 PyQt6 中强大的图形绘制类,适合需要定制图形的应用程序开发。通过熟练使用其方法集,开发人员可以实现丰富的视觉效果。推荐结合案例实践以掌握使用规范:最新 PyQt6 版本为稳定可靠的选择。






