费用工具教程#
- 在本教程中,您将学习以下概念
通过编程创建用户界面
布局和小部件
重载 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 中。
应用程序将看起来像这样
现在您可以看到整个代码
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())