【开发杂记】基于Python和TK的exe应用程序开发

0 前言

   一开始也没想到要做这个,人力的领导嫌用Excel太烦了想把一些机械重复劳动以程序替换,所以产生了这个需求。由于是面向个人需求开发的产品,所以做成单机的,一套做下来感觉事倍功半,以后还是B/S架构做成网页好点,这里避免工作浪费在这里总结一下全过程。

1 数据

  数据是员工技能考试数据,进行了简单的脱敏处理,大致如下:
在这里插入图片描述

2 需求

  其实需求没有太复杂,实现简单只是效率和前端上经不起考验。

  • 实现表格的查询功能包括单条件查询和多条件查询。
  • 实现简单的业务计算并产生报表。
  • 所有功能要交互完成。

3 GUI工具TK(tkinter)

  用python做GUI的情况应该比较少,我也就随大流选了TK工具包,其难点还是在于网格式布局的理解以及功能套件封装没有那么易用,比如滚轮滑动都费了较大力气才弄好,对这方面感兴趣的可以看Python的GUI图形界面工具大全

4 功能实现

4.1 整体框架

  TK的布局分为层级式和网格式,当然网格式比较灵活,但比较难分清楚每个部件构成。本文采取的是网格式,就像画画一样,所有功能按钮都是一层层铺上去的,这里先建个底层,也就是主界面。

4.11主界面

  主界面采用全屏展示,再把横竖的滚动条也加上,注意此处的滚动条仅有其形,功能还没加上。

root = tk.Tk()
root.title("乐川考试系统")    # #窗口标题
w = root.winfo_screenwidth()
h = root.winfo_screenheight()
root.geometry("%dx%d" %(w,h))
canvas= tk.Canvas(root, width=w, height=h)
xscrollbar = tk.Scrollbar(root,orient=tk.HORIZONTAL)
yscrollbar = tk.Scrollbar(root)
yscrollbar.config(command=canvas.yview)
yscrollbar.pack(side=tk.RIGHT, fill=tk.Y) 
xscrollbar.config(command=canvas.xview)
xscrollbar.pack(side=tk.BOTTOM, fill=tk.X) 
canvas.config(xscrollcommand=xscrollbar.set,yscrollcommand=yscrollbar.set)
canvas.pack(side="left", fill="both", expand=True) #side=LEFT,expand=True,fill=BOTH
main_frm = tk.Frame(canvas)
canvas.create_window(0,0,window=main_frm, anchor='nw')
4.12 主界面配件

  初始界面很简单,就文件上传(数据加载)和功能按钮栏。
在这里插入图片描述

# In[]
path = tk.StringVar()     # #创建变量,便于取值
path.set('')

upload_frm = tk.Frame(master=main_frm) #上传组件
upload_frm.grid(row=0,column=0)

entry1 = tk.Entry(upload_frm,textvariable=path,width='40')

entry1.grid(row=0, column=0)
btn = tk.Button(upload_frm, text='上传文件', command=lambda:upload_file(upload_frm,path))
btn.grid(row=0, column=1,padx=20)


func_frm = tk.Frame(main_frm) #功能索引
# func_frm.pack()
func_frm.grid(row=1, column=0,padx=20, pady=20)
select_button =tk.Button(func_frm,bg='skyblue',text='进入查询界面',width=15,height=1,
                            command=lambda :select(main_frm, path))
select_button.grid(row=2, column=0)
# select_button.pack()

func_button_1 =tk.Button(func_frm,bg='skyblue',text='功能按钮2',width=15,height=1,
                            command=lambda :func_cal_pass(main_frm, path))
func_button_1.grid(row=2, column=1)

func_button_2 =tk.Button(func_frm,bg='skyblue',text='功能按钮3',width=15,height=1,
                            command=lambda :func3())
func_button_2.grid(row=2, column=2)


cur_row = 2

4.2 上传功能实现

def upload_file(root,path):
    selectFile = filedialog.askopenfilename()  # askopenfilename 1次上传1个;askopenfilenames1次上传多个
    entry1.insert(0, selectFile)
    path.set(selectFile)

4.3 查询功能实现

  查询主要由查询条件、查询算法、结果展示三部分构成。
在这里插入图片描述

4.31 查询条件交互功能实现

  这里展示出数据所有的列,再根据每一列的需求进行筛选。这里只做了“==”的条件查询。

def select(root,path):
    data_path = path.get()
    data = pd.read_excel(data_path)
    data = data.round(2) 
    data = data.applymap(str)

    colnms = list(data.columns)
            
    values = [sorted(data[nm].drop_duplicates()) for nm in colnms]
    for i in range (len(values)):
        values[i].insert(0, '--请选择')
        values[i] = tuple(values[i])
    # print(values)
    
    start_row = 3
    start_col = 0
    QCONDI_ROW_CNTS = 5 #查询条件多少行一列
    textvs = ['']*len(colnms)
    global cur_row
    condi_frm = tk.Frame(master=root)
    # condi_frm.pack()
    condi_frm.grid(row=cur_row+1)
    for i in range(len(colnms)):
        textvs[i] = tk.StringVar()
        frm = tk.Frame(master=condi_frm)
        frm.grid(row = start_row + i%QCONDI_ROW_CNTS,column = start_col+i//QCONDI_ROW_CNTS,sticky = E)
        com = ttk.Combobox(frm, textvariable=textvs[i])     # #创建下拉菜单
        tk.Label(frm, text = colnms[i]).grid(row= start_row + i,column=0,sticky = W,padx=5,pady=10)
        com.grid(row=start_row + i, column=1,sticky = E,padx=20)     # #将下拉菜单绑定到窗体
        com["value"] = values[i]   
        com.current(0)    
        cur_row = start_row + i
    query_button =tk.Button(root,bg='skyblue',text='查询',width=12,height=1,command=lambda:query(root,data,textvs))
    query_button.grid(row=cur_row+1, columnspan=3)
    root.update()
4.32 查询算法实现

  其实就是数据库的单表查询,直接用最笨的方法,筛完一个再继续,直到为空或者条件筛完。

def query(root,data,textvs):
    global cur_row
    ipts = [v.get() for v in textvs]
    DEFALUT = '--请选择'
    colnms = list(data.columns)
    sub_data = data.copy()
    for i in range(len(ipts)):
        if ipts[i] == DEFALUT:
            pass
        else:
            sub_data = sub_data[sub_data[colnms[i]]==ipts[i]]
    show_data(root,sub_data,pd.DataFrame([]))
4.33 查询结果展示功能实现

  最多展示只展示10条。在这里插入图片描述

def show_data(root,sub_data,detail_data):
    global cur_row
    cur_row = 10
    table_frm = tk.Frame(root)
    table_frm.grid(row=cur_row+1)
    cur_row = cur_row+1
    
    tree = ttk.Treeview(table_frm)      # #创建表格对象
    tree.delete(*tree.get_children())
    tree["columns"] = list(sub_data.columns)     # #定义列
    
    for coln in tree["columns"]:
        tree.column(coln, width=100, anchor = "center")          # #设置列
        tree.heading(coln, text=coln)        # #设置显示的表头名
    
    for i in range(min(10,len(sub_data))): #最多展示
        # print(sub_data.iloc[i])
        tree.insert("", i, text="第"+str(i+1)+"行", values=tuple(sub_data.iloc[i].values))    # #给第0行添加数据,索引值可重复
    
    tree.grid(padx=10, pady=20,row = cur_row+1,columnspan=3)
    cur_row = cur_row + 10
    
    dlpath = tk.StringVar()
    dlpath.set('')
    dl_frm = tk.Frame(root)
    dl_frm.grid(row=cur_row+1, columnspan=3)

    cur_row = cur_row + 1
    entry1 = tk.Entry(dl_frm,textvariable=dlpath,width='40')
    entry1.grid(row=cur_row+1, column=0,padx=20)
    cur_row = cur_row + 1 
    btn = tk.Button(dl_frm, text='保存结果', command=lambda:download_file(sub_data,detail_data,root,dlpath))
    btn.grid(row=cur_row, column=1)
    cur_row = cur_row + 1 
    # cur_row = 0

4.4 业务计算功能实现

   这里的业务也就是常规的统计计算。
在这里插入图片描述

def func_cal_pass(root, path):
    data_path = path.get()
    data = pd.read_excel(data_path)
    colnms = list(data.columns)
        
    # print(colnms)
    dep = 0
    etype = 0
    for i in range(len(colnms)):
        if '所在机构和部门' in colnms[i]:
            dep = i
        if '报考科目' in colnms[i]:
            etype = i
               
    dep_data = sorted(data[colnms[dep]].drop_duplicates())
    # print(dep_data)
    func_data = pd.DataFrame(columns = ["所在机构和部门", "报考人数", "合格人数", "不合格人数","缺考人数","合格率","不合格率","缺考率"])
    for i in range(len(dep_data)):
        sub_data = data[data[colnms[dep]]==dep_data[i]]
        pass_num = len(sub_data[sub_data['合格情况']=='合格']) #通过
        abs_num = len(sub_data[sub_data['成绩']==0]) #缺考
        un_pass_num = len(sub_data)-pass_num-abs_num #未通过
        row = [dep_data[i],len(sub_data),pass_num,un_pass_num,abs_num,round(1.0*pass_num/len(sub_data),2),
               round(1.0*un_pass_num/len(sub_data),2),round(1.0*abs_num/len(sub_data),2)]
        # print(row)
        func_data.loc[i]=row
    
    detail_list = []
    etype_data = sorted(data[colnms[etype]].drop_duplicates())
    for i in range(len(dep_data)):
        sub_data_1 = data[data[colnms[dep]]==dep_data[i]] #先找部门
        for j in range(len(etype_data)):
            sub_data_2 = sub_data_1[sub_data_1[colnms[etype]]==etype_data[j]] #再找科目
            # print(len(sub_data_2))
            if not sub_data_2.empty:
                pass_num = len(sub_data_2[sub_data_2['合格情况']=='合格']) #通过
                abs_num = len(sub_data_2[sub_data_2['成绩']==0]) #缺考
                un_pass_num = len(sub_data_2)-pass_num-abs_num #未通过
                row = [str(dep_data[i]),etype_data[j],len(sub_data_2),pass_num,un_pass_num,abs_num,round(1.0*pass_num/len(sub_data_2),2),
                       round(1.0*un_pass_num/len(sub_data_2),2),round(1.0*abs_num/len(sub_data_2),2)]
                detail_list.append(row)
                print(row)
    detail_data = pd.DataFrame(detail_list,columns = ["所在机构和部门", "报考科目","报考人数", "合格人数", "不合格人数","缺考人数","合格率","不合格率","缺考率"])
    # print(detail_data)
    
    # print(func_data)
    show_data(root,func_data,detail_data)

4.5 结果保存功能实现

def download_file(data,detail_data,root,path):
    selectFile = filedialog.asksaveasfilename()  # askopenfilename 1次上传1个;askopenfilenames1次上传多个
    entry1.insert(0, selectFile)
    path.set(selectFile)
    writer = pd.ExcelWriter(selectFile+'.xlsx')
    if selectFile:
        data.to_excel(writer,index=False,sheet_name='汇总')
        
        if not detail_data.empty:
            detail_data.to_excel(writer,index=False,sheet_name='明细')
        messagebox.showinfo(title = '提示',message='保存成功!')
        writer.save()

4.6 全局滚动条实现

root.update()
canvas.config(scrollregion=[0,0,w,h])
def processWheel(event):  
    a= int(-(event.delta)/60)
    canvas.yview_scroll(a,'units')
    # canvas.xview_scroll(a,'units')
main_frm.bind_all("<MouseWheel>", processWheel)
root.mainloop()   # #窗口持久化

5 打包成.exe文件

   主要用pyinstaller打包,详见Python PyInstaller安装和使用教程(详解版)

6 完整代码

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 14 22:37:56 2021

@author: Administrator
"""
import pandas as pd
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
from tkinter import W,N,E,S,HORIZONTAL,BOTTOM,X,VERTICAL,Y,RIGHT,LEFT,BOTH
# import Pmw

def upload_file(root,path):
    selectFile = filedialog.askopenfilename()  # askopenfilename 1次上传1个;askopenfilenames1次上传多个
    entry1.insert(0, selectFile)
    path.set(selectFile)
    

def func_cal_pass(root, path):
    data_path = path.get()
    data = pd.read_excel(data_path)
    colnms = list(data.columns)
        
    # print(colnms)
    dep = 0
    etype = 0
    for i in range(len(colnms)):
        if '所在机构和部门' in colnms[i]:
            dep = i
        if '报考科目' in colnms[i]:
            etype = i
               
    dep_data = sorted(data[colnms[dep]].drop_duplicates())
    # print(dep_data)
    func_data = pd.DataFrame(columns = ["所在机构和部门", "报考人数", "合格人数", "不合格人数","缺考人数","合格率","不合格率","缺考率"])
    for i in range(len(dep_data)):
        sub_data = data[data[colnms[dep]]==dep_data[i]]
        pass_num = len(sub_data[sub_data['合格情况']=='合格']) #通过
        abs_num = len(sub_data[sub_data['成绩']==0]) #缺考
        un_pass_num = len(sub_data)-pass_num-abs_num #未通过
        row = [dep_data[i],len(sub_data),pass_num,un_pass_num,abs_num,round(1.0*pass_num/len(sub_data),2),
               round(1.0*un_pass_num/len(sub_data),2),round(1.0*abs_num/len(sub_data),2)]
        # print(row)
        func_data.loc[i]=row
    
    detail_list = []
    etype_data = sorted(data[colnms[etype]].drop_duplicates())
    for i in range(len(dep_data)):
        sub_data_1 = data[data[colnms[dep]]==dep_data[i]] #先找部门
        for j in range(len(etype_data)):
            sub_data_2 = sub_data_1[sub_data_1[colnms[etype]]==etype_data[j]] #再找科目
            # print(len(sub_data_2))
            if not sub_data_2.empty:
                pass_num = len(sub_data_2[sub_data_2['合格情况']=='合格']) #通过
                abs_num = len(sub_data_2[sub_data_2['成绩']==0]) #缺考
                un_pass_num = len(sub_data_2)-pass_num-abs_num #未通过
                row = [str(dep_data[i]),etype_data[j],len(sub_data_2),pass_num,un_pass_num,abs_num,round(1.0*pass_num/len(sub_data_2),2),
                       round(1.0*un_pass_num/len(sub_data_2),2),round(1.0*abs_num/len(sub_data_2),2)]
                detail_list.append(row)
                print(row)
    detail_data = pd.DataFrame(detail_list,columns = ["所在机构和部门", "报考科目","报考人数", "合格人数", "不合格人数","缺考人数","合格率","不合格率","缺考率"])
    # print(detail_data)
    
    # print(func_data)
    show_data(root,func_data,detail_data)

def func3():
    return

def select(root,path):
    data_path = path.get()
    data = pd.read_excel(data_path)
    data = data.round(2) 
    data = data.applymap(str)

    colnms = list(data.columns)
            
    values = [sorted(data[nm].drop_duplicates()) for nm in colnms]
    for i in range (len(values)):
        values[i].insert(0, '--请选择')
        values[i] = tuple(values[i])
    # print(values)
    
    start_row = 3
    start_col = 0
    QCONDI_ROW_CNTS = 5 #查询条件多少行一列
    textvs = ['']*len(colnms)
    global cur_row
    condi_frm = tk.Frame(master=root)
    # condi_frm.pack()
    condi_frm.grid(row=cur_row+1)
    for i in range(len(colnms)):
        textvs[i] = tk.StringVar()
        frm = tk.Frame(master=condi_frm)
        frm.grid(row = start_row + i%QCONDI_ROW_CNTS,column = start_col+i//QCONDI_ROW_CNTS,sticky = E)
        com = ttk.Combobox(frm, textvariable=textvs[i])     # #创建下拉菜单
        tk.Label(frm, text = colnms[i]).grid(row= start_row + i,column=0,sticky = W,padx=5,pady=10)
        com.grid(row=start_row + i, column=1,sticky = E,padx=20)     # #将下拉菜单绑定到窗体
        com["value"] = values[i]   
        com.current(0)    
        cur_row = start_row + i
    query_button =tk.Button(root,bg='skyblue',text='查询',width=12,height=1,command=lambda:query(root,data,textvs))
    query_button.grid(row=cur_row+1, columnspan=3)
    root.update()
    
def query(root,data,textvs):
    global cur_row
    ipts = [v.get() for v in textvs]
    DEFALUT = '--请选择'
    colnms = list(data.columns)
    sub_data = data.copy()
    for i in range(len(ipts)):
        if ipts[i] == DEFALUT:
            pass
        else:
            sub_data = sub_data[sub_data[colnms[i]]==ipts[i]]
    show_data(root,sub_data,pd.DataFrame([]))

    
def download_file(data,detail_data,root,path):
    selectFile = filedialog.asksaveasfilename()  # askopenfilename 1次上传1个;askopenfilenames1次上传多个
    entry1.insert(0, selectFile)
    path.set(selectFile)
    writer = pd.ExcelWriter(selectFile+'.xlsx')
    if selectFile:
        data.to_excel(writer,index=False,sheet_name='汇总')
        
        if not detail_data.empty:
            detail_data.to_excel(writer,index=False,sheet_name='明细')
        messagebox.showinfo(title = '提示',message='保存成功!')
        writer.save()
        
def show_data(root,sub_data,detail_data):
    global cur_row
    cur_row = 10
    table_frm = tk.Frame(root)
    table_frm.grid(row=cur_row+1)
    cur_row = cur_row+1
    
    tree = ttk.Treeview(table_frm)      # #创建表格对象
    tree.delete(*tree.get_children())
    tree["columns"] = list(sub_data.columns)     # #定义列
    
    for coln in tree["columns"]:
        tree.column(coln, width=100, anchor = "center")          # #设置列
        tree.heading(coln, text=coln)        # #设置显示的表头名
    
    for i in range(min(10,len(sub_data))): #最多展示
        # print(sub_data.iloc[i])
        tree.insert("", i, text="第"+str(i+1)+"行", values=tuple(sub_data.iloc[i].values))    # #给第0行添加数据,索引值可重复
    
    tree.grid(padx=10, pady=20,row = cur_row+1,columnspan=3)
    cur_row = cur_row + 10
    
    dlpath = tk.StringVar()
    dlpath.set('')
    dl_frm = tk.Frame(root)
    dl_frm.grid(row=cur_row+1, columnspan=3)

    cur_row = cur_row + 1
    entry1 = tk.Entry(dl_frm,textvariable=dlpath,width='40')
    entry1.grid(row=cur_row+1, column=0,padx=20)
    cur_row = cur_row + 1 
    btn = tk.Button(dl_frm, text='保存结果', command=lambda:download_file(sub_data,detail_data,root,dlpath))
    btn.grid(row=cur_row, column=1)
    cur_row = cur_row + 1 
    # cur_row = 0

    
root = tk.Tk()
root.title("乐川考试系统")    # #窗口标题
w = root.winfo_screenwidth()
h = root.winfo_screenheight()

root.geometry("%dx%d" %(w,h))

canvas= tk.Canvas(root, width=w, height=h)

xscrollbar = tk.Scrollbar(root,orient=tk.HORIZONTAL)
yscrollbar = tk.Scrollbar(root)

yscrollbar.config(command=canvas.yview)
yscrollbar.pack(side=tk.RIGHT, fill=tk.Y) 
xscrollbar.config(command=canvas.xview)
xscrollbar.pack(side=tk.BOTTOM, fill=tk.X) 

canvas.config(xscrollcommand=xscrollbar.set,yscrollcommand=yscrollbar.set)


canvas.pack(side="left", fill="both", expand=True) #side=LEFT,expand=True,fill=BOTH
main_frm = tk.Frame(canvas)
canvas.create_window(0,0,window=main_frm, anchor='nw')

# In[]
path = tk.StringVar()     # #创建变量,便于取值
path.set('')

upload_frm = tk.Frame(master=main_frm) #上传组件
upload_frm.grid(row=0,column=0)

entry1 = tk.Entry(upload_frm,textvariable=path,width='40')

entry1.grid(row=0, column=0)
btn = tk.Button(upload_frm, text='上传文件', command=lambda:upload_file(upload_frm,path))
btn.grid(row=0, column=1,padx=20)


func_frm = tk.Frame(main_frm) #功能索引
# func_frm.pack()
func_frm.grid(row=1, column=0,padx=20, pady=20)
select_button =tk.Button(func_frm,bg='skyblue',text='进入查询界面',width=15,height=1,
                            command=lambda :select(main_frm, path))
select_button.grid(row=2, column=0)
# select_button.pack()

func_button_1 =tk.Button(func_frm,bg='skyblue',text='功能按钮2',width=15,height=1,
                            command=lambda :func_cal_pass(main_frm, path))
func_button_1.grid(row=2, column=1)

func_button_2 =tk.Button(func_frm,bg='skyblue',text='功能按钮3',width=15,height=1,
                            command=lambda :func3())
func_button_2.grid(row=2, column=2)


cur_row = 2
# In[]

root.update()
canvas.config(scrollregion=[0,0,w,h])
def processWheel(event):  
    a= int(-(event.delta)/60)
    canvas.yview_scroll(a,'units')
    # canvas.xview_scroll(a,'units')
main_frm.bind_all("<MouseWheel>", processWheel)
root.mainloop()   # #窗口持久化