exe文件打包与UI界面设计

  • 前言
  • 一、基于tkinter实现的UI设计
    • 1.1 库的选择及思路
    • 1.2 定位方法的选用
    • 1.3 Frame控件
    • 1.4 变量设置
    • 1.5 批量设置
    • 1.6 Text文本框
    • 1.7 总体界面设计
    • 1.8 功能函数
    • 1.9 使用效果
  • 二、 使用pyinstaller打包exe文件
    • 2.1 pyinstaller的参数设置
    • 2.2 打包方式的选择

前言

又是半年时间过去了,终于有有时间摸鱼学一点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. 每个单元格的大小尺寸很难去进行自定义,布置好控件之后,尺寸基本上是自动设置,不能随心所欲的设定每个格子内控件的大小

  2. 它的单元格之间都是紧凑排列的,什么意思呢,比如在1行1列布置了控件,想要在3行1列布置另一个控件,中间空置一行是做不到的。这种情况下等同于布置在2行1列,会自动忽略未布置控件的行或列。这就有一种往一起堆的趋势,有点类似pack()的感觉。

  3. 回过头来看,也许先布置多个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. 打卡过程中的数据交换 : 包括历史信息的获取,本地信息的提交,如何将这一部分数据填入或从输入框中提取出来。

  2. 界面本身的设置 : 一些功能选择状态的记忆等。

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学习笔记–每日健康打卡及离校报备)所写,有一定程度改动。

  1. 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')
  1. 保存输入信息以及软件设置。如果不加入此功能,每次打开软件都是空白一片,需要重新填写一遍,软件也就失去了意义。将所需信息写入到配置文件(info.ini)中,每次打开软件进行读取即可实现历史信息的保存。
    def save_info():global save_valuesave_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('保存成功!')
  1. 软件的初始化 : 每次打开软件进入界面前所运行的函数,包括配置文件的读取,状态设定等。如第一次运行,则会创建空白配置文件;如果目录下已有配置文件则读取其中数据。
    def start():notice('初始化中,检测配置文件...')tf = os.path.exists('info.ini')  # 检测配置文件,不存在则创建 #if tf:notice('正在读取配置文件...')s_list = []count = 2with 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 += 1var3.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('配置文件创建成功!')
  1. 控制是否显示密码 : 改变密码明文\加密(*****)状态。
    def pwd_state():if var1.get() == 1:f22_entry2.configure(show='*')if var1.get() == 0:f22_entry2.configure(show='')
  1. 输入框状态设定 : 在可输入\只读状态之间切换。由手动设置页中进行选择,默认自动设置。(其实应该合在一起,但这几个函数都是之前版本删改之后的结果,懒得动就直接用了。。)
    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')
  1. 清空输入框内信息 : 不知道为啥要写这个功能,意义不明。。。(了解了一下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')
  1. 关闭软件自动保存数据 : 懒人福音。核心在于protocol(),本质上是捕获软件退出时发出的命令,将destory()方法替换为可以自动保存的closewin()方法。
 window.protocol('WM_DELETE_WINDOW', closewin)def closewin():save_info()window.destroy()
  1. 登录打卡系统 : (这位更是重量级 ) 不登录你打个屁卡?利用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.cookiescookie = 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, statstoday = date_res_json['datas']['hunch_list'][0]['date1']yesterday = date_res_json['datas']['hunch_list'][1]['date1']stats = 0if date_res_json['datas']['hunch_list'][0]['state'] == 1:stats += 1notice('来自 %s,%s 的%s,今日(%s)已经打卡!' % (user_institute, user_class, user_name, today))else:notice('来自 %s,%s 的%s,今日(%s)尚未打卡!' % (user_institute, user_class, user_name, today))
  1. 获取历史打卡信息 : 重中之重,点一下直接获取所有需要的打卡信息。(因为学校所使用的打卡系统的特点,打卡信息基本不会变化,因此获取之前填报的打卡信息是最方便的办法。唯一需要修改的地方就在打卡位置以及是否变动这些,可以通过手动获取并修改保存的方法,最大程度上节约时间成本。)

    需要注意的是:上篇文章提到的打卡过程中涉及到的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)  # 获取数据填入表格 ## 组合生成提交数据所需dictpunch_dict = dict(zip(key_list, history_list))punch_form_dict = {'punch_form': str(punch_dict)}date_dict = {'date': today}global data_punchdata_punch = dict(punch_form_dict, **date_dict)
  1. 一键打卡 : 套娃+提交数据。
    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('今日已经打卡,请勿重复打卡!')
  1. 手动打卡 : 没什么好说的,套娃就完事了。
    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,启动较多文件方式慢上几秒)。

单文件模式方便使用,启动速度可以忽略,最终选择生成单文件。

Python学习笔记--exe文件打包与UI界面设计相关推荐

  1. Python学习笔记:文件(File)

    Python学习笔记:文件(File) 打开一个文件用于读写,在Python里十分简单,利用内置open函数,可以用绝对路径,也可以用相对路径. 默认模式是'r',只读模式. 文件句柄f是一个可迭代对 ...

  2. Python学习笔记之文件

    一.从文件中读取数据:有时候,往往会把相关的信息生成一个文件存储在里面,如果要使用文本文件中的信息,首先需要将信息读取到内存中.为此,你可以一次性读取文件的全部内容,也可以以每次一行的方式逐步读取. ...

  3. python 学习笔记 5 -- 文件输入输出

    本文主要介绍python下的文件输入输出操作,python的文件操作简单易用-通过本文相信你可以熟练掌握file和pickle这两种输入输出操作! 1.文件 你可以通过创建一个file类的对象来打开一 ...

  4. python学习笔记-36 文件读写

    读写文件是最常见的IO操作.Python内置了读写文件的函数,用法和C是兼容的. 读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘, ...

  5. Python学习笔记9—文件

       在Python中,要对一个文件进行操作,只需用内置的open函数打开文件即可. Signature: open(file, mode='r', buffering=-1, encoding=No ...

  6. Python学习笔记:文件读/写方法汇总

    # ############# 文件操作方法# 重点常用方法标红# ############import time, sys # ########### 读文件 ################### ...

  7. python学习笔记十-文件操作

    对文件操作流程 1.打开文件,得到文件句柄并赋值给一个变量 2.通过句柄对文件进行操作 3.关闭文件 操作如下: 花间一壶酒,独酌无相亲. 举杯邀明月,对影成三人. 月既不解饮,影徒随我身. 暂伴月将 ...

  8. Lawliet|Python学习笔记4——文件处理

    1.文件的使用 a.文件的类型 文件是数据的抽象和集合 文件是存储在辅助存储器上的数据序列 文件是数据存储的一种形式 文件展现形态:文本文件和二进制文件 文本文件:由单一特定编码组成的文件,如:UTF ...

  9. python学习笔记day08 文件功能详解

    file.read():读取文件的全部内容 file=open("dang",mode='r+',encoding='utf-8') print(type(file.read()) ...

最新文章

  1. colorAccent,colorPrimary,colorPrimaryDark 作用的地方
  2. 类和对象—对象特性—空指针访问成员函数
  3. java 继承多态的一些理解和不理解
  4. java 异常堆栈日志分析_Java 进阶之路:深入解读 Java 异常堆栈丢失原因
  5. C语言---链表的基本应用
  6. 力改变物体形状举例_对旋转问题的思考-在离心力确定的情况下,物体的旋转情况如何通过宇宙中的相对运动情况和质量分布确定?...
  7. python中for用法_python中for的用法探索
  8. trunk配置功能详解
  9. 用原生NodeJS实现简易的静态web
  10. Java基础0311
  11. sql删除表中各类重复数据
  12. Win10 利用快捷键,快速新建TXT文档
  13. oracle gbk 无法识别,oracle 字符集总结(超出GBK范围的字符存取问题未解决)
  14. kaldi 语音识别 lattice-free MMI声学训练
  15. 1050Ti解决csgo打不开、电脑无缘无故蓝屏的终极方法
  16. JavaWeb笔记(五)后端
  17. 2021 年第一个双月总结
  18. http协议_代理服务(proxy)
  19. 使用 jQuery Validate 进行表单验证
  20. java 变量重名_关于java中变量的重名问题

热门文章

  1. ADSafe导致github等很多网站无法打开
  2. ssl证书如何安装?常见的四类ssl证书安装方法介绍
  3. 台湾大学林轩田机器学习技法课程学习笔记11 -- Gradient Boosted Decision Tree
  4. K8S系列(六)DaemonSet详解
  5. 第一组生活日历软件的应用体验
  6. AR9344中ethernet处理方式分析
  7. 【数字逻辑】逻辑函数式化简为其他形式
  8. android 模拟器 haxm,Android模拟器不使用HAXM
  9. 三方线上美食城|基于Springboot的三方线上美食商城系统
  10. P1610 鸿山洞的灯 - 贪心