水泊梁山迎毛线的圣诞?
还 Python 抽奖助好汉?你来抽个我看看!
——息怒息怒,是这样:
下周我们公司的圣诞 Party 活动安排有抽奖环节,由于不方便采用手机抽奖,且目前选用的电脑端在线抽奖会出现卡顿情况,最近我就尝试着用 Python 实现抽奖功能。目前进展不错,也想分享给大家,由于涉及隐私嘛,做了番保密修改,将要展示的抽奖公司搬到了水泊梁山,助力 108 好汉进行抽奖。
人家需不需要呢咱也不敢问,反正抽奖程序是做好了,请大家过目:
运行前准备好参与抽奖的好汉名单,本程序会自动读取表格文件,将待抽奖的各位好汉展示在左侧奖池中,只要点击图中小鹿的红鼻子,会默认抽取三等奖(共十位)。
文章结尾附有完整版细节视频。可以看到,抽奖时好汉名字会在中央滚动展示,当再次点击红鼻子完成单次抽奖时,中奖的名字会从左侧奖池转移到右侧获奖榜上。
一、二等奖分别五位,与三等奖抽取的区别在于要先选择右侧 1 号金色或 2 号银色标志,根据点选标志抽取相应奖项。
当然,如果三等奖未完成,也可以点选 1 号金标 或 2 号银标 先行抽取,之后再通过点 3 号铜标 完成三等奖的抽取。在获奖榜满额时,再次抽奖会触发弹框提醒。
此外,左下方的 "Let's go!" 字样是重置开关,点击会重新载入数据进行抽奖。
当然,除了鼠标点击事件的控制,该抽奖程序也添加了键盘控制:例如数字键可以直接选择奖项,空格键等同于红鼻子控制,Esc 键退出抽奖等。
如上便是目前抽奖程序的功能和界面了,下面分享下我在设计与编码过程中的路线和想法。
需求与设计
首先归纳下整个抽奖程序的需求:
-
基本功能是实现名单中的随机抽取
-
活动穿插三轮抽奖,不能重复中奖
-
尽量美观
-
打消暗箱操作的怀疑
基于总结的需求点,我整理的设计方案如下:
-
名单自动载入至列表中
-
随机抽取名单列表,抽中后移除该元素
-
图形界面展现抽奖过程和结果,选用 tkinter 来实现
-
绑定鼠标、键盘控制抽奖过程
滚动随机数
首先搜索 “Python 抽奖程序”,在众多素材中看到了一份可以 tkinter 界面动态展示随机数的代码。拷贝过来运行,效果如图:
点击图中按钮时,屏幕中滚动出现 1000 以内的随机数,代码逻辑如下:
# while 循环控制界面
while True:
# 延时操作
time.sleep(0.1)
# 产生随机数
r = random.choice(range(1000))
# 将随机数赋给页面的组件上
self.btn1['text'] = r
在 while 循环中设置 0.1 秒延迟,通过 random.choice() 在 range(1000) 生成随机数,将其绑定在 tkinter 界面上展现。这样随着 while 循环的进行,每个随机数在界面上停留 0.1 秒,就产生了滚动随机数的效果。
由于该代码中将整个抽奖过程定义为了一个对象,果断选取此份代码当作核心代码来予以拓展,也借此机会加深下相关理解。
我们要做的就是先消化吸收此代码,然后站在其肩膀上定制并完善自己需要的功能。
import tkinter
import time
import threading
import random
class Choujiang:
#初始化魔术方法
def __init__(self):
#准备好界面
self.root = tkinter.Tk()
self.root.title('lowB版转盘')
self.root.minsize(600, 600)
# 声明一个是否按下开始的变量
self.isloop = False
self.newloop = False
#调用设置界面的方法
self.setwindow()
self.root.mainloop()
#界面布局方法
def setwindow(self):
#开始停止按钮
self.btn_start = tkinter.Button(self.root, text = '开始/停止', command = self.newtask,bg='gold')
self.btn_start.place(x=250, y=250, width=100, height=50)
self.btn1 = tkinter.Button(self.root, text='', bg='red')
self.btn1.config(font=("Courier", 50))
self.btn1.place(x=200, y=130, width=200, height=50)
self.girlfrends = list(range(1000))
def rounds(self):
# 判断是否开始循环
if self.isloop == True:
return
# 初始化计数 变量
i = 0
# 死循环
while True:
if self.newloop == True:
self.newloop = False
return
# 延时操作
time.sleep(0.1)
# 将所有的组件背景变为白色
r = random.choice(range(1000))
self.btn1['text'] = r
# 建立一个新线程的函数
def newtask(self):
if self.isloop == False:
# 建立线程
t = threading.Thread(target = self.rounds)
# 开启线程运行
t.start()
# 设置循环开始标志ask(self):nknewtas
self.isloop = True
elif self.isloop == True:
self.isloop = False
self.newloop = True
c = Choujiang()
滚动名单
因为公司有提供员工名做抽奖用,类比着来,也准备了一份梁山好汉的名单表格。基于刚刚动态展示随机数的代码,我们要做的是载入表格,将名单数据转化为列表。
# 读取表格中的名单数据,存入列表中
data = pd.read_excel("demo.xlsx")
name_list=[item for item in data['名字']]
把刚代码中的提取随机数代码替换成 random.choice(名单列表) 就可以了。
# 仍是 while 循环控制界面
while True:
# 延时操作
time.sleep(0.1)
# 将随机数抽取范围替换成名字列表
r = random.choice(name_list)
# 将随机名字赋值给页面组件
self.btn1['text'] = r
效果如下:
界面初始布局
在动态随机名字的界面基础上,添加背景图展现:
# 准备好界面
self.root = tkinter.Tk()
self.root.title("水泊梁山 2019 圣诞大抽奖")
self.root.geometry('1360x700+0+0') # 定义界面大小
self.root.resizable(False, False)
# 添加背景图片
self.canvas = tkinter.Canvas(self.root,
width=1360, # 指定Canvas组件的宽度
height=700, # 指定Canvas组件的高度
bg='white') # 指定Canvas组件的背景色
self.image = Image.open("static/background.png")
self.im = ImageTk.PhotoImage(self.image)
self.canvas.create_image(680, 350, image=self.im) # 使用create_image将图片添加到Canvas组件中
self.canvas.pack()
基于奖项划分,添加相应的按钮控制和中奖名单展现:
# 以一等奖为例,此处代码只是示例,参数不准确
# 添加一等奖的按钮组件 Button
self.first = tkinter.Button(self.root, text = '一等奖', command = self.set_first,bg='gold')
# 将该按钮置于界面相应位置
self.first.place(x=1070, y=180, width=80, height=50)
# 设置获奖榜展示字体
myFont = font.Font(size=20)
# 添加一等奖的获奖名单展示组件 Listbox
self.target_1 = tkinter.Listbox(self.root,height=5,font=myFont,bg="lemonchiffon",bd=0,fg="olive")
# 将展示组件添加到界面中
self.target_1.place(x=1080, y=240, width=70, height=130)
添加总名单的展示区域以及重设按钮:
# 总名单展示区域选用 Text 组件
self.source = tkinter.Text(self.root,bg="navajowhite",fg="dimgray")
# 将该组件添加到界面中
self.source.place(x=100, y=250, width=300, height=290)
# 向组件中插入数据
self.source.insert(tkinter.END,("、").join(self.data))
# 重启按钮 Button
self.btn_reset = tkinter.Button(self.root, text = '重启', command = self.reset ,bg='gold')
# 重启按钮添加到界面中
self.btn_reset.place(x=180, y=550, width=100, height=50)
经过上述代码的布局,最初界面效果如下图:
进一步绑定并对应好数据,便可实现基本功能。
界面优化
首先是布局,两个思路:其一是在背景图上做文章,相应位置添加装饰元素;其二是 tkinter 组件上下功夫,优化组件展示样式或者替换成更美观的样式。
说实话,写功能代码的时间和此部分界面优化的时间比起来真的小巫见大巫了。首先并不清楚能不能实现某种样式,其次 tkinter 自己也不熟,好多对组件都是自己在 Photoshop 中定位并加工处理的。
背景图优化
背景图方面,添加了图中的小鹿,原图是这样的:
通过 PS 将其抠图放到背景图中当作“摇奖展示台”。同时背景图添加公司名称、活动作为标题,并在左侧为总名单展示区域添加边框。最终效果如下:
组件优化
最初的想法是,按钮不好看,尝试隐藏按钮保留功能,尝试无果。最终方案是,将 Button 组件换成 Label 组件,在 Label 中展示相应位置的背景图,通过 bind 绑定鼠标点击事件。换句话说,我将红鼻子区域的图片当成一个 Label 置于界面中并绑定抽奖动作,那么一点击红鼻子就会开启/停止抽奖。
所有用来替换 Button 组件的 Label 图片如上:
-
nose.png 即红鼻子局部图,对应 “开始/停止”;
-
001.png 即 1 号金标图,对应 “一等奖”;
-
002.png 即 2 号银标图,对应 “二等奖”;
-
003.png 即 3 号铜标图,对应 “三等奖”;
-
reset.png 为 "Let's go!" 局部图,对应 “重启”
对于展现获奖名单的 Listbox 组件,利用其每项占一行的特点,通过调整其组件大小和颜色,稍作优化,未做大改。对于被展现名字的处理,我采用的策略是检测到两个字的名字就给其中间加个中文空格,这样所有名字都会转化为三个中文字符,方便统一样式处理。
# temp 获取到被抽中的名字
temp = random.choice(self.data)
# 判断名字是否为2字
if len(temp) == 2:
# 二字名字中间添加个中文字符
r = temp[0]+f"{chr(12288)}"+temp[1]
else:
# 三字名字保持不变(数据中无四字名字出现)
r = temp
至于展现总名单的 Text 组件,简单调整大小、修改背景和字体颜色。
最终再将各组件安排到设计好的位置中,就产生了开头时抽奖程序的界面:
功能添加
因为是 Party 需要,如果鼠标点击不方便的话,还可以对程序添加键盘控制:
# 添加键盘控制
self.root.bind("1", self.set_first)
self.root.bind("2", self.set_second)
self.root.bind("3", self.set_third)
# 空格键 开始/停止
self.root.bind("<space>", self.newtask)
# Esc 键退出程序界面
self.root.bind("<Escape>", self.close)
总结
思路与流程大致如上,现在做完了再回顾,确实,优化细节比实现功能要繁琐的多。尤其是用到 tkinter 图形界面中的诸多组件,要配合着通过 PS 来修改图片样式等。说实话,python 有没长进不知道,PS 技术蹭蹭蹭地涨。
想要从零实现本篇中的程序,可能对 tkinter 的使用、多线程的应用以及对象的了解都要求比较高。但像我这次的选择,基于半成品的 tkinter 抽取随机数代码的基础上去尝试,更多涉及的就是功能完善和界面上优化了,就完成任务而言还是可以加速很多的。
总体来看本次任务完成度,个人感觉还不错,该有的都有、也添加了些小细节展示。后续可以添加导出中奖名单、抽奖过程存档等功能,但我们这边目前还用不到就先不添加了。
整个编码过程也是尽力在找更好的展现或实现方法,可能有些组件或方法自己没接触过没有概念,也欢迎大家予以指点!