利用 Qt Designer 为考勤程序做一个 UI 界面 (二)

前言:需要根据闸机进出记录和排班表做一个考勤小程序,现已按需求实现闸机数据分析功能,为了便于使用,利用 Qt Designer 为考勤程序做一个 UI 界面,记录一下实现的全过程以及过程中遇到的问题。

        由于篇幅过长,UI 界面布局以及如何将 Qt 文件编译为 Python 文件部分见:利用 Qt Designer 为考勤程序做一个 UI 界面 (一)-优快云博客,本篇介绍 UI 界面与数据分析代码链接的部分见。

        在实现 Python 代码逻辑时,发现界面有一些功能缺失及美观性欠缺的问题,下面分两部分进行总结:先将 Qt Designer 中需要调整的部分集中处理,然后再详细讲解 Python 逻辑。

目录

一、查漏补缺 Qt Designer 部分

1、利用 Qt Designer 新增用户输入部分

2、设置滚动条

3、编译 .ui 文件

二、Python 代码实现

1、通用代码框架

2、案例代码框架

def(1)初始化、界面设置、按钮事件绑定

def(2)鼠标移动界面函数

def(3)上传文件函数

def(4)核心数据分析函数

def(5)文件下载函数

def(6)主函数

三、运行效果

一、查漏补缺 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_())

 固定写法,照着写即可

三、运行效果

        初始界面

        输入分析时间段,上传文件,运行代码

        结果输出

        点击下载

至此,我们已经完成了一个考勤小程序的开发,快去试试吧 ~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值