0. 前言

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

屏幕截图:


1. 后端

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

from collections import defaultdictclass ApiError(RuntimeError):passdef 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 resdef 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 datadef total(data):in_total = out_total = 0for _, _, mdelta, _ in data:mdelta = int(mdelta)if mdelta < 0:out_total -= mdeltaelse:in_total += mdeltareturn in_total, out_totaldef totalByEvent(data):cnt = defaultdict(lambda: [0, 0])for _, event, mdelta, _ in data:mdelta = int(mdelta)if mdelta < 0:cnt[event][1] -= mdeltaelse:cnt[event][0] += mdeltareturn cntdef totalByDate(data):cnt = defaultdict(lambda: [0, 0])for date, _, mdelta, _ in data:mdelta = int(mdelta)if mdelta < 0:cnt[date][1] -= mdeltaelse:cnt[date][0] += mdeltareturn cnt

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

2. 前端

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

2.1 准备资源

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

2.2 Designer 窗口绘制

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


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_Dialogclass 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, "错误", "事件不能为空,请重新填写。")returnif self.ui.moneyEdit.text() in ('', '+', '-'):QMessageBox.critical(self, "错误", "金额不能为空,请重新填写。")returnreturn super().accept()

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

from bisect import bisect_left, bisect_rightfrom PySide6.QtCore import QDate, Qt
from PySide6.QtWidgets import *
from PySide6.QtCharts import QBarCategoryAxis, QBarSeries, QBarSet, QChart, QChartView, QValueAxisfrom api import total, totalByDate, totalByEvent
from ui_dlgCharts import Ui_Dialogclass dlgCharts(QDialog):def __init__(self, data, parent=None):super().__init__(parent)self.ui = Ui_Dialog()self.ui.setupUi(self)self.showMaximized()self.data = dataminDate = 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)@staticmethoddef 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_tabfrom PySide6.QtWidgets import *
from PySide6.QtCore import Slot, QDate
from PySide6.QtGui import QStandardItem, QStandardItemModelfrom 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 = '账本 ' + VERSIONunsaved_tip = '*'SUPPORTED_FILTERS = '账本文件(*.abf);;文本文件(*.txt);;所有文件(*.*)'def __init__(self, parent=None):# Initialize windowsuper().__init__(parent)self.ui = Ui_MainWindow()self.ui.setupUi(self)self.setWindowTitle('账本 ' + VERSION)self.labStatus = QLabel(self)self.ui.statusBar.addWidget(self.labStatus)# Initialize tableself.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 slotsself.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 = filenamedef __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: returnif 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)returnrow = self.__data.pop(i)row[j] = newinsort_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): returnfilename = 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. 总结

本项目到此结束。

【附:项目地址】

  • GitHub 地址:https://github.com/GoodCoder666/AccountBook
  • Gitee 镜像地址:https://gitee.com/GoodCoder666/AccountBook

记得点个Star哦~


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

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

  1. 精品向丨软件测试企业级Web自动化测试项目实战(附完整项目)

    今天给大家分享一个简单易操作的实战项目(已开源) 项目名称 ET开源商场系统 项目描述 ETshop是一个电子商务B2C电商平台系统,功能强大,安全便捷.适合企业及个人快速构建个性化网上商城. 包含P ...

  2. Unity游戏制作:2D弹球游戏 Pong(附完整项目)

    介绍 这里,又来做弹球游戏了--(^_^) 之前自学了一段时间的 unity,还是先做一款 2D 的小游戏吧,运行效果如下: 目录 下载 项目制作过程 一.拼界面 二.主程序Game.cs 1)显示初 ...

  3. 一个“周公解梦”小程序的实现,前端和后台源码分析-附完整项目

    之前写的一个小东西,虽然开发出来,但是由于涉及到封建迷信并不能上架.对于这个问题,我也很是不解,现在各个平台星座类的小程序大行其道,弄点传统文化娱乐项目就成了封建迷行了. 首先需要声明,本项目的小程序 ...

  4. 如何实现抖音狗头,人工智能,附完整项目代码

    详细见 人工智能python+dlib+opencv技术10分钟实现抖音人脸变狗头详细图文教程和完整项目代码 https://blog.csdn.net/wyx100/article/details/ ...

  5. Spark Core项目实战(1) | 准备数据与计算Top10 热门品类(附完整项目代码及注释)

      大家好,我是不温卜火,是一名计算机学院大数据专业大二的学生,昵称来源于成语-不温不火,本意是希望自己性情温和.作为一名互联网行业的小白,博主写博客一方面是为了记录自己的学习过程,另一方面是总结自己 ...

  6. 现在android开发都会用到那些快速开发框架或者第三库?Android百大框架分享,附完整项目

    一.榜单介绍 排行榜包括四大类: 单一框架:仅提供路由.网络层.UI层.通信层或其他单一功能的框架 混合开发框架:提供开发hybrid app.h5与webview结合能力.web app能力的框架 ...

  7. JQuery极果商城项目实战(附完整代码)

    JQuery极果商城前端页面 效果图 技术点 HTML页面结构 CSS reset.css common.css index.css JavaScript JQuery json 效果图 技术点 本次 ...

  8. Java实现QQ邮箱登录,实现邮箱验证码三分钟失效,代码实现发送验证码和登录全过程思路。内附完整项目。

    温馨提示: 如果感觉本文章困难,请移步简单的邮箱验证,不涉及数据库和Redis,点击我进行跳转 使用技术: 1. MySQL数据库 2. Redis缓存(极其简单)点击此处学习 功能介绍: 发送验证码 ...

  9. 电子病历结构化之实体识别(附完整项目代码)

    对于医学领域的自然语言文献,例如医学教材.医学百科.临床病例.医学期刊.入院记录.检验报告等,这些文本中蕴含大量医学专业知识和医学术语.将实体识别技术与医学专业领域结合,利用机器读取医学文本,可以显著 ...

最新文章

  1. NIPS 2018论文解读 | 基于条件对抗网络的领域自适应方法
  2. python3用list实现栈
  3. 多线程----join插队
  4. java merge into_Oracle merge into的使用
  5. hbuilder php mysql_xampp本地服务器+HBuilder配置php环境
  6. 李嘉诚的四句话,和各位共勉,让我们干了这碗鸡汤吧,共同挑战未来
  7. 大型网站架构系列:负载均衡详解(4)
  8. java非阻塞io流_阻塞式和非阻塞io流初认识
  9. 解决键盘老是不消失实现delegate委托实例化过程
  10. 仿淘宝网站基于html网页模板设计静态网页模板参考.rar(项目源码)
  11. 地图编辑器开发(四)
  12. Java自学1(哭唧唧又重头开始学了)
  13. mysql操作语句类型DQL\DML\DDL\DCL
  14. linux r的数据是存在,R语言通过loess去除某个变量对数据的影响
  15. 28年蛰伏,易特驰打响「软件定义汽车」硬战
  16. 光猫桥接后宽带降速问题解决
  17. 4K屏幕/高分辨率屏幕运行VMware虚拟机图标字体太小问题解决方案(linux系统)
  18. windows7安装打印机提示“本地打印后台处理程序服务没有运行”
  19. 便宜自动驾驶定位方案
  20. 非苹果机安装 Mac OS X 全教程~~~~转

热门文章

  1. CSRF cookie not set
  2. 使用CSS设计了一个导航栏(仿康盛创想)
  3. 智能合约案例(1)-----永载史册的结婚证书
  4. 使用原子电子和vue.js制作的简单RSS阅读器
  5. Unity跨平台UI解决方案:可能是最全的FairyGUI系列教程.Part1
  6. AutoCAD .Net 通过块参照获取块名
  7. SQL注入--HTTP头部注入
  8. 用java实现MP3播放器
  9. /WEB-INF/classes 目录下存放的文件
  10. 计算机研究生 在职2018,在职人员选择报考2018年五月计算机技术在职研究生的原因是什么...