前言
又是半年時間過去了,終于有有時間摸魚學(xué)一點python了。本次練習(xí)主要針對之前寫過的自動打卡腳本,將其打包成exe文件,并加上UI界面。其實對于自動打卡這個功能來說,UI界面并不是必需品,加上了界面反而有些麻煩。
一、基于tkinter實現(xiàn)的UI設(shè)計
1.1 庫的選擇及思路
我比較熟悉的UI相關(guān)的庫主要有easygui,tkinter,pyqt5這些。之前學(xué)習(xí)的時候嘗試過easygui,pyqt感覺又過于復(fù)雜,所以本次打算以tkinter為基礎(chǔ)來進行練習(xí)。
代碼大體可以分為兩部分,功能實現(xiàn)部分以及界面設(shè)置部分。實現(xiàn)功能的主要代碼采用之前寫過的自動打卡代碼,因為加入了界面,因此要進行一定程度的改動,加入互交邏輯。而界面設(shè)計部分主要涉及到輸入,按鈕,位置布置等方面以及所需調(diào)用的功能函數(shù)。
1.2 定位方法的選用
先放一下第一版很丑的設(shè)計:
其中,label,entry,button及其相應(yīng)的功能使用起來還是比較簡單的,麻煩的點就在于定位。
tkinter提供了三種定位方式:pack(),grid(),place()
,具體的區(qū)別與應(yīng)用網(wǎng)上都有,就不再贅述了,主要講講我的體會。
上圖這個很丑的布局就是用grid()
方法進行布置的。該方法類似于在一個隱形的excel表格上安排各個控件,每個控件分別占據(jù)第幾行第幾列,最終組成界面。
登錄信息 | 分隔符 |
---|---|
賬號 | 輸入框 |
密碼 | 輸入框 |
比如上圖中左上角的登錄信息就位于0行0列,而下方的賬號就是1行0列,登錄信息右側(cè)的分割線則是0行1列,依此類推。
簡單了解之后,以我粗淺的理解發(fā)現(xiàn)了幾點問題:
-
每個單元格的大小尺寸很難去進行自定義,布置好控件之后,尺寸基本上是自動設(shè)置,不能隨心所欲的設(shè)定每個格子內(nèi)控件的大小。
-
它的單元格之間都是緊湊排列的,什么意思呢,比如在1行1列布置了控件,想要在3行1列布置另一個控件,中間空置一行是做不到的。這種情況下等同于布置在2行1列,會自動忽略未布置控件的行或列。這就有一種往一起堆的趨勢,有點類似
pack()
的感覺。 -
回過頭來看,也許先布置多個frame再分別在各個frame上進行布局能夠解決控件對齊等問題(之后所用的
pack()
方法就是使用了多個frame進行布置)。
最終采用了最簡單的pack()
方法配合frame控件進行設(shè)計,網(wǎng)上說這種方式有點像堆積木,我覺得更像是華容道的棋盤,每一個控件的布局就像一顆棋子。值得一提的是,根據(jù)網(wǎng)上大佬分享的經(jīng)驗,不同布局方式之間不能混用,我并沒有進行測試,所有位置布局都采用pack()
的方法進行。
1.3 Frame控件
這是個很基礎(chǔ)也很常用的控件,菜鳥上的介紹是作為一個容器,來盛放其他控件。根據(jù)我的理解,如果建立一個最簡單的界面,并直接布置各個控件,就相當(dāng)于該界面上存在一個frame,各個控件都是布置于其上的。
上圖就是一個最簡單的界面,而下圖中則相當(dāng)于將Label,Entry控件布置在了一個frame之上。對于非常簡單的界面來說,可以不必使用frame控件。
關(guān)于Frame控件,一個比較典型的應(yīng)用場景就是配合Notebook控件來生成不同標簽頁(需要import tkinter.ttk
),具體效果如下:
為了使每一個標簽頁下都對應(yīng)不同的內(nèi)容,就需要每一標簽頁面都設(shè)置不同的Frame來承載不同的控件。如登錄信息和打卡信息這兩個標簽頁,需要分別設(shè)置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='手動設(shè)置')
notebook.add(frame_5, text='打卡設(shè)置')
notebook.pack(padx=10, pady=5, fill=tkinter.BOTH, expand=True)
notebook.select(4) # 初始頁面選擇 #
以上代碼可生成5個空白的標簽頁以及5個frame,接下來就是對每個頁面進行布局。
由于pack()
方法本身的特點(具體細節(jié)見:用tkinter.pack設(shè)計復(fù)雜界面布局,講的非常好!),如果界面上存在較多的控件,最好使用frame先占位,再將控件布置在frame之上,可以達到較好的效果。
舉個栗子,上圖登錄信息頁面中,包括了以下三種控件:
-
Label : 賬號,密碼
-
Entry : 賬號密碼后對應(yīng)輸入框
-
Checkbutton : 隱藏密碼勾選框
假設(shè)該頁面對應(yīng)frame_2,在其上再設(shè)置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() # 復(fù)選框用變量 #
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 變量設(shè)置
主要涉及兩部分內(nèi)容:
-
打卡過程中的數(shù)據(jù)交換 : 包括歷史信息的獲取,本地信息的提交,如何將這一部分數(shù)據(jù)填入或從輸入框中提取出來。
-
界面本身的設(shè)置 : 一些功能選擇狀態(tài)的記憶等。
1.3中登錄信息頁的設(shè)計中包含了三個變量:str_v21, str_v22, var1
,和兩種類型:tk.StringVar(), tk.IntVar()
,其中:
-
tk.StringVar()用來實時接收和傳遞字符串類型的數(shù)據(jù),即輸入框中的內(nèi)容,類似于一個中轉(zhuǎn)站,能夠?qū)崿F(xiàn)所見(輸入框中顯示內(nèi)容)即所得(變量賦值)。
-
tk.IntVar()用來作為功能狀態(tài)判別的開關(guān),通過改變變量的值來控制功能的啟停。以復(fù)選框為例:變量為1則設(shè)置為已勾選狀態(tài),為0則設(shè)置為未勾選狀態(tài)。
1.5 批量設(shè)置
在該頁面上,共有11個Label和Entry,分別布置在11個Frame之上。重復(fù)度較高,可以考慮批量生成并布置。
f3_info = [' 目前所在地', '位置是否變化', ' 身體狀況',
'接觸人員狀況', ' 隔離情況', ' 今日體溫',
'個人手機號碼', '家人聯(lián)系方式', ' 行程時間', ' 隔離地點', ' 打卡位置'
]
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) # 控制狀態(tài)用 #
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) # 控制狀態(tài)用 #
批量生成變量名可以利用exec()
函數(shù),control_list用于控制輸入框輸入狀態(tài)及輸入信息提取,通過設(shè)置textvariable與對應(yīng)的變量(tk.StringVar()
類型)即可實現(xiàn)數(shù)據(jù)的傳遞。
1.6 Text文本框
Text控件可以用來進行信息說明或進度提示。
‘
圖上所展示的Text控件處于只讀(state='disabled'
)狀態(tài),只作為信息展示之用。Text控件邊框樣式選擇也可以通過relief=
屬性實現(xiàn)。
如果需要帶有滾動條的文本框,可以使用scrolledtext(需要from tkinter import scrolledtext
),具體使用方法與Text差不多:scrolledtext.ScrolledText()
,Text的一些屬性與方法也可以使用。效果如下:
1.7 總體界面設(shè)計
每個頁面布局如下:
說明頁:主要對使用方法和使用過程中的注意事項進行說明。
登錄信息頁:賬號密碼的輸入以及是否以 *****的形式隱藏密碼。若勾選則不顯示明文密碼,默認勾選隱藏密碼。
打卡信息頁:打卡所需信息的傳遞。
手動設(shè)置頁:如果需要核對或修改打卡信息時可能使用到的功能。
打卡設(shè)置頁:
-
保存信息 : 打卡信息,功能狀態(tài)的保存(存在配置文件中)。
-
重置信息 : 清空所有輸入框內(nèi)信息(沒啥用。。)。
-
一鍵打卡 : 點一下,就打卡(
人被殺,就會死)。 -
自動打卡 : 勾選后,打開軟件自動進行打卡。
-
進度欄 : 顯示運行狀態(tài),操作時間及結(jié)果,錯誤提示等。
1.8 功能函數(shù)
界面設(shè)計的結(jié)束,只完成了一半的任務(wù)?,F(xiàn)在的軟件還只是一個空殼,無法實現(xiàn)任何功能。真正的核心任務(wù):完成打卡及配套功能還需要構(gòu)造相應(yīng)的函數(shù)。
涉及到打卡登錄部分的功能代碼基本來自于我上篇文章(Python學(xué)習(xí)筆記–每日健康打卡及離校報備)所寫,有一定程度改動。
- Text中輸出提示信息 :方便查看任務(wù)進度,狀態(tài)以及操作時間。方便其他功能函數(shù)運行后調(diào)用,并自定義輸出內(nèi)容。
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')
- 保存輸入信息以及軟件設(shè)置。如果不加入此功能,每次打開軟件都是空白一片,需要重新填寫一遍,軟件也就失去了意義。將所需信息寫入到配置文件(info.ini)中,每次打開軟件進行讀取即可實現(xiàn)歷史信息的保存。
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('保存成功!')
- 軟件的初始化 : 每次打開軟件進入界面前所運行的函數(shù),包括配置文件的讀取,狀態(tài)設(shè)定等。如第一次運行,則會創(chuàng)建空白配置文件;如果目錄下已有配置文件則讀取其中數(shù)據(jù)。
def start():
notice('初始化中,檢測配置文件...')
tf = os.path.exists('info.ini') # 檢測配置文件,不存在則創(chuàng)建 #
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('未檢測到配置文件,將自動創(chuàng)建...')
with open("info.ini", "w") as ff: # 新建空白配置文件 #
ff.write('\n\n\n\n\n\n\n\n\n\n\n\n\n')
notice('配置文件創(chuàng)建成功!')
- 控制是否顯示密碼 : 改變密碼明文\加密(*****)狀態(tài)。
def pwd_state():
if var1.get() == 1:
f22_entry2.configure(show='*')
if var1.get() == 0:
f22_entry2.configure(show='')
- 輸入框狀態(tài)設(shè)定 : 在可輸入\只讀狀態(tài)之間切換。由手動設(shè)置頁中進行選擇,默認自動設(shè)置。(其實應(yīng)該合在一起,但這幾個函數(shù)都是之前版本刪改之后的結(jié)果,懶得動就直接用了。。)
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')
- 清空輸入框內(nèi)信息 : 不知道為啥要寫這個功能,意義不明。。。(了解了一下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')
- 關(guān)閉軟件自動保存數(shù)據(jù) : 懶人福音。核心在于
protocol()
,本質(zhì)上是捕獲軟件退出時發(fā)出的命令,將destory()
方法替換為可以自動保存的closewin()
方法。
window.protocol('WM_DELETE_WINDOW', closewin)
def closewin():
save_info()
window.destroy()
- 登錄打卡系統(tǒng) : (
這位更是重量級) 不登錄你打個屁卡?利用StringVar().get()
方法獲取輸入的帳號以及密碼并提交。需要注意的是:StringVar()
實例化后并不能直接調(diào)用,直接使用無法獲取變量的賦值,需要用到get()
。登陸成功后會返回用戶信息(學(xué)院,班級,姓名,打卡日期),失敗則會返回錯誤原因。
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 # 后面要用到,設(shè)為全局變量,其他全局變量相同 #
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)已經(jīng)打卡!' % (user_institute, user_class, user_name, today))
else:
notice('來自 %s,%s 的%s,今日(%s)尚未打卡!' % (user_institute, user_class, user_name, today))
-
獲取歷史打卡信息 : 重中之重,點一下直接獲取所有需要的打卡信息。(因為學(xué)校所使用的打卡系統(tǒng)的特點,打卡信息基本不會變化,因此獲取之前填報的打卡信息是最方便的辦法。唯一需要修改的地方就在打卡位置以及是否變動這些,可以通過手動獲取并修改保存的方法,最大程度上節(jié)約時間成本。)
需要注意的是:上篇文章提到的打卡過程中涉及到的3次post請求,實際上是4次。分別是登錄(login),獲取日期(getHomeDate),獲取歷史打卡信息(getPunchForm)以及提交今日打卡信息(punchForm)。除了獲取日期外,均需要在post過程中傳入數(shù)據(jù)(
data=json.dumps()
)才能得到正確的結(jié)果。
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) # 獲取數(shù)據(jù)填入表格 #
# 組合生成提交數(shù)據(jù)所需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)
- 一鍵打卡 : 套娃+提交數(shù)據(jù)。
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('今日已經(jīng)打卡,請勿重復(fù)打卡!')
- 手動打卡 : 沒什么好說的,套娃就完事了。
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('今日已經(jīng)打卡,請勿重復(fù)打卡!')
1.9 使用效果
打開軟件首先輸入賬號密碼。
點擊一鍵打卡。
查看獲取的歷史信息。
二、 使用pyinstaller打包exe文件
2.1 pyinstaller的參數(shù)設(shè)置
默認情況下,使用pyinstaller打包為exe文件后,運行時都會帶有一個cmd窗口。如果沒有設(shè)計圖形界面,則需要用該窗口進行互交,但帶有界面的軟件運行時就不需要這個窗口了。
解決辦法(Pycharm下) : 點擊File–Settings–Tools–External Tools,雙擊pyinstaller。在出現(xiàn)的窗口中找到Arguments,加入 -w
即可。
2.2 打包方式的選擇
同樣可以通過設(shè)置Arguments參數(shù)進行打包方式的選擇。
-
-D
:生成一個文件夾,里面是多文件模式,啟動快,但體積過大(只導(dǎo)入了不到10個包,居然達到了20多M)。
-
-F
: 僅生成一個文件,不暴露其他信息,啟動較慢(大小接近10M,啟動較多文件方式慢上幾秒)。
文章來源:http://www.zghlxwxcb.cn/news/detail-448501.html
單文件模式方便使用,啟動速度可以忽略,最終選擇生成單文件。文章來源地址http://www.zghlxwxcb.cn/news/detail-448501.html
到了這里,關(guān)于Python學(xué)習(xí)筆記--exe文件打包與UI界面設(shè)計的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!