费用工具教程#

在本教程中,您将学习以下概念
  • 通过编程创建用户界面

  • 布局和小部件

  • 重载 Qt 类

  • 连接信号和槽

  • 与 QWidgets 交互

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

要求
  • 应用程序的一个简单窗口(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 继承的类。

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

现在,我们的类已经定义,创建其实例并调用 show()

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

空白小部件和数据#

Qt的<惜ritte QMainWindow>允许我们设置一个在显示窗口时将显示的中心小部件(更多信息)。这个中心小部件可以是另一个从QWidget派生出的类。

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

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

有了Widget类,就修改<惜ritte MainWindow>的初始化代码

1    # QWidget
2    widget = Widget()
3    # QMainWindow using QWidget as central widget
4    window = MainWindow(widget)

窗口布局#

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

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

注意

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

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

如您所见,代码还包括了一个<惜ritte QHBoxLayout>,该布局提供了容器以便水平放置小部件。

此外,<惜ritte QTableWidget>允许自定义它,例如添加将用于的两个列的标签,以及将内容拉伸以使用整个Widget空间。

代码的最后一条提到了填写表格,执行此任务的相关代码如下。

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

将此过程放置在单独的方法中是良好的实践,这样可以使构造函数更易于阅读,并将类的主要功能分割成独立的进程。

右侧布局#

因为所使用的数据只是一个示例,需要包括一个机制以向表格中输入项,额外的按钮来清除表格内容,以及退出应用程序。

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

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

在左侧留出表格,并在此新增的小部件向您之前所见的示例中添加布局。

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

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

添加元素#

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

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

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

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

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

将元素添加到表格的机制描述如下:

  • 从字段获取描述价格

  • 向表格中插入新的空行,

  • 为每列设置空行的值,

  • 清除输入文本字段,

  • 包含全局表行计数。

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

验证步骤#

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

您可以使用 来自 QLineEdit 的信号 textChanged,该信号会在内部发生变化时发出,即:每次按键。

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

1        self.description.textChanged.connect(self.check_disable)
2        self.price.textChanged.connect(self.check_disable)

check_disable 槽的内容将会很简单。

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

您有两个选择,编写基于您检索到的字符串当前值的验证,或者手动获取两个 QLineEdit 的全部内容。第二种在这种情况下更受青睐,这样您就可以验证两个输入是否不为空,以启用 添加 按钮。

注意

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

空图表视图#

您可以向表中添加新项目,并且到目前为止可视化已经就绪,但您可以通过图形方式表示数据来做得更多。

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

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

此外,您将如何包含工具到右侧的 QVBoxLayout 的顺序也将改变。

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

请注意,在我们之前有一个带有 self.right.addStretch() 的行来填充 添加清除 按钮之间的垂直空间,但现在,有了 QChartView 就不再需要了。

另外,如果您想按需进行,您需要包含一个 绘图 按钮。

完整应用程序#

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

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

这并没有什么新东西,因为您已经为其他按钮做过了,但是现在看看如何创建一个图表并将其包含在 QChartView 中。

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

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