
Python公司年会抽奖(开源版):可视化界面与抽奖程序
本系统基于Python的Tkinter GUI框架,实现了企业年会场景下的智能化抽奖解决方案。系统突破传统抽奖模式,通过Excel数据管理、可视化界面交互、智能随机算法三大核心模块,实现了从员工名单管理到奖项配置、实时抽奖的全流程数字化。该抽奖系统完美展现了如何将传统Python脚本升级为企业级解决方案。通过模块化设计、健壮性增强、可视化优化,打造出可直接部署的抽奖平台。
·引言
为满足公司年会抽奖需求,本文章将详细展示一个结构清晰、功能完整的抽奖系统实现方案。该方案兼顾实用性与扩展性,可作为企业级应用程序的基础架构,为公司各类抽奖活动提供技术支持。
温馨提示:由于文章篇幅较长,读者可查找目录栏,各取所需。祝您阅读愉快!
·程序逻辑
程序思路:通过主类LotteryApp管理整个程序应用,即通过多个自定义函数[def xx_x():]方法实现对应功能,并封装在名为LotteryApp的类(class)中,由这个类来运行整个程序。(类似交响乐团,主类像一个乐队指挥,而不同函数像各个演奏者,运行的程序就是交响乐);首先实现核心抽奖程序,再去不断丰富拓展更多功能与界面显示,最后实现不断迭代优化、满足使用需求。(本文则直接展示较为完善的代码,请根据自身需求,合理阅读。若有不便,欢迎评论区交流学习。)
程序逻辑:抽奖逻辑即程序逻辑(此处不再过多赘述)
·程序结构
首先,根据程序使用需求,划分所需结构:采用面向对象设计,主类LotteryApp管理整个应用程序,包含以下核心组件:
-
GUI界面:基于Tkinter构建独立窗口界面(GUI:图形用户界面)
-
数据处理:考虑抽奖名单数据、奖项配置数据、抽奖结果数据等
-
业务逻辑:抽奖流程控制(包括抽奖前后人数变化逻辑等)、名单结果处理
接着,进行UI设计,对复杂程序界面进行功能分区。此处为方便读者参照理解,给出下图:
注意到:主菜单中有2个选项,其中 “文件” 包含 “导入员工名单”、“重置员工名单”、“导出中奖结果” 以及 “退出” 按钮,4个选项;“设置” 包含 “设置背景” 及 “管理奖项” 2个选项。(见下文 “②界面创建—主菜单” 详解)
·总程序
·①创建窗口
注意,此前还需要先导入必要的库。代码如下:
import tkinter as tk
#python的标准GUI库,用于创建图形用户界面
from tkinter import ttk,filedialog,messagebox
# ttk:thinter的主题化小部件集,提供更美观的界面组件
# filedialog:用于打开和保存文件的对话框
# messagebox:用于显示消息框,如警告、错误和信息提示
import pandas as pd
# pandas:用于处理和分析数据,特别是读取和写入 Excel 文件
import random
# random:用于生成随机数,在抽奖过程中随机选择员工
from PIL import ImageTk, Image
# PIL(Python Imaging Library):用于处理图像,如加载和调整背景图片大小
库的安装步骤: (PyCharm:ctrl+alt+S打开设置→项目→Python解释器→点击“+”号→搜索并安装)
接下来,定义类、初始化对象、创建窗口。代码如下:
# 定义一个名为LotteryApp的类,用于封装抽奖系统的功能
class LotteryApp:
def __init__(self, root):
# "__init__()"是类的构造函数,用于初始化对象
# 接收一个root参数(代表Tkinter的根窗口对象)
# 窗口管理系统
self.root = root # 将传入的Tkinter根窗口保存为实例变量
self.root.title("公司年会抽奖系统") # 设置窗口标题为"公司年会抽奖系统"
self.root.geometry('1920x1080') # geometry(): 设置窗口初始大小为1920x1080(全屏高清分辨率)
self.root.minsize(800, 600) # minsize(): 设置窗口最小尺寸(800x600)保证界面可用性
# 数据管理系统
#(双名单机制)防止重复抽奖
self.employees = [] # 当前可抽奖员工列表(动态减少)
self.original_employees = [] # 原始导入名单(用于重置)
self.winners = [] # 历史中奖记录
self.is_rolling = False # 布尔值控制抽奖动画的启停
self.awards = [] # 奖项配置列表
self.current_award_index = -1 # 初始化为-1表示未设置奖项(当前奖项索引)
#背景管理
self.background_path = None # 背景图片路径
self.original_image = None # 原始图片对象
# 创建UI
self.create_menu() # 创建菜单栏(文件/设置)
self.create_widgets() # 创建主界面组件(按钮/名单显示)
self.create_award_frame() # 创建奖项配置区域
# 自适应设计:事件绑定,实现背景自适应
self.root.bind('<Configure>', lambda e: self.resize_background())
# 动态更新按钮状态,用于禁用已完成的抽奖操作
self.update_button_state()
上述代码中,窗口管理系统能够兼顾大屏展示(年会现场投影)和小屏调试(开发测试);在awards中配置多个奖项(如一等奖、二等奖);current_award_index在抽完一个奖项后可自动更新显示下一个奖项。
·②界面创建
主菜单
完成窗口创建后,开始界面主菜单创建。代码如下:
# 主菜单
def create_menu(self):
# 创建一个菜单栏对象,将其关联到主窗口 self.root 上
menu_bar = tk.Menu(self.root)
# 文件菜单
file_menu = tk.Menu(menu_bar, tearoff=0) # 创建一个“文件”子菜单对象,tearoff=0 表示该菜单不能被拖离菜单栏
file_menu.add_command(label="导入员工名单", command=self.import_employees) # 为“文件”菜单添加“导入员工名单”子菜单项,并绑定 self.import_employees 方法作为点击事件处理函数
file_menu.add_command(label="重置员工名单", command=self.reset_employees) # 同上
file_menu.add_command(label="导出中奖结果", command=self.export_winners) # 同上
file_menu.add_separator() # 横线
file_menu.add_command(label="退出", command=self.root.quit) # 为“文件”菜单添加“退出”子菜单项,并绑定 self.root.quit 方法作为点击事件处理函数,用于退出应用程序
menu_bar.add_cascade(label="文件", menu=file_menu) # 将“文件”菜单添加到菜单栏中,使用 add_cascade 方法将其作为一个级联菜单显示,标签为“文件”
# 设置菜单
settings_menu = tk.Menu(menu_bar, tearoff=0)
settings_menu.add_command(label="设置背景", command=self.set_background)
settings_menu.add_command(label="管理奖项", command=self.manage_awards)
menu_bar.add_cascade(label="设置", menu=settings_menu)
# 将创建好的菜单栏配置到主窗口上,使其显示在主窗口的顶部
self.root.config(menu=menu_bar)
上述代码中,我们在tkinter应用程序的主窗口里,创建了一个包含 “文件” 和 “设置” 两个主菜单的菜单栏,每个主菜单下又有不同的子菜单项,用户点击这些子菜单项时会触发相应的处理函数,实现不同功能。“文件” 效果如图:
需要注意:代码中self.import_employees、self.reset_employees方法需要在类的其他地方定义,以实现具体业务逻辑。
主界面组件
主界面即该抽奖界面的主要显示区域,用于操作抽奖程序与查看抽奖信息。代码如下:
# 主界面组件
def create_widgets(self):
# 创建一个标签作为背景,将其关联到主窗口 self.root 上
self.bg_label = tk.Label(self.root)
# 使用相对布局实现居中显示
self.bg_label.place(x=0, y=0, relwidth=1, relheight=1) # 使用 place 几何管理器将背景标签放置在主窗口的左上角,relwidth=1 和 relheight=1 表示标签的宽度和高度与主窗口相同
# 主框架
main_frame = tk.Frame(self.bg_label) # 创建一个框架 main_frame,将其关联到背景标签 self.bg_label 上
main_frame.place(relx=0.5, rely=0.5, anchor="center") # 使用 place 几何管理器将主框架放置在背景标签的中心位置
# 员工信息
self.employee_count_label = tk.Label(main_frame, text="已加载员工:0人", font=("Arial", 12)) # 创建一个标签,用于显示已加载的员工数量,文本初始为“已加载员工:0人”,字体为 Arial 12 号
self.employee_count_label.pack(pady=5) # 使用 pack 几何管理器将该标签添加到主框架中,并在垂直方向上留出 5 像素的间距
# 抽奖显示
self.name_label = tk.Label(main_frame, text="准备就绪", font=("Arial", 48), bg="white", width=20) # 创建一个标签,用于显示抽奖时的姓名,文本为“准备就绪”,字体为 Arial 48 号,背景为白色,宽度为 20 个字符
self.name_label.pack(pady=20)
# 控制按钮
btn_frame = tk.Frame(main_frame) # 创建一个按钮框架 btn_frame,将其关联到主框架 main_frame 上
btn_frame.pack(pady=10)
self.start_btn = tk.Button(btn_frame, text="开始抽奖", command=self.start_lottery, width=15) # 创建一个“开始抽奖”按钮,将其关联到按钮框架 btn_frame 上,点击该按钮会调用 self.start_lottery 方法
self.start_btn.pack(side="left", padx=5) # 使用 pack 几何管理器将该按钮添加到按钮框架中,靠左排列,并在水平方向上留出 5 像素的间距
self.stop_btn = tk.Button(btn_frame, text="停止抽奖", command=self.stop_lottery, width=15, state="disabled") # 创建一个“停止抽奖”按钮,将其关联到按钮框架 btn_frame 上,点击该按钮会调用 self.stop_lottery 方法
# 初始状态为禁用(不可点击)
self.stop_btn.pack(side="left", padx=5)
# 中奖名单
# Treeview组件实现表格化数据显示
self.winner_list = ttk.Treeview(main_frame, columns=("name", "award"), show="headings", height=10) # 创建一个树形视图控件 winner_list,用于显示中奖者信息,包含“name”和“award”两列,show="headings" 表示只显示列标题,高度为 10 行
self.winner_list.heading("name", text="姓名") # 设置“name”列的标题为“姓名”
self.winner_list.heading("award", text="奖项") # 设置“award”列的标题为“奖项”
self.winner_list.pack(pady=10) # 使用 pack 几何管理器将树形视图添加到主框架中,并在垂直方向上留出 10 像素的间距
# 新增刷新界面
self.refresh_btn = tk.Button(btn_frame, text='刷新界面', command=self.refresh_ui, width=15) # 创建一个“刷新界面”按钮,将其关联到按钮框架 btn_frame 上,点击该按钮会调用 self.refresh_ui 方法
self.refresh_btn.pack(side='left', padx=5) # 使用 pack 几何管理器将该按钮添加到按钮框架中,靠左排列,并在水平方向上留出 5 像素的间距
上述代码中,通过tkinter和ttk库,创建了一个包含多个控件的GUI,用于实现抽奖程序的基本交互。代码中self.start_lottery、self.stop_lottery、self.refresh_ui方法需要在类的其他地方进行定义,以实现具体功能。
主界面刷新
为确保抽奖结果能够有效显示在主页面,此处添加方法刷新GUI并更新按钮状态。代码如下:
# 刷新UI界面
def refresh_ui(self):
self.root.update_idletasks() # 强制更新界面的空闲任务
self.resize_background() # 调用 resize_background 方法来调整背景的大小
# 更新奖项显示
self.update_current_award_display() # 调用 update_current_award_display 方法来更新当前奖项的显示
# 更新员工数量显示
self.employee_count_label.config(text=f'已加载员工:{len(self.employees)}人') # 更新员工数量标签的文本,显示当前已加载的员工数量
# 更新按钮状态
self.update_button_state()
# 更新按钮状态
def update_button_state(self):
# 检查当前奖项索引是否超出了奖项列表的长度
if self.current_award_index >= len(self.awards):
self.start_btn.config(state='disabled') # 如果超出了奖项列表的长度,说明所有奖项都已抽取完毕,将开始抽奖按钮禁用
else:
self.start_btn.config(state='normal') # 如果未超出奖项列表的长度,说明还有奖项可以抽取,将开始抽奖按钮启用
上述两个方法:refresh_ui方法用于刷新整个GUI界面的显示和状态,而update_button_state方法则是专门用于更新按钮的启用或禁用状态。
副界面显示
为显示抽奖动态,将采用副界面显示的方式,反映当前奖项与抽奖剩余名额。代码如下:
# 创建副界面窗口(初始)
def create_award_frame(self):
self.award_frame = tk.Frame(self.bg_label) # 创建一个新的框架,将其作为背景标签 self.bg_label 的子组件
self.award_frame.place(relx=0.1, rely=0.1) # 使用 place 几何管理器将该框架放置在背景标签中,相对 x 坐标为 0.1,相对 y 坐标为 0.1
self.current_award_label = tk.Label(self.award_frame, text="当前奖项:未设置", font=("Arial", 12)) # 创建一个标签,用于显示当前奖项信息,初始文本为“当前奖项:未设置”,字体为 Arial 12 号
self.current_award_label.pack() # 使用 pack 几何管理器将该标签添加到奖项框架中
self.remaining_label = tk.Label(self.award_frame, text="剩余名额:0", font=("Arial", 12))
self.remaining_label.pack()
上述代码中,create_award_frame创建了副界面窗口,并设置默认奖项(默认未设置)和剩余名额(即奖项设置之前的显示内容)
副界面更新
开始抽奖后,需根据抽奖前后人数与奖项变化逻辑,实时更新显示内容。代码如下:
# 更新奖项显示
def update_current_award_display(self):
# 检查奖项列表 self.awards 是否为空
if not self.awards:
self.current_award_index = -1 # 如果奖项列表为空,将当前奖项索引设置为 -1
self.current_award_label.config(text="当前奖项:未设置") # 更新当前奖项标签的文本为“当前奖项:未设置”
self.remaining_label.config(text="剩余名额:0") # 更新剩余名额标签的文本为“剩余名额:0”
return
# 如果当前奖项索引为 -1,将其设置为 0,表示从第一个奖项开始
if self.current_award_index == -1:
self.current_award_index = 0
# 检查当前奖项索引是否在有效范围内(大于等于 0 且小于奖项列表的长度)
if 0 <= self.current_award_index < len(self.awards):
current_award = self.awards[self.current_award_index] # 获取当前奖项的信息
remaining = current_award["quota"] - len(current_award["winners"]) # 计算当前奖项的剩余名额,即总名额减去已中奖人数
self.current_award_label.config(text=f"当前奖项:{current_award['name']}") # 更新当前奖项标签的文本,显示当前奖项的名称
# 更新剩余名额标签的文本,显示剩余名额以及已中奖人数和总名额的信息
self.remaining_label.config(
text=f"剩余名额:{remaining} ({len(current_award['winners'])}/{current_award['quota']})")
else:
# 如果当前奖项索引超出了有效范围,说明所有奖项都已抽取完毕
self.current_award_label.config(text="所有奖项已抽取完毕")
self.remaining_label.config(text="剩余名额:0")
上述代码用于更新显示奖项信息。即根据当前奖项列表情况和奖项索引,动态更新 “当前奖项” 和 “剩余名额” 标签文本,方便我们获取实时信息。
·③数据处理
数据导入
界面创建完成后,下一步是数据处理,我们先从员工名单数据导入开始。代码如下:
#数据导入
def import_employees(self):
# 弹出文件选择对话框,让用户选择一个 Excel 文件
file_path = filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx")])
# 检查用户是否选择了文件
if file_path:
try:
df = pd.read_excel(file_path) # 使用 pandas 库读取用户选择的 Excel 文件
# 检查 DataFrame 中是否包含 '姓名' 列
if "姓名" not in df.columns:
messagebox.showerror("错误", "Excel文件中必须包含'姓名'列") # 如果不包含 '姓名' 列,弹出错误消息框提示用户
return
self.original_employees = df["姓名"].tolist() # 将 '姓名' 列的数据转换为列表,并赋值给 self.original_employees
self.employees = self.original_employees.copy() # 复制一份 self.original_employees 列表,并赋值给 self.employees
self.employee_count_label.config(text=f"已加载员工:{len(self.employees)}人") # 更新员工数量标签的文本,显示当前已加载的员工数量
messagebox.showinfo("成功", "员工名单导入成功!") # 弹出信息消息框,提示用户员工名单导入成功
except Exception as e: # 如果读取文件过程中出现异常,弹出错误消息框显示具体错误信息
messagebox.showerror("错误", f"文件读取失败:{str(e)}")
此处需注意两点:
一、需在Pycharm设置中安装openpyxl插件,用于识别Excel文件。
安装步骤: (ctrl+alt+S打开设置→项目→Python解释器→点击“+”号→搜索并安装)
二、Excel文件中,第一行必须含有“姓名”一列,否则无法导入名单。(示例如下图)
数据重置
同时,提供人员名单数据重置选项,允许用户将员工列表恢复到最初导入时的状态。代码如下:
# 数据重置
def reset_employees(self):
# 弹出确认对话框,询问用户是否确定要重置员工名单
if messagebox.askyesno("确认", "确定要重置员工名单吗?"):
# 如果用户点击了 '是',将 self.employees 重置为 self.original_employees 的副本
self.employees = self.original_employees.copy()
# 更新员工数量标签的文本,显示重置后的员工数量
self.employee_count_label.config(text=f"已加载员工:{len(self.employees)}人")
上述两个方法通过tkinter库的对话框和消息框与用户进行交互,增强程序的用户体验。
奖项管理
为满足自定义添加奖项的功能,需创建一个子窗口进行奖项配置管理。代码如下:
# 奖项管理
def manage_awards(self):
# 创建子窗口用于奖项管理
award_window = tk.Toplevel(self.root)
award_window.title("奖项管理")
# 创建并布局“奖项名称”标签和输入框
tk.Label(award_window, text="奖项名称:").grid(row=0, column=0)
name_entry = tk.Entry(award_window)
name_entry.grid(row=0, column=1)
tk.Label(award_window, text="获奖人数:").grid(row=1, column=0)
count_entry = tk.Entry(award_window)
count_entry.grid(row=1, column=1)
# 在关闭奖项管理窗口时刷新界面
award_window.protocol('WM_DELETE_WINDOW', lambda: self.on_award_window_close(award_window))
# 添加奖项逻辑
def add_award():
# 获取输入的奖项名称和获奖人数,并去除首尾空格
name = name_entry.get().strip()
count_str = count_entry.get().strip()
# 用户数据校验,检查奖项名称是否为空
if not name:
messagebox.showerror("错误", "奖项名称不能为空")
return
try:
count = int(count_str) # 将获奖人数转换为整数
if count <= 0:
raise ValueError
except ValueError:
# 处理输入不是有效正整数的情况
messagebox.showerror("错误", "请输入有效的正整数")
return
# 检查奖项名称是否已存在
if any(award["name"] == name for award in self.awards):
messagebox.showerror("错误", "该奖项名称已存在")
return
# 将新奖项添加到奖项列表中
self.awards.append({
"name": name,
"quota": count,
"winners": []
})
update_listbox() # 更新列表框显示
# 清空输入框
name_entry.delete(0, tk.END)
count_entry.delete(0, tk.END)
self.update_current_award_display() # 更新当前奖项显示
# 删除奖项逻辑
def delete_award():
selected = listbox.curselection() # 获取列表框中选中的项
if not selected:
# 处理未选中任何项的情况
messagebox.showwarning("警告", "请先选择奖项再删除")
return
index = selected[0]
# 检查该奖项是否已有中奖者
if len(self.awards[index]["winners"]) > 0:
messagebox.showwarning("警告", "该奖项已有中奖者,不能删除")
return
del self.awards[index] # 删除选中的奖项
update_listbox() # 更新列表框显示
self.update_current_award_display() # 更新当前奖项显示
# 创建并布局“添加奖项并保存”按钮
add_btn = tk.Button(award_window, text="添加奖项并保存", command=add_award)
add_btn.grid(row=2, columnspan=2)
# 创建并布局“删除选中奖项”按钮
del_btn = tk.Button(award_window, text="删除选中奖项", command=delete_award)
del_btn.grid(row=4, columnspan=2)
# 创建并布局列表框,用于显示所有奖项
listbox = tk.Listbox(award_window, width=50)
listbox.grid(row=3, columnspan=2)
# 更新列表逻辑
def update_listbox():
listbox.delete(0, tk.END) # 清空列表框
# 遍历所有奖项,将奖项信息插入列表框
for award in self.awards:
listbox.insert(tk.END, f"{award['name']} - {award['quota']}人")
update_listbox() # 初始化列表框显示
# 设置奖项管理窗口为模态窗口,使其始终位于主窗口之上
award_window.transient(self.root)
award_window.grab_set()
self.root.wait_window(award_window) # 等待奖项管理窗口关闭
上述代码中,使用函数嵌套方法,创建了一个奖项管理窗口,用户可以在该窗口中添加和删除奖项,提升抽奖交互体验。
奖项检查
为确保抽奖数据精准无误,需对奖项与奖项配额进行检查。代码如下:
# 检查是否还有未完成的奖项
def check_awards_status(self):
# 检查当前奖项索引是否超出奖项列表长度
if self.current_award_index >= len(self.awards):
return False
# 获取当前奖项信息
current_award = self.awards[self.current_award_index]
# 检查当前奖项的中奖人数是否达到配额
return len(current_award['winners']) < current_award['quota']
上述方法用于检查当前奖项是否还有剩余中奖名额。如果当前奖项索引超出了奖项列表的长度,或当前奖项的中奖人数已经达到了配额,则返回False,否则返回True。
UI界面刷新
关闭奖项管理窗口时,刷新GUI,确保数据同步显示。代码如下:
# UI界面刷新
def on_award_window_close(self, window):
window.destroy() # 销毁奖项管理窗口
self.refresh_ui() # 刷新用户界面
self.update_button_state() # 更新按钮状态
上述方法在用户关闭窗口时,会销毁该窗口,并调用refresh_ui方法刷新用户界面,调用update_button_state方法更新按钮状态。
·特色功能
背景设置
为提升用户使用体验,设置自定义背景图功能。代码如下:
# 背景设置
def set_background(self):
# 打开文件选择对话框
file_path = filedialog.askopenfilename(filetypes=[("图片文件", "*.jpg *.png")])
# 检查是否选择了文件
if file_path:
try:
# 加载图片
self.background_path = file_path
self.original_image = Image.open(file_path)
# 调整背景图片大小
self.resize_background()
except Exception as e: # 异常处理
messagebox.showerror("错误", f"图片加载失败:{str(e)}")
上述代码中,用于让用户选择一张图片作为抽奖系统的背景图,并尝试加载该图片。调用resize_background方法,根据当前窗口大小对背景图进行调整。
背景调整
调整图片,并将调整后的图片显示在界面上。代码如下:
# 背景调整
def resize_background(self):
if self.original_image: # 判断是否已经加载了原始图片,如果存在原始图片,则继续执行后续操作
try:
# 获取窗口的宽度和高度
window_width = self.root.winfo_width()
window_height = self.root.winfo_height()
# 调整图片大小
resized_image = self.original_image.resize((window_width, window_height), Image.Resampling.LANCZOS)
# 将调整后的图片转换为 Tkinter 可用的图片对象
self.bg_image = ImageTk.PhotoImage(resized_image)
self.bg_label.config(image=self.bg_image) # 更新背景标签的图片
except Exception as e: # 异常处理
print(f"调整背景大小出错: {str(e)}")
上述代码中,使用resize方法将原始图片调整为与窗口大小相同的尺寸。
注意:如遇到背景图覆盖主界面的情况,导致主界面信息显示异常,可点击窗口右上角的 “窗口” 图标尝试解决问题。
·④核心抽奖程序
抽奖逻辑
下面,将编写程序启动核心抽奖流程。代码如下:
# 抽奖逻辑(···抽奖核心流程···)
def start_lottery(self):
# 状态检查
if not self.awards:
messagebox.showwarning("警告", "请先设置奖项")
return
# 所有奖项已完成的情况处理
if self.current_award_index >= len(self.awards):
messagebox.showinfo('提示', '所有奖项已抽取完毕,感谢参与!')
self.start_btn.config(state='disabled')
return
# 自动切换到下一个未完成奖项
while self.current_award_index < len(self.awards):
current_award = self.awards[self.current_award_index]
if len(current_award['winners']) < current_award['quota']:
break
self.current_award_index += 1
# 最终状态检查
if self.current_award_index >= len(self.awards):
messagebox.showinfo('提示', '所有奖项已抽取完毕,感谢参与!')
self.start_btn.config(state='disabled')
return
# 处理索引越界情况
if self.current_award_index >= len(self.awards):
self.current_award_index = 0
# 检查是否导入了员工名单
if not self.employees:
messagebox.showwarning("警告", "请导入员工名单文件")
return
# 获取当前奖项
current_award = self.awards[self.current_award_index]
# 检查当前奖项是否已满额
if len(current_award["winners"]) >= current_award["quota"]:
self.current_award_index += 1
if self.current_award_index >= len(self.awards):
messagebox.showinfo("提示", "所有奖项已抽取完毕,感谢参与!")
return
# 启动抽奖动画
self.is_rolling = True # 设置抽奖状态为正在进行
self.start_btn.config(state="disabled") # 禁用开始抽奖按钮
self.stop_btn.config(state="normal") # 启用停止抽奖按钮
self.roll() # 开始滚动显示抽奖结果
上述代码,启动抽奖流程之前,会进行一系列检查,确保满足抽奖条件(已设置奖项、有可用员工名单、当前奖项还有剩余名额等)。如条件满足则开始启动抽奖程序。
滚动效果
为营造良好抽奖氛围,将使用名字滚动显示的效果。代码如下:
# 滚动效果
def roll(self):
if self.is_rolling and self.employees:
current_name = random.choice(self.employees) # 随机选择一个员工姓名
self.name_label.config(text=current_name) # 更新显示的姓名
self.root.after(50, self.roll) # 每隔 50 毫秒再次调用 roll 方法
else:
self.stop_lottery() # 停止抽奖
上述方法中,用于滚动显示抽奖结果,每隔50毫秒随机选择一个员工姓名。
停止抽奖
开始抽奖后,需要停止抽奖来显示中奖结果,并实时记录更新抽奖动态。代码如下:
# 停止抽奖
def stop_lottery(self):
# 检查当前是否正在抽奖,如果没有则直接返回
if not self.is_rolling:
return
self.is_rolling = False # 将抽奖状态设置为停止
self.start_btn.config(state="normal") # 启用开始抽奖按钮
self.stop_btn.config(state="disabled") # 禁用停止抽奖按钮
if not self.employees: # 检查员工名单是否为空,如果为空则弹出警告框并返回
messagebox.showwarning("警告", "员工名单已空")
return
current_winner = self.name_label.cget("text") # 获取当前显示的中奖者姓名
if current_winner not in self.employees: # 检查该中奖者是否在员工名单中,如果不在则直接返回
return
try:
# 记录抽奖者
current_award = self.awards[self.current_award_index] # 获取当前正在抽取的奖项
current_award["winners"].append(current_winner) # 将中奖者添加到当前奖项的中奖者列表中
# 更新界面
self.winner_list.insert("", "end", values=(current_winner, current_award["name"])) # 将中奖者信息插入到中奖者列表的界面显示中
self.employees.remove(current_winner) # 从员工名单中移除该中奖者
self.employee_count_label.config(text=f"已加载员工:{len(self.employees)}人") # 更新界面上显示的员工数量
self.update_current_award_display() # 更新当前奖项的显示信息(如剩余名额等)
# 检查奖项完成状态
if len(current_award["winners"]) >= current_award["quota"]: # 检查当前奖项的中奖者数量是否达到配额
self.current_award_index += 1 # 如果达到配额,将当前奖项索引加 1,准备抽取下一个奖项
if self.current_award_index < len(self.awards): # 检查是否还有下一个奖项
messagebox.showinfo("提示", f"{current_award['name']}抽奖完成,即将开始下一个奖项") # 如果有下一个奖项,弹出提示框告知用户当前奖项抽奖完成,即将开始下一个奖项
self.update_current_award_display() # 再次更新当前奖项的显示信息
except IndexError: # 捕获索引错误,如奖项索引越界
messagebox.showerror("错误", "无效的奖项索引")
except ValueError: # 捕获值错误,如在员工名单中找不到中奖者
messagebox.showerror("错误", "找不到中奖员工")
# 最终状态检查
if self.current_award_index >= len(self.awards): # 检查是否所有奖项都已抽取完毕
self.start_btn.config(state='disabled') # 如果所有奖项都已抽完,禁用开始抽奖按钮
messagebox.showinfo('提示', '所有奖项已抽取完毕,感谢参与!') # 弹出提示框告知用户所有奖项已抽取完毕
# 强制界面刷新
self.root.update_idletasks() # 强制刷新界面以确保更新生效
return
# 更新按钮状态
if not self.check_awards_status(): # 检查当前是否还有可用的奖项(即还有剩余名额的奖项)
self.start_btn.config(state='disabled') # 如果没有可用奖项,禁用开始抽奖按钮
# 强制界面刷新
self.root.update_idletasks()
上述代码中,使用stop_lottery方法在抽奖停止时,完成了一系列与抽奖结果处理和界面更新相关操作,确保系统状态和界面显示一致性,同时对可能出现的异常情况进行了处理,提高系统健壮性。
·⑤结果处理
结果导出
为完善系统体验,支持将中奖结果导出至Excel文件。代码如下:
# 抽奖结果导出
def export_winners(self):
# 检查是否有中奖数据
if not any(len(award["winners"]) > 0 for award in self.awards):
messagebox.showwarning("警告", "没有中奖数据可导出")
return
try:
data = [] # 整理中奖数据
for award in self.awards:
for winner in award["winners"]:
data.append([winner, award["name"]])
df = pd.DataFrame(data, columns=["姓名", "奖项"]) # 创建 DataFrame 对象
# 弹出保存文件对话框
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx")]
)
if file_path: # 如果用户选择了保存路径
df.to_excel(file_path, index=False) # 将 DataFrame 保存为 Excel 文件
messagebox.showinfo("成功", "导出成功!")
# 异常处理
except PermissionError:
messagebox.showerror("错误", "文件被占用,请关闭文件后重试")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
程序入口
最后写入程序入口,完成封装,作为程序入口创建主窗口并启动抽奖系统主界面。代码如下:
if __name__ == "__main__" :
root = tk.TK()
app = LotteryApp(root)
root.mainloop()
综上所述,您已完成公司年会抽奖系统。
·完整代码
感谢您能用心看到这,相信您已有所收获。成功不是一蹴而就,而是来自坚定的信念,奔腾不息的热情和每一步的脚踏实地。
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import random
from PIL import ImageTk, Image
class LotteryApp:
def __init__(self, root):
self.root = root
self.root.title("公司年会抽奖系统 CSDN @Liam Jaming 源码开发")
self.root.geometry('1920x1080')
self.root.minsize(800, 600)
self.employees = []
self.original_employees = []
self.winners = []
self.is_rolling = False
self.awards = []
self.current_award_index = -1
self.background_path = None
self.original_image = None
self.create_menu()
self.create_widgets()
self.create_award_frame()
self.root.bind('<Configure>', lambda e: self.resize_background())
self.update_button_state()
def create_menu(self):
menu_bar = tk.Menu(self.root)
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label="导入员工名单", command=self.import_employees)
file_menu.add_command(label="重置员工名单", command=self.reset_employees)
file_menu.add_command(label="导出中奖结果", command=self.export_winners)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
menu_bar.add_cascade(label="文件", menu=file_menu)
settings_menu = tk.Menu(menu_bar, tearoff=0)
settings_menu.add_command(label="设置背景", command=self.set_background)
settings_menu.add_command(label="管理奖项", command=self.manage_awards)
menu_bar.add_cascade(label="设置", menu=settings_menu)
self.root.config(menu=menu_bar)
def create_widgets(self):
self.bg_label = tk.Label(self.root)
self.bg_label.place(x=0, y=0, relwidth=1, relheight=1)
main_frame = tk.Frame(self.bg_label)
main_frame.place(relx=0.5, rely=0.5, anchor="center")
self.employee_count_label = tk.Label(main_frame, text="已加载员工:0人", font=("Arial", 12))
self.employee_count_label.pack(pady=5)
self.name_label = tk.Label(main_frame, text="CSDN @Liam Jaming", font=("Arial", 48), bg="white", width=20)
self.name_label.pack(pady=20)
btn_frame = tk.Frame(main_frame)
btn_frame.pack(pady=10)
self.start_btn = tk.Button(btn_frame, text="开始抽奖", command=self.start_lottery, width=15)
self.start_btn.pack(side="left", padx=5)
self.stop_btn = tk.Button(btn_frame, text="停止抽奖", command=self.stop_lottery, width=15, state="disabled")
self.stop_btn.pack(side="left", padx=5)
self.winner_list = ttk.Treeview(main_frame, columns=("name", "award"), show="headings", height=10)
self.winner_list.heading("name", text="姓名")
self.winner_list.heading("award", text="奖项")
self.winner_list.pack(pady=10)
self.refresh_btn = tk.Button(btn_frame, text='刷新界面', command=self.refresh_ui, width=15)
self.refresh_btn.pack(side='left', padx=5)
def refresh_ui(self):
self.root.update_idletasks()
self.resize_background()
self.update_current_award_display()
self.employee_count_label.config(text=f'已加载员工:{len(self.employees)}人')
self.update_button_state()
def update_button_state(self):
if self.current_award_index >= len(self.awards):
self.start_btn.config(state='disabled')
else:
self.start_btn.config(state='normal')
def create_award_frame(self):
self.award_frame = tk.Frame(self.bg_label)
self.award_frame.place(relx=0.1, rely=0.1)
self.current_award_label = tk.Label(self.award_frame, text="当前奖项:未设置", font=("Arial", 12))
self.current_award_label.pack()
self.remaining_label = tk.Label(self.award_frame, text="剩余名额:0", font=("Arial", 12))
self.remaining_label.pack()
def update_current_award_display(self):
if not self.awards:
self.current_award_index = -1
self.current_award_label.config(text="当前奖项:未设置")
self.remaining_label.config(text="剩余名额:0")
return
if self.current_award_index == -1:
self.current_award_index = 0
if 0 <= self.current_award_index < len(self.awards):
current_award = self.awards[self.current_award_index]
remaining = current_award["quota"] - len(current_award["winners"])
self.current_award_label.config(text=f"当前奖项:{current_award['name']}")
self.remaining_label.config(
text=f"剩余名额:{remaining} ({len(current_award['winners'])}/{current_award['quota']})")
else:
self.current_award_label.config(text="所有奖项已抽取完毕")
self.remaining_label.config(text="剩余名额:0")
def import_employees(self):
file_path = filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx")])
if file_path:
try:
df = pd.read_excel(file_path)
if "姓名" not in df.columns:
messagebox.showerror("错误", "Excel文件中必须包含'姓名'列")
return
self.original_employees = df["姓名"].tolist()
self.employees = self.original_employees.copy()
self.employee_count_label.config(text=f"已加载员工:{len(self.employees)}人")
messagebox.showinfo("成功", "员工名单导入成功!")
except Exception as e:
messagebox.showerror("错误", f"文件读取失败:{str(e)}")
def reset_employees(self):
if messagebox.askyesno("确认", "确定要重置员工名单吗?"):
self.employees = self.original_employees.copy()
self.employee_count_label.config(text=f"已加载员工:{len(self.employees)}人")
def manage_awards(self):
award_window = tk.Toplevel(self.root)
award_window.title("奖项管理")
tk.Label(award_window, text="奖项名称:").grid(row=0, column=0)
name_entry = tk.Entry(award_window)
name_entry.grid(row=0, column=1)
tk.Label(award_window, text="获奖人数:").grid(row=1, column=0)
count_entry = tk.Entry(award_window)
count_entry.grid(row=1, column=1)
award_window.protocol('WM_DELETE_WINDOW', lambda: self.on_award_window_close(award_window))
def add_award():
name = name_entry.get().strip()
count_str = count_entry.get().strip()
if not name:
messagebox.showerror("错误", "奖项名称不能为空")
return
try:
count = int(count_str)
if count <= 0:
raise ValueError
except ValueError:
messagebox.showerror("错误", "请输入有效的正整数")
return
if any(award["name"] == name for award in self.awards):
messagebox.showerror("错误", "该奖项名称已存在")
return
self.awards.append({
"name": name,
"quota": count,
"winners": []
})
update_listbox()
name_entry.delete(0, tk.END)
count_entry.delete(0, tk.END)
self.update_current_award_display()
def delete_award():
selected = listbox.curselection()
if not selected:
messagebox.showwarning("警告", "请先选择奖项再删除")
return
index = selected[0]
if len(self.awards[index]["winners"]) > 0:
messagebox.showwarning("警告", "该奖项已有中奖者,不能删除")
return
del self.awards[index]
update_listbox()
self.update_current_award_display()
add_btn = tk.Button(award_window, text="添加奖项并保存", command=add_award)
add_btn.grid(row=2, columnspan=2)
del_btn = tk.Button(award_window, text="删除选中奖项", command=delete_award)
del_btn.grid(row=4, columnspan=2)
listbox = tk.Listbox(award_window, width=50)
listbox.grid(row=3, columnspan=2)
def update_listbox():
listbox.delete(0, tk.END)
for award in self.awards:
listbox.insert(tk.END, f"{award['name']} - {award['quota']}人")
update_listbox()
award_window.transient(self.root)
award_window.grab_set()
self.root.wait_window(award_window)
def check_awards_status(self):
if self.current_award_index >= len(self.awards):
return False
current_award = self.awards[self.current_award_index]
return len(current_award['winners']) < current_award['quota']
def on_award_window_close(self, window):
window.destroy()
self.refresh_ui()
self.update_button_state()
def set_background(self):
file_path = filedialog.askopenfilename(filetypes=[("图片文件", "*.jpg *.png")])
if file_path:
try:
self.background_path = file_path
self.original_image = Image.open(file_path)
self.resize_background()
except Exception as e:
messagebox.showerror("错误", f"图片加载失败:{str(e)}")
def resize_background(self):
if self.original_image:
try:
window_width = self.root.winfo_width()
window_height = self.root.winfo_height()
resized_image = self.original_image.resize((window_width, window_height), Image.Resampling.LANCZOS)
self.bg_image = ImageTk.PhotoImage(resized_image)
self.bg_label.config(image=self.bg_image)
except Exception as e:
print(f"调整背景大小出错: {str(e)}")
def start_lottery(self):
if not self.awards:
messagebox.showwarning("警告", "请先设置奖项")
return
if self.current_award_index >= len(self.awards):
messagebox.showinfo('提示', '所有奖项已抽取完毕,感谢参与!')
self.start_btn.config(state='disabled')
return
while self.current_award_index < len(self.awards):
current_award = self.awards[self.current_award_index]
if len(current_award['winners']) < current_award['quota']:
break
self.current_award_index += 1
if self.current_award_index >= len(self.awards):
messagebox.showinfo('提示', '所有奖项已抽取完毕,感谢参与!')
self.start_btn.config(state='disabled')
return
if self.current_award_index >= len(self.awards):
self.current_award_index = 0
if not self.employees:
messagebox.showwarning("警告", "请导入员工名单文件")
return
current_award = self.awards[self.current_award_index]
if len(current_award["winners"]) >= current_award["quota"]:
self.current_award_index += 1
if self.current_award_index >= len(self.awards):
messagebox.showinfo("提示", "所有奖项已抽取完毕,感谢参与!")
return
self.is_rolling = True
self.start_btn.config(state="disabled")
self.stop_btn.config(state="normal")
self.roll()
def roll(self):
if self.is_rolling and self.employees:
current_name = random.choice(self.employees)
self.name_label.config(text=current_name)
self.root.after(50, self.roll)
else:
self.stop_lottery()
def stop_lottery(self):
if not self.is_rolling:
return
self.is_rolling = False
self.start_btn.config(state="normal")
self.stop_btn.config(state="disabled")
if not self.employees:
messagebox.showwarning("警告", "员工名单已空")
return
current_winner = self.name_label.cget("text")
if current_winner not in self.employees:
return
try:
current_award = self.awards[self.current_award_index]
current_award["winners"].append(current_winner)
self.winner_list.insert("", "end", values=(current_winner, current_award["name"]))
self.employees.remove(current_winner)
self.employee_count_label.config(text=f"已加载员工:{len(self.employees)}人")
self.update_current_award_display()
if len(current_award["winners"]) >= current_award["quota"]:
self.current_award_index += 1
if self.current_award_index < len(self.awards):
messagebox.showinfo("提示", f"{current_award['name']}抽奖完成,即将开始下一个奖项")
self.update_current_award_display()
except IndexError:
messagebox.showerror("错误", "无效的奖项索引")
except ValueError:
messagebox.showerror("错误", "找不到中奖员工")
if self.current_award_index >= len(self.awards):
self.start_btn.config(state='disabled')
messagebox.showinfo('提示', '所有奖项已抽取完毕,感谢参与!')
self.root.update_idletasks()
return
if not self.check_awards_status():
self.start_btn.config(state='disabled')
self.root.update_idletasks()
def export_winners(self):
if not any(len(award["winners"]) > 0 for award in self.awards):
messagebox.showwarning("警告", "没有中奖数据可导出")
return
try:
data = []
for award in self.awards:
for winner in award["winners"]:
data.append([winner, award["name"]])
df = pd.DataFrame(data, columns=["姓名", "奖项"])
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx")])
if file_path:
df.to_excel(file_path, index=False)
messagebox.showinfo("成功", "导出成功!")
except PermissionError:
messagebox.showerror("错误", "文件被占用,请关闭文件后重试")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = LotteryApp(root)
root.mainloop()
使用指南
运行程序→导入员工名单→添加奖项保存后点击“x”→点击 “开始抽奖” →完成抽奖→导出中奖结果(如有需要)
注意:名单文件仅支持Excel文件,且 “姓名” 一列需在第一行。
·总结
本系统基于Python的Tkinter GUI框架,实现了企业年会场景下的智能化抽奖解决方案。系统突破传统抽奖模式,通过Excel数据管理、可视化界面交互、智能随机算法三大核心模块,实现了从员工名单管理到奖项配置、实时抽奖的全流程数字化。该抽奖系统完美展现了如何将传统Python脚本升级为企业级解决方案。通过模块化设计、健壮性增强、可视化优化,打造出可直接部署的抽奖平台。
该抽奖系统还存在大量可持续优化空间,若有更多功能需求未能满足,敬请谅解。
最后,欢迎广大读者在评论区积极交流指正。下文再见。
更多推荐
所有评论(0)