文章目录
- 一、项目背景
- 二、需求分析
- UI界面设计如下:
- 具体需求如下:
- 二、实现思路
- 三、项目关键代码
- 读取excel中的人员名单
- 实现随机滚动抽取
- 主函数中Tkinter的界面相关操作实现
- 窗口相关
- 背景图设置
- 组件相关
- 完整代码
- 四、将程序封装成.exe可执行文件
- 将代码转换成.py文件
- 五、总结与拓展
- 总结:
- 拓展:
一、项目背景
受朋友所托,帮他在公司年会活动上做一个点名抽奖的小工具。经过沟通后,他发给我一个人员名单表格,是xlsx格式的excel工作表,并大概设计了一下抽奖工具的界面以及相关要求。话不多说,马上开始项目流程。
二、需求分析
客户需求总结如下:
UI界面设计如下:
这也是最终我们工具的实现效果,其中包含工具标题“秋夜派对”、一张喜庆的背景图、活动标题“谁是幸运儿?”表示抽奖、抽取人数输入栏、中间被抽取人显示区域、以及开始和结束按钮。
具体需求如下:
- 要求每个被抽取人出现的概率一致,并随机抽取,而不是按名单顺序滚动
- 对于滚动出现的名字,要求滚动速度达到肉眼看不出人名的效果,避免操作员根据人名点击,造成抽奖不是完全随机
- 界面美观,组件居中,并且足够大,因为要在活动的投影上放映。
- 保证可移植性,封装成应用程序,在任何PC机器上都可以无差错运行。
二、实现思路
针对本项目的客户要求,项目类型为面向客户的UI界面可视化问题,并与相关组件发生事件触发以实现目的。具体实现相关思路如下:
1. 本项目使用Python编写,小项目使用Python的GUI库Tkinter来编写比较方便直观。
2. 客户端给的输入为人员名单,是xlsx文件。那么我们需要应用openpyxl库来读取表格中的数据。
3. 实现名单中人员随机滚动需要用到random库以及tk中的after函数来实现给定时间调用递归函数,以此来达到控制滚动速度的目的。
4. 对于Tk中的窗口背景,还需要PIL库来导入图片。
5. 使用pyinstaller将程序封装成.exe可执行文件,相关依赖也要一起封装
其他具体的实现细节详见代码部分。
三、项目关键代码
读取excel中的人员名单
打开excel文件,观察到人员名单如下:
可以看到,人员名单是在sheet1中的第二行第二列(B列)开始的,第一列为人员序号,第一行为每项属性标识,这里只单拿出来第二列作为例子,人名是我随机生成的,如有雷同,纯属巧合!
观察过名单格式后,我们编写读取名单函数:
#获取excel中的名单
def getPeopleList():
#读取已有工作簿:路径,data_only为True表示计算excel中公式的值,本例没用,因为都是人名
workbook = openpyxl.load_workbook("./people2.xlsx", data_only=True)
list = [] #存储人名列表
sheet = workbook[workbook.sheetnames[0]] #得到sheet1,excel第一页
#按行读取,从表格第二行开始读取,直到最后,名字从第二行开始
for row in range(2,sheet.max_row+1):
if sheet.cell(row=row,column=2).value == "": #如果第二列为空则跳到下一行
continue
else:
list.append(sheet.cell(row=row,column=2).value) #第二列不为空则添加到list中
return list #返回得到的名字列表
其中,根据表格数据中的不同,可以改变相关的数值。比如文件名路径、不同sheet、从第几列读取,可以改变代码中相应的值,如果从第二行第一列开始,就将其中的column改为=1。
实现随机滚动抽取
这里我们用两个函数来实现,一个是最底层的randomResult来根据list名单实现随机结果,并传递给窗口。全局变量is_run初始设置为False。另一个是开始按钮的事件触发函数randomRun,这里我们帮朋友多做一个需求,那就是可以同时抽取多人。虽然也可以一个一个抽取,但是如果有其它游戏环节需要分组抽人,那么一组一组抽取显然比一个一个抽取效率要高。具体函数实现如下:
# 随机滚动得到所抽取的人数,递归
def randomResult(list,num):
global is_run
result = random.sample(list, num) #使结果随机
var.set(result) #将随机结果传给窗口,并在窗口中显示随机结果
if is_run:
# 在给定时间后调用函数一次
# 参数:毫秒,需要调用的函数,函数的参数,函数参数
window.after(30, randomResult, list, num) #第一个参数控制速度,越小滚动越快
# 点击开始按钮触发的函数
def randomRun(list):
global is_run
if is_run:#此部分用于避免多次点击开始造成程序重复执行的错误
return
is_run = True
num = insert_point()
#如果输入人数不为空,则调用randomResult
if num:
num = int(num)
randomResult(list,num)
else:
is_run = False
主函数中Tkinter的界面相关操作实现
在主函数中我们实现窗口的UI设计,各种组件布局等,包括背景图实现、label组件、按钮button组件、输入框entry组件等。比如我们要实现前文中UI界面的效果,简单举例如下:
窗口相关
from tkinter import *
if __name__ == '__main__': #主程序
#window = Toplevel()
window = Tk() #初始化TK窗口
# 设定窗口大小
# 格式:w*h±x±y
# wh窗口宽高,xy窗口显示位置横纵坐标,加减表示正方向或负方向,左上角为原点,下和右是正方向
window.geometry('1024x648+250+150')
window.resizable(0,0) #将窗口设置为不可拉伸,否则不同分辨率屏幕组件位置会出问题
window.title('8.25秋夜派对') #设置窗口命名
背景图设置
这里我们使用tk的Canvas画布,将所给图像放在画布上,这里要注意层级关系,不然背景图可能会变成前景图挡住其他组件。背景图我这里是image2.jpg。
import tkinter
from PIL import Image,ImageTk
# 设置背景图
canvas_window = tkinter.Canvas(window,width=1024,height=648)
im = Image.open('./image2.jpg').resize((1024,648))
im_window = ImageTk.PhotoImage(im, master=window)
# 前两个参数是图像中心在画布中的位置,那么一半的话正好图像与窗口大小相同
canvas_window.create_image(1024//2,648//2,image=im_window)
canvas_window.pack(fill=BOTH,expand=YES)
由于我们设置了画布大小与窗口大小一致,所以读取图片时要将图片resize成画布大小,并将图像中心放置在画布中心,以此控制背景图完美契合窗口大小。这段代码可以作为tk窗口设置图像相关的模板范例。
组件相关
大标题我们用Label组件实现,使用place布局方式灵活的将其放置在我们预想的位置,由于Label组件有填充区域,所以将组件背景设置为透明,避免它和背景图直接有色彩不美观。
noteLable1 = Label(text="谁是幸运儿?",font='微软雅黑, 60')
noteLable1.place(anchor=NW,x=280,y=100)
noteLable1.config(bg=window["bg"])
第二行输入框我们用Label和entry组件实现,注意设置相同高度即可
noteLable = Label(text="请输入人数:",font='微软雅黑, 25')
noteLable.place(anchor=NW, x=270, y=230)
input = Entry(canvas_window, show=None,width=10,font='微软雅黑, 25') #设置一个输入框,用于输入抽奖人数
input.place(anchor=NW, x=570, y=230)
开始和结束按钮组件
这里比较重要的是按钮触发事件实现,在Button方法里command用lambda范式的方式执行按钮触发函数,比如开始按钮触发前文的randomRun函数实现名单随机滚动,停止按钮触发finalResult实现暂停功能并显示抽取结果。
startBt = Button(text="开始", command=lambda: randomRun(list=list),font='微软雅黑, 25') #开始按钮
confirmBt = Button(text="结束", command=lambda: finalResult(),font='微软雅黑, 25') #停止按钮
#pack()按钮放置位置
#布局排列:pack,grid,place
#不建议用place,虽然place可以精确组件位置,但是在不同电脑不同分辨率上会有问题
#pack可以选择参数fill填充,expand=True按照窗口大小伸缩
#grid的自适应性也比较强,有configure方法实现frame大小扩充
startBt.place(anchor=NW, x=350, y=500)
confirmBt.place(anchor=NW, x=550, y=500)
完整代码
本项目完整代码如下:
from tkinter import *
import tkinter
import tkinter.messagebox
import random
import openpyxl
from PIL import Image,ImageTk
is_run = False
#获取excel中的名单
def getPeopleList():
#读取已有工作簿:路径,data_only为True表示计算excel中公式的值,本例没用,因为都是人名
workbook = openpyxl.load_workbook("./people2.xlsx", data_only=True)
list = [] #存储人名列表
sheet = workbook[workbook.sheetnames[0]] #得到sheet1,excel第一页
#按行读取,从表格第二行开始读取,直到最后,名字从第二行开始
for row in range(2,sheet.max_row+1):
if sheet.cell(row=row,column=2).value == "": #如果第二列为空则跳到下一行
continue
else:
list.append(sheet.cell(row=row,column=2).value) #第二列不为空则添加到list中
return list #返回得到的名字列表
# 点击开始按钮触发的函数
def randomRun(list):
global is_run
if is_run:#此部分用于避免多次点击开始造成程序重复执行的错误
return
is_run = True
num = insert_point()
#如果输入人数不为空,则调用randomResult
if num:
num = int(num)
randomResult(list,num)
else:
is_run = False
# 获取输入人数
def insert_point():
var = input.get() #获取输入的信息
return var
# 随机滚动得到所抽取的人数,递归
def randomResult(list,num):
global is_run
result = random.sample(list, num) #使结果随机
var.set(result) #将随机结果传给窗口,并在窗口中显示随机结果
if is_run:
# 在给定时间后调用函数一次
# 参数:毫秒,需要调用的函数,函数的参数,函数参数
window.after(30, randomResult, list, num) #第一个参数控制速度,越小滚动越快
#点击结束时触发,is_run置为false,使递归函数停止
def finalResult():
global is_run
is_run = False
# 打开指定图片文件,缩放指定尺寸
def get_image(filename,width,height):
im = Image.open(filename).resize((width,height))
return ImageTk.PhotoImage(im, master=window)
if __name__ == '__main__': #主程序
#window = Toplevel()
window = Tk() #初始化TK窗口
# 设定窗口大小
# 格式:w*h±x±y
# wh窗口宽高,xy窗口显示位置横纵坐标,加减表示正方向或负方向,左上角为原点,下和右是正方向
#window.geometry('1024x648+250+150')
window.resizable(0,0) #将窗口设置为不可拉伸,否则不同分辨率屏幕组件位置会出问题
window.title('8.25秋夜派对')
list = getPeopleList() #从peopleList.xlsx中获取待抽奖人员名单
# 设置背景图
canvas_window = tkinter.Canvas(window,width=1024,height=648)
#canvas_window = tkinter.Canvas(window,width=1920,height = 1080)
im = Image.open('./image2.jpg').resize((1024,648))
#im = Image.open('./image2.jpg').resize((1920,1080))
im_window = ImageTk.PhotoImage(im, master=window)
# 前两个参数是图像中心在画布中的位置,那么一半的话正好图像与窗口大小相同
canvas_window.create_image(1024//2,648//2,image=im_window)
#canvas_window.create_image(0,0,image=im_window)
canvas_window.pack(fill=BOTH,expand=YES)
var = StringVar() #初始化一个字符串变量,用于滚动显示抽奖结果
noteLable1 = Label(text="谁是幸运儿?",font='微软雅黑, 60')
#noteLable1.pack(fill=)
noteLable1.place(anchor=NW,x=280,y=100)
noteLable1.config(bg=window["bg"])
noteLable = Label(text="请输入人数:",font='微软雅黑, 25')
noteLable.place(anchor=NW, x=270, y=230)
input = Entry(canvas_window, show=None,width=10,font='微软雅黑, 25') #设置一个输入框,用于输入抽奖人数
input.place(anchor=NW, x=570, y=230)
resultLable = Label(textvariable=var,font='微软雅黑, 100',foreground='RED') #设置一个显示抽奖结果的文本框
resultLable.place(anchor=NW,x=300,y=320)
startBt = Button(text="开始", command=lambda: randomRun(list=list),font='微软雅黑, 25') #开始按钮
confirmBt = Button(text="结束", command=lambda: finalResult(),font='微软雅黑, 25') #停止按钮
#pack()按钮放置位置
#布局排列:pack,grid,place
#不建议用place,虽然place可以精确组件位置,但是在不同电脑不同分辨率上会有问题
#pack可以选择参数fill填充,expand=True按照窗口大小伸缩
#grid的自适应性也比较强,有configure方法实现frame大小扩充
startBt.place(anchor=NW, x=350, y=500)
confirmBt.place(anchor=NW, x=550, y=500)
window.mainloop() #渲染窗口,一直显示
运行效果展示:
至此,我们实现了客户的需求,完成了随机点名/抽奖小工具的实现。
四、将程序封装成.exe可执行文件
将代码转换成.py文件
由于我是用jupyter notebook写的代码,所以要将.ipynb文件转换为.py文件。具体方法是将待转换的代码段放到要给单独的.ipynb文件下,该文件下不要有其他cell。
转换成.py文件后,我们配置打包的封装环境。
首先安装pyinstaller。
pip install pyinstaller
然后
pip list
看看库列表中是否有pyinstaller。若有,则安装成功。找到Pyinstaller.exe的位置
如图所示,第六行有from pyinstaller的目录,找到该目录的上一级目录,pyinstaller.exe的位置一般在Scripts中,我的目录是D:\Anaconda\Scripts。或者直接搜索pyinstaller.exe的位置即可。
接下来跳转到Scripts目录下:
(base) C:\Users\yzcong>D:
(base) D:\>cd :\Anaconda\Scripts
在该目录下执行:
pyinstaller -F D:\test\choujiang.py
-F是只生成一个.exe文件,-D是生成所有相关的依赖,最后是要打包的py文件的目录。
这里可能会出现提示,让我们一处pathlib,因为pyinstaller和它不兼容。
按照要求执行:
conda remove pathlib
conda命令可能比较慢,多等一会。出现
wu
输入y继续执行,等结束后重新执行:
pyinstaller -F D:\test\choujiang.py
会进入程序打包阶段,打包完成后在当前的Scripts目录下会新生成个list文件夹,里面就是exe文件。
但是,
上述方法只是将一个简单的.py文件打包成.exe,但如果程序中有相关的依赖,那么这种方式就不适用。
本例运行exe后会产生错误:
因为我们使用了openpyxl库,而我们却没有将其一同打包。所以,将打包命令改为:
pyinstaller -F D:\test\choujiang.py --hidden-import openpyxl.cell._writer
缺什么补什么,将隐藏的依赖openpyxl.cell._writer包括进去即可。打包完成后提示如下:
注意要将人员表格和背景图与生成的.exe放在同一个目录下:
运行后测试成功!至此我们完成了本项目的实现。
五、总结与拓展
总结:
本项目实际上还存在许多问题:
- 由于每个组件都是使用place精确布局的,这就导致组件没有办法自适应窗口。也就是说,本例只能设置固定窗口大小。如果设置窗口大小可变,那么组件会错位。后续可能优化组件自适应窗口大小变化,现已能实现背景图随着窗口大小变化。
- 同上所述,中间显示滚动名称的字符串位置有限,如果同时抽取多个人,那么由于place精确位置的原因,多个人会显示不全。
- 封装后的.exe文件很大,可以考虑优化压缩大小,采用虚拟封装。
拓展:
- 窗口组件可以进一步优化,实现更丰富的功能。目前研究组件随窗口自适应大小变化,实现思路是当窗口分辨率改变时获取到大小,然后其他组件和背景图随着比例变化而更新。
- 打包方式可以拓展,除了pyinstaller之外,.py文件还有其他更高效的打包方式,值得拓展学习
- 本项目拓展到的知识点总结:
- 背景图随窗口自适应改变大小
- 各种组件的测试
- 点击按钮实现界面切换
- 获取窗口大小,屏幕真实分辨率和缩放分辨率
- .py文件的其他打包方法(有没有可能用tkinter写一个基于pyinstaller的打包界面?)