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 mathfrom PyQt6.QtCore import Qt, QTimer, QRectF, QRect, QEvent, QPointFfrom PyQt6.QtGui import QPainter, QPaintEvent, QImage, QLinearGradient, QPen, QColor, QMouseEventfrom PyQt6.QtWidgets import QWidgetclass 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 = 0self.doorRightX = 0self.ledOpacity = 1.0# 基础常量self.dialSize = 250self.indictorSize = self.dialSize + 50self.startAngle = 95self.endAngle = 265self.currentAngle = self.startAngleself.rectDial = QRectF()self.deltaDegrees = 0.0self.recordAngle = 0.0# 动画参数:左右门独立速度,右门更快抵消长距离self.speedLeft = 7self.speedRight = 8self.ledStep = 0.02self.ledForward = Falseself.isAnimDoor = Falseself.targetLeftX = 0self.targetRightX = 0# 防重复解锁标记self._unlockTriggered = Falseself._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 = Noneself._cacheAngle = -1000# 全局主定时器 10msself.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# 锁屏静止状态(无动画、未解锁):仅更新目标,不直接覆盖doorXif self.isVisible() and not self._unlockTriggered and not self.isAnimDoor:self.targetLeftX = -w / 2 + 30self.targetRightX = w / 2 - self.indictorSize / 2# 仅完全静止时同步当前门位置,不破坏动画self.doorLeftX = self.targetLeftXself.doorRightX = self.targetRightX# 正在执行解锁动画,更新飞出目标坐标elif self.isAnimDoor:self.targetLeftX = -w - 10self.targetRightX = wself.update()# 定时器每一帧更新动画def _tick(self):w = self.width()h = self.height()if w <= 0 or h <= 0:return# LED明暗呼吸if self.ledForward:self.ledOpacity += self.ledStepif self.ledOpacity >= 1.0:self.ledOpacity = 1.0self.ledForward = Falseelse:self.ledOpacity -= self.ledStepif self.ledOpacity <= 0.0:self.ledOpacity = 0.0self.ledForward = Trueself.update()# 门平滑移动插值if self.isAnimDoor:changed = False# 左门移动逻辑if abs(self.doorLeftX - self.targetLeftX) > self.speedLeft:if self.doorLeftX < self.targetLeftX:self.doorLeftX += self.speedLeftelse:self.doorLeftX -= self.speedLeftchanged = Trueelse:self.doorLeftX = self.targetLeftX# 右门移动逻辑(独立更高速度)if abs(self.doorRightX - self.targetRightX) > self.speedRight:if self.doorRightX < self.targetRightX:self.doorRightX += self.speedRightelse:self.doorRightX -= self.speedRightchanged = Trueelse:self.doorRightX = self.targetRightX# 门动画结束逻辑if not changed:self.isAnimDoor = Falseif self.doorLeftX <= -w:self.setVisible(False)self.currentAngle = self.startAngleself._unlockTriggered = Falseself.update()# 上锁解锁对外接口def lockScreen(self, is_lock: bool):w = self.width()if w <= 0:returnif is_lock:self._unlockTriggered = Falseself.setVisible(True)self.raise_()self.targetLeftX = -w / 2 + 30self.targetRightX = w / 2 - self.indictorSize / 2# 只设置目标,不强行覆盖当前门坐标,保留动画self.isAnimDoor = Trueelse:self.targetLeftX = -w - 10self.targetRightX = wself.isAnimDoor = Truedef 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:returnborderWidth = 6# 初始化门默认位置if self.doorLeftX == 0:self.doorLeftX = -w - 10if 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 * 16arcSpan = -(self.currentAngle - self.startAngle) * 16painter.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.startAngleif 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 = currentRotatepainter.drawImage(rectLine, self._lineCache)painter.setOpacity(1.0)painter.restore()# 缺口图片centerX = self.rectDial.x() + self.rectDial.width() / 2centerY = self.rectDial.y() + self.rectDial.height() / 2circleR = self.rectDial.width() / 2 - 50rad = math.radians(self.currentAngle)notchX = centerX + circleR * math.cos(rad) - 32notchY = centerY + circleR * math.sin(rad) - 32rectNotch = 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 - 30ledY = rectIndicator.y() + rectIndicator.height() - 20rectLed = 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 = eventif 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() - cxdy = pt.y() - cyangleRad = math.atan2(dy, dx)newAngle = math.degrees(angleRad)newAngle = self.normalize_angle(newAngle)self.deltaDegrees = newAngleself.recordAngle = self.currentAngleelif event.type() == QEvent.Type.MouseButtonRelease:if isinstance(event, QMouseEvent):mouse_evt = eventif mouse_evt.button() == Qt.MouseButton.LeftButton:self._mousePressed = Falseelif event.type() == QEvent.Type.MouseMove:if self._mousePressed and isinstance(event, QMouseEvent):mouse_evt = eventpos = mouse_evt.pos()pt = QPointF(pos)cx = self.rectDial.center().x()cy = self.rectDial.center().y()dx = pt.x() - cxdy = pt.y() - cyangleRad = 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 = Trueself.lockScreen(False)return super().eventFilter(obj, event)
2.2、main.py
import sysfrom PyQt6.QtWidgets import QApplicationfrom PyQt6.QtWidgets import QMainWindow, QPushButton, QWidget, QVBoxLayoutfrom PyQt6.QtCore import Qtfrom lock_screen import LockScreenclass 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, QApplicationfrom PyQt6.QtGui import QPainter, QPen, QColorfrom PyQt6.QtCore import Qtclass CustomWidget(QWidget):def paintEvent(self, event):painter = QPainter(self) # 绑定到当前widgetpainter.setPen(QPen(Qt.GlobalColor.red, 2)) # 设置画笔样式painter.drawLine(10, 10, 100, 100) # 绘制一条线app = QApplication([])window = CustomWidget()window.show()app.exec()
在这个例子中:
创建一个 QPainter对象并传入目标设备(如窗体)。使用 setPen()设置画笔的属性。调用 drawLine()进行实际绘制。结束时,系统自动处理资源的释放。
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) | |
setBrush(QBrush) | |
setFont(QFont) | |
setTransform(QTransform) | |
resetTransform() | |
translate(qreal, qreal) | |
scale(qreal, qreal) | |
rotate(qreal) | |
save() | |
restore() | save() 保存的状态。 |
drawPoint(QPoint) | |
drawPoints(QPolygon) | |
drawLine(QLine) | |
drawLines(QLineF) | |
drawRect(QRect) | |
drawEllipse(QRect) | |
drawRoundedRect(QRect) | |
drawPolygon(QPolygon) | |
drawPath(QPainterPath) | |
drawText(QPoint, QString) | |
drawTextItem(QPoint, QTextItem) | |
drawPixmap(QPoint, QPixmap) | |
drawImage(QPoint, QImage) | |
setRenderHint(RenderHint) | |
backgroundMode() | |
clipPath() | |
transform() | |
isActive() | |
worldTransform() | |
combinedTransform() | |
viewport() | |
updateClipRegion() | |
drawStaticText() | |
setClipRect(QRect) | |
setClipPath(QPainterPath) | |
setCompositionMode() |
说明:
此表格覆盖了 QPainter 的大部分常用方法。实际方法可能因 PyQt6 版本而略有差异。 绝大多数方法接受多种重载形式,例如 drawRect()支持 QRect 或单独的坐标参数。数学表达式中如变换缩放比例通常为数值类型( qreal在 Python 中对应浮点类型),例如缩放因子可以是
,满足
。在代码中使用这些方法时,建议参考 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 版本为稳定可靠的选择。

夜雨聆风