费用工具教程

In this tutorial you will learn the following concepts:
  • 以编程方式创建用户界面,

  • 布局和小部件,

  • 重载Qt类,

  • 连接信号和槽,

  • 与QWidgets交互,

  • 并构建您自己的应用程序。

The requirements:
  • 应用程序的简单窗口 (QMainWindow)。

  • 一个用于跟踪支出的表格 (QTableWidget).

  • 两个输入字段用于添加费用信息 (QLineEdit)。

  • 用于向表格添加信息、绘制数据、清除表格和退出应用程序的按钮 (QPushButton)。

  • 一个验证步骤,以避免无效的数据输入。

  • 一个用于可视化支出数据的图表 (QChart),它将 嵌入到一个图表视图 (QChartView)中。

空窗口

QApplication 的基本结构位于 if __name__ == “__main__”: 代码块内。

1 if __name__ == "__main__":
2     app = QApplication([])
3     # ...
4     sys.exit(app.exec())

现在,要开始开发,创建一个名为MainWindow的空窗口。 你可以通过定义一个继承自QMainWindow的类来实现这一点。

 1
 2class MainWindow(QMainWindow):
 3    def __init__(self):
 4        super().__init__()
 5        self.setWindowTitle("Tutorial")
 6
 7if __name__ == "__main__":
 8    # Qt Application
 9    app = QApplication(sys.argv)
10
11    window = MainWindow()
12    window.resize(800, 600)
13    window.show()
14
15    # Execute application

现在我们的类已经定义好了,创建一个它的实例并调用show()

 1
 2class MainWindow(QMainWindow):
 3    def __init__(self):
 4        super().__init__()
 5        self.setWindowTitle("Tutorial")
 6
 7if __name__ == "__main__":
 8    # Qt Application
 9    app = QApplication(sys.argv)
10
11    window = MainWindow()
12    window.resize(800, 600)
13    window.show()
14
15    # Execute application

空的小部件和数据

QMainWindow 使我们能够设置一个在显示窗口时展示的中心部件 (read more)。 这个中心部件可以是另一个从 QWidget 派生的类。

此外,您将定义示例数据以便稍后可视化。

1
2class Widget(QWidget):
3    def __init__(self):
4        super().__init__()
5
6        # Example data
7        self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0,
8                      "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85,

有了Widget类后,修改MainWindow的初始化代码

1    app = QApplication(sys.argv)
2    # QWidget
3    widget = Widget()
4    # QMainWindow using QWidget as central widget

窗口布局

现在主空窗口已经就位,你需要开始添加小部件以实现创建费用应用程序的主要目标。

在声明示例数据后,您可以将其可视化在一个简单的QTableWidget上。为此,您需要将此过程添加到Widget构造函数中。

警告

仅用于示例目的,将使用QTableWidget, 但对于性能要求更高的应用程序,建议使用模型和QTableView的组合。

 1class Widget(QWidget):
 2    def __init__(self):
 3        super().__init__()
 4        self.items = 0
 5
 6        # Example data
 7        self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0,
 8                      "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85,
 9                      "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120}
10
11        # Left
12        self.table = QTableWidget()
13        self.table.setColumnCount(2)
14        self.table.setHorizontalHeaderLabels(["Description", "Price"])
15        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
16
17        # QWidget Layout
18        self.layout = QHBoxLayout(self)
19        self.layout.addWidget(self.table)
20
21        # Fill example data

如你所见,代码还包括一个QHBoxLayout,它提供了一个容器来水平放置小部件。

此外,QTableWidget 允许进行自定义,例如添加将使用的两列的标签,并拉伸内容以使用整个Widget空间。

代码的最后一行提到了填充表格*,执行该任务的代码如下所示。

1
2    def fill_table(self, data=None):
3        data = self._data if not data else data
4        for desc, price in data.items():
5            self.table.insertRow(self.items)
6            self.table.setItem(self.items, 0, QTableWidgetItem(desc))
7            self.table.setItem(self.items, 1, QTableWidgetItem(str(price)))

将此过程放在一个单独的方法中是一种良好的实践,可以使构造函数更具可读性,并将类的主要功能拆分为独立的过程。

右侧布局

由于使用的数据只是一个示例,您需要包含一个机制来向表中输入项目,以及额外的按钮来清除表的内容,并退出应用程序。

对于带有描述性标签的输入行,您将使用QFormLayout。然后,您将表单布局与按钮一起嵌套到QVBoxLayout中。

 1
 2        # Right
 3        self.description = QLineEdit()
 4        self.description.setClearButtonEnabled(True)
 5        self.price = QLineEdit()
 6        self.price.setClearButtonEnabled(True)
 7
 8        self.add = QPushButton("Add")
 9        self.clear = QPushButton("Clear")
10
11        form_layout = QFormLayout()
12        form_layout.addRow("Description", self.description)
13        form_layout.addRow("Price", self.price)
14        self.right = QVBoxLayout()
15        self.right.addLayout(form_layout)
16        self.right.addWidget(self.add)
17        self.right.addStretch()

将表格留在左侧,并将这些新包含的小部件放在右侧,只需像前面的示例中看到的那样,向我们的主QHBoxLayout添加一个布局即可:

1
2        # QWidget Layout
3        self.layout = QHBoxLayout(self)
4        self.layout.addWidget(self.table)

下一步将是将这些新按钮连接到插槽。

添加元素

每个QPushButton都有一个名为clicked的信号,当你点击按钮时会发出这个信号。 对于这个例子来说,这已经足够了,但你可以在官方文档中查看其他信号。

1
2        # Signals and Slots
3        self.add.clicked.connect(self.add_element)

正如你在前面的行中看到的,我们将每个点击信号连接到不同的槽。 在这个例子中,槽是负责执行与我们的按钮相关的特定任务的普通类方法。 用@Slot()装饰每个方法声明非常重要,这样PySide6内部就知道如何将它们注册到Qt中, 并且当连接时,它们可以从QObjects的Signals中调用。

 1
 2    @Slot()
 3    def add_element(self):
 4        des = self.description.text()
 5        price = self.price.text()
 6
 7        self.table.insertRow(self.items)
 8        self.table.setItem(self.items, 0, QTableWidgetItem(des))
 9        self.table.setItem(self.items, 1, QTableWidgetItem(price))
10
11        self.description.clear()
12        self.price.clear()
13
14        self.items += 1
15
16    def fill_table(self, data=None):
17        data = self._data if not data else data
18        for desc, price in data.items():
19            self.table.insertRow(self.items)
20            self.table.setItem(self.items, 0, QTableWidgetItem(desc))
21            self.table.setItem(self.items, 1, QTableWidgetItem(str(price)))
22            self.items += 1
23
24    @Slot()
25    def clear_table(self):
26        self.table.setRowCount(0)

由于这些槽是方法,我们可以访问类变量,比如我们的QTableWidget来与之交互。

向表中添加元素的机制描述如下:

  • 从字段中获取描述价格

  • 在表中插入一个新的空行,

  • 为每列中的空行设置值,

  • 清除输入文本字段,

  • 包括表的全局行数。

要退出应用程序,您可以使用唯一的QApplication实例的quit()方法,要清除表格的内容,您只需将表格的行数和内部计数设置为零。

验证步骤

向表中添加信息需要是一个关键操作,需要一个验证步骤以避免添加无效信息,例如空信息。

你可以使用来自QLineEdit的信号textChanged,每当内部内容发生变化时,即每次按键时,都会发出这个信号。

你可以将两个不同对象的信号连接到同一个槽,这在你当前的应用程序中将是这种情况:

1        self.clear.clicked.connect(self.clear_table)
2        self.description.textChanged.connect(self.check_disable)

check_disable 插槽的内容将非常简单:

1
2    @Slot()
3    def check_disable(self, s):
4        enabled = bool(self.description.text() and self.price.text())

你有两个选择,根据你检索到的字符串的当前值编写验证,或者手动获取两个QLineEdit的整个内容。在这种情况下,第二种方法是首选,因此你可以验证两个输入是否不为空以启用添加按钮。

注意

Qt 还提供了一个名为 QValidator 的特殊类,您可以使用它来验证任何输入。

空图表视图

可以向表中添加新项目,目前的可视化效果还不错,但通过图形化表示数据可以实现更多功能。

首先,你将在应用程序的右侧包含一个空的QChartView占位符。

1
2        # Chart
3        self.chart_view = QChartView()

此外,您将小部件添加到右侧的顺序 QVBoxLayout 也会改变。

1
2        form_layout = QFormLayout()
3        form_layout.addRow("Description", self.description)
4        form_layout.addRow("Price", self.price)
5        self.right = QVBoxLayout()
6        self.right.addLayout(form_layout)
7        self.right.addWidget(self.add)
8        self.right.addWidget(self.plot)
9        self.right.addWidget(self.chart_view)

注意,之前我们有一行代码self.right.addStretch()用于填充添加清除按钮之间的垂直空间,但现在有了QChartView,这将不再必要。

此外,如果您想按需执行此操作,您需要包含一个绘图按钮。

完整应用程序

在最后一步,您需要将Plot按钮连接到一个槽,该槽将创建一个图表并将其包含到您的QChartView中。

1
2        # Signals and Slots
3        self.add.clicked.connect(self.add_element)
4        self.plot.clicked.connect(self.plot_data)
5        self.clear.clicked.connect(self.clear_table)
6        self.description.textChanged.connect(self.check_disable)

这并不新鲜,因为你已经为其他按钮做了,但现在看看如何创建一个图表并将其包含到你的QChartView中。

 1
 2    @Slot()
 3    def plot_data(self):
 4        # Get table information
 5        series = QPieSeries()
 6        for i in range(self.table.rowCount()):
 7            text = self.table.item(i, 0).text()
 8            number = float(self.table.item(i, 1).text())
 9            series.append(text, number)
10
11        chart = QChart()
12        chart.addSeries(series)
13        chart.legend().setAlignment(Qt.AlignLeft)

以下步骤展示了如何填充QPieSeries

  • 创建一个 QPieSeries,

  • 遍历表格行ID,

  • 获取第 i 个位置的项,

  • 将这些值添加到系列中。

一旦系列中填充了我们的数据,您就可以创建一个新的QChart,将系列添加到其中,并可选地为图例设置对齐方式。

最后一行 self.chart_view.setChart(chart) 负责将你新创建的图表带到 QChartView 中。

应用程序将如下所示:

../../_images/expenses_tool.png

现在你可以看到完整的代码:

  1# Copyright (C) 2022 The Qt Company Ltd.
  2# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
  3from __future__ import annotations
  4
  5import sys
  6from PySide6.QtCore import Qt, Slot
  7from PySide6.QtGui import QPainter
  8from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView,
  9                               QHBoxLayout, QLineEdit, QMainWindow,
 10                               QPushButton, QTableWidget, QTableWidgetItem,
 11                               QVBoxLayout, QWidget)
 12from PySide6.QtCharts import QChartView, QPieSeries, QChart
 13
 14
 15class Widget(QWidget):
 16    def __init__(self):
 17        super().__init__()
 18        self.items = 0
 19
 20        # Example data
 21        self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0,
 22                      "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85,
 23                      "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120}
 24
 25        # Left
 26        self.table = QTableWidget()
 27        self.table.setColumnCount(2)
 28        self.table.setHorizontalHeaderLabels(["Description", "Price"])
 29        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
 30
 31        # Chart
 32        self.chart_view = QChartView()
 33        self.chart_view.setRenderHint(QPainter.Antialiasing)
 34
 35        # Right
 36        self.description = QLineEdit()
 37        self.description.setClearButtonEnabled(True)
 38        self.price = QLineEdit()
 39        self.price.setClearButtonEnabled(True)
 40
 41        self.add = QPushButton("Add")
 42        self.clear = QPushButton("Clear")
 43        self.plot = QPushButton("Plot")
 44
 45        # Disabling 'Add' button
 46        self.add.setEnabled(False)
 47
 48        form_layout = QFormLayout()
 49        form_layout.addRow("Description", self.description)
 50        form_layout.addRow("Price", self.price)
 51        self.right = QVBoxLayout()
 52        self.right.addLayout(form_layout)
 53        self.right.addWidget(self.add)
 54        self.right.addWidget(self.plot)
 55        self.right.addWidget(self.chart_view)
 56        self.right.addWidget(self.clear)
 57
 58        # QWidget Layout
 59        self.layout = QHBoxLayout(self)
 60        self.layout.addWidget(self.table)
 61        self.layout.addLayout(self.right)
 62
 63        # Signals and Slots
 64        self.add.clicked.connect(self.add_element)
 65        self.plot.clicked.connect(self.plot_data)
 66        self.clear.clicked.connect(self.clear_table)
 67        self.description.textChanged.connect(self.check_disable)
 68        self.price.textChanged.connect(self.check_disable)
 69
 70        # Fill example data
 71        self.fill_table()
 72
 73    @Slot()
 74    def add_element(self):
 75        des = self.description.text()
 76        price = float(self.price.text())
 77
 78        self.table.insertRow(self.items)
 79        description_item = QTableWidgetItem(des)
 80        price_item = QTableWidgetItem(f"{price:.2f}")
 81        price_item.setTextAlignment(Qt.AlignRight)
 82
 83        self.table.setItem(self.items, 0, description_item)
 84        self.table.setItem(self.items, 1, price_item)
 85
 86        self.description.clear()
 87        self.price.clear()
 88
 89        self.items += 1
 90
 91    @Slot()
 92    def check_disable(self, s):
 93        enabled = bool(self.description.text() and self.price.text())
 94        self.add.setEnabled(enabled)
 95
 96    @Slot()
 97    def plot_data(self):
 98        # Get table information
 99        series = QPieSeries()
100        for i in range(self.table.rowCount()):
101            text = self.table.item(i, 0).text()
102            number = float(self.table.item(i, 1).text())
103            series.append(text, number)
104
105        chart = QChart()
106        chart.addSeries(series)
107        chart.legend().setAlignment(Qt.AlignLeft)
108        self.chart_view.setChart(chart)
109
110    def fill_table(self, data=None):
111        data = self._data if not data else data
112        for desc, price in data.items():
113            description_item = QTableWidgetItem(desc)
114            price_item = QTableWidgetItem(f"{price:.2f}")
115            price_item.setTextAlignment(Qt.AlignRight)
116            self.table.insertRow(self.items)
117            self.table.setItem(self.items, 0, description_item)
118            self.table.setItem(self.items, 1, price_item)
119            self.items += 1
120
121    @Slot()
122    def clear_table(self):
123        self.table.setRowCount(0)
124        self.items = 0
125
126
127class MainWindow(QMainWindow):
128    def __init__(self, widget):
129        super().__init__()
130        self.setWindowTitle("Tutorial")
131
132        # Menu
133        self.menu = self.menuBar()
134        self.file_menu = self.menu.addMenu("File")
135
136        # Exit QAction
137        exit_action = self.file_menu.addAction("Exit", self.close)
138        exit_action.setShortcut("Ctrl+Q")
139
140        self.setCentralWidget(widget)
141
142
143if __name__ == "__main__":
144    # Qt Application
145    app = QApplication(sys.argv)
146    # QWidget
147    widget = Widget()
148    # QMainWindow using QWidget as central widget
149    window = MainWindow(widget)
150    window.resize(800, 600)
151    window.show()
152
153    # Execute application
154    sys.exit(app.exec())