PyQt6/PySide6:账本项目前端制作【附完整项目地址】

0. 前言

最近在家里闲着没事,正好又看到朋友@studentWheat发了篇用Tkinter做的账本,于是决定跟他一起改进这个程序。

屏幕截图:
MainWindow
dlgAdd
stat

1. 后端

后端主要是朋友做的,在这里就不多说了,放个代码:
src/api.py

from collections import defaultdict

class ApiError(RuntimeError):
    pass

def openFile(filename):
    '''
    Open file.
    File format: 4 lines per record for date, event type, money delta, and note.
    Such as:
    (file.example, encoding=utf-8)
      (Record 1)
        (ln 1) date1
        (ln 2) event_type1
        (ln 3) money_delta1
        (ln 4) note1
      (Record 2)
        (ln 5) date2
        (ln 6) event_type2
        (ln 7) money_delta2
        (ln 8) note2
    @param filename: File name.
    Returns: data in the format [[date1, event_type1, money_delta1, note1], ...]
    '''
    with open(filename, 'r', encoding='utf-8') as f:
        res = []
        while date := f.readline():
            if (etype := f.readline()) and (mdelta := f.readline()) and (note := f.readline()):
                res.append([date.rstrip('\n'), etype.rstrip('\n'), mdelta.rstrip('\n'), note.rstrip('\n')])
            else:
                raise ApiError('Unexpected EOF at ' + filename)
        return res

def saveFile(filename, data): # Save
    '''
    Save with the same format mentioned in openFile().
    @param filename: File name.
    @param data: Data with the same format returned in openFile().
    '''
    with open(filename, 'w', encoding='utf-8') as f:
        for line in data:
            print(*line, sep='\n', file=f)

def query(data, key):
    return [record for record in data if any(key in x for x in record)] if key else data

def total(data):
    in_total = out_total = 0
    for _, _, mdelta, _ in data:
        mdelta = int(mdelta)
        if mdelta < 0:
            out_total -= mdelta
        else:
            in_total += mdelta
    return in_total, out_total

def totalByEvent(data):
    cnt = defaultdict(lambda: [0, 0])
    for _, event, mdelta, _ in data:
        mdelta = int(mdelta)
        if mdelta < 0:
            cnt[event][1] -= mdelta
        else:
            cnt[event][0] += mdelta
    return cnt

def totalByDate(data):
    cnt = defaultdict(lambda: [0, 0])
    for date, _, mdelta, _ in data:
        mdelta = int(mdelta)
        if mdelta < 0:
            cnt[date][1] -= mdelta
        else:
            cnt[date][0] += mdelta
    return cnt

详见https://blog.csdn.net/qq_67190987/article/details/125918530

2. 前端

正如标题中所说,框架采用Qt6+Python,一般有两种选择(PyQt6PySide6),我这里使用的是PySide6

2.1 准备资源

src/icons下存好所有图片资源:
图片

2.2 Designer 窗口绘制

用Qt Designer绘制好各个窗口,如图:

QMainWindow
QDialog

2.3 安装依赖项

准备一份requirements.txt,内容如下:

PySide6>=6.3.1

然后,cmd中输入:

pip install -r requirements.txt

搞定安装。

2.4 编译资源和UI

这个就不用多说了,直接用pyside6-uicpyside6-rcc命令编译文件,编译出的文件列表如下:

AccountBook
└─src
    │  dlgAdd.ui
    │  dlgCharts.ui
    │  MainWindow.ui
    │  res.qrc
    │  res_rc.py
    │  ui_dlgAdd.py
    │  ui_dlgCharts.py
    │  ui_dlgHelp.py
    │  ui_MainWindow.py

2.5 代码编写

src/dlgAdd.py
“添加账目”窗口,一个简单的QDialog实例。

from PySide6.QtWidgets import *
from PySide6.QtCore import QDate, QRegularExpression
from PySide6.QtGui import QRegularExpressionValidator
from ui_dlgAdd import Ui_Dialog

class dlgAdd(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        self.ui.dateEdit.setDate(QDate.currentDate())
        self.ui.moneyEdit.setValidator(QRegularExpressionValidator(QRegularExpression(r'(\+|\-)[1-9]+[0-9]*')))
        self.ui.buttonBox.button(QDialogButtonBox.Ok).setText('确定')
        self.ui.buttonBox.button(QDialogButtonBox.Cancel).setText('取消')
    
    def getRow(self):
        date = self.ui.dateEdit.text()
        event = self.ui.eventEdit.text()
        money = self.ui.moneyEdit.text()
        note = self.ui.noteEdit.text()
        return [date, event, money, note]

    def accept(self):
        if not self.ui.eventEdit.text():
            QMessageBox.critical(self, "错误", "事件不能为空,请重新填写。")
            return
        if self.ui.moneyEdit.text() in ('', '+', '-'):
            QMessageBox.critical(self, "错误", "金额不能为空,请重新填写。")
            return
        return super().accept()

src/dlgCharts.py
图表展示窗口,使用QtCharts(用法跟PyQt5/PySide2略有区别)绘制柱状图。后续会考虑增加更多图表。

from bisect import bisect_left, bisect_right

from PySide6.QtCore import QDate, Qt
from PySide6.QtWidgets import *
from PySide6.QtCharts import QBarCategoryAxis, QBarSeries, QBarSet, QChart, QChartView, QValueAxis

from api import total, totalByDate, totalByEvent
from ui_dlgCharts import Ui_Dialog

class dlgCharts(QDialog):
    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        self.showMaximized()

        self.data = data
        minDate = QDate.fromString(data[0][0], 'yyyy/MM/dd')
        maxDate = QDate.fromString(data[-1][0], 'yyyy/MM/dd')
        self.ui.startDateEdit.setDateRange(minDate, maxDate)
        self.ui.endDateEdit.setDateRange(minDate, maxDate)
        self.ui.startDateEdit.setDate(minDate)
        self.ui.endDateEdit.setDate(maxDate)

        self.__update_totalChart(*total(data))
        self.__update_eventChart(totalByEvent(data))
        self.__update_dateChart(totalByDate(data))

        self.ui.startDateEdit.editingFinished.connect(self.__updateCharts)
        self.ui.endDateEdit.editingFinished.connect(self.__updateCharts)

    @staticmethod
    def createChart(chartView: QChartView, title, xAxis, yAxisList):
        chart = QChart()
        chart.setTitle(title)
        chart.setAnimationOptions(QChart.SeriesAnimations)

        series = QBarSeries()
        for axisName, data in yAxisList:
            barSet = QBarSet(axisName)
            barSet.append(data)
            series.append(barSet)
        chart.addSeries(series)

        axisX = QBarCategoryAxis()
        axisX.append(xAxis)
        chart.addAxis(axisX, Qt.AlignBottom)
        series.attachAxis(axisX)

        axisY = QValueAxis()
        axisY.setLabelFormat('%d')
        chart.addAxis(axisY, Qt.AlignLeft)
        series.attachAxis(axisY)

        chartView.setChart(chart)

    def __update_totalChart(self, total_in, total_out):
        self.createChart(
            chartView = self.ui.totalView,
            title     = '总收支',
            xAxis     = ['收入', '支出'], 
            yAxisList = [
                ('金额', [total_in, total_out])
            ]
        )

    def __update_eventChart(self, events):
        self.createChart(
            chartView = self.ui.eventView,
            title     = '收支分类',
            xAxis     = list(events.keys()),
            yAxisList = [
                ('收入', list(map(lambda x: x[0], events.values()))),
                ('支出', list(map(lambda x: x[1], events.values())))
            ]
        )

    def __update_dateChart(self, dates):
        self.createChart(
            chartView = self.ui.dateView,
            title     = '每日收支',
            xAxis     = list(dates.keys()),
            yAxisList = [
                ('收入', list(map(lambda x: x[0], dates.values()))),
                ('支出', list(map(lambda x: x[1], dates.values())))
            ]
        )

    def __updateCharts(self):
        startDate = self.ui.startDateEdit.text()
        endDate = self.ui.endDateEdit.text()
        left = bisect_left(self.data, startDate, key=lambda x: x[0])
        right = bisect_right(self.data, endDate, key=lambda x: x[0])
        data = self.data[left:right]
        self.__update_totalChart(*total(data))
        self.__update_eventChart(totalByEvent(data))
        self.__update_dateChart(totalByDate(data))

src/main.py
主程序,同时管理主窗口。最麻烦的地方是QTableView,要同时处理搜索和排序问题。

import sys
from bisect import insort_right
from functools import partial
from os.path import basename
from webbrowser import open_new_tab

from PySide6.QtWidgets import *
from PySide6.QtCore import Slot, QDate
from PySide6.QtGui import QStandardItem, QStandardItemModel

from api import ApiError, openFile, query, saveFile
from dlgAdd import dlgAdd
from dlgCharts import dlgCharts
from ui_dlgHelp import Ui_Dialog as Ui_dlgHelp
from ui_MainWindow import Ui_MainWindow

# Version info
VERSION = '1.0.1'
CHANNEL = 'stable'
BUILD_DATE = '2022-07-01'
FULL_VERSION = f'{VERSION}-{CHANNEL} ({BUILD_DATE}) on {sys.platform}'

app = QApplication(sys.argv)

class AccountBookMainWindow(QMainWindow):
    version_str = '账本 ' + VERSION
    unsaved_tip = '*'
    SUPPORTED_FILTERS = '账本文件(*.abf);;文本文件(*.txt);;所有文件(*.*)'

    def __init__(self, parent=None):
        # Initialize window
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setWindowTitle('账本 ' + VERSION)
        self.labStatus = QLabel(self)
        self.ui.statusBar.addWidget(self.labStatus)

        # Initialize table
        self.model = QStandardItemModel(0, 4, self)
        self.model.setHorizontalHeaderLabels(['日期', '事项', '金额', '备注'])
        self.ui.table.setModel(self.model)
        self.ui.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        self.__data = []
        self.on_actFile_New_triggered()
        self.ui.actEdit_Remove.setEnabled(False)

        # Connect slots
        self.ui.table.selectionModel().selectionChanged.connect(self.__selectionChanged)
        self.model.itemChanged.connect(self.__itemChanged)

    def __updateTable(self, data):
        self.model.itemChanged.disconnect(self.__itemChanged)
        self.model.setRowCount(len(data))
        for row in range(len(data)):
            for col in range(len(data[row])):
                self.model.setItem(row, col, QStandardItem(data[row][col]))
        self.model.itemChanged.connect(self.__itemChanged)

    def __openFile(self, filename):
        try:
            self.__data = openFile(filename)
        except IOError:
            QMessageBox.critical(self, '错误', '文件打开失败。请稍后再试。')
        except ApiError:
            QMessageBox.critical(self, '错误', '文件格式错误。请检查文件完整性。')
        except Exception as e:
            QMessageBox.critical(self, '错误', '未知错误:' + str(e.with_traceback()))
        else:
            self.ui.searchEdit.clear()
            self.__key = ''
            self.__updateTable(self.__data)
            self.labStatus.setText(filename)
            self.setWindowTitle(self.version_str)
            self.__filename = filename

    def __saveFile(self, filename):
        try:
            saveFile(filename, self.__data)
        except IOError:
            QMessageBox.critical(self, '错误', '文件保存错误。请稍后再试。')
        except Exception as e:
            QMessageBox.critical(self, '错误', '未知错误:' + str(e.with_traceback()))
        else:
            self.labStatus.setText('保存成功:' + filename)
            self.setWindowTitle(self.version_str)
            self.__filename = filename

    @Slot()
    def on_actFile_New_triggered(self):
        self.__filename = self.__key = ''
        self.setWindowTitle(self.unsaved_tip + self.version_str)
        self.labStatus.setText('新文件')
        self.model.setRowCount(0)
        self.__data.clear()

    @Slot()
    def on_actFile_Open_triggered(self):
        filename, _ = QFileDialog.getOpenFileName(self, '打开', filter=self.SUPPORTED_FILTERS)
        if filename:
            self.__openFile(filename)

    @Slot()
    def on_actFile_Save_triggered(self):
        if self.__filename:
            self.__saveFile(self.__filename)
        else:
            filename, _ = QFileDialog.getSaveFileName(self, '保存', filter=self.SUPPORTED_FILTERS)
            if filename:
                self.__saveFile(filename)

    @Slot()
    def on_actFile_SaveAs_triggered(self):
        filename, _ = QFileDialog.getSaveFileName(self, '另存为', filter=self.SUPPORTED_FILTERS)
        if filename:
            self.__saveFile(filename)

    @Slot()
    def on_actHelp_About_triggered(self):
        dialog = QDialog(self)
        ui = Ui_dlgHelp()
        ui.setupUi(dialog)
        for link in (ui.githubLink, ui.giteeLink, ui.licenseLink, ui.readmeLink):
            link.clicked.connect(partial(open_new_tab, link.description()))
        ui.labVersion.setText('版本号:' + FULL_VERSION)
        ui.btnUpdate.clicked.connect(partial(open_new_tab, 'https://github.com/GoodCoder666/AccountBook/releases'))
        dialog.exec()

    @Slot()
    def on_actHelp_AboutQt_triggered(self):
        QMessageBox.aboutQt(self, '关于Qt')

    @Slot()
    def on_actEdit_Add_triggered(self):
        dialog = dlgAdd(self)
        if dialog.exec() == QDialog.Accepted:
            row = dialog.getRow()
            insort_right(self.__data, row)
            self.__updateTable(query(self.__data, self.__key))
            self.setWindowTitle(self.unsaved_tip + self.version_str)

    @Slot()
    def on_actEdit_Remove_triggered(self):
        rows = list(set(map(lambda idx: idx.row(), self.ui.table.selectedIndexes())))
        for row in rows:
            self.__data.remove([self.model.item(row, col).text() for col in range(self.model.columnCount())])
        self.model.itemChanged.disconnect(self.__itemChanged)
        self.model.removeRows(rows[0], len(rows))
        self.model.itemChanged.connect(self.__itemChanged)
        self.setWindowTitle(self.unsaved_tip + self.version_str)

    def __selectionChanged(self):
        self.ui.actEdit_Remove.setEnabled(self.ui.table.selectionModel().hasSelection())

    def __itemChanged(self, item: QStandardItem):
        i, j, new = item.row(), item.column(), item.text()
        if (old := self.__data[i][j]) == new: return
        if j == 0 and not QDate.fromString(new, 'yyyy/MM/dd').isValid():
            QMessageBox.critical(self, '错误', '日期格式错误。')
            self.model.itemChanged.disconnect(self.__itemChanged)
            item.setText(old)
            self.model.itemChanged.connect(self.__itemChanged)
            return
        row = self.__data.pop(i)
        row[j] = new
        insort_right(self.__data, row)
        self.__updateTable(query(self.__data, self.__key))
        self.setWindowTitle(self.unsaved_tip + self.version_str)

    @Slot()
    def on_searchEdit_textChanged(self):
        self.__key = self.ui.searchEdit.text()
        self.__updateTable(query(self.__data, self.__key))

    @Slot()
    def on_actStat_Show_triggered(self):
        if self.__data:
            dlgCharts(self.__data, self).exec()
        else:
            QMessageBox.information(self, '提示', '请添加数据以使用统计功能。')

    def closeEvent(self, event):
        if not self.windowTitle().startswith(self.unsaved_tip): return
        filename = basename(self.__filename) if self.__filename else '新文件'
        messageBox = QMessageBox(
            parent=self, icon=QMessageBox.Warning, windowTitle='提示',
            text=f'是否要保存对 {filename} 的更改?', informativeText='如果不保存,你的更改将丢失。',
            standardButtons=QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
        )
        messageBox.setButtonText(QMessageBox.Save, '保存')
        messageBox.setButtonText(QMessageBox.Discard, '不保存')
        messageBox.setButtonText(QMessageBox.Cancel, '取消')
        reply = messageBox.exec()
        if reply == QMessageBox.Save:
            self.on_actFile_Save_triggered()
            event.accept()
        elif reply == QMessageBox.Discard:
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        self.__openFile(event.mimeData().text()[8:]) # [8:] is to get rid of 'file:///'

mainform = AccountBookMainWindow()
mainform.show()

sys.exit(app.exec())

3. 总结

本项目到此结束。

【附:项目地址】

记得点个Star哦~


创作不易,若您喜欢这篇文章就请点个三连吧!万分感激!!!

【资源介绍】 基于PySide6+Python的企业员工业绩管理系统源码(注释拉满)+项目说明.zip 运行 安装 Python 3 (已在3.8.10上测试通过) 安装 PySide6 python -m pip install pyside6 运行 python main.py 本课题要求编写Python程序实现对员工信息和业绩信息的管理。一个综合的员工业绩管理系统,要求能够管理若干个员工各季度工作业绩,需要实现以下功能:读取以数据文件形式存储的员工信息;可以按工号增加、修改、删除员工的信息;按照工号、姓名、名次等方式查询员工信息;可以按照工号顺序浏览员工信息;可以统计各季度工作的最高业绩、最低业绩和平均业绩;计算每个员工的总业绩并进行排名。 系统内的所有信息必须以文件的方式存储在硬盘中,员工信息文件,存放了员工的工号,姓名,性别,各个季度的业绩,四个季度的总业绩,依据总业绩的排名。格式如下: ```csv B01,Tom,Male,70,80,90,85 B02,Rose,Female,70,80,90,75 B03,Jack,Male,80,90, 95,70 …… ``` ## 2 要求及提示 ### 2.1 基本要求 系统提供的基本功能包括: 1. 系统内的相关信息文件由程序设计人员预先从键盘上录入,文件中的数据记录不得少于20条; 2. 设计并实现系统的相关界面,提供良好的交互界面; 3. 排序功能:能实现由用户选择按各项数据升序或降序排序对查询出的信息进行显示; 4. 可以添加/删除/修改员工信息; 5. 可以添加/删除/修改业绩信息; 6. 查询员工信息: - 输入一个工号,查出此人的基本信息并显示输出。 - 输入一个工号,查询出此员工的所有业绩情况。 7. 查询业绩信息: - 输入一个季度时,查询此季度的最高业绩、最低业绩和平均业绩。 该项目是个人毕设项目,答辩评审分达到95分,代码都经过调试测试,确保可以运行!欢迎下载使用,可用于小白学习、进阶。 该资源主要针对计算机、通信、人工智能、自动化等相关专业的学生、老师或从业者下载使用,亦可作为期末课程设计、课程大作业、毕业设计等。 项目整体具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现不同的功能。 欢迎下载交流,互相学习,共同进步!
【资源介绍】 基于PySide6框架+python的智能充电桩调度计费系统前端源码+项目说明文档.zip 基于 PySide6 框架与 Python3.10 开发。 开发环境配置 下载安装 Python3.10(Windows 建议使用微软商店) 克隆项目仓库 在项目根目录执行 python3.10 -m pip install -r requirements.txt 使用 python3.10 admin/main.py 与 python3.10 user/main.py 分别启动用户客户端与管理员客户端程序 调试方法 调试 AcssFrontend 的方法与调试 AcssBackend 的方法一致 使用 Visual Studio Code 打开本项目,并点击右下角 Python 版本切换至 Python3.10 在调试选项中选择 Python 文件 并创建 launch.json 在希望调试的位置添加断点 使用功能键 F5 启动调试 代码要求 不可以在代码中使用同步阻塞操作,例如 sleep(1); 所有阻塞操作均需要使用使用支持 asyncio 的函数,并在函数前添加 await 关键字; 使用 await 关键字的函数 def 前需要添加 async 关键字。 基本代码逻辑说明 程序入口模块 main.py 这里执行一些基本初始化操作,并且定义按钮点击事件的响应函数,并将它们绑定到 Qt 控件上。 Qt 以阻塞的方式调用 Python 函数,所以直接在函数中执行网络IO是不可行的(等待响应的过程用户界面会失去响应),所以这里使用 qasync 模块兼容了 Python 的异步框架 asyncio。通过这种方法,所有阻塞操作全部可以使用异步方式调用,这样就不会阻塞用户界面了。异步调用示例如下: import api import qasync @qasync.asyncSlot() async def on_login_clicked(): try: kwargs = await api.login('jinuo', 'i-hate-bupt') except ApiError as e: some_toast(str(e)).show() token = kwargs['token'] is_admin = kwargs['is_admin'] # rest codes 响应函数调用 API 模块提供的函数以与服务端进行交互。捕获 ApiError 并显示提示框,若未发生异常则从返回值中获取服务器返回的数据,并对用户界面进行相应的更新。 主窗口模块 mainwindow.py 该模块负责加载窗口,并保存控件的 Python 对象。显然这样的写法不是最佳实践,我没有花时间找到加载UI文件并继承 QWidget 类的方法(又不是不能用)。 API 模块 api.py 所有对网络的调用全部在 API 模块内完成。 API 模块内的 api_post 与 api_get 两个函数封装了 requests 库的 get 与 post 操作。这两个函数需要能够自动在其中添加 Authorization 请求头。 api_post 与 api_get 中使用 requests 模块时,需要捕获网络 IO 异常(如超时),并且使用 raise ApiError("网络异常") from e 将相关异常以 ApiError 异常的类型抛出。 api_post 与 api_get 在获取到响应时,需要判断 code 是否为 0,为 -1 时需要抛出 ApiError(response['message']) 异常。 API 模块除 api_post 与 api_get 的函数与 开放 API 文档 内的接口一一对应,需要调用 api_post 与 api_get 对服务端发起请求。 这些函数不需要捕获 ApiError 异常,对异常的捕获应该发生在 API 模块的调用者处,以决定是否要反馈到用户界面中(Toast)。 调用者捕获到 ApiError 异常时,仅需要使用 str(e) 即可获取到异常的文本信息, 该项目是个人毕设项目,答辩评审分达到95分,代码都经过调试测试,确保可以运行!欢迎下载使用,可用于小白学习、进阶。 该资源主要针对计算机、通信、人工智能、自动化等相关专业的学生、老师或从业者下载使用,亦可作为期末课程设计、课程大作业、毕业设计等。 项目整体具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现不同的功能。 欢迎下载交流,互相学习,共同进步!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值