效果预览
转盘支持 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