使用 PyQt 制作简易软件简明笔记

缘由

最近在统计多组数据的重复性,利用到了标准差相关的公式,自然而然想到是使用 Excel 来处理公式。总之,模板是实现了,将公式一步一步拆解,饶了一大圈,发现最后 Excel 本身就内置了求取标准差的函数,还不如不要让我知道这个结果呢,伤心: (

image.png

不过,这也带来了一些思考。自认为自己用 Excel 还算用得多的了,根本没有听过用过这样的内置函数,能不能写个简单的程序,只要将数据往里面一扔,就可以自动出来计算结果呢 —> 于是开始了折腾

Step 1 搭建环境

  • 这里选择社区版的 Pycharm,免费,集成开发环境用起来也方便
  • 程序开发使用的是 PyQt 框架

刚安装完 Pycharm,会检测系统中是否已经安装好了 python,如果恰好没有安装过,那么根据软件的建议安装即可

写好的代码中若检测有问题,Pycharm 会在对应的位置进行提示,如一下这段程序,刚安装的 python 肯定是没有 PyQT6 的程序包的,可以通过提示(小红灯泡处,Alt+Enter)进行安装

image.png

新建项目,新建 python 文件,粘贴如下代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton  

app = QApplication([]) # 创建应用程序对象

window = QWidget() # 创建窗口部件
layout = QVBoxLayout() # 创建布局管理器

button = QPushButton("点击我") # 创建按钮
layout.addWidget(button) # 将按钮添加到布局管理器中

window.setLayout(layout) # 设置窗口部件的布局
window.show() # 显示窗口

app.exec() # 进入应用程序的主循环

image.png

很好,说明各种环境已经搭建好了!

Step 2 实现基础功能

接下来尽管提需求,从开发者的角度出发,一步步给 GPT 进行引导,从易到难,功能从简单到复杂

例如我开始给 GPT 提需求:

我提供一组数据,点击按钮之后,就可以在一个输出位置输出这组数据的标准差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import sys  
import numpy as np
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QLabel, QLineEdit

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()

self.setWindowTitle("标准差计算器")
self.setGeometry(100, 100, 400, 200)

# 主部件
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)

# 布局
self.layout = QVBoxLayout()
self.central_widget.setLayout(self.layout)

# 输入框
self.data_input = QLineEdit()
self.data_input.setPlaceholderText("输入数据,用逗号分隔")
self.layout.addWidget(self.data_input)

# 计算按钮
self.calculate_button = QPushButton("计算标准差")
self.calculate_button.clicked.connect(self.calculate_standard_deviation)
self.layout.addWidget(self.calculate_button)

# 输出标签
self.result_label = QLabel("标准差:")
self.layout.addWidget(self.result_label)

def calculate_standard_deviation(self):
# 获取输入数据
data_text = self.data_input.text()
if not data_text:
self.result_label.setText("请先输入数据")
return

# 解析数据
try:
data = list(map(float, data_text.split(',')))
except ValueError:
self.result_label.setText("数据格式错误,请输入有效的数字")
return

# 计算标准差
std_dev = np.std(data)
self.result_label.setText(f"标准差:{std_dev:.2f}")

if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

image.png

好了,手动输入几组数据,确实能够计算出来标准差了,不过我们的数据来自 Excel,而且用逗号分隔数据的方式也不方便。于是就有了如下需求:

但是,你知道的,我的输入数据一般从 excel 中复制过来的,这种用逗号分隔数字的方式让我在输入的时候非常麻烦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QPlainTextEdit, QLabel
import numpy as np

class DataProcessingApp(QWidget):
def __init__(self):
super().__init__()

# 设置窗口标题和尺寸
self.setWindowTitle("数据处理应用")
self.setGeometry(100, 100, 400, 300)

# 创建布局和组件
layout = QVBoxLayout()

self.text_edit = QPlainTextEdit(self)
self.text_edit.setPlaceholderText("在这里粘贴数据...")
layout.addWidget(self.text_edit)

self.calculate_button = QPushButton("计算标准差", self)
self.calculate_button.clicked.connect(self.calculate_std_dev)
layout.addWidget(self.calculate_button)

self.result_label = QLabel("标准差:", self)
layout.addWidget(self.result_label)

self.setLayout(layout)

def calculate_std_dev(self):
# 获取文本内容
text = self.text_edit.toPlainText()

# 处理文本数据
try:
# 将文本数据按行分隔,然后按空格或制表符分隔
data = [float(num) for line in text.splitlines() for num in line.split()]
if data:
# 计算标准差
std_dev = np.std(data)
self.result_label.setText(f"标准差:{std_dev:.2f}")
else:
self.result_label.setText("标准差:无效数据")
except ValueError:
self.result_label.setText("标准差:数据格式错误")

app = QApplication([])
window = DataProcessingApp()
window.show()
app.exec()

image.png

以上这组数据就是直接从 Excel 粘贴进去的。看来功能 GPT 是帮我们实现了

Step 3 完善程序

接下来按照上面的步骤,进一步提需求,进一步完善程序代码
例如,可以增加一些数据清洗、数据输入验证、错处处理、历史记录、导出数据等功能
也可以对程序进行拆分,不同功能的代码写到不同的 py 文件中

image.png

当前的程序结构为

1
2
3
4
5
6
data_processor/

├── main.py
├── ui.py
├── processor.py
└── utils.py

主程序

main.py
1
2
3
4
5
6
7
8
9
import sys
from PyQt6.QtWidgets import QApplication
from ui import DataProcessorApp

if __name__ == "__main__":
app = QApplication(sys.argv)
processor = DataProcessorApp()
processor.show()
sys.exit(app.exec())

UI 界面

ui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextEdit, QLabel  
from PyQt6.QtGui import QFont, QColor
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPalette
from processor import DataProcessor


class DataProcessorApp(QWidget):
def __init__(self):
super().__init__()
self.processor = DataProcessor()
self.init_ui()

def init_ui(self):
# Set the overall layout and style
layout = QVBoxLayout()
self.setStyleSheet("background-color: #f0f0f0;")
self.setPalette(QPalette(QColor('#f0f0f0')))

# Create and style widgets
self.input_text = QTextEdit(self)
self.input_text.setPlaceholderText("在这里粘贴数据,每行一个数字")
self.input_text.setStyleSheet("font-size: 14px; padding: 10px; border: 1px solid #ccc;")
layout.addWidget(self.input_text)

self.mean_label = QLabel("平均值:", self)
self.mean_label.setStyleSheet("font-size: 16px; color: #333;")
layout.addWidget(self.mean_label)

self.variance_label = QLabel("方差:", self)
self.variance_label.setStyleSheet("font-size: 16px; color: #333;")
layout.addWidget(self.variance_label)

self.std_dev_label = QLabel("标准差:", self)
self.std_dev_label.setStyleSheet("font-size: 16px; color: #333;")
layout.addWidget(self.std_dev_label)

self.reliability_label = QLabel("重复性:", self)
self.reliability_label.setStyleSheet("font-size: 16px; color: #333;")
layout.addWidget(self.reliability_label)

process_button = QPushButton("计算标准差", self)
process_button.setStyleSheet(
"background-color: #4CAF50; color: white; font-size: 14px; padding: 10px; border: none; border-radius: 5px;")
process_button.clicked.connect(self.calculate_std_dev)
layout.addWidget(process_button)

reliability_button = QPushButton("计算重复性", self)
reliability_button.setStyleSheet(
"background-color: #2196F3; color: white; font-size: 14px; padding: 10px; border: none; border-radius: 5px;")
reliability_button.clicked.connect(self.calculate_reliability)
layout.addWidget(reliability_button)

self.history_text = QTextEdit(self)
self.history_text.setPlaceholderText("历史记录")
self.history_text.setReadOnly(True)
self.history_text.setStyleSheet("font-size: 14px; padding: 10px; border: 1px solid #ccc;")
layout.addWidget(self.history_text)

self.setLayout(layout)
self.setWindowTitle("数据处理程序")
self.setGeometry(100, 100, 500, 400)
self.setFont(QFont('Arial', 12))

def calculate_std_dev(self):
data = self.input_text.toPlainText().strip()
try:
numbers = [float(x) for x in data.split() if x.replace('.', '', 1).isdigit()]
if numbers:
std_dev = self.processor.calculate_std_dev(numbers)
mean, variance, std_dev, _ = self.processor.calculate_reliability(numbers)
self.mean_label.setText(f"平均值:{mean:.6f}")
self.variance_label.setText(f"方差:{variance:.6f}")
self.std_dev_label.setText(f"标准差:{std_dev:.6f}")
else:
self.std_dev_label.setText("标准差:无效数据")
self.mean_label.setText("平均值:无效数据")
self.variance_label.setText("方差:无效数据")
history = self.history_text.toPlainText()
new_entry = f"数据:{', '.join(map(str, numbers))} | 标准差:{std_dev:.6f} | 平均值:{mean:.6f} | 方差:{variance:.6f}\n"
self.history_text.setText(history + new_entry)
except Exception as e:
self.std_dev_label.setText(f"错误:{e}")
self.mean_label.setText("平均值:无效数据")
self.variance_label.setText("方差:无效数据")

def calculate_reliability(self):
data = self.input_text.toPlainText().strip()
try:
numbers = [float(x) for x in data.split() if x.replace('.', '', 1).isdigit()]
if numbers:
mean, variance, std_dev, reliability = self.processor.calculate_reliability(numbers)
self.mean_label.setText(f"平均值:{mean:.6f}")
self.variance_label.setText(f"方差:{variance:.6f}")
self.std_dev_label.setText(f"标准差:{std_dev:.6f}")
self.reliability_label.setText(f"重复性:{reliability:.6f}")
else:
self.reliability_label.setText("重复性:无效数据")
self.mean_label.setText("平均值:无效数据")
self.variance_label.setText("方差:无效数据")
self.std_dev_label.setText("标准差:无效数据")
history = self.history_text.toPlainText()
new_entry = f"数据:{', '.join(map(str, numbers))} | 标准差:{std_dev:.6f} | 重复性:{reliability:.6f} | 平均值:{mean:.6f} | 方差:{variance:.6f}\n"
self.history_text.setText(history + new_entry)
except Exception as e:
self.reliability_label.setText(f"错误:{e}")
self.mean_label.setText("平均值:无效数据")
self.variance_label.setText("方差:无效数据")
self.std_dev_label.setText("标准差:无效数据")

数据处理

processor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np

class DataProcessor:
def calculate_std_dev(self, numbers):
return np.std(numbers, ddof=1)

def calculate_reliability(self, numbers):
mean = np.mean(numbers)
variance = np.var(numbers, ddof=1)
std_dev = np.sqrt(variance)
reliability = std_dev / mean
return mean, variance, std_dev, reliability

Step 4 打包程序

想要将整个程序打包成 exe 文件,这样方便在没有 python 环境时依然可以运行,步骤如下:

  • 安装 PyInstaller 包(可以直接在 powershell 中安装)
  • 使用 PyInstaller 打包脚本

1、在 PowerShell 中,运行如下命令

1
pip install pyinstaller

注意:前面已经安装了 python 程序,应该是可以使用 pip 命令了的。可输入 pip --version 命令查看。若该命令无法使用,很可能是没有将 python 解释器添加到环境变量中,可进行如下操作:

1
2
C:\Users\Administrator\AppData\Local\Programs\Python\Python312\
C:\Users\Administrator\AppData\Local\Programs\Python\Python312\Scripts\

这里的路径请替换为实际安装 Python 的路径

image.png

2、运行 pyinstaller 命令打包程序

1
pyinstaller --onefile --windowed your_script.py
  • --onefile:将所有内容打包成一个单独的 .exe 文件。
  • --windowed:用于创建一个没有命令行窗口的图形界面应用。

打包完成后,会生成几个文件夹:

  • dist 中包含生成的 exe 文件
  • build 中包含构建过程中的临时文件,可以删除。
  • your_script.spec 文件是 PyInstaller 的配置文件,可以根据需要进行修改。

image.png

.gif

恭喜你,得到了一款简易的小程序:)

明明程序实现的功能很简单,为什么生成的程序还那么大呢,例如生成上面的这个 exe 文件就有 50M。因为软件将一些必要的程序包(如 numpy 模块)都打包进去了,若之后再增加一些计算相关的各种功能进去,程序体积可能也只会大一丢丢吧

另外,打包时如何优化资源,排除掉一些不必要的模块还值得学学。不过,尽管文件体积大点,功能总算是实现了,还是蛮有成就感的