效果预览

转盘支持 2-12 个奖项,每格颜色自动分配,旋转时有缓动动画,中奖后弹出结果提示框。深色主题适合直播场景。

核心设计

奖项数据结构

# prizes.json
{
  "prizes": [
    {"name": "一等奖 iPhone 15", "weight": 1,  "color": "#FF4444"},
    {"name": "二等奖 AirPods",   "weight": 5,  "color": "#FF8C00"},
    {"name": "三等奖 100元券",   "weight": 20, "color": "#FFD700"},
    {"name": "四等奖 10元券",    "weight": 50, "color": "#32CD32"},
    {"name": "谢谢参与",         "weight": 100, "color": "#4169E1"}
  ]
}

概率归一化算法

import random

def normalize_weights(prizes: list) -> list:
    # 将权重归一化为概率区间
    total = sum(p["weight"] for p in prizes)
    intervals = []
    acc = 0
    for p in prizes:
        prob = p["weight"] / total
        intervals.append((acc, acc + prob, p["name"]))
        acc += prob
    return intervals

def draw(prizes: list) -> str:
    # 抽奖
    intervals = normalize_weights(prizes)
    r = random.random()
    for start, end, name in intervals:
        if start <= r < end:
            return name
    return intervals[-1][2]  # 保底

PyQt6 转盘控件

from PyQt6.QtWidgets import QWidget
from PyQt6.QtCore import QPropertyAnimation, QEasingCurve, pyqtProperty, Qt
from PyQt6.QtGui import QPainter, QColor, QFont, QPen, QBrush
import math

WHEEL_BG = "#0D1117"

class WheelWidget(QWidget):
    def __init__(self, prizes, parent=None):
        super().__init__(parent)
        self.prizes = prizes
        self._rotation = 0.0
        self.setMinimumSize(400, 400)
        self.setStyleSheet(f"background: {WHEEL_BG};")

    @pyqtProperty(float)
    def rotation(self):
        return self._rotation

    @rotation.setter
    def rotation(self, value):
        self._rotation = value
        self.update()  # 触发重绘

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        cx, cy = w // 2, h // 2
        radius = min(w, h) // 2 - 20

        n = len(self.prizes)
        arc = 360 / n

        for i, prize in enumerate(self.prizes):
            start_angle = int((self._rotation + i * arc) * 16)
            span_angle = int(arc * 16)

            painter.setBrush(QBrush(QColor(prize["color"])))
            painter.setPen(QPen(QColor("#0D1117"), 2))
            painter.drawPie(cx - radius, cy - radius,
                            radius * 2, radius * 2,
                            start_angle, span_angle)

            # 绘制文字
            mid_angle = math.radians(self._rotation + i * arc + arc / 2)
            tx = cx + (radius * 0.65) * math.cos(mid_angle)
            ty = cy - (radius * 0.65) * math.sin(mid_angle)
            painter.save()
            painter.translate(tx, ty)
            painter.rotate(-(self._rotation + i * arc + arc / 2))
            painter.setPen(QColor("white"))
            painter.setFont(QFont("微软雅黑", 11, QFont.Weight.Bold))
            painter.drawText(-60, -12, 120, 24,
                             Qt.AlignmentFlag.AlignCenter, prize["name"])
            painter.restore()

        # 中心圆
        painter.setBrush(QBrush(QColor("#161B22")))
        painter.setPen(QPen(QColor("#30363D"), 3))
        painter.drawEllipse(cx - 30, cy - 30, 60, 60)

    def spin(self, target_prize_index: int):
        # 旋转到指定奖项
        n = len(self.prizes)
        arc = 360 / n
        # 目标角度:让指针(顶部)对准目标扇区
        target_angle = -(target_prize_index * arc + arc / 2)
        # 加上随机整圈数(5-10圈),确保动画足够长
        full_rounds = random.randint(5, 10) * 360
        end_angle = full_rounds + target_angle - (self._rotation % 360)

        self.anim = QPropertyAnimation(self, b"rotation")
        self.anim.setDuration(4000)
        self.anim.setStartValue(self._rotation)
        self.anim.setEndValue(self._rotation + end_angle)
        self.anim.setEasingCurve(QEasingCurve.Type.OutCubic)
        self.anim.start()
        return self.anim

主窗口整合

from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, QLabel, QMessageBox
import json

class LotteryWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("直播间抽奖转盘")
        self.resize(500, 600)
        self.setStyleSheet(f"background: {WHEEL_BG}; color: white;")

        with open("prizes.json", encoding="utf-8") as f:
            data = json.load(f)
        self.prizes = data["prizes"]

        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)

        self.wheel = WheelWidget(self.prizes)
        layout.addWidget(self.wheel)

        self.spin_btn = QPushButton("🎯 开始抽奖")
        self.spin_btn.setStyleSheet('''
            QPushButton {
                background: #3B82F6; color: white;
                border-radius: 8px; padding: 12px;
                font-size: 16px; font-weight: bold;
            }
            QPushButton:hover { background: #2563EB; }
            QPushButton:disabled { background: #374151; }
        ''')
        self.spin_btn.clicked.connect(self.do_spin)
        layout.addWidget(self.spin_btn)

    def do_spin(self):
        self.spin_btn.setEnabled(False)
        winner_idx = self._draw_index()
        anim = self.wheel.spin(winner_idx)
        anim.finished.connect(lambda: self._show_result(winner_idx))

    def _draw_index(self) -> int:
        intervals = normalize_weights(self.prizes)
        r = random.random()
        for i, (start, end, _) in enumerate(intervals):
            if start <= r < end:
                return i
        return len(self.prizes) - 1

    def _show_result(self, idx: int):
        name = self.prizes[idx]["name"]
        QMessageBox.information(self, "🎉 恭喜!", f"恭喜获得:{name}")
        self.spin_btn.setEnabled(True)

数据持久化

def save_prizes(prizes: list):
    with open("prizes.json", "w", encoding="utf-8") as f:
        json.dump({"prizes": prizes}, f, ensure_ascii=False, indent=2)

def load_prizes() -> list:
    try:
        with open("prizes.json", encoding="utf-8") as f:
            return json.load(f)["prizes"]
    except FileNotFoundError:
        return DEFAULT_PRIZES

打包为 exe

pip install pyinstaller
pyinstaller --onefile --windowed --name "抽奖转盘" main.py

注意 PyInstaller 打包 PyQt6 时需要包含资源文件:

pyinstaller --onefile --windowed \
  --add-data "prizes.json;." \
  --name "抽奖转盘" main.py