Pyqt6 / pyside6: première ligne du projet de compte [adresse complète du projet jointe]

Goodcoder666 2022-07-23 20:37:30 阅读数:198

pyqt6pyqtpyside6pysidepremi

0. Préface

C'est bon d'être à la maison ces derniers temps,J'ai vu un [email protected]studentWheatJ'ai envoyé un article.AvecTkinterLes livres de comptes faits,Il a donc décidé d'améliorer le programme avec lui.

Capture d'écran:
MainWindow
dlgAdd
stat

1. Arrière - plan

L'arrière - plan est principalement fait par des amis,Il n'y a pas grand - chose à dire ici,Mets un code.:
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

Voir détailshttps://blog.csdn.net/qq_67190987/article/details/125918530.

2. Front End

Comme indiqué dans le titre ,Adoption du cadreQt6+Python,Il y a généralement deux options(PyQt6EtPySide6),J'utilise iciPyQt6.

2.1 Préparation des ressources

Insrc/icons Enregistrer toutes les ressources d'image :
Photos

2.2 Designer Dessin de fenêtre

AvecQt Designer Dessiner les fenêtres ,Comme le montre la figure:

QMainWindow
QDialog

2.3 Installer les dépendances

Préparer une copierequirements.txt,Il se lit comme suit::

PySide6>=6.3.1

Et puis,cmdEntrez:

pip install -r requirements.txt

Installation terminée .

2.4 Compiler les ressources et UI

Inutile de dire ça,Utilisation directepyside6-uicEtpyside6-rccCommande compiler le fichier, La liste des fichiers compilés est la suivante: :

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 Codage

src/dlgAdd.py
“ Ajouter un compte ”Fenêtre,Un simpleQDialogExemple.

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('C'est sûr.')
self.ui.buttonBox.button(QDialogButtonBox.Cancel).setText('Annulation')
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, "Erreur", " L'événement ne peut pas être vide ,Veuillez remplir à nouveau.")
return
if self.ui.moneyEdit.text() in ('', '+', '-'):
QMessageBox.critical(self, "Erreur", "Le montant ne peut pas être vide,Veuillez remplir à nouveau.")
return
return super().accept()

src/dlgCharts.py
Fenêtre d'affichage du diagramme ,UtiliserQtCharts(Utilisation avecPyQt5/PySide2Légèrement différent)Dessiner un histogramme. D'autres graphiques seront envisagés ultérieurement. .

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 = 'Total des recettes et des dépenses',
xAxis = ['Recettes', 'Dépenses'],
yAxisList = [
('Montant', [total_in, total_out])
]
)
def __update_eventChart(self, events):
self.createChart(
chartView = self.ui.eventView,
title = ' Classification des recettes et des dépenses ',
xAxis = list(events.keys()),
yAxisList = [
('Recettes', list(map(lambda x: x[0], events.values()))),
('Dépenses', list(map(lambda x: x[1], events.values())))
]
)
def __update_dateChart(self, dates):
self.createChart(
chartView = self.ui.dateView,
title = ' Recettes et dépenses quotidiennes ',
xAxis = list(dates.keys()),
yAxisList = [
('Recettes', list(map(lambda x: x[0], dates.values()))),
('Dépenses', 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
Procédure principale, Gérer simultanément la fenêtre principale . L'endroit le plus gênant est QTableView, Pour traiter à la fois les questions de recherche et de tri .

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 = 'Livres comptables ' + VERSION
unsaved_tip = '*'
SUPPORTED_FILTERS = ' Documents comptables (*.abf);;Fichier texte(*.txt);;Tous les documents(*.*)'
def __init__(self, parent=None):
# Initialize window
super().__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.setWindowTitle('Livres comptables ' + VERSION)
self.labStatus = QLabel(self)
self.ui.statusBar.addWidget(self.labStatus)
# Initialize table
self.model = QStandardItemModel(0, 4, self)
self.model.setHorizontalHeaderLabels(['Date', 'Questions', 'Montant', 'Remarques'])
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, 'Erreur', 'L'ouverture du fichier a échoué.Veuillez réessayer plus tard..')
except ApiError:
QMessageBox.critical(self, 'Erreur', 'Mauvais format de fichier. Veuillez vérifier l'intégrité du document .')
except Exception as e:
QMessageBox.critical(self, 'Erreur', 'Erreur inconnue:' + 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, 'Erreur', ' Erreur d'enregistrement du fichier .Veuillez réessayer plus tard..')
except Exception as e:
QMessageBox.critical(self, 'Erreur', 'Erreur inconnue:' + str(e.with_traceback()))
else:
self.labStatus.setText('Enregistrer avec succès:' + 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('Nouveaux documents')
self.model.setRowCount(0)
self.__data.clear()
@Slot()
def on_actFile_Open_triggered(self):
filename, _ = QFileDialog.getOpenFileName(self, 'Ouvre.', 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, 'Enregistrer', filter=self.SUPPORTED_FILTERS)
if filename:
self.__saveFile(filename)
@Slot()
def on_actFile_SaveAs_triggered(self):
filename, _ = QFileDialog.getSaveFileName(self, 'Enregistrer sous', 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('Numéro de version:' + 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, 'À propos deQt')
@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, 'Erreur', 'Mauvais format de date.')
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, 'Conseils', ' Veuillez ajouter des données pour utiliser la fonction statistique .')
def closeEvent(self, event):
if not self.windowTitle().startswith(self.unsaved_tip): return
filename = basename(self.__filename) if self.__filename else 'Nouveaux documents'
messageBox = QMessageBox(
parent=self, icon=QMessageBox.Warning, windowTitle='Conseils',
text=f' Voulez - vous enregistrer la paire {
filename} Changement de?', informativeText='Si vous ne sauvegardez pas, Vos modifications seront perdues .',
standardButtons=QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
)
messageBox.setButtonText(QMessageBox.Save, 'Enregistrer')
messageBox.setButtonText(QMessageBox.Discard, 'Ne pas enregistrer')
messageBox.setButtonText(QMessageBox.Cancel, 'Annulation')
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. Résumé

C'est la fin de ce projet. .

【Annexe:Adresse du projet】

N'oublie pas.StarOh, mon Dieu.~


La création n'est pas facile, Si vous aimez cet article, commandez un triple lien. !Merci beaucoup.!!!

Copyright:Cet article est[Goodcoder666]Établi,Veuillez apporter le lien original pour réimprimer,remercier。 https://fra.fheadline.com/2022/204/202207232016176202.html