前言
又是半年时间过去了,终于有有时间摸鱼学一点python了。本次练习主要针对之前写过的自动打卡脚本,将其打包成exe文件,并加上UI界面。其实对于自动打卡这个功能来说,UI界面并不是必需品,加上了界面反而有些麻烦。
一、基于tkinter实现的UI设计
1.1 库的选择及思路
我比较熟悉的UI相关的库主要有easygui,tkinter,pyqt5这些。之前学习的时候尝试过easygui,pyqt感觉又过于复杂,所以本次打算以tkinter为基础来进行练习。
代码大体可以分为两部分,功能实现部分以及界面设置部分。实现功能的主要代码采用之前写过的自动打卡代码,因为加入了界面,因此要进行一定程度的改动,加入互交逻辑。而界面设计部分主要涉及到输入,按钮,位置布置等方面以及所需调用的功能函数。
1.2 定位方法的选用
先放一下第一版很丑的设计:
其中,label,entry,button及其相应的功能使用起来还是比较简单的,麻烦的点就在于定位。
tkinter提供了三种定位方式:pack(),grid(),place()
,具体的区别与应用网上都有,就不再赘述了,主要讲讲我的体会。
上图这个很丑的布局就是用grid()
方法进行布置的。该方法类似于在一个隐形的excel表格上安排各个控件,每个控件分别占据第几行第几列,最终组成界面。
登录信息 | 分隔符 |
---|---|
账号 | 输入框 |
密码 | 输入框 |
比如上图中左上角的登录信息就位于0行0列,而下方的账号就是1行0列,登录信息右侧的分割线则是0行1列,依此类推。
简单了解之后,以我粗浅的理解发现了几点问题:
-
每个单元格的大小尺寸很难去进行自定义,布置好控件之后,尺寸基本上是自动设置,不能随心所欲的设定每个格子内控件的大小。
-
它的单元格之间都是紧凑排列的,什么意思呢,比如在1行1列布置了控件,想要在3行1列布置另一个控件,中间空置一行是做不到的。这种情况下等同于布置在2行1列,会自动忽略未布置控件的行或列。这就有一种往一起堆的趋势,有点类似
pack()
的感觉。 -
回过头来看,也许先布置多个frame再分别在各个frame上进行布局能够解决控件对齐等问题(之后所用的
pack()
方法就是使用了多个frame进行布置)。
最终采用了最简单的pack()
方法配合frame控件进行设计,网上说这种方式有点像堆积木,我觉得更像是华容道的棋盘,每一个控件的布局就像一颗棋子。值得一提的是,根据网上大佬分享的经验,不同布局方式之间不能混用,我并没有进行测试,所有位置布局都采用pack()
的方法进行。
1.3 Frame控件
这是个很基础也很常用的控件,菜鸟上的介绍是作为一个容器,来盛放其他控件。根据我的理解,如果建立一个最简单的界面,并直接布置各个控件,就相当于该界面上存在一个frame,各个控件都是布置于其上的。
上图就是一个最简单的界面,而下图中则相当于将Label,Entry控件布置在了一个frame之上。对于非常简单的界面来说,可以不必使用frame控件。
关于Frame控件,一个比较典型的应用场景就是配合Notebook控件来生成不同标签页(需要import tkinter.ttk
),具体效果如下:
为了使每一个标签页下都对应不同的内容,就需要每一标签页面都设置不同的Frame来承载不同的控件。如登录信息和打卡信息这两个标签页,需要分别设置frame进行绑定。
notebook = ttk.Notebook(window)
frame_1, frame_2, frame_3, frame_4, frame_5 = [tk.Frame() for i in range(5)]
notebook.add(frame_1, text='说明')
notebook.add(frame_2, text='登录信息')
notebook.add(frame_3, text='打卡信息')
notebook.add(frame_4, text='手动设置')
notebook.add(frame_5, text='打卡设置')
notebook.pack(padx=10, pady=5, fill=tkinter.BOTH, expand=True)
notebook.select(4) # 初始页面选择 #
以上代码可生成5个空白的标签页以及5个frame,接下来就是对每个页面进行布局。
由于pack()
方法本身的特点(具体细节见:用tkinter.pack设计复杂界面布局,讲的非常好!),如果界面上存在较多的控件,最好使用frame先占位,再将控件布置在frame之上,可以达到较好的效果。
举个栗子,上图登录信息页面中,包括了以下三种控件:
-
Label : 账号,密码
-
Entry : 账号密码后对应输入框
-
Checkbutton : 隐藏密码勾选框
假设该页面对应frame_2,在其上再设置3个frame,分别放置账号及其输入框,密码及其输入框以及隐藏密码勾选框:
frame_21, frame_22, frame_23 = [tk.Frame(frame_2) for i in range(3)]
frame_21.pack()
frame_22.pack()
frame_23.pack()
准备并布置好frame后即可进行各个控件的布置:
str_v21, str_v22 = tk.StringVar(), tk.StringVar()
f21_label0 = tk.Label(frame_21)
f21_label0.pack(padx=10, pady=5, side='top')
f21_label1 = tk.Label(frame_21, text='账号')
f21_label1.pack(padx=10, pady=5, side='left', anchor='nw')
f21_entry1 = tk.Entry(frame_21, textvariable=str_v21)
f21_entry1.pack(padx=10, pady=5, side='top', anchor='n')
f22_label0 = tk.Label(frame_22)
f22_label0.pack(padx=10, pady=5, side='top')
f22_label2 = tk.Label(frame_22, text='密码')
f22_label2.pack(padx=10, pady=5, side='left', anchor='nw')
f22_entry2 = tk.Entry(frame_22, textvariable=str_v22, show='*')
f22_entry2.pack(padx=10, pady=5, side='top', anchor='n')
f22_label3 = tk.Label(frame_22)
f22_label3.pack(padx=10, pady=5, side='top')
var1 = tk.IntVar() # 复选框用变量 #
var1.set(1) # 初始化 #
f23_cb1 = tk.Checkbutton(frame_23, text='隐藏密码', variable=var1, command=pwd_state)
f23_cb1.pack(padx=10, pady=5)
其中label0是拿来占空白位用的,使用pady=
进行填充也可以。(有空可以精简一下这部分代码)
1.4 变量设置
主要涉及两部分内容:
-
打卡过程中的数据交换 : 包括历史信息的获取,本地信息的提交,如何将这一部分数据填入或从输入框中提取出来。
-
界面本身的设置 : 一些功能选择状态的记忆等。
1.3中登录信息页的设计中包含了三个变量:str_v21, str_v22, var1
,和两种类型:tk.StringVar(), tk.IntVar()
,其中:
-
tk.StringVar()用来实时接收和传递字符串类型的数据,即输入框中的内容,类似于一个中转站,能够实现所见(输入框中显示内容)即所得(变量赋值)。
-
tk.IntVar()用来作为功能状态判别的开关,通过改变变量的值来控制功能的启停。以复选框为例:变量为1则设置为已勾选状态,为0则设置为未勾选状态。
1.5 批量设置
在该页面上,共有11个Label和Entry,分别布置在11个Frame之上。重复度较高,可以考虑批量生成并布置。
f3_info = [' 目前所在地', '位置是否变化', ' 身体状况',
'接触人员状况', ' 隔离情况', ' 今日体温',
'个人手机号码', '家人联系方式', ' 行程时间', ' 隔离地点', ' 打卡位置'
]
f3_list, f3_strv_list, f3_label1_list, f3_entry0_list, control_list1, control_list2 = [[] for i in range(6)]
# 批量生成label,entry变量名
for i in range(1, 12):
exec('f3_{} = "frame_3{}"'.format(i, i))
exec('f3_list.append(f3_{})'.format(i))
exec('f3_label1{} = "f3{}_label1"'.format(i, i))
exec('f3_label1_list.append(f3_label1{})'.format(i))
exec('f3_entry0{} = "f3{}_entry0"'.format(i, i))
exec('f3_entry0_list.append(f3_entry0{})'.format(i))
exec('f3_strv{} = "strv_3{}"'.format(i, i))
exec('f3_strv_list.append(f3_strv{})'.format(i))
# 批量布置位置
for x, y, z, j, k in zip(f3_label1_list, f3_entry0_list, f3_strv_list, f3_list, f3_info):
j = tk.Frame(frame_3)
j.pack(expand=True)
z = tk.StringVar()
control_list1.append(z) # 控制状态用 #
x = tk.Label(j, text=k)
x.pack(padx=10, pady=5, side='left', anchor='nw')
y = tk.Entry(j, textvariable=z)
y.pack(padx=10, pady=5, side='top', anchor='n')
control_list2.append(y) # 控制状态用 #
批量生成变量名可以利用exec()
函数,control_list用于控制输入框输入状态及输入信息提取,通过设置textvariable与对应的变量(tk.StringVar()
类型)即可实现数据的传递。
1.6 Text文本框
Text控件可以用来进行信息说明或进度提示。
‘
图上所展示的Text控件处于只读(state='disabled'
)状态,只作为信息展示之用。Text控件边框样式选择也可以通过relief=
属性实现。
如果需要带有滚动条的文本框,可以使用scrolledtext(需要from tkinter import scrolledtext
),具体使用方法与Text差不多:scrolledtext.ScrolledText()
,Text的一些属性与方法也可以使用。效果如下:
1.7 总体界面设计
每个页面布局如下:
说明页:主要对使用方法和使用过程中的注意事项进行说明。
登录信息页:账号密码的输入以及是否以 *****的形式隐藏密码。若勾选则不显示明文密码,默认勾选隐藏密码。
打卡信息页:打卡所需信息的传递。
手动设置页:如果需要核对或修改打卡信息时可能使用到的功能。
打卡设置页:
-
保存信息 : 打卡信息,功能状态的保存(存在配置文件中)。
-
重置信息 : 清空所有输入框内信息(没啥用。。)。
-
一键打卡 : 点一下,就打卡(
人被杀,就会死)。 -
自动打卡 : 勾选后,打开软件自动进行打卡。
-
进度栏 : 显示运行状态,操作时间及结果,错误提示等。
1.8 功能函数
界面设计的结束,只完成了一半的任务。现在的软件还只是一个空壳,无法实现任何功能。真正的核心任务:完成打卡及配套功能还需要构造相应的函数。
涉及到打卡登录部分的功能代码基本来自于我上篇文章(Python学习笔记–每日健康打卡及离校报备)所写,有一定程度改动。
- Text中输出提示信息 :方便查看任务进度,状态以及操作时间。方便其他功能函数运行后调用,并自定义输出内容。
def notice(text):
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) # 保存操作时间 #
f53_text.configure(state='normal')
f53_text.insert('insert', '[' + now + ']' + ' ' + text + '\n') # 带上操作时间 #
f53_text.configure(state='disabled')
- 保存输入信息以及软件设置。如果不加入此功能,每次打开软件都是空白一片,需要重新填写一遍,软件也就失去了意义。将所需信息写入到配置文件(info.ini)中,每次打开软件进行读取即可实现历史信息的保存。
def save_info():
global save_value
save_value = []
save_value.append(str_v21.get()) # 获取输入信息 #
save_value.append(str_v22.get())
for vs in control_list1:
save_value.append(vs.get())
save_value.append(str(var3.get()))
with open("info.ini", "w") as f: # 信息写入配置文件 #
for vb in save_value:
f.write(vb)
f.write('\n')
notice('保存成功!')
- 软件的初始化 : 每次打开软件进入界面前所运行的函数,包括配置文件的读取,状态设定等。如第一次运行,则会创建空白配置文件;如果目录下已有配置文件则读取其中数据。
def start():
notice('初始化中,检测配置文件...')
tf = os.path.exists('info.ini') # 检测配置文件,不存在则创建 #
if tf:
notice('正在读取配置文件...')
s_list = []
count = 2
with open("info.ini", "r") as ft:
data = ft.readlines()
for line in data:
line = line.strip('\n')
s_list.append(line)
# 读取保存的配置
str_v21.set(s_list[0])
str_v22.set(s_list[1])
for e in control_list1:
e.set(s_list[count])
count += 1
var3.set(s_list[-1])
bt_state()
notice('初始化成功!')
if var3.get() == 1: # 是否自动打卡 #
onekey_checkin()
else:
notice('未检测到配置文件,将自动创建...')
with open("info.ini", "w") as ff: # 新建空白配置文件 #
ff.write('\n\n\n\n\n\n\n\n\n\n\n\n\n')
notice('配置文件创建成功!')
- 控制是否显示密码 : 改变密码明文\加密(*****)状态。
def pwd_state():
if var1.get() == 1:
f22_entry2.configure(show='*')
if var1.get() == 0:
f22_entry2.configure(show='')
- 输入框状态设定 : 在可输入\只读状态之间切换。由手动设置页中进行选择,默认自动设置。(其实应该合在一起,但这几个函数都是之前版本删改之后的结果,懒得动就直接用了。。)
def bt_state():
if var2.get() == 1:
active_entry()
f44_button1.configure(state='normal')
f44_button2.configure(state='normal')
if var2.get() == 2:
disable_entry()
f44_button1.configure(state='disabled')
f44_button2.configure(state='disabled')
# 禁止修改信息
def disable_entry():
for each in control_list2:
each.configure(state='readonly')
# 允许修改信息
def active_entry():
for each in control_list2:
each.configure(state='normal')
- 清空输入框内信息 : 不知道为啥要写这个功能,意义不明。。。(了解了一下delete方法)
def reset_entry(): # 写这个功能有啥用?? #
notice('已清空信息!')
active_entry()
f21_entry1.delete(0, 'end')
f22_entry2.delete(0, 'end')
for each in control_list2:
each.delete(0, 'end')
- 关闭软件自动保存数据 : 懒人福音。核心在于
protocol()
,本质上是捕获软件退出时发出的命令,将destory()
方法替换为可以自动保存的closewin()
方法。
window.protocol('WM_DELETE_WINDOW', closewin)
def closewin():
save_info()
window.destroy()
- 登录打卡系统 : (
这位更是重量级) 不登录你打个屁卡?利用StringVar().get()
方法获取输入的帐号以及密码并提交。需要注意的是:StringVar()
实例化后并不能直接调用,直接使用无法获取变量的赋值,需要用到get()
。登陆成功后会返回用户信息(学院,班级,姓名,打卡日期),失败则会返回错误原因。
def login():
urllib3.disable_warnings() # SSL验证错误,忽略 #
data_login['user_account'] = str_v21.get()
data_login['user_password'] = str_v22.get()
login_res = requests.post(login_url, headers=headers, verify=False, data=json.dumps(data_login)) # 提交登录请求 #
global cookie # 后面要用到,设为全局变量,其他全局变量相同 #
cookies = login_res.cookies
cookie = requests.utils.dict_from_cookiejar(cookies)
login_res_json = login_res.json()
if login_res_json['code'] == 200:
notice('登录成功!')
else:
notice('登录失败!')
notice('错误原因:%s' % (login_res_json['msg']))
date_res = requests.post(date_url, headers=headers, verify=False, cookies=cookie)
date_res_json = date_res.json()
# 获取登陆用户信息
user_class = date_res_json['datas']['user_info']['bj']
user_institute = date_res_json['datas']['user_info']['bm']
user_name = date_res_json['datas']['user_info']['user_name']
global today, yesterday, stats
today = date_res_json['datas']['hunch_list'][0]['date1']
yesterday = date_res_json['datas']['hunch_list'][1]['date1']
stats = 0
if date_res_json['datas']['hunch_list'][0]['state'] == 1:
stats += 1
notice('来自 %s,%s 的%s,今日(%s)已经打卡!' % (user_institute, user_class, user_name, today))
else:
notice('来自 %s,%s 的%s,今日(%s)尚未打卡!' % (user_institute, user_class, user_name, today))
-
获取历史打卡信息 : 重中之重,点一下直接获取所有需要的打卡信息。(因为学校所使用的打卡系统的特点,打卡信息基本不会变化,因此获取之前填报的打卡信息是最方便的办法。唯一需要修改的地方就在打卡位置以及是否变动这些,可以通过手动获取并修改保存的方法,最大程度上节约时间成本。)
需要注意的是:上篇文章提到的打卡过程中涉及到的3次post请求,实际上是4次。分别是登录(login),获取日期(getHomeDate),获取历史打卡信息(getPunchForm)以及提交今日打卡信息(punchForm)。除了获取日期外,均需要在post过程中传入数据(
data=json.dumps()
)才能得到正确的结果。
def get_history():
active_entry()
history_list = []
key_list = []
login()
date = {"date": yesterday}
res_history = requests.post(history_url, headers=headers, verify=False, cookies=cookie, data=json.dumps(date))
res_history_json = res_history.json()
for i in range(11):
history_list.append(res_history_json['datas']['fields'][i]['user_set_value'])
key_list.append(res_history_json['datas']['fields'][i]['field_code'])
for j, k in zip(control_list1, history_list):
j.set(k) # 获取数据填入表格 #
# 组合生成提交数据所需dict
punch_dict = dict(zip(key_list, history_list))
punch_form_dict = {'punch_form': str(punch_dict)}
date_dict = {'date': today}
global data_punch
data_punch = dict(punch_form_dict, **date_dict)
- 一键打卡 : 套娃+提交数据。
def onekey_checkin():
get_history()
if stats == 0:
res_submit = requests.post(submit_url, headers=headers, verify=False, data=json.dumps(data_punch),
cookies=cookie)
res_submit_json = res_submit.json()
if res_submit_json['code'] == 200:
notice('打卡成功!')
else:
notice('打卡失败!')
notice('错误来源:%s' % (res_submit_json['msg']))
else:
notice('今日已经打卡,请勿重复打卡!')
- 手动打卡 : 没什么好说的,套娃就完事了。
def checkin():
login()
list_key = ['mqszd', 'sfybh', 'mqstzk', 'jcryqk', 'glqk', 'jrcltw', 'sjhm', 'jrlxfs', 'xcsj', 'gldd', 'zddw']
list_value = []
for i in control_list1:
list_value.append(i.get())
punch_dict = dict(zip(list_key, list_value))
punch_form_dict = {'punch_form': str(punch_dict)}
date_dict = {'date': today}
data_punch = dict(punch_form_dict, **date_dict)
if stats == 0:
res_submit = requests.post(submit_url, headers=headers, verify=False, data=json.dumps(data_punch),
cookies=cookie)
res_submit_json = res_submit.json()
if res_submit_json['code'] == 200:
notice('打卡成功!')
else:
notice('打卡失败!')
notice('错误来源:%s' % (res_submit_json['msg']))
else:
notice('今日已经打卡,请勿重复打卡!')
1.9 使用效果
打开软件首先输入账号密码。
点击一键打卡。
查看获取的历史信息。
二、 使用pyinstaller打包exe文件
2.1 pyinstaller的参数设置
默认情况下,使用pyinstaller打包为exe文件后,运行时都会带有一个cmd窗口。如果没有设计图形界面,则需要用该窗口进行互交,但带有界面的软件运行时就不需要这个窗口了。
解决办法(Pycharm下) : 点击File–Settings–Tools–External Tools,双击pyinstaller。在出现的窗口中找到Arguments,加入 -w
即可。
2.2 打包方式的选择
同样可以通过设置Arguments参数进行打包方式的选择。
-
-D
:生成一个文件夹,里面是多文件模式,启动快,但体积过大(只导入了不到10个包,居然达到了20多M)。
-
-F
: 仅生成一个文件,不暴露其他信息,启动较慢(大小接近10M,启动较多文件方式慢上几秒)。
文章来源:https://www.toymoban.com/news/detail-448501.html
单文件模式方便使用,启动速度可以忽略,最终选择生成单文件。文章来源地址https://www.toymoban.com/news/detail-448501.html
到了这里,关于Python学习笔记--exe文件打包与UI界面设计的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!