跳到主要内容Python PyQt 上位机设计与实战指南 | 极客日志Python大前端
Python PyQt 上位机设计与实战指南
介绍使用 Python 和 PyQt 构建上位机软件的完整流程。内容涵盖 PyQt 框架优势分析、基础窗口搭建、多线程串口通信实现、实时波形图绘制(pyqtgraph)、系统架构设计以及常见开发陷阱(如乱码、资源占用、内存泄漏)的解决方案。通过模块化设计和配置管理,实现高性能、可维护的工控界面应用。
清酒独酌30 浏览 Python PyQt 上位机开发基础
为什么是 PyQt?它真的适合做工业级上位机吗?
很多人认为 Python 做上位机太慢或不稳定,但现代 PyQt 的实际能力已能满足需求。
PyQt 是什么
PyQt 是 Python 版的 Qt,直接调用原生 C++ Qt 库。这意味着界面渲染走系统级图形 API(DirectX / OpenGL),控件外观与本地应用一致,内存管理由底层引擎处理,性能接近原生程序。
| 关注点 | 实际情况 |
|---|
| 跨平台 | Windows/Linux/macOS 一键运行,代码几乎不用改 |
| 界面美观 | 支持 QSS 样式表,可做出媲美 Web 的视觉效果 |
| 多线程 | 完全支持,有安全机制避免 UI 卡顿 |
| 绘图性能 | 配合 pyqtgraph,轻松实现每秒上千点的实时刷新 |
| 发布方便 | 用 PyInstaller 打包成单个 exe,客户双击即用 |
搭建你的第一个 PyQt 窗口
网上教程常直接甩代码,我们拆解关键步骤。
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn = QPushButton('开始采集', self)
self.btn.clicked.connect(self.on_start_clicked)
layout = QVBoxLayout()
layout.addWidget(self.btn)
self.setLayout(layout)
self.setWindowTitle('上位机主界面')
self.resize(300, 200)
():
()
__name__ == :
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
def
on_start_clicked
self
print
"数据采集已启动..."
if
'__main__'
四个关键角色
- QApplication:整个 GUI 程序的'心脏',负责事件循环、消息分发。
- MainWindow (继承自 QWidget):主窗口本身,放置各种控件。
- QPushButton:最常见的交互元素之一,点击后发出
clicked 信号。
信号与槽机制:这是 PyQt 的灵魂。通过 self.btn.clicked.connect(self.on_start_clicked),当用户点击按钮时自动调用函数,让界面和逻辑解耦。
串口通信:解决阻塞问题
在主线程直接读串口会导致界面卡死。正确做法是将通信放入独立线程。
import serial
import threading
from PyQt5.QtCore import pyqtSignal, QObject
class SerialWorker(QObject):
data_received = pyqtSignal(str)
def __init__(self, port, baudrate=115200):
super().__init__()
self.port = port
self.baudrate = baudrate
self.is_running = False
def start(self):
self.is_running = True
self.thread = threading.Thread(target=self._read_loop)
self.thread.start()
def _read_loop(self):
try:
ser = serial.Serial(self.port, self.baudrate, timeout=1)
while self.is_running:
if ser.in_waiting > 0:
line = ser.readline().decode('utf-8').strip()
self.data_received.emit(line)
ser.close()
except Exception as e:
self.data_received.emit(f"ERROR: {str(e)}")
def stop(self):
self.is_running = False
重点在于 data_received = pyqtSignal(str) 是跨线程安全的通信通道。子线程发射信号,由 Qt 内部机制投递到主线程处理。
self.serial_worker = SerialWorker('COM3', 115200)
self.serial_worker.data_received.connect(self.handle_serial_data)
self.serial_worker.start()
def handle_serial_data(self, data):
print("收到:", data)
数据显示:表格与状态指示
假设传感器返回格式为 TEMP:23.5,HUMI:45.2,需结构化展示。
方案一:表格显示历史记录
from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem
self.table = QTableWidget(100, 2)
self.table.setHorizontalHeaderLabels(['时间', '数值'])
def add_table_row(self, value):
row = self.table.rowCount()
for i in range(row - 1):
for col in range(2):
item = self.table.item(i+1, col)
if item:
self.table.setItem(i, col, QTableWidgetItem(item.text()))
self.table.setItem(row-1, 0, QTableWidgetItem(time.strftime("%H:%M:%S")))
self.table.setItem(row-1, 1, QTableWidgetItem(value))
方案二:状态指示灯
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QLabel
self.status_label = QLabel("正常")
self.status_label.setStyleSheet("background-color: green; color: white; padding: 5px")
def update_status(self, temp):
if float(temp) > 30:
self.status_label.setText("高温报警!")
self.status_label.setStyleSheet("background-color: red; color: white; padding: 5px")
else:
self.status_label.setText("正常")
self.status_label.setStyleSheet("background-color: green; color: white; padding: 5px")
实时波形图:使用 pyqtgraph
Matplotlib 不适合实时监控,推荐使用专为科学计算优化的 pyqtgraph。
import pyqtgraph as pg
import numpy as np
plot_widget = pg.PlotWidget()
plot_curve = plot_widget.plot(pen='y')
plot_widget.setLabel('left', '电压', units='V')
plot_widget.setLabel('bottom', '时间', units='s')
plot_widget.setTitle('实时采样波形')
buffer_size = 1000
x_data = np.linspace(0, 10, buffer_size)
y_data = np.zeros(buffer_size)
def update_plot(new_value):
global y_data
y_data = np.append(y_data[1:], new_value)
plot_curve.setData(x_data, y_data)
from PyQt5.QtCore import QTimer
timer = QTimer()
timer.timeout.connect(lambda: update_plot(np.random.rand()))
timer.start(50)
关键点:使用 NumPy 数组存储数据,setData() 增量更新,配合定时器形成稳定动画节奏。
复杂系统架构设计
┌─────────────────────┐
│ 用户界面层 (UI) │ ← 用户看到的一切
├─────────────────────┤
│ 控制逻辑层 (Controller)│ ← 参数处理、状态切换
├─────────────────────┤
│ 数据通信层 (Serial) │ ← 串口/网络收发
├─────────────────────┤
│ 数据模型层 (Model) │ ← 缓冲区、协议解析、文件保存
└─────────────────────┘
每一层只和相邻层交互。好处包括修改通信协议不影响界面、更换连接方式只需替换底层模块、单元测试更容易。
常见开发陷阱
坑 1:中文乱码 or 数据断包
原因:编码不统一 + 未处理粘包问题。
✅ 正确做法:
line = ser.readline().decode('utf-8', errors='ignore').strip()
data = ser.read(32)
更高级的做法是定义帧头帧尾,例如 $DATA,23.5,*7F\n。
坑 2:关闭程序后串口仍被占用
原因:异常退出时没有正确释放资源。
✅ 解决方案:
def closeEvent(self, event):
if hasattr(self, 'serial_worker'):
self.serial_worker.stop()
self.serial_worker.thread.join(timeout=1.0)
event.accept()
坑 3:长时间运行内存暴涨
原因:不断往列表追加数据却不清理。
✅ 解决办法:限制缓冲区大小、定期清理日志文本框、使用 weakref 防止循环引用。
提升产品感的加分项
✅ 配置记忆功能
from PyQt5.QtCore import QSettings
settings = QSettings("MyCompany", "SensorMonitor")
settings.setValue("port", "COM3")
settings.setValue("baudrate", 115200)
last_port = settings.value("port", "COM1")
last_baud = int(settings.value("baudrate", 9600))
✅ 日志输出面板
加个 QTextEdit 实时显示通信日志,故障排查效率翻倍。
✅ 数据导出按钮
with open('data.csv', 'w') as f:
for t, v in zip(x_data, y_data):
f.write(f"{t:.3f},{v:.3f}\n")
✅ 图形美化
btn.setStyleSheet("""
QPushButton {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a9, stop:1 #488);
color: white;
border-radius: 8px;
padding: 10px;
}
""")
相关免费在线工具
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online