·引言

  为满足公司年会抽奖需求,本文章将详细展示一个结构清晰、功能完整的抽奖系统实现方案。该方案兼顾实用性与扩展性,可作为企业级应用程序的基础架构,为公司各类抽奖活动提供技术支持。

温馨提示:由于文章篇幅较长,读者可查找目录栏,各取所需。祝您阅读愉快!


·程序逻辑

  程序思路:通过主类LotteryApp管理整个程序应用,即通过多个自定义函数[def xx_x():]方法实现对应功能并封装在名为LotteryApp的类(class)中,由这个类来运行整个程序。(类似交响乐团,主类像一个乐队指挥,而不同函数像各个演奏者,运行的程序就是交响乐);首先实现核心抽奖程序,去不断丰富拓展更多功能与界面显示,最后实现不断迭代优化、满足使用需求。(本文则直接展示较为完善的代码,请根据自身需求,合理阅读。若有不便,欢迎评论区交流学习。)

  程序逻辑抽奖逻辑即程序逻辑(此处不再过多赘述)


·程序结构

  首先,根据程序使用需求,划分所需结构:采用面向对象设计,主类LotteryApp管理整个应用程序,包含以下核心组件:

  1. GUI界面:基于Tkinter构建独立窗口界面(GUI:图形用户界面)

  2. 数据处理:考虑抽奖名单数据、奖项配置数据、抽奖结果数据等

  3. 业务逻辑:抽奖流程控制(包括抽奖前后人数变化逻辑等)、名单结果处理

  接着,进行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_employeesself.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 像素的间距

  上述代码中,通过tkinterttk库,创建了一个包含多个控件的GUI,用于实现抽奖程序的基本交互。代码中self.start_lotteryself.stop_lotteryself.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脚本升级为企业级解决方案。通过模块化设计、健壮性增强、可视化优化,打造出可直接部署的抽奖平台。

  该抽奖系统还存在大量可持续优化空间,若有更多功能需求未能满足,敬请谅解。

  最后,欢迎广大读者在评论区积极交流指正。下文再见。

Logo

永洪科技,致力于打造全球领先的数据技术厂商,具备从数据应用方案咨询、BI、AIGC智能分析、数字孪生、数据资产、数据治理、数据实施的端到端大数据价值服务能力。

更多推荐