Python+PyQt实现Android手势解锁界面(附源码)

 更新时间:2026年08月12日 09:27:43   作者:小灰灰搞电子  
本文深入讲解了PyQt中QPainter的核心绘图技巧,从坐标变换、QPainterPath平滑连线到渐变填充和性能优化,手把手教你实现流畅的动画效果,附赠完整源码和常见问题解决方案,让你的GUI项目立刻提升一个档次

一、效果展示

二、源码分享

1、main.py

import sys
import random
import math
from PyQt6.QtCore import (Qt, QPoint, QPointF, QRect, QTimer, QPropertyAnimation,
                          QEasingCurve, pyqtProperty)
from PyQt6.QtGui import (QPainter, QColor, QPen, QLinearGradient, QBrush, QPainterPath)
from PyQt6.QtWidgets import (QApplication, QWidget, QPushButton,
                             QVBoxLayout, QHBoxLayout, QLabel)


class PatternLockWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedSize(360, 640)
        self.setWindowTitle("PyQt6 手势图案解锁")

        # 业务数据
        self.correct_pattern = [0, 1, 2, 4, 8]
        self.current_pattern = []
        self.pattern_correct = False
        self.pattern_error = False

        # 九宫格布局参数
        self.grid_size = 100
        self.panel_w = 320
        self.panel_h = 320
        self.panel_offset_x = (self.width() - self.panel_w) // 2
        self.panel_offset_y = (self.height() - self.panel_h) // 2

        # 背景粒子
        self.particle_count = 20
        self.particles = []
        self.init_particles()

        # 鼠标拖拽临时终点
        self.drag_pos: QPoint | None = None

        # 动画属性
        self._scale = 1.0
        self._rotate = 0.0
        self._offset_x = 0

        self.setup_ui_layout()

        # 粒子刷新定时器
        self.particle_timer = QTimer(self)
        self.particle_timer.setInterval(50)
        self.particle_timer.timeout.connect(self.update_particles)
        self.particle_timer.start()

    def init_particles(self):
        self.particles.clear()
        for _ in range(self.particle_count):
            px = random.randint(0, self.width())
            py = random.randint(0, self.height())
            cycle = random.randint(1000, 3000)
            self.particles.append({
                "x": px,
                "y": py,
                "cycle": cycle,
                "phase": random.random() * cycle,
                "opacity": 0.6
            })

    def update_particles(self):
        for p in self.particles:
            p["phase"] += 50
            rad = 2 * math.pi * (p["phase"] % p["cycle"]) / p["cycle"]
            p["opacity"] = 0.5 + 0.3 * math.sin(rad)
        self.update()

    def setup_ui_layout(self):
        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)

        # 顶部标题
        self.label_status = QLabel("绘制解锁图案")
        self.label_status.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.label_status.setStyleSheet("font-size:24px; font-weight:bold; color:#ffffff;")

        self.label_tip = QLabel("连接至少4个点")
        self.label_tip.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.label_tip.setStyleSheet("font-size:14px; color:#8e8e93; margin-bottom:80px;")

        main_layout.addWidget(self.label_status)
        main_layout.addWidget(self.label_tip)
        main_layout.addStretch()

        # 底部按钮
        btn_layout = QHBoxLayout()
        btn_layout.setSpacing(20)

        self.btn_reset = QPushButton("重置")
        self.btn_reset.setFixedSize(120, 44)
        self.btn_reset.clicked.connect(self.reset_all)

        self.btn_forget = QPushButton("忘记图案")
        self.btn_forget.setFixedSize(120, 44)
        self.btn_forget.clicked.connect(self.fill_correct_pattern)

        btn_style = """
        QPushButton {
            border:1px solid #4a4a6a;
            border-radius:22px;
            background:transparent;
            color:#ffffff;
            font-size:16px;
        }
        QPushButton:pressed {
            background:#ffffff;
            color:#0f0f23;
        }
        """
        self.btn_reset.setStyleSheet(btn_style)
        forget_style = btn_style.replace("color:#ffffff", "color:#8e8e93")
        self.btn_forget.setStyleSheet(forget_style)

        btn_layout.addWidget(self.btn_reset)
        btn_layout.addWidget(self.btn_forget)
        main_layout.addLayout(btn_layout)
        main_layout.addSpacing(60)

    # 动画属性
    @pyqtProperty(float)
    def scale(self):
        return self._scale

    @scale.setter
    def scale(self, v):
        self._scale = v
        self.update()

    @pyqtProperty(float)
    def rotate(self):
        return self._rotate

    @rotate.setter
    def rotate(self, v):
        self._rotate = v
        self.update()

    @pyqtProperty(int)
    def offset_x(self):
        return self._offset_x

    @offset_x.setter
    def offset_x(self, v):
        self._offset_x = v
        self.update()

    # 核心绘制
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 1. 渐变背景
        grad_bg = QLinearGradient(0, 0, 0, self.height())
        grad_bg.setColorAt(0.0, QColor("#0f0f23"))
        grad_bg.setColorAt(0.5, QColor("#1a1a2e"))
        grad_bg.setColorAt(1.0, QColor("#16213e"))
        painter.fillRect(self.rect(), grad_bg)

        # 2. 绘制背景粒子
        for p in self.particles:
            painter.setOpacity(p["opacity"])
            painter.setBrush(QColor("#4cc9f0"))
            painter.setPen(Qt.PenStyle.NoPen)
            painter.drawEllipse(QPoint(p["x"], p["y"]), 1, 1)
        painter.setOpacity(1.0)

        # 3. 状态颜色与文字
        if self.pattern_correct:
            line_color = QColor("#4cd964")
            status_bar_w = 120
            txt1 = "✓ 解锁成功"
            txt2 = "欢迎回来"
        elif self.pattern_error:
            line_color = QColor("#ff375f")
            status_bar_w = 120
            txt1 = "✗ 图案错误"
            txt2 = "请重试"
        else:
            line_color = QColor("#4cc9f0")
            status_bar_w = 80
            txt1 = "绘制解锁图案"
            txt2 = "连接至少4个点"

        self.label_status.setText(txt1)
        self.label_tip.setText(txt2)
        if self.pattern_correct:
            self.label_status.setStyleSheet("font-size:24px; font-weight:bold; color:#4cd964;")
        elif self.pattern_error:
            self.label_status.setStyleSheet("font-size:24px; font-weight:bold; color:#ff375f;")
        else:
            self.label_status.setStyleSheet("font-size:24px; font-weight:bold; color:#ffffff;")

        # 4. 变换坐标系(缩放、旋转、偏移)
        painter.save()
        center_x = self.panel_offset_x + self.panel_w / 2 + self._offset_x
        center_y = self.panel_offset_y + self.panel_h / 2
        painter.translate(center_x, center_y)
        painter.rotate(self._rotate)
        painter.scale(self._scale, self._scale)
        painter.translate(-center_x, -center_y)

        # ========== 使用 QPainterPath 绘制连线光晕 ==========
        if len(self.current_pattern) > 1:
            path = QPainterPath()
            start_pt = self.get_point_pos(self.current_pattern[0]).toPointF()
            path.moveTo(start_pt)
            for idx in range(1, len(self.current_pattern)):
                pt = self.get_point_pos(self.current_pattern[idx]).toPointF()
                path.lineTo(pt)

            # 外层光晕
            pen_glow = QPen(line_color, 12)
            pen_glow.setCapStyle(Qt.PenCapStyle.RoundCap)
            pen_glow.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
            pen_glow.setColor(QColor(line_color.red(), line_color.green(), line_color.blue(), 60))
            painter.setPen(pen_glow)
            painter.setBrush(Qt.BrushStyle.NoBrush)
            painter.drawPath(path)

            # 中层过渡
            pen_mid = QPen(line_color, 7)
            pen_mid.setCapStyle(Qt.PenCapStyle.RoundCap)
            pen_mid.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
            pen_mid.setColor(QColor(line_color.red(), line_color.green(), line_color.blue(), 120))
            painter.setPen(pen_mid)
            painter.setBrush(Qt.BrushStyle.NoBrush)
            painter.drawPath(path)

            # 内层主线
            pen_main = QPen(line_color, 4)
            pen_main.setCapStyle(Qt.PenCapStyle.RoundCap)
            pen_main.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
            painter.setPen(pen_main)
            painter.setBrush(Qt.BrushStyle.NoBrush)
            painter.drawPath(path)

        # 拖拽实时拉线(修复QPoint转QPointF)
        if self.drag_pos is not None and len(self.current_pattern) > 0:
            last_p = self.get_point_pos(self.current_pattern[-1]).toPointF()
            drag_end = self.drag_pos.toPointF()
            drag_path = QPainterPath()
            drag_path.moveTo(last_p)
            drag_path.lineTo(drag_end)

            pen_glow = QPen(line_color, 12)
            pen_glow.setCapStyle(Qt.PenCapStyle.RoundCap)
            pen_glow.setColor(QColor(line_color.red(), line_color.green(), line_color.blue(), 60))
            painter.setPen(pen_glow)
            painter.drawPath(drag_path)

            pen_mid = QPen(line_color, 7)
            pen_mid.setCapStyle(Qt.PenCapStyle.RoundCap)
            pen_mid.setColor(QColor(line_color.red(), line_color.green(), line_color.blue(), 120))
            painter.setPen(pen_mid)
            painter.drawPath(drag_path)

            pen_main = QPen(line_color, 4)
            pen_main.setCapStyle(Qt.PenCapStyle.RoundCap)
            painter.setPen(pen_main)
            painter.drawPath(drag_path)

        # 绘制9个圆点
        for i in range(9):
            pt = self.get_point_pos(i)
            is_active = i in self.current_pattern
            is_last = len(self.current_pattern) > 0 and self.current_pattern[-1] == i

            # 外圈大圆
            painter.setBrush(line_color if is_active else Qt.BrushStyle.NoBrush)
            painter.setPen(Qt.PenStyle.NoPen)
            painter.drawEllipse(pt, 20, 20)

            # 内小圆
            inner_color = QColor("#ffffff") if is_active else QColor("#3a3a4a")
            painter.setBrush(inner_color)
            painter.drawEllipse(pt, 8, 8)

            # 最后一个点外圈描边
            if is_last:
                painter.setPen(QPen(line_color, 2))
                painter.setBrush(Qt.BrushStyle.NoBrush)
                painter.drawEllipse(pt, 25, 25)

        painter.restore()

        # 底部状态条
        bar_y = self.panel_offset_y + self.panel_h + 40
        bar_x = (self.width() - status_bar_w) // 2
        painter.setBrush(line_color)
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawRoundedRect(QRect(bar_x, bar_y, status_bar_w, 4), 2, 2)

    def get_point_pos(self, index: int) -> QPoint:
        col = index % 3
        row = index // 3
        x = self.panel_offset_x + col * self.grid_size + 50
        y = self.panel_offset_y + row * self.grid_size + 50
        return QPoint(int(x), int(y))

    def get_index_by_pos(self, pos: QPoint) -> int | None:
        for i in range(9):
            pt = self.get_point_pos(i)
            dx = pos.x() - pt.x()
            dy = pos.y() - pt.y()
            if math.hypot(dx, dy) < 30:
                return i
        return None

    # 鼠标事件
    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.reset_status_flags()
            self.drag_pos = event.pos()
            idx = self.get_index_by_pos(event.pos())
            if idx is not None and idx not in self.current_pattern:
                self.current_pattern.append(idx)
                self.run_vibration_anim()
            self.update()

    def mouseMoveEvent(self, event):
        self.drag_pos = event.pos()
        idx = self.get_index_by_pos(event.pos())
        if idx is not None and idx not in self.current_pattern:
            self.current_pattern.append(idx)
            self.run_vibration_anim()
        self.update()

    def mouseReleaseEvent(self, event):
        self.drag_pos = None
        self.check_pattern_result()
        self.update()

    # 业务逻辑
    def reset_status_flags(self):
        self.current_pattern.clear()
        self.pattern_correct = False
        self.pattern_error = False
        self._scale = 1.0
        self._rotate = 0.0
        self._offset_x = 0

    def reset_all(self):
        self.reset_status_flags()
        self.update()

    def fill_correct_pattern(self):
        self.reset_status_flags()
        for num in self.correct_pattern:
            self.current_pattern.append(num)
            self.run_vibration_anim()
        self.check_pattern_result()
        self.update()

    def arrays_equal(self, a: list, b: list) -> bool:
        if len(a) != len(b):
            return False
        for x, y in zip(a, b):
            if x != y:
                return False
        return True

    def check_pattern_result(self):
        ok = self.arrays_equal(self.current_pattern, self.correct_pattern)
        if ok and len(self.current_pattern) >= 4:
            self.pattern_correct = True
            self.play_success_anim()
        else:
            self.pattern_error = True
            self.play_error_anim()

    # 动画
    def run_vibration_anim(self):
        anim1 = QPropertyAnimation(self, b"scale")
        anim1.setDuration(50)
        anim1.setStartValue(1.0)
        anim1.setEndValue(1.02)

        anim2 = QPropertyAnimation(self, b"scale")
        anim2.setDuration(50)
        anim2.setStartValue(1.02)
        anim2.setEndValue(1.0)

        anim1.finished.connect(anim2.start)
        anim1.start()

    def play_success_anim(self):
        anim_scale = QPropertyAnimation(self, b"scale")
        anim_scale.setDuration(300)
        anim_scale.setStartValue(1.0)
        anim_scale.setEndValue(1.1)
        anim_scale.setEasingCurve(QEasingCurve.Type.OutCubic)

        anim_rot1 = QPropertyAnimation(self, b"rotate")
        anim_rot1.setDuration(200)
        anim_rot1.setStartValue(0)
        anim_rot1.setEndValue(5)

        anim_rot2 = QPropertyAnimation(self, b"rotate")
        anim_rot2.setDuration(200)
        anim_rot2.setStartValue(5)
        anim_rot2.setEndValue(0)

        anim_rot1.finished.connect(anim_rot2.start)
        anim_scale.start()
        anim_rot1.start()

    def play_error_anim(self):
        seq = [
            {"x": -8, "s": 0.98, "dur": 50},
            {"x": 8, "s": 1.02, "dur": 50},
            {"x": 0, "s": 1.0, "dur": 50},
        ]
        self._run_error_seq(seq, 0)

    def _run_error_seq(self, seq_list, idx):
        if idx >= len(seq_list):
            return
        cfg = seq_list[idx]
        anim_x = QPropertyAnimation(self, b"offset_x")
        anim_x.setDuration(cfg["dur"])
        anim_x.setEndValue(cfg["x"])

        anim_s = QPropertyAnimation(self, b"scale")
        anim_s.setDuration(cfg["dur"])
        anim_s.setEndValue(cfg["s"])

        def next_step():
            self._run_error_seq(seq_list, idx + 1)

        anim_x.finished.connect(next_step)
        anim_x.start()
        anim_s.start()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = PatternLockWidget()
    win.show()
    sys.exit(app.exec())

2、库安装

三、实现原理

1、 QPainter 核心概念

QPainter 是 PyQt6 中用于执行绘图操作的核心类,它提供了丰富的 API 来绘制各种图形元素。QPainter 采用**立即模式(immediate mode)**绘图,即绘图指令会立即执行并显示在设备上。

主要特点:

  • 设备无关性:可在 QWidget、QPixmap、QImage、QPrinter 等多种设备上绘制
  • 坐标系系统:支持世界坐标、窗口坐标和视口坐标的变换
  • 渲染质量:支持抗锯齿、平滑变换等高质量渲染选项
  • 绘图状态:通过 save()/restore() 管理绘图状态栈

2、 基本使用流程

# 1. 创建 QPainter 对象
painter = QPainter(self)  # 在 QWidget 上绘制

# 2. 设置渲染提示(抗锯齿)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)

# 3. 设置画笔(轮廓)
pen = QPen(QColor("#4cc9f0"), 4)  # 颜色, 线宽
pen.setCapStyle(Qt.PenCapStyle.RoundCap)  # 线帽样式
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)  # 连接样式
painter.setPen(pen)

# 4. 设置画刷(填充)
brush = QBrush(QColor("#ffffff"))
painter.setBrush(brush)

# 5. 执行绘图操作
painter.drawEllipse(QPoint(100, 100), 50, 50)  # 绘制圆形
painter.drawRect(50, 50, 200, 100)  # 绘制矩形
painter.drawText(100, 100, "Hello QPainter")  # 绘制文本

# 6. 自动释放资源(离开作用域时)

3、 坐标变换系统

QPainter 支持多种坐标变换,这在手势解锁的动画效果中尤为重要:

# 保存当前绘图状态
painter.save()

# 平移变换
painter.translate(center_x, center_y)

# 旋转变换
painter.rotate(45)  # 旋转45度

# 缩放变换
painter.scale(1.2, 1.2)  # X/Y轴分别缩放

# 执行绘图(在变换后的坐标系中)
painter.drawEllipse(QPoint(0, 0), 50, 50)

# 恢复原始绘图状态
painter.restore()

4、 QPainterPath 高级绘图

QPainterPath 用于创建复杂的矢量图形路径,特别适合绘制平滑曲线和自定义形状:

# 创建路径对象
path = QPainterPath()

# 移动到起点
path.moveTo(start_point)

# 添加直线段
path.lineTo(point1)
path.lineTo(point2)

# 添加二次贝塞尔曲线
path.quadTo(control_point, end_point)

# 添加三次贝塞尔曲线
path.cubicTo(ctrl1, ctrl2, end_point)

# 闭合路径
path.closeSubpath()

# 使用 QPainter 绘制路径
painter.drawPath(path)

在本项目中的应用:

  • 手势连线的主路径使用 QPainterPath 构建
  • 通过多层不同宽度的画笔绘制光晕效果
  • 支持圆角线帽和连接样式,使连线更平滑

5、 渐变填充

QPainter 支持多种渐变填充方式,用于创建丰富的视觉效果:

# 线性渐变
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, QColor("#0f0f23"))  # 起始颜色
gradient.setColorAt(0.5, QColor("#1a1a2e"))  # 中间颜色
gradient.setColorAt(1.0, QColor("#16213e"))  # 结束颜色

# 径向渐变
radial_grad = QRadialGradient(center, radius)
radial_grad.setColorAt(0.0, QColor("#ffffff"))
radial_grad.setColorAt(1.0, QColor("#000000"))

# 锥形渐变
conical_grad = QConicalGradient(center, start_angle)
conical_grad.setColorAt(0.0, QColor("#ff0000"))
conical_grad.setColorAt(0.5, QColor("#00ff00"))
conical_grad.setColorAt(1.0, QColor("#0000ff"))

# 应用渐变填充
painter.setBrush(QBrush(gradient))
painter.drawRect(self.rect())

6、 透明度与混合模式

# 设置透明度(0.0-1.0)
painter.setOpacity(0.7)

# 设置混合模式
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
# 其他模式:CompositionMode_SourceIn, CompositionMode_Multiply 等

# 绘制半透明图形
painter.setBrush(QColor(255, 0, 0, 128))  # ARGB格式,128表示50%透明度
painter.drawEllipse(QPoint(100, 100), 50, 50)

7、性能优化技巧

  1. 批量绘制:将多个相同样式的图形合并绘制
  2. 避免频繁状态切换:合理安排绘图顺序
  3. 使用局部更新:只重绘需要更新的区域
  4. 缓存静态内容:将不变的内容绘制到 QPixmap 缓存
  5. 合理使用抗锯齿:只在需要时开启

8、 常见问题与解决方案

问题1:绘图闪烁

# 解决方案:启用双缓冲
self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True)
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)

问题2:坐标计算错误

# 解决方案:使用正确的坐标转换
# 屏幕坐标转窗口坐标
window_pos = self.mapFromGlobal(screen_pos)
# 窗口坐标转局部坐标
local_pos = event.pos()

问题3:文本渲染模糊

# 解决方案:设置文本渲染提示
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)

9、 实战应用:手势解锁中的 QPainter

在本项目中,QPainter 主要完成以下任务:

  1. 背景绘制:使用线性渐变创建深色背景
  2. 粒子系统:绘制动态背景粒子效果
  3. 手势连线:使用 QPainterPath 绘制三层光晕连线
  4. 圆点绘制:绘制九宫格解锁点及其状态
  5. 状态指示器:绘制底部进度条
  6. 动画效果:通过坐标变换实现缩放、旋转动画

以上就是Python+PyQt实现Android手势解锁界面(附源码)的详细内容,更多关于Python Android手势解锁界面的资料请关注脚本之家其它相关文章!

相关文章

最新评论