<think>我们有一个操作ADAM4017设备的类,现在需要使用Qt Designer设计一个UI界面,然后将其与代码整合。
步骤:
1. 使用Qt Designer设计一个简单的界面,包含:
- 一个按钮用于“打开串口”
- 一个按钮用于“发送命令”
- 一个按钮用于“读取并解析数据”
- 一个文本显示区域(比如QTextEdit或QPlainTextEdit)用于显示读取到的数据和解析结果
2. 将设计好的.ui文件转换为.py文件(例如,使用pyuic5工具)
3. 创建一个主程序,将ADAM4017类与UI界面进行整合,并实现事件处理。
4. 注意:由于串口操作是阻塞的,为了避免界面卡死,可能需要使用多线程(但根据用户代码,目前我们先按照单线程实现,后续可以优化)。
设计思路:
在UI类中,我们将实例化一个ADAM4017对象,然后将按钮的点击事件连接到该对象的方法上,并将结果显示在文本区域。
但是,注意用户代码中的ADAM4017类在实例化时已经设置了串口参数(如COM200),我们可以暂时不考虑在界面上设置这些参数,但也可以预留。
由于用户代码中已经有一个ADAM4017类,我们将在这个基础上构建。
我们按照以下步骤进行:
步骤1:设计UI(使用Qt Designer)
创建一个窗体,包含三个按钮和一个文本显示框。
步骤2:将.ui文件转换成.py文件(假设为ui_adam4017.py)
步骤3:编写主程序,整合UI和ADAM4017类。
由于用户代码中已经有一个ADAM4017类,我们需要在UI界面中调用它。
注意:用户代码中的ADAM4017类在__main__中已经有一个简单的测试,我们将这个测试转换为UI事件。
以下是主程序的示例代码:
我们创建一个名为MainWindow的类,它继承自我们转换的UI类,并包含ADAM4017的实例。
代码结构如下:
```python
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
import serial
import time
from ui_adam4017 import Ui_MainWindow # 假设转换后的UI文件为ui_adam4017.py
# 这里应该包含用户原来的ADAM4017类(略作修改,将print改为返回字符串,或者我们通过信号传递信息,但简单起见,我们直接更新界面)
class ADAM4017:
... # 原来的类定义,这里我们稍作修改,比如将一些打印改为返回字符串,以便在界面上显示
class MyMainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyMainWindow, self).__init__()
self.setupUi(self)
self.adam = ADAM4017() # 创建ADAM4017实例
# 连接按钮的点击事件
self.btn_open.clicked.connect(self.open_serial)
self.btn_send.clicked.connect(self.send_command)
self.btn_parse.clicked.connect(self.parse_data)
def open_serial(self):
# 调用ADAM4017的open_serial方法
success = self.adam.open_serial()
if success:
self.text_output.append("成功打开串口")
else:
self.text_output.append("打开串口失败")
def send_command(self):
# 发送命令
self.adam.send_command()
self.text_output.append("已发送命令")
def parse_data(self):
# 读取并解析数据
result = self.adam.parse_adam4017_data() # 这个方法会返回一个字典
# 显示结果
if result:
self.text_output.append("解析结果:")
for key, value in result.items():
if key == 'all_channels':
# 单独处理all_channels,因为它是一个列表
self.text_output.append(f"{key}: {', '.join(value)}")
else:
self.text_output.append(f"{key}: {value}")
else:
self.text_output.append("解析失败")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyMainWindow()
window.show()
sys.exit(app.exec_())
```
但是,我们需要修改原来的ADAM4017类中的一些方法,使其返回字符串而不是打印,或者我们仍然保留打印,同时将信息发送到UI界面。这里我们修改为通过返回值或者修改方法使其将信息返回。
然而,在parse_adam4017_data方法中,它调用了read_data,而read_data中也有打印。我们可以修改这些方法,使其返回状态字符串,或者我们在UI类中捕获打印的内容(这需要重定向标准输出,比较复杂)。因此,我们选择修改ADAM4017类,使其方法返回操作结果和状态。
但是,考虑到时间,我们简单处理:在ADAM4017类中,我们保留原来的打印(用于调试),同时我们将关键信息返回给调用者(UI),以便显示在界面上。
另外,注意parse_adam4017_data方法中调用了read_data,而read_data会返回None,所以我们需要确保在读取数据失败时也能处理。
由于原代码中parse_adam4017_data方法已经返回一个字典,我们可以直接使用。但是,原方法中打印了各通道详情,这些信息我们也想在UI上显示。我们可以修改parse_adam4017_data方法,将原本打印的信息作为字符串返回,或者我们可以在UI中格式化输出。
这里我们选择在UI中格式化输出,所以parse_adam4017_data方法返回的字典我们直接展示。
但是,原方法中有一个print("各通道数据详情(从通道0开始)")以及每个通道的打印,这些我们可以在UI中通过遍历字典来实现。
所以,我们不必修改ADAM4017类中的parse_adam4017_data方法,只需要在UI中显示它返回的字典即可。
但是,注意原方法中使用了print,这会在控制台打印,而不会在UI显示。所以,我们可以修改这个方法,去掉print,或者将这些信息整合到返回的字典中。这里为了简单,我们就不修改ADAM4017类,而是将打印信息重定向到UI。但是重定向标准输出比较麻烦,因此我们修改ADAM4017类。
修改建议:将原来的打印语句改为返回一个包含更多信息的字符串,或者我们将这些信息记录在类内部的一个日志属性中,然后由UI读取。但这样改动较大。
考虑到时间,我们修改parse_adam4017_data方法,让它返回两个值:一个是解析的字典,另一个是描述字符串(包含各通道详情)。但这样会改变返回结构,需要调整UI。
另一种做法:我们在UI中模拟打印这些信息,即根据返回的字典自己构造一个字符串。
由于我们在UI的parse_data方法中已经遍历了字典,那么原来parse_adam4017_data方法中打印的“各通道数据详情”就可以在UI中自己构造。
所以,我们修改UI中的parse_data方法:
self.text_output.append("各通道数据详情(从通道0开始):")
for i, chunk in enumerate(result['all_channels']):
self.text_output.append(f"通道{i}: {chunk}")
然后,我们还可以显示返回字典中的其他键(除了all_channels)?其实我们返回的字典中,all_channels已经包含了所有通道,其他键(channel_0等)其实是冗余的。所以我们可以只显示all_channels,或者显示所有。
为了与原程序保持一致,我们可以显示所有通道,以及单独提取的channel_2和channel_5。
但是,在parse_data方法中,我们只显示all_channels的详细信息,其他键可以不用显示(因为all_channels已经包含)。
因此,在UI的parse_data方法中,我们可以这样:
if result:
self.text_output.append("各通道数据详情(从通道0开始):")
all_channels = result['all_channels']
for i, chunk in enumerate(all_channels):
self.text_output.append(f"通道{i}: {chunk}")
# 还可以单独显示某个通道,如果需要的话
# 例如:self.text_output.append(f"通道2: {result['channel_2']}")
但是,原方法返回的字典中还有channel_0, channel_1等,它们和all_channels中的内容是重复的。所以我们也可以不显示整个字典,只显示all_channels。
因此,我们修改ADAM4017类的parse_adam4017_data方法,让它只返回一个包含所有通道的列表(即all_channels)?但为了保持接口不变,我们还是返回字典,但UI中可以选择显示哪些。
考虑到简单,我们就在UI中显示all_channels的每个通道,然后也显示字典中其他的单独通道(如果有需要的话,比如用户可能只关心某几个通道)。
但是,原字典中还有channel_2和channel_5,它们是从all_channels中提取出来的,所以重复了。所以我们在UI中可以不显示这些重复的内容,只显示all_channels。
所以,我们修改UI中的parse_data方法:
if result:
self.text_output.append("各通道数据详情(从通道0开始):")
all_channels = result['all_channels']
for i, chunk in enumerate(all_channels):
self.text_output.append(f"通道{i}: {chunk}")
# 如果你想单独显示通道2和5,可以这样:
if result['channel_2'] is not None:
self.text_output.append(f"单独提取的通道2: {result['channel_2']}")
if result['channel_5'] is not None:
self.text_output.append(f"单独提取的通道5: {result['channel_5']}")
这样,界面就会显示所有通道,以及单独提取的两个通道(如果存在)。
但是,原ADAM4017类中,parse_adam4017_data方法在调用read_data时可能返回None,然后整个方法就没有返回字典,而是继续执行后面的代码。如果read_data返回None,那么hex_data就是None,然后对None进行切片会导致异常。所以我们需要修改parse_adam4017_data方法,使其在read_data返回None时返回None。
原parse_adam4017_data方法:
def parse_adam4017_data(self):
hex_data=self.read_data() # 如果read_data返回None,那么hex_data为None
data_start=6
chunks = [hex_data[i:i+4] for i in range(data_start, len(hex_data), 4)] # 这里会出错
...
因此,我们需要修改:
def parse_adam4017_data(self):
hex_data = self.read_data()
if hex_data is None:
print("没有读取到数据")
return None
...
同样,在read_data方法中,如果读取失败,返回None。
我们修改后的ADAM4017类(部分修改)如下:
def read_data(self):
try:
time.sleep(0.1)
if self.ser.in_waiting > 0:
self.data = self.ser.read(21)
print(f"成功收到数据:{self.data.hex()}")
return self.data.hex()
else:
print("没有数据可读")
return None
except Exception as e:
print(f"读取数据失败:{e}")
return None
def parse_adam4017_data(self):
hex_data = self.read_data()
if hex_data is None:
print("没有数据可供解析")
return None
data_start=6
# 注意:hex_data是字符串,每个字节用两个字符表示,所以每4个字符(即两个字节)是一个通道的数据
# 因此,hex_data的长度应该是42(21字节*2)
# 检查数据的长度
if len(hex_data) < 42:
print(f"数据长度不足,只有{len(hex_data)}个字符,期望42个字符")
return None
chunks = [hex_data[i:i+4] for i in range(data_start, len(hex_data), 4)]
# 打印各通道详情(在控制台)
print("各通道数据详情(从通道0开始):")
for i, chunk in enumerate(chunks):
print(f"通道{i}: {chunk}")
channel_2_data = chunks[2] if len(chunks) > 2 else None
channel_5_data = chunks[5] if len(chunks) > 5 else None
return {
'channel_0': chunks[0],
'channel_1': chunks[1],
'channel_2': channel_2_data,
'channel_3': chunks[3],
'channel_4': chunks[4],
'channel_5': channel_5_data,
'all_channels': chunks
}
这样,我们就修改了可能的错误。
步骤4:设计UI文件(使用Qt Designer)
我们设计一个简单的界面,包含:
三个按钮:btn_open, btn_send, btn_parse
一个文本编辑器:text_output(使用QPlainTextEdit或QTextEdit)
保存为adam4017.ui。
然后,使用命令将.ui文件转换为.py文件:
pyuic5 -o ui_adam4017.py adam4017.ui
步骤5:编写主程序,将上述的MyMainWindow类与转换的UI类结合。
注意:由于串口操作可能会阻塞GUI,所以如果遇到界面无响应,可能需要使用多线程。但在这个简单示例中,我们假设读取很快。
下面提供一个完整的主程序代码,其中包含了修改后的ADAM4017类(与原来基本一致,只做了上述小修改)。
注意:由于原来的ADAM4017类中,open_serial方法中设置了串口参数为固定值(COM200, 9600, 2秒超时),所以我们在UI中没有提供设置串口参数的控件。如果需要,可以增加。
完整代码:
文件1:adam4017_ui_main.py(主程序)
文件2:ui_adam4017.py(由Qt Designer生成的界面代码)
由于内容较长,我们将两个文件分开。
首先,生成ui_adam4017.py(通过Qt Designer设计并转换,这里不展示转换后的代码,因为每个人设计可能不同,但下面给出一个简单的设计示例)。
然后,在主程序中,我们整合。
这里假设ui_adam4017.py已经存在,并且其中包含一个名为Ui_MainWindow的类。
然后,我们编写主程序。
以下是主程序的完整代码(包含修改后的ADAM4017类):
</think># ADAM4017串口数据读取工具 - Qt UI界面实现
下面是根据您的代码设计的Qt UI界面实现。这个界面允许用户操作ADAM4017设备的串口通信和数据读取功能。
## 完整代码实现
```python
import sys
import serial
import time
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QTextEdit, QLabel, QComboBox, QGroupBox, QStatusBar)
from PyQt5.QtCore import Qt, QTimer
class ADAM4017:
def __init__(self, port='COM200', baudrate=9600, timeout=1):
self.port = port
self.baudrate = baudrate
self.timeout = timeout
self.ser = None
self.data = None
def open_serial(self):
try:
self.ser = serial.Serial(
port=self.port,
baudrate=self.baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=self.timeout
)
if self.ser.is_open:
return True, f"成功打开串口 {self.port}"
else:
return False, f"串口无法打开 {self.port}"
except serial.SerialException as e:
return False, f"串口打开失败: {e}"
def close_serial(self):
if self.ser and self.ser.is_open:
self.ser.close()
return True, f"已关闭串口 {self.port}"
return False, "串口未打开"
def send_command(self):
if self.ser and self.ser.is_open:
try:
self.ser.write(b"\x02\x03\x00\x00\x00\x08\x44\x3F")
return True, "命令发送成功"
except Exception as e:
return False, f"命令发送失败: {e}"
return False, "串口未打开,无法发送命令"
def read_data(self):
try:
if self.ser and self.ser.is_open:
time.sleep(0.1)
if self.ser.in_waiting > 0:
self.data = self.ser.read(21)
return True, self.data.hex()
else:
return False, "没有收到数据"
else:
return False, "串口未打开"
except Exception as e:
return False, f"读取数据失败: {e}"
def parse_adam4017_data(self):
success, result = self.read_data()
if not success:
return False, result
hex_data = result
if len(hex_data) < 42: # 21字节 * 2个十六进制字符
return False, f"数据长度不足: 收到{len(hex_data)}字符, 需要42字符"
data_start = 6
chunks = [hex_data[i:i+4] for i in range(data_start, len(hex_data), 4)]
# 确保有足够的通道数据
if len(chunks) < 6:
return False, f"通道数据不足: 解析到{len(chunks)}个通道, 需要至少6个"
result = {
'channel_0': chunks[0],
'channel_1': chunks[1],
'channel_2': chunks[2],
'channel_3': chunks[3],
'channel_4': chunks[4],
'channel_5': chunks[5],
'all_channels': chunks
}
return True, result
class ADAM4017UI(QMainWindow):
def __init__(self):
super().__init__()
self.adam = ADAM4017()
self.initUI()
def initUI(self):
self.setWindowTitle('ADAM4017串口数据读取工具')
self.setGeometry(300, 300, 800, 600)
# 创建主窗口部件
main_widget = QWidget()
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# 串口配置区域
config_group = QGroupBox("串口配置")
config_layout = QHBoxLayout()
self.port_combo = QComboBox()
# 添加常见COM端口
self.port_combo.addItems([f"COM{i}" for i in range(1, 21)])
self.port_combo.setCurrentText('COM200')
self.baudrate_combo = QComboBox()
self.baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
self.baudrate_combo.setCurrentText('9600')
config_layout.addWidget(QLabel("端口:"))
config_layout.addWidget(self.port_combo)
config_layout.addWidget(QLabel("波特率:"))
config_layout.addWidget(self.baudrate_combo)
self.open_btn = QPushButton("打开串口")
self.open_btn.clicked.connect(self.toggle_serial)
config_layout.addWidget(self.open_btn)
config_group.setLayout(config_layout)
main_layout.addWidget(config_group)
# 控制按钮区域
btn_layout = QHBoxLayout()
self.send_btn = QPushButton("发送命令")
self.send_btn.clicked.connect(self.send_command)
self.send_btn.setEnabled(False)
btn_layout.addWidget(self.send_btn)
self.read_btn = QPushButton("读取数据")
self.read_btn.clicked.connect(self.read_data)
self.read_btn.setEnabled(False)
btn_layout.addWidget(self.read_btn)
self.parse_btn = QPushButton("解析数据")
self.parse_btn.clicked.connect(self.parse_data)
self.parse_btn.setEnabled(False)
btn_layout.addWidget(self.parse_btn)
main_layout.addLayout(btn_layout)
# 数据显示区域
data_group = QGroupBox("数据输出")
data_layout = QVBoxLayout()
self.data_display = QTextEdit()
self.data_display.setReadOnly(True)
self.data_display.setFontFamily("Courier New")
data_layout.addWidget(self.data_display)
data_group.setLayout(data_layout)
main_layout.addWidget(data_group, 1) # 使用拉伸因子1使其占据更多空间
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
# 设置定时器自动读取数据
self.read_timer = QTimer()
self.read_timer.timeout.connect(self.read_data)
self.auto_read = False
# 自动读取复选框
auto_layout = QHBoxLayout()
self.auto_read_cb = QLabel("自动读取: 未激活")
auto_layout.addWidget(self.auto_read_cb)
self.toggle_auto_btn = QPushButton("启用自动读取")
self.toggle_auto_btn.clicked.connect(self.toggle_auto_read)
self.toggle_auto_btn.setEnabled(False)
auto_layout.addWidget(self.toggle_auto_btn)
main_layout.addLayout(auto_layout)
# 日志区域
log_group = QGroupBox("日志")
log_layout = QVBoxLayout()
self.log_display = QTextEdit()
self.log_display.setReadOnly(True)
self.log_display.setMaximumHeight(100)
log_layout.addWidget(self.log_display)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
def toggle_serial(self):
"""切换串口状态(打开/关闭)"""
if self.adam.ser and self.adam.ser.is_open:
success, message = self.adam.close_serial()
self.log(message)
self.status_bar.showMessage(message)
self.open_btn.setText("打开串口")
self.send_btn.setEnabled(False)
self.read_btn.setEnabled(False)
self.parse_btn.setEnabled(False)
self.toggle_auto_btn.setEnabled(False)
self.toggle_auto_btn.setText("启用自动读取")
self.auto_read_cb.setText("自动读取: 未激活")
self.read_timer.stop()
else:
# 获取UI中的配置
port = self.port_combo.currentText()
baudrate = int(self.baudrate_combo.currentText())
self.adam.port = port
self.adam.baudrate = baudrate
success, message = self.adam.open_serial()
self.log(message)
self.status_bar.showMessage(message)
if success:
self.open_btn.setText("关闭串口")
self.send_btn.setEnabled(True)
self.read_btn.setEnabled(True)
self.parse_btn.setEnabled(True)
self.toggle_auto_btn.setEnabled(True)
def send_command(self):
"""发送命令到设备"""
success, message = self.adam.send_command()
self.log(message)
self.status_bar.showMessage(message)
def read_data(self):
"""从设备读取数据"""
success, message = self.adam.read_data()
if success:
self.data_display.clear()
self.data_display.append("原始数据(十六进制):")
self.data_display.append(message)
# 格式化显示二进制数据
hex_str = message
formatted = " ".join([hex_str[i:i+2] for i in range(0, len(hex_str), 2)])
self.data_display.append("\n格式化显示:")
self.data_display.append(formatted)
self.log(f"成功读取数据: {len(message)}字节")
else:
self.log(f"读取失败: {message}")
self.status_bar.showMessage(message)
def parse_data(self):
"""解析读取到的数据"""
success, result = self.adam.parse_adam4017_data()
if not success:
self.log(f"解析失败: {result}")
self.status_bar.showMessage(f"解析失败: {result}")
return
self.data_display.clear()
self.data_display.append("解析结果:")
self.data_display.append(f"通道 0: {result['channel_0']}")
self.data_display.append(f"通道 1: {result['channel_1']}")
self.data_display.append(f"通道 2: {result['channel_2']}")
self.data_display.append(f"通道 3: {result['channel_3']}")
self.data_display.append(f"通道 4: {result['channel_4']}")
self.data_display.append(f"通道 5: {result['channel_5']}")
self.data_display.append("\n所有通道数据:")
for i, chunk in enumerate(result['all_channels']):
self.data_display.append(f"通道 {i}: {chunk}")
self.log("数据解析成功")
self.status_bar.showMessage("数据解析成功")
def toggle_auto_read(self):
"""切换自动读取模式"""
if self.auto_read:
self.read_timer.stop()
self.auto_read = False
self.toggle_auto_btn.setText("启用自动读取")
self.auto_read_cb.setText("自动读取: 未激活")
self.log("自动读取已禁用")
else:
self.read_timer.start(1000) # 1秒间隔
self.auto_read = True
self.toggle_auto_btn.setText("停止自动读取")
self.auto_read_cb.setText("自动读取: 激活中 (1秒/次)")
self.log("自动读取已启用")
def log(self, message):
"""添加日志信息"""
timestamp = time.strftime("%H:%M:%S")
self.log_display.append(f"[{timestamp}] {message}")
# 自动滚动到底部
scrollbar = self.log_display.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = ADAM4017UI()
main_window.show()
sys.exit(app.exec_())
```
## 界面功能说明
这个UI界面实现了以下功能:
1. **串口配置区域**:
- 可选择COM端口(COM1-COM20)
- 可选择波特率(9600-115200)
- 打开/关闭串口按钮
2. **控制按钮区域**:
- 发送命令:发送设备指令
- 读取数据:从设备读取原始数据
- 解析数据:解析读取到的通道数据
- 自动读取:定时自动读取数据(1秒间隔)
3. **数据显示区域**:
- 显示原始十六进制数据
- 格式化显示二进制数据
- 解析后的通道数据显示(通道0-5)
4. **日志区域**:
- 显示所有操作的状态信息
- 带时间戳记录所有操作
5. **状态栏**:
- 显示当前操作状态
## 使用说明
1. 选择串口和波特率(默认COM200,9600)
2. 点击"打开串口"按钮连接设备
3. 使用按钮发送命令、读取数据、解析数据
4. 可启用自动读取功能定时获取数据
5. 日志区域显示所有操作记录
## 设计特点
1. **用户友好界面**:所有功能清晰分组,操作直观
2. **错误处理**:完善的错误处理和状态反馈
3. **数据格式化**:原始数据格式化显示,便于阅读
4. **自动读取**:可选定时自动读取功能
5. **详细日志**:完整记录所有操作和状态变化
## 运行要求
- Python 3.x
- PyQt5 (`pip install PyQt5`)
- pyserial (`pip install pyserial`)
这个UI界面紧密集成了您的ADAM4017类,提供了图形化的操作方式,使设备操作更加直观和方便[^1]。