前言:需要根据闸机进出记录和排班表做一个考勤小程序,现已按需求实现闸机数据分析功能,为了便于使用,利用 Qt Designer 为考勤程序做一个 UI 界面,记录一下实现的全过程以及过程中遇到的问题。
由于篇幅过长,UI 界面布局以及如何将 Qt 文件编译为 Python 文件部分见:利用 Qt Designer 为考勤程序做一个 UI 界面 (一)-优快云博客,本篇介绍 UI 界面与数据分析代码链接的部分见。
在实现 Python 代码逻辑时,发现界面有一些功能缺失及美观性欠缺的问题,下面分两部分进行总结:先将 Qt Designer 中需要调整的部分集中处理,然后再详细讲解 Python 逻辑。
目录
一、查漏补缺 Qt Designer 部分
1、利用 Qt Designer 新增用户输入部分
在 Python 代码中绑定控件事件时发现,忘记在 UI 界面留用户输入部分了,因为考勤程序设计了用户自主输入考勤时间段的逻辑。
打开 Qt Designer
这里选择 “打开”,打开需要修改的 .ui 文件
在界面上,设置 “对比开始日期” 和 “对比结束日期”
这里 dateEdit_start 和 dateEdit_end 样式是一样的,如下:
QDateEdit {
border: 1px solid #dcdcdc;
border-radius: 4px;
padding: 2px 4px;
font-size: 14px;
color: #333333;
background-color: #ffffff;
selection-background-color: #0078d7;
selection-color: #ffffff;
}
QDateEdit:hover {
border-color: #0078d7;
}
QDateEdit:focus {
border-color: #005a9e;
outline: none;
}
重新调整布局后,界面效果如下(这里可以看出,我重新调整了各个框的大小及位置,因为输出结果后,发现 textEdit 太窄了):
2、设置滚动条
Python 代码实现后,发现结果输出部分,竖向自动生产了滚动条,横向根据 textEdit 的宽度,自动将输出内容进行了换行,这样不便于观看,所以希望横向也加上滚动条
点击 textEdit ,选中要编辑的结果输出框,关闭自动换行
这里提示一下,horizontalScrollBarPolicy 和 verticalScrollBarPolicy 属性,这里不用动,默认是 ScrollBarAsNeeded,即仅在需要时显示
由于显示内容为表格数据(DataFrame),可以在 Qt Designer 中设置 QTextEdit 的字体为等宽字体(如 Consolas 或 Monospace),使列对齐更整齐
3、编译 .ui 文件
因为修改了 .ui 文件,需要重新编译 checkUI.ui
二、Python 代码实现
1、通用代码框架
# 导入包
import shutil
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
from PyQt5.QtCore import QDate
from PyQt5 import QtCore
from checkUI import Ui_MainWindow # 这里注意 checkUI 是用 Qtdesigner 写的文件
import pandas as pd
import sys
import os
import re
from datetime import datetime, timedelta
# 实际问题处理部分代码
def
# 类,处理 ui 界面和 python 事件绑定
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self) # 初始化界面
# 绑定按钮事件,一个事件对应一个函数(还记得这里在 Qtdesigner 中为控件取名字吗)
self.pushButton_schedule.clicked.connect(self.upload_schedule)
self.pushButton_records.clicked.connect(self.upload_records)
self.pushButton_analysis.clicked.connect(self.analyze_data)
self.pushButton_download.clicked.connect(self.download_result)
self.pushButton_1.clicked.connect(self.download_result_1)
self.pushButton_2.clicked.connect(self.download_result_2)
self.pushButton_3.clicked.connect(self.download_result_3)
# 按钮事件对应的函数
def upload_schedule(self):
def upload_records(self):
def analyze_data(self):
def download_result(self):
def download_result_1(self):
def download_result_2(self):
def download_result_3(self):
# 主函数
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
2、案例代码框架
这里结合上面通用框架一起看,本部分会在案例基础上详细介绍每一部分的代码
- class MainWindow(QMainWindow, Ui_MainWindow) 之前的所有代码:
import shutil
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
from PyQt5.QtCore import QDate
from PyQt5 import QtCore
from checkUI import Ui_MainWindow
import pandas as pd
import sys
import os
import re
from datetime import datetime, timedelta
# 定义了基础目录,以及班表和进出记录的目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DOOR_RECORD_FOLDER = os.path.join(BASE_DIR, "door_records")
SCHEDULE_FOLDER = os.path.join(BASE_DIR, "schedules")
# 确保文件夹存在
os.makedirs(DOOR_RECORD_FOLDER, exist_ok=True)
os.makedirs(SCHEDULE_FOLDER, exist_ok=True)
# 读 excel,这个函数是用来处理 ui 界面上传的文件
def read_excel_from_folder(folder_path, header_row):
dfs = []
files = os.listdir(folder_path)
for file in files:
if file.endswith('.xls') or file.endswith('.xlsx'):
file_path = os.path.join(folder_path, file)
df = pd.read_excel(file_path, header=header_row)
dfs.append(df)
return dfs
# 检查迟到早退离岗等情况,这部分是案例具体需求,因涉及内部信息,不具体讲解,大家按各自需求写
def check_late_employees(records_list, schedule_list, start_date, end_date):
# 这里会返回详细数据,以及三个部门的情况
return merged_df, df_01, df_02, df_03
- class MainWindow(QMainWindow, Ui_MainWindow) 部分代码:(这以后的所有代码都会详细介绍)
本部分介绍的所有 def 函数都在 class MainWindow(QMainWindow, Ui_MainWindow) 下
def(1)初始化、界面设置、按钮事件绑定
完整代码:
def __init__(self):
super().__init__()
self.setupUi(self) # 初始化界面
# 隐藏框
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# 用于跟踪拖动的变量
self.dragging = False
self.offset = QtCore.QPoint()
# 初始化路径配置
self.BASE_DIR = os.path.dirname(os.path.abspath(__file__))
self.DOOR_RECORD_FOLDER = os.path.join(self.BASE_DIR, "door_records")
self.SCHEDULE_FOLDER = os.path.join(self.BASE_DIR, "schedules")
# 初始化日期控件
self.dateEdit_start.setDate(QDate.currentDate().addDays(-30)) # 默认30天前
self.dateEdit_end.setDate(QDate.currentDate()) # 默认今天
# 初始化文件记录
self.schedule_file = ''
self.records_files = []
# 绑定按钮事件,一个事件对应一个函数
self.pushButton_schedule.clicked.connect(self.upload_schedule)
self.pushButton_records.clicked.connect(self.upload_records)
self.pushButton_analysis.clicked.connect(self.analyze_data)
self.pushButton_download.clicked.connect(self.download_result)
self.pushButton_1.clicked.connect(self.download_result_1)
self.pushButton_2.clicked.connect(self.download_result_2)
self.pushButton_3.clicked.connect(self.download_result_3)
详细介绍:
隐藏框代码
# 隐藏框
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
隐藏前,可以看到除了设置的圆角白色背景外,还有一个 “MainWindow” 边框,而且明显存在两个 “×” 号关闭按钮(这两个按钮都可以实现关闭界面的效果)
隐藏框后,可以看到整个界面就简约了很多(后面的黑色是我的电脑桌面...)
鼠标跟踪代码初始化
# 用于跟踪拖动的变量
self.dragging = False
self.offset = QtCore.QPoint()
当设置 FramelessWindowHint 隐藏窗口边框后,默认的标题栏被移除,而标题栏是系统提供的窗口拖动区域。透明背景进一步移除了窗口的默认事件响应区域,导致无法通过鼠标拖动。(白话版:运行程序后,出现的窗口,初始位置在哪里就固定住了,我们没办法通过鼠标将窗口移动至其他位置),但是设置鼠标跟踪拖动的变量,就可以自由的移动界面了,这部分也属于初始化变量,具体会和后面的 def 函数联动使用
日历控件初始化
# 初始化日期控件
self.dateEdit_start.setDate(QDate.currentDate().addDays(-30)) # 默认30天前
self.dateEdit_end.setDate(QDate.currentDate()) # 默认今天
运行程序后,界面显示的日历控件,可以通过初始化,设置默认显示时间
按钮事件绑定
# 绑定按钮事件,一个事件对应一个函数
self.pushButton_schedule.clicked.connect(self.upload_schedule)
self.pushButton_records.clicked.connect(self.upload_records)
self.pushButton_analysis.clicked.connect(self.analyze_data)
self.pushButton_download.clicked.connect(self.download_result)
self.pushButton_1.clicked.connect(self.download_result_1)
self.pushButton_2.clicked.connect(self.download_result_2)
self.pushButton_3.clicked.connect(self.download_result_3)
这里不多说了,固定写法,照着写即可, pushButton_schedule、pushButton_records、pushButton_analysis、pushButton_download、pushButton_1、pushButton_2、pushButton_3 是在 QtDesigner 中为控件起的名字,每个按钮按下后会触发 upload_schedule、upload_records、analyze_data、download_result、download_result_1、download_result_2、download_result_3 这些函数,后面会介绍。
def(2)鼠标移动界面函数
def mousePressEvent(self, event):
"""鼠标按下事件"""
if event.button() == QtCore.Qt.LeftButton:
# 记录鼠标按下时的位置(相对窗口的位置)
self.offset = event.pos()
self.dragging = True
def mouseMoveEvent(self, event):
"""鼠标移动事件"""
if self.dragging:
# 计算窗口需要移动到的位置(鼠标当前位置 - 偏移量)
self.move(event.globalPos() - self.offset)
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
if event.button() == QtCore.Qt.LeftButton:
self.dragging = False
这里和 “鼠标跟踪代码初始化” 部分是联合使用的,前面解释过为什么要设置鼠标移动界面,这3个函数就是鼠标跟踪的具体实现逻辑
def(3)上传文件函数
def upload_schedule(self):
options = QFileDialog.Options()
file, _ = QFileDialog.getOpenFileName(self, "上传排班表", "", "Excel Files (*.xls *.xlsx);;All Files (*)",
options=options)
if file:
self.schedule_file = file
shutil.copy(file, SCHEDULE_FOLDER)
self.label_schedule.setText(os.path.basename(file))
def upload_records(self):
options = QFileDialog.Options()
files, _ = QFileDialog.getOpenFileNames(self, "上传闸机记录", "", "Excel Files (*.xls *.xlsx);;All Files (*)",
options=options)
if files:
self.records_files = files
for file in files:
shutil.copy(file, DOOR_RECORD_FOLDER)
self.label_records.setText("; ".join([os.path.basename(file) for file in files]))
上传班表和上传闸机记录的函数是一样的,这里一起介绍,下面这句话中 “上传排班表” 以及 “Excel Files (*.xls *.xlsx);;All Files (*)” 所显示的位置如图,点击上传按钮后,弹出的对话框会有所体现
file, _ = QFileDialog.getOpenFileName(self, "上传排班表", "", "Excel Files (*.xls *.xlsx);;All Files (*)",
options=options)
def(4)核心数据分析函数
def analyze_data(self):
if not self.schedule_file or not self.records_files:
self.textEdit.setText("请先上传排班表和闸机记录文件。")
return
try:
# 获取日期范围
start_date = self.dateEdit_start.date().toString("yyyy-MM-dd")
end_date = self.dateEdit_end.date().toString("yyyy-MM-dd")
if self.dateEdit_start.date() > self.dateEdit_end.date():
QMessageBox.warning(self, "错误", "结束日期不能早于开始日期!")
return
# 读取数据
records_list = read_excel_from_folder(self.DOOR_RECORD_FOLDER, header_row=1)
schedule_list = read_excel_from_folder(self.SCHEDULE_FOLDER, header_row=2)
merged_df, forecast_df, equipment_df, info_df = check_late_employees(
records_list, schedule_list, start_date, end_date
)
# 直接指定要提取的列名列表
selected_columns = [
"日期",
"姓名",
"部门名称",
"岗位名称",
"上午班到岗",
"下午班到岗",
"下班状态1",
"下班状态2",
"在岗状态"
]
# 使用列表推导式提取每一行的指定列
forecast_df_show = forecast_df[selected_columns]
info_df_show = info_df[selected_columns]
equipment_df_show = equipment_df[selected_columns]
# 显示结果
self.textEdit_1.setText(forecast_df_show.to_string(index=False))
self.textEdit_2.setText(info_df_show.to_string(index=False))
self.textEdit_3.setText(equipment_df_show.to_string(index=False))
# 输出到Excel文件(默认保存到当前目录)
output_dir = os.path.join(self.BASE_DIR, "results")
os.makedirs(output_dir, exist_ok=True)
forecast_df.to_excel(os.path.join(output_dir, "结果1.xlsx"), index=False)
info_df.to_excel(os.path.join(output_dir, "结果2.xlsx"), index=False)
equipment_df.to_excel(os.path.join(output_dir, "结果3.xlsx"), index=False)
merged_df.to_excel(os.path.join(output_dir, "数据分析明细.xlsx"), index=False)
except Exception as e:
QMessageBox.critical(self, "错误", f"分析失败: {str(e)}")
finally:
# 安全清空上传目录
shutil.rmtree(self.DOOR_RECORD_FOLDER)
shutil.rmtree(self.SCHEDULE_FOLDER)
os.makedirs(DOOR_RECORD_FOLDER, exist_ok=True)
os.makedirs(SCHEDULE_FOLDER, exist_ok=True)
# 清空上传记录
self.schedule_file = ''
self.records_files = []
这部分首先做了个文件判断,如果未上传文件就直接点击按钮进行分析,会给出弹窗提醒。由于通过日历控件输入 “开始时间” 和 “结束时间” ,所以做了一个基本的日期判断。后面基本是一些纯 Python 的逻辑,下面抽出的代码部分,是 textEdit 用来显示内容的用法:
# 显示结果
self.textEdit_1.setText(forecast_df_show.to_string(index=False))
self.textEdit_2.setText(info_df_show.to_string(index=False))
self.textEdit_3.setText(equipment_df_show.to_string(index=False))
def(5)文件下载函数
def download_result(self):
"""下载数据分析明细"""
self.download_excel_file("数据分析明细.xlsx", "保存数据分析明细")
def download_result_1(self):
"""下载结果1"""
self.download_excel_file("结果1.xlsx", "保存结果1")
def download_result_2(self):
"""下载结果2"""
self.download_excel_file("结果2.xlsx", "保存结果2")
def download_result_3(self):
"""下载结果3"""
self.download_excel_file("结果3.xlsx", "保存结果3")
def download_excel_file(self, filename, dialog_title):
"""通用下载文件方法"""
try:
# 构建完整文件路径
output_dir = os.path.join(self.BASE_DIR, "results")
source_path = os.path.join(output_dir, filename)
# 检查源文件是否存在
if not os.path.exists(source_path):
self.textEdit.append(f"找不到结果文件 {filename},请先进行分析")
return
# 获取保存路径
options = QFileDialog.Options()
save_path, _ = QFileDialog.getSaveFileName(
self,
dialog_title,
filename, # 默认文件名
"Excel Files (*.xlsx);;All Files (*)",
options=options
)
if not save_path:
return # 用户取消保存
# 复制文件(更高效的方式)
try:
shutil.copyfile(source_path, save_path)
self.textEdit.append(f"文件已成功保存到:{save_path}")
except Exception as e:
self.textEdit.append(f"保存文件时出错:{str(e)}")
except Exception as e:
self.textEdit.append(f"操作过程中发生错误:{str(e)}")
download_result、download_result_1、download_result_2、download_result_3 这4个函数是前面按钮事件绑定的跳转函数,由于 4 个按钮都是下载事件,具有通用性,所以建了一个 download_excel_file 函数。
def(6)主函数
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
固定写法,照着写即可
三、运行效果
初始界面
输入分析时间段,上传文件,运行代码
结果输出
点击下载
至此,我们已经完成了一个考勤小程序的开发,快去试试吧 ~