财务经理教程 - 第一部分

在本教程中,我们将使用QtQuick和PySide6创建一个财务管理应用程序。该应用程序将允许您添加您的支出,并根据支出类别使用饼图进行可视化。该应用程序的设计使其兼容桌面和Android平台。

Finance Manager GIF

Finance Manager Android

要下载本教程的完整源代码,请访问 Finance Manager Example - Part 1

先决条件

在我们开始之前,首先确保您的Python环境中已安装Python 3.9+和PySide6。您可以使用pip进行安装:

pip install PySide6

项目设计

财务经理应用程序是一个简单的应用程序,展示了如何使用PySide6将QtQuick与Python集成,从而实现用户界面的QML和后端逻辑的Python的无缝结合。它将包含以下组件:

  1. 费用列表:此列表将显示所有输入的费用,显示费用名称、金额、类别和日期。费用按月份和年份组织。

  2. PieChart: 该图表将根据类别可视化支出,为用户提供清晰的消费习惯概览。

  3. 添加支出:一个允许用户添加新支出的对话框。

整体项目结构如下:

finance_manager/
├── main.py
├── financemodel.py
├── Finance/
│   ├── Main.qml
│   ├── FinanceView.qml
│   ├── FinanceDelegate.qml
│   ├── FinancePieChart.qml
│   ├── AddDialog.qml
│   └── qmldir

让我们开始吧!

组件概述

在本教程的第一部分中,我们将从使用一些预定义的费用创建费用列表开始。为此,我们将创建一个新的Python文件financemodel.py,该文件定义了一个类FinanceModel,该类将用于从Python管理费用并将其暴露给QML。

financemodel.py
FinanceModel class definition
 1# Copyright (C) 2024 The Qt Company Ltd.
 2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4from datetime import datetime
 5from dataclasses import dataclass
 6from enum import IntEnum
 7from collections import defaultdict
 8
 9from PySide6.QtCore import (QAbstractListModel, QEnum, Qt, QModelIndex, Slot,
10                            QByteArray)
11from PySide6.QtQml import QmlElement
12
13QML_IMPORT_NAME = "Finance"
14QML_IMPORT_MAJOR_VERSION = 1
15
16
17@QmlElement
18class FinanceModel(QAbstractListModel):
19
20    @QEnum
21    class FinanceRole(IntEnum):
22        ItemNameRole = Qt.DisplayRole
23        CategoryRole = Qt.UserRole
24        CostRole = Qt.UserRole + 1
25        DateRole = Qt.UserRole + 2
26        MonthRole = Qt.UserRole + 3
27
28    @dataclass
29    class Finance:
30        item_name: str
31        category: str
32        cost: float
33        date: str
34
35        @property
36        def month(self):
37            return datetime.strptime(self.date, "%d-%m-%Y").strftime("%B %Y")
38
39    def __init__(self, parent=None) -> None:
40        super().__init__(parent)
41        self.m_finances = []
42        self.m_finances.append(self.Finance("Mobile Prepaid", "Electronics", 20.00, "15-02-2024"))
43        self.m_finances.append(self.Finance("Groceries-Feb-Week1", "Groceries", 60.75,
44                                            "16-01-2024"))
45        self.m_finances.append(self.Finance("Bus Ticket", "Transport", 5.50, "17-01-2024"))
46        self.m_finances.append(self.Finance("Book", "Education", 25.00, "18-01-2024"))
47
48    def rowCount(self, parent=QModelIndex()):
49        return len(self.m_finances)
50
51    def data(self, index: QModelIndex, role: int):
52        row = index.row()
53        if row < self.rowCount():
54            finance = self.m_finances[row]
55            if role == FinanceModel.FinanceRole.ItemNameRole:
56                return finance.item_name
57            if role == FinanceModel.FinanceRole.CategoryRole:
58                return finance.category
59            if role == FinanceModel.FinanceRole.CostRole:
60                return finance.cost
61            if role == FinanceModel.FinanceRole.DateRole:
62                return finance.date
63            if role == FinanceModel.FinanceRole.MonthRole:
64                return finance.month
65        return None
66
67    @Slot(result=dict)
68    def getCategoryData(self):
69        category_data = defaultdict(float)
70        for finance in self.m_finances:
71            category_data[finance.category] += finance.cost
72        return dict(category_data)
73
74    def roleNames(self):
75        roles = super().roleNames()
76        roles[FinanceModel.FinanceRole.ItemNameRole] = QByteArray(b"item_name")
77        roles[FinanceModel.FinanceRole.CategoryRole] = QByteArray(b"category")
78        roles[FinanceModel.FinanceRole.CostRole] = QByteArray(b"cost")
79        roles[FinanceModel.FinanceRole.DateRole] = QByteArray(b"date")
80        roles[FinanceModel.FinanceRole.MonthRole] = QByteArray(b"month")
81        return roles
82
83    @Slot(int, result='QVariantMap')
84    def get(self, row: int):
85        finance = self.m_finances[row]
86        return {"item_name": finance.item_name, "category": finance.category,
87                "cost": finance.cost, "date": finance.date}
88
89    @Slot(str, str, float, str)
90    def append(self, item_name: str, category: str, cost: float, date: str):
91        finance = self.Finance(item_name, category, cost, date)
92        self.beginInsertRows(QModelIndex(), 0, 0)  # Insert at the front
93        self.m_finances.insert(0, finance)  # Insert at the front of the list
94        self.endInsertRows()

以下是FinanceModel类、其组件和方法的简要概述:

  1. QML 类型注册

    • FinanceModel 类使用 @QmlElement 装饰器注册为 QML 类型。此装饰器用于将 Python 类定义为 QML 类型,使其可以在 QML 文件中使用。

    • QML_IMPORT_NAME 变量用于定义在 QML 中导入以访问 FinanceModel 类的模块名称。

  2. 成员

    • FinanceRole 枚举: 定义模型数据的自定义角色,例如 ItemNameRole, CategoryRole, CostRole, DateRoleMonthRole.

    • Finance 数据类: 表示一个单独的支出,具有属性 item_name, category, cost, datemonth.

    • init 方法: 使用一些预定义的支出初始化模型。

    • rowCount 方法: 返回模型中项目的数量。

    • data 方法: 返回模型中给定角色和索引的数据。

    • getCategoryData 方法: 返回模型中每个类别的总成本的字典。此方法具有 @Slot 装饰器,使其可以从 QML 访问。

    • roleNames 方法: 将角色名称映射到它们的 QByteArray 值。

    • get 方法: 一个 @Slot 方法,用于获取给定索引的支出数据。

    • append 方法: 一个 @Slot 方法,用于向模型追加新的支出。

为了在QML中的ListView组件中用作数据模型,需要rowCountdataroleNames方法。

既然我们已经定义了FinanceModel类,让我们创建QML组件来显示支出。首先,我们创建Finance/Main.qml文件,它将是我们应用程序的主QML文件。

Main.qml
Main.qml
  1// Copyright (C) 2024 The Qt Company Ltd.
  2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
  3
  4import QtQuick
  5import QtQuick.Controls
  6import QtQuick.Layouts
  7import QtQuick.Controls.Material
  8import Finance
  9
 10ApplicationWindow {
 11    id: window
 12    Material.theme: Material.Dark
 13    Material.accent: Material.Gray
 14    width: Screen.width * 0.3
 15    height: Screen.height * 0.5
 16    visible: true
 17    title: qsTr("Finance Manager")
 18
 19    // Add a toolbar for the application, only visible on mobile
 20    header: ToolBar {
 21        Material.primary: "#5c8540"
 22        visible: Qt.platform.os == "android"
 23        RowLayout {
 24            anchors.fill: parent
 25            Label {
 26                text: qsTr("Finance Manager")
 27                font.pixelSize: 20
 28                Layout.alignment: Qt.AlignCenter
 29            }
 30        }
 31    }
 32
 33    ColumnLayout {
 34        anchors.fill: parent
 35
 36        TabBar {
 37            id: tabBar
 38            Layout.fillWidth: true
 39
 40            TabButton {
 41                text: qsTr("Expenses")
 42                font.pixelSize: Qt.platform.os == "android" ?
 43                    Math.min(window.width, window.height) * 0.04 :
 44                    Math.min(window.width, window.height) * 0.02
 45                onClicked: stackView.currentIndex = 0
 46            }
 47
 48            TabButton {
 49                text: qsTr("Charts")
 50                font.pixelSize: Qt.platform.os == "android" ?
 51                    Math.min(window.width, window.height) * 0.04 :
 52                    Math.min(window.width, window.height) * 0.02
 53                onClicked: stackView.currentIndex = 1
 54            }
 55        }
 56
 57        StackLayout {
 58            id: stackView
 59            Layout.fillWidth: true
 60            Layout.fillHeight: true
 61
 62            Item {
 63                id: expensesView
 64                Layout.fillWidth: true
 65                Layout.fillHeight: true
 66
 67                FinanceView {
 68                    id: financeView
 69                    anchors.fill: parent
 70                    financeModel: finance_model
 71                }
 72            }
 73
 74            Item {
 75                id: chartsView
 76                Layout.fillWidth: true
 77                Layout.fillHeight: true
 78
 79                FinancePieChart {
 80                    id: financePieChart
 81                    anchors.fill: parent
 82                    Component.onCompleted: {
 83                        var categoryData = finance_model.getCategoryData()
 84                        updateChart(categoryData)
 85                    }
 86                }
 87            }
 88        }
 89    }
 90
 91    // Model to store the finance data. Created from Python.
 92    FinanceModel {
 93        id: finance_model
 94    }
 95
 96    // Add a dialog to add new entries
 97    AddDialog {
 98        id: addDialog
 99        onFinished: function(item_name, category, cost, date) {
100            finance_model.append(item_name, category, cost, date)
101            var categoryData = finance_model.getCategoryData()
102            financePieChart.updateChart(categoryData)
103        }
104    }
105
106    // Add a button to open the dialog
107    ToolButton {
108        id: roundButton
109        text: qsTr("+")
110        highlighted: true
111        Material.elevation: 6
112        width: Qt.platform.os === "android" ?
113            Math.min(parent.width * 0.2, Screen.width * 0.15) :
114            Math.min(parent.width * 0.060, Screen.width * 0.05)
115        height: width  // Keep the button circular
116        anchors.margins: 10
117        anchors.right: parent.right
118        anchors.bottom: parent.bottom
119        background: Rectangle {
120            color: "#5c8540"
121            radius: roundButton.width / 2
122        }
123        font.pixelSize: width * 0.4
124        onClicked: {
125            addDialog.createEntry()
126        }
127    }
128}

Main.qml中,我们导入了创建的Finance QML模块文件,该文件包含以下组件:

  1. ApplicationWindow:

    • 应用程序的主窗口。

    • 将主题设置为 Material.Dark 并将强调色设置为 Material.Gray

    • 根据屏幕尺寸调整窗口大小。

    • 包含标题“Finance Manager”。

  2. 工具栏:

    • 一个仅在移动平台(Android 和 iOS)上可见的工具栏。请注意,PySide6 仅支持 Android,但您可以使用相同的代码与 Qt C++ 用于 iOS。

    • 包含一个带有文本“Finance Manager”的标签

  3. ColumnLayout:

    • 一种将其子元素排列成一列的布局。

    • 填满整个窗口。

  4. TabBar:

    • 包含两个TabButton组件,用于在ExpenseCharts视图之间切换。

  5. StackLayout:

    • 一种布局,将其子元素堆叠在一起。

    • 包含两个Item组件,用于“Expenses”和“Charts”视图。

  6. FinanceView:

    • 一个自定义组件,用于显示支出列表。

    • 绑定到 finance_model

    • 该组件定义在 FinanceView.qml 文件中。

  7. FinancePieChart:

    • 一个自定义组件,用于按类别显示支出的饼图。

    • 当组件完成时,使用finance_model.getCategoryData()中的数据更新图表。

    • 该组件定义在FinancePieChart.qml文件中。

  8. FinanceModel:

    • 从Python创建的数据模型,用于存储财务数据。这是通过在Main.qml文件中导入QML模块Finance来实现的。

  9. AddDialog:

    • 用于添加新支出条目的对话框。

    • 将新条目附加到 finance_model 并更新饼图。

  10. RoundButton:

    • 一个圆形按钮,用于打开AddDialog.qml

    • 位于窗口的右下角。

    • 包含一个“+”符号,并具有高亮外观。

现在我们有了主QML文件的基本结构,让我们创建FinanceView.qml文件:

FinanceView.qml
FinanceView.qml
 1// Copyright (C) 2024 The Qt Company Ltd.
 2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4import QtQuick
 5import QtQuick.Controls
 6import QtQuick.Controls.Material
 7
 8ListView {
 9    id: listView
10    anchors.fill: parent
11    height: parent.height
12    property var financeModel
13
14    delegate: FinanceDelegate {
15        id: delegate
16        width: listView.width
17    }
18
19    model: financeModel
20
21    section.property: "month"  // Group items by the "month" property
22    section.criteria: ViewSection.FullString
23    section.delegate: Component {
24        id: sectionHeading
25        Rectangle {
26            width: listView.width
27            height:  Qt.platform.os == "android" ?
28                Math.min(window.width, window.height) * 0.05 :
29                Math.min(window.width, window.height) * 0.03
30            color: "#5c8540"
31
32            required property string section
33
34            Text {
35                text: parent.section
36                font.bold: true
37                // depending on the screen density, adjust the font size
38                font.pixelSize: Qt.platform.os == "android" ?
39                    Math.min(window.width, window.height) * 0.03 :
40                    Math.min(window.width, window.height) * 0.02
41                color: Material.primaryTextColor
42            }
43        }
44    }
45
46    ScrollBar.vertical: ScrollBar { }
47}

FinanceView.qml 包含以下组件:

  1. ListView:

    • 用于显示项目列表的主要容器。

    • 使用anchors.fill: parent填充整个父容器。

    • 使用financeModel属性作为其数据模型。

  2. property var financeModel:

    • 一个属性,用于保存列表的数据模型。

    • 这个模型预期从父组件传递过来。在这种情况下,它是从Main.qml文件传递过来的。

  3. delegate:

    • 定义如何在ListView中显示每个项目。

    • 使用自定义组件FinanceDelegate来渲染每个项目。该组件在FinanceDelegate.qml文件中定义。

    • 设置每个委托的宽度以匹配ListView的宽度。

  4. model:

    • ListView 绑定到 financeModel 属性。

    • ListView 将根据 financeModel 中的数据显示项目。

  5. section:

    • section 属性用于根据支出的月份对列表视图中的项目进行分组。

  6. ScrollBar.vertical:

    • ListView添加垂直滚动条。

    • 确保如果内容超出可见区域,用户可以滚动列表。

这些组件共同创建了一个可滚动的列表视图,用于显示财务数据,每个项目都使用FinanceDelegate组件进行渲染。

接下来,让我们创建FinanceDelegate.qml文件:

FinanceDelegate.qml
FinanceDelegate.qml
 1// Copyright (C) 2024 The Qt Company Ltd.
 2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4import QtQuick
 5import QtQuick.Layouts
 6import QtQuick.Controls
 7import QtQuick.Controls.Material
 8
 9ItemDelegate {
10    id: delegate
11    checkable: true
12    width: parent.width
13    height: Qt.platform.os == "android" ?
14        Math.min(window.width, window.height) * 0.15 :
15        Math.min(window.width, window.height) * 0.1
16
17    contentItem:
18    RowLayout {
19        Label {
20            id: dateLabel
21            font.pixelSize: Qt.platform.os == "android" ?
22                Math.min(window.width, window.height) * 0.03 :
23                Math.min(window.width, window.height) * 0.02
24            text: date
25            elide: Text.ElideRight
26            Layout.fillWidth: true
27            Layout.preferredWidth: 1
28            color: Material.primaryTextColor
29        }
30
31        ColumnLayout {
32            spacing: 5
33            Layout.fillWidth: true
34            Layout.preferredWidth: 1
35
36            Label {
37                text: item_name
38                color: "#5c8540"
39                font.bold: true
40                elide: Text.ElideRight
41                font.pixelSize:  Qt.platform.os == "android" ?
42                    Math.min(window.width, window.height) * 0.03 :
43                    Math.min(window.width, window.height) * 0.02
44                Layout.fillWidth: true
45            }
46
47            Label {
48                text: category
49                elide: Text.ElideRight
50                Layout.fillWidth: true
51                font.pixelSize:  Qt.platform.os == "android" ?
52                    Math.min(window.width, window.height) * 0.03 :
53                    Math.min(window.width, window.height) * 0.02
54            }
55        }
56
57        Item {
58        Layout.fillWidth: true  // This item will take up the remaining space
59        }
60
61        ColumnLayout {
62            spacing: 5
63            Layout.fillWidth: true
64            Layout.preferredWidth: 1
65
66            Label {
67                text: "you spent:"
68                color: "#5c8540"
69                elide: Text.ElideRight
70                Layout.fillWidth: true
71                font.pixelSize:  Qt.platform.os == "android" ?
72                    Math.min(window.width, window.height) * 0.03 :
73                    Math.min(window.width, window.height) * 0.02
74            }
75
76            Label {
77                text: cost + "€"
78                elide: Text.ElideRight
79                Layout.fillWidth: true
80                font.pixelSize:  Qt.platform.os == "android" ?
81                    Math.min(window.width, window.height) * 0.03 :
82                    Math.min(window.width, window.height) * 0.02
83            }
84        }
85    }
86}

FinanceDelegate.qml 包含以下组件:

  1. ItemDelegate:

    • 委托的根元素。

    • 表示ListView中的单个项目。

  2. RowLayout:

    • 一种将其子元素水平排列的布局。

    • 包含多个元素以显示财务数据的不同部分。

  3. 标签 (dateLabel):

    • 显示费用的日期。

  4. ColumnLayout:

    • 一种将其子元素垂直排列的布局。

    • 包含项目名称和类别的标签。

  5. 标签 (item_name):

    • 显示项目的名称。

  6. 标签(类别):

    • 显示交易的类别。

  7. 项目:

    • 一个占位符项目,用于占据RowLayout中的剩余空间,以便最后一个标签右对齐。

  8. ColumnLayout(成本部分):

    • 一种将其子元素垂直排列的布局。

    • 包含成本描述和实际成本的标签。

  9. 标签 (“you spent:”):

    • 显示静态文本 “you spent:”

  10. 标签(成本):

    • 显示交易的成本。

这些组件共同创建了一个详细且结构化的视觉表示,用于在ListView中展示每笔财务交易,以可读的格式显示日期、项目名称、类别和成本。

然后我们创建FinancePieChart.qml文件:

FinancePieChart.qml
FinancePieChart.qml
 1// Copyright (C) 2024 The Qt Company Ltd.
 2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 3
 4pragma ComponentBehavior: Bound
 5import QtQuick
 6import QtGraphs
 7import QtQuick.Controls.Material
 8
 9Item {
10    width: Screen.width
11    height: Screen.height
12
13    GraphsView {
14        id: chart
15        anchors.fill: parent
16        antialiasing: true
17
18        theme: GraphsTheme {
19            colorScheme: Qt.Dark
20            theme: GraphsTheme.Theme.QtGreenNeon
21        }
22
23        PieSeries {
24            id: pieSeries
25        }
26    }
27
28    Text {
29        id: chartTitle
30        text: "Total Expenses Breakdown by Category"
31        color: "#5c8540"
32        font.pixelSize: Qt.platform.os == "android" ?
33            Math.min(window.width, window.height) * 0.04 :
34            Math.min(window.width, window.height) * 0.03
35        anchors.horizontalCenter: parent.horizontalCenter
36        anchors.top: parent.top
37        anchors.topMargin: 20
38    }
39
40    function updateChart(data) {
41        pieSeries.clear()
42        for (var category in data) {
43            var slice = pieSeries.append(category, data[category])
44            slice.label = category + ": " + data[category] + "€"
45            slice.labelVisible = true
46        }
47    }
48}

FinancePieChart.qml 包含以下组件:

  1. 项目:

    • QML文件的根元素。

    • 设置宽度和高度以匹配屏幕尺寸。

  2. GraphsView:

    • 用于显示图表的容器。这是在Qt 6.8中引入的,带有Qt Graphs模块。

  3. PieSeries:

    • 用于创建饼图的系列类型。这也是Qt Graphs模块的一部分。

  4. 文本

    • 饼图的标题。

  5. updateChart(data):

    • 一个用于用新数据更新饼图的JavaScript函数。

    • 清除PieSeries中现有的切片。

    • 遍历提供的数据以创建新的切片。

    • 每个切片都标有类别名称和以欧元表示的值。

这些组件共同创建了一个响应式饼图,可以使用新数据动态更新。

最后,我们创建AddDialog.qml文件:

AddDialog.qml
AddDialog.qml
  1// Copyright (C) 2024 The Qt Company Ltd.
  2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
  3
  4import QtQuick
  5import QtQuick.Controls
  6import QtQuick.Layouts
  7
  8Dialog {
  9    id: dialog
 10
 11    signal finished(string itemName, string category, real cost, string date)
 12
 13    contentItem: ColumnLayout {
 14        id: form
 15        spacing: 10
 16        property alias itemName: itemName
 17        property alias category: category
 18        property alias cost: cost
 19        property alias date: date
 20
 21        GridLayout {
 22            columns: 2
 23            columnSpacing: 20
 24            rowSpacing: 10
 25            Layout.fillWidth: true
 26
 27            Label {
 28                text: qsTr("Item Name:")
 29                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 30            }
 31
 32            TextField {
 33                id: itemName
 34                focus: true
 35                Layout.fillWidth: true
 36                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 37            }
 38
 39            Label {
 40                text: qsTr("Category:")
 41                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 42            }
 43
 44            TextField {
 45                id: category
 46                focus: true
 47                Layout.fillWidth: true
 48                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 49            }
 50
 51            Label {
 52                text: qsTr("Cost:")
 53                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 54            }
 55
 56            TextField {
 57                id: cost
 58                focus: true
 59                Layout.fillWidth: true
 60                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 61                placeholderText: qsTr("€")
 62                inputMethodHints: Qt.ImhFormattedNumbersOnly
 63            }
 64
 65            Label {
 66                text: qsTr("Date:")
 67                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 68            }
 69
 70            TextField {
 71                id: date
 72                Layout.fillWidth: true
 73                Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
 74                // placeholderText: qsTr("dd-mm-yyyy")
 75                validator: RegularExpressionValidator { regularExpression: /^[0-3]?\d-[01]?\d-\d{4}$/ }
 76                // code to add the - automatically
 77                onTextChanged: {
 78                    if (date.text.length === 2 || date.text.length === 5) {
 79                        date.text += "-"
 80                    }
 81                }
 82                Component.onCompleted: {
 83                var today = new Date();
 84                var day = String(today.getDate()).padStart(2, '0');
 85                var month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based
 86                var year = today.getFullYear();
 87                date.placeholderText = day + "-" + month + "-" + year;
 88                }
 89            }
 90        }
 91    }
 92
 93    function createEntry() {
 94        form.itemName.clear()
 95        form.category.clear()
 96        form.cost.clear()
 97        form.date.clear()
 98        dialog.title = qsTr("Add Finance Item")
 99        dialog.open()
100    }
101
102    x: parent.width / 2 - width / 2
103    y: parent.height / 2 - height / 2
104
105    focus: true
106    modal: true
107    title: qsTr("Add Finance Item")
108    standardButtons: Dialog.Ok | Dialog.Cancel
109
110    Component.onCompleted: {
111        dialog.visible = false
112        Qt.inputMethod.visibleChanged.connect(adjustDialogPosition)
113    }
114
115    function adjustDialogPosition() {
116        if (Qt.inputMethod.visible) {
117            // If the keyboard is visible, move the dialog up
118            dialog.y = parent.height / 4 - height / 2
119        } else {
120            // If the keyboard is not visible, center the dialog
121            dialog.y = parent.height / 2 - height / 2
122        }
123    }
124
125    onAccepted: {
126        finished(form.itemName.text, form.category.text, parseFloat(form.cost.text), form.date.text)
127    }
128}

AddDialog.qml 包含以下组件:

  1. 对话框:

    • 对话框的根元素:对话框的标识符。

    • signal finished(...): 当对话框被接受时发出的自定义信号。在这种情况下,当用户添加新费用时发出。

  2. ColumnLayout:

    • 对话框字段的容器。

  3. TextField:

    • 输入项目名称、类别、成本和日期的输入字段。

  4. 函数 createEntry():

    • 清除表单字段。

    • 设置对话框标题。

    • 打开对话框。

  5. 对话框属性:

    • title: qsTr("添加 财务 项目"): 设置对话框标题。

    • standardButtons: Dialog.Ok | Dialog.Cancel: 添加标准的确定和取消按钮。

    • Component.onCompleted: 当组件首次完成时隐藏对话框。

    • onAccepted: 当对话框被接受时,调用finished函数并传递表单数据。

  6. 函数 adjustDialogPosition:

    • 当虚拟键盘显示时,调整对话框位置使其稍微上移。这仅适用于移动平台。

主Python文件

现在我们已经创建了主QML文件和必要的组件,我们可以运行应用程序来查看费用列表视图的实际效果。创建一个新的Python文件main.py并添加以下代码:

main.py
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import sys
from pathlib import Path
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine

from financemodel import FinanceModel  # noqa: F401

if __name__ == '__main__':
    app = QApplication(sys.argv)
    QApplication.setOrganizationName("QtProject")
    QApplication.setApplicationName("Finance Manager")
    engine = QQmlApplicationEngine()

    engine.addImportPath(Path(__file__).parent)
    engine.loadFromModule("Finance", "Main")

    if not engine.rootObjects():
        sys.exit(-1)

    exit_code = app.exec()
    del engine
    sys.exit(exit_code)

main.py文件中,我们创建了一个QApplication实例,加载了Main.qml文件。Python的导入语句from financemodel import FinanceModelFinanceModel类注册为QML类型,使其可以在QML文件中使用。

运行应用程序

要运行应用程序,请使用Python执行main.py文件:

python main.py

部署应用程序

要在桌面上部署应用程序,您可以使用pyside6-deploy: the deployment tool for Qt for Python工具。从项目目录运行以下命令:

pyside6-deploy --name FinanceManager

这将在项目目录中为应用程序创建一个独立的可执行文件。

要部署到Android,你可以使用pyside6-android-deploy: the Android deployment tool for Qt for Python工具。从项目目录运行以下命令:

pyside6-android-deploy --name FinanceManager --wheel-pyside=<path_to_pyside6_wheel>
                       --wheel-shiboken=<path_to_shiboken_wheel>

这将在项目目录中创建一个可以在Android设备上安装的APK文件。

总结

在本教程的这一部分中,我们创建了一个基本的财务管理应用程序,包括按月份和年份分类的支出列表视图、饼图以及添加支出对话框。我们还定义了FinanceModel类,用于管理财务数据并将其暴露给QML。在教程的下一部分中,我们将继续在此基础上,将支出数据移动到基于sqlalchemy Python包的数据库中。