费用工具教程¶
- 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 中。
应用程序将如下所示:
现在你可以看到完整的代码:
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())