帮我看看UI部分要怎么修改,感觉好丑
import sysimport re
import time
import serial
import logging
from functools import partial
from PyQt5.QtWidgets import (
QApplication, QWidget, QCheckBox, QPushButton, QLabel,
QGridLayout, QLineEdit, QTextEdit, QVBoxLayout, QGroupBox,
QHBoxLayout, QComboBox, QFormLayout, QSpacerItem, QSizePolicy
)
from PyQt5.QtGui import QFont
from PyQt5.QtCore import QTimer, pyqtSignal, Qt, QThread, QMetaObject, Qt
from datetime import datetime
import serial.tools.list_ports
import unittest
# 配置日志系统
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler("auto_test_app.log", encoding='utf-8')
]
)
logger = logging.getLogger(__name__)
class PowerMeterThread(QThread):
data_ready = pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.running = True
def run(self):
while self.running:
if not self.parent.port_states['POWER_METER']['open']:
self.msleep(1000)
continue
commands = {
'Urms': b':NUMeric:NORMal:VALue? 1\r\n',
'Irms': b':NUMeric:NORMal:VALue? 6\r\n',
'P': b':NUMeric:NORMal:VALue? 11\r\n'
}
result = {}
try:
for key, cmd in commands.items():
self.parent._clear_serial_buffer('POWER_METER')
self.parent.serial_ports['POWER_METER'].write(cmd)
time.sleep(0.1)
response = self.parent.serial_ports['POWER_METER'].readline()
self.parent._process_power_meter_response(key, response)
result = self.parent.power_meter_data
self.data_ready.emit(result)
except serial.SerialException as e:
logger.error(f"Power meter error: {str(e)}")
self.msleep(self.parent.POWER_METER_QUERY_INTERVAL)
def stop(self):
self.running = False
self.wait()
class PowerTesterLogic:
def _format_measurement(self, value, unit):
return self.logic.format_measurement(value, unit)
def _send_voltage_command(self, value, unit):
if unit == 'mV':
value = value / 1000.0
# 示例发送命令到 LMCI
# self.serial_ports['LMCI'].write(f"VOLT {value}\n".encode())
# 示例发送命令到 LMCI
print(f"Sent voltage command: V = {value} V")
class AutoTestApp(QWidget):
serial_port_opened = pyqtSignal(str, bool)
POWER_METER_QUERY_INTERVAL = 1000# 毫秒
SERIAL_REFRESH_INTERVAL = 2000 # 毫秒
def __init__(self):
super().__init__()
self.logic = PowerTesterLogic()
self._init_serial_ports()
self._init_ui_state()
self._setup_serial_parameters()
self.initUI()
self._setup_timers()
self._connect_signals()
# 日志控制变量
self.last_power_error_time = 0
self.power_error_count = 0
# 初始化线程
self.power_meter_thread = PowerMeterThread(self)
self.power_meter_thread.data_ready.connect(self._update_power_meter_display_from_thread)
# 启动线程
self.power_meter_thread.start()
def _init_serial_ports(self):
"""初始化串口相关配置"""
self.serial_ports = {
'LMCI': serial.Serial(baudrate=115200, timeout=1),
'LOAD': serial.Serial(baudrate=9600, timeout=1),
'POWER_METER': serial.Serial(baudrate=9600, timeout=1)
}
self.port_states = {
typ: {'open': False, 'port': None}
for typ in ['LMCI', 'LOAD', 'POWER_METER']
}
def _init_ui_state(self):
"""初始化UI状态"""
self.load_active = False
self.power_meter_data = {
'Urms': None,
'Irms': None,
'P': None
}
self.custom_voltage = {'value': None, 'unit': 'V'}
self.custom_load = {'value': None}
def _setup_serial_parameters(self):
"""配置串口参数"""
for port in self.serial_ports.values():
port.bytesize = serial.EIGHTBITS
port.parity = serial.PARITY_NONE
port.stopbits = serial.STOPBITS_ONE
port.rts = False
def initUI(self):
"""初始化用户界面"""
self.setWindowTitle('Auto Test System - Modern UI')
screen = QApplication.primaryScreen().availableGeometry()
width = int(screen.width() * 0.8)
height = int(screen.height() * 0.7)
self.resize(width, height)
self.setStyleSheet("background-color: #f4f4f4;")
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(30, 20, 30, 20)
main_layout.addWidget(self._create_voltage_group())
main_layout.addWidget(self._create_load_group())
main_layout.addLayout(self._create_feedback_section())
main_layout.addLayout(self._create_control_section())
main_layout.addWidget(self._create_status_bar())
self.setLayout(main_layout)
def _create_voltage_group(self):
group = QGroupBox("Voltage Settings")
layout = QFormLayout()
voltage_row = QHBoxLayout()
self.standard_voltage_buttons = ]
for btn in self.standard_voltage_buttons:
voltage_row.addWidget(btn)
voltage_row.addStretch()
custom_voltage_row = QHBoxLayout()
self.custom_voltage_input = QLineEdit()
self.voltage_unit_combo = QComboBox()
self.voltage_unit_combo.addItems(["mV", "V"])
send_voltage_btn = QPushButton("Send Voltage")
send_voltage_btn.setStyleSheet(self.get_button_style("#2196F3"))
custom_voltage_row.addWidget(self.custom_voltage_input)
custom_voltage_row.addWidget(self.voltage_unit_combo)
custom_voltage_row.addWidget(send_voltage_btn)
layout.addRow("Standard Voltage:", voltage_row)
layout.addRow("Custom Voltage:", custom_voltage_row)
group.setLayout(layout)
return group
def _create_load_group(self):
group = QGroupBox("Load Settings")
layout = QFormLayout()
load_row = QHBoxLayout()
self.standard_load_buttons =
for btn in self.standard_load_buttons:
load_row.addWidget(btn)
load_row.addStretch()
select_all_btn = QPushButton("Select All")
clear_all_btn = QPushButton("Clear All")
select_all_btn.setStyleSheet(self.get_button_style("#4CAF50"))
clear_all_btn.setStyleSheet(self.get_button_style("#f44336"))
btn_row = QHBoxLayout()
btn_row.addWidget(select_all_btn)
btn_row.addWidget(clear_all_btn)
custom_load_row = QHBoxLayout()
self.custom_load_input = QLineEdit()
send_load_btn = QPushButton("Send Load")
send_load_btn.setStyleSheet(self.get_button_style("#FF9800"))
custom_load_row.addWidget(self.custom_load_input)
custom_load_row.addWidget(send_load_btn)
layout.addRow("Standard Load:", load_row)
layout.addRow("", btn_row)
layout.addRow("Custom Load:", custom_load_row)
group.setLayout(layout)
select_all_btn.clicked.connect(self._select_all_loads)
clear_all_btn.clicked.connect(self._clear_all_loads)
return group
def get_button_style(self, color):
return f"""
QPushButton {{
background-color: {color};
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-weight: bold;
}}
QPushButton:hover {{
background-color: {self.darken_color(color)};
}}
QPushButton:pressed {{
background-color: {self.lighten_color(color)};
}}
"""
def darken_color(self, hex_color):
r, g, b = int(hex_color, 16), int(hex_color, 16), int(hex_color, 16)
r, g, b = max(r - 30, 0), max(g - 30, 0), max(b - 30, 0)
return f"#{r:02X}{g:02X}{b:02X}"
def lighten_color(self, hex_color):
r, g, b = int(hex_color, 16), int(hex_color, 16), int(hex_color, 16)
r, g, b = min(r + 30, 255), min(g + 30, 255), min(b + 30, 255)
return f"#{r:02X}{g:02X}{b:02X}"
def _create_feedback_section(self):
layout = QHBoxLayout()
self.system_msg_box = QTextEdit()
self.system_msg_box.setReadOnly(True)
self.system_msg_box.setPlaceholderText("System messages...")
self.power_meter_box = QTextEdit()
self.power_meter_box.setReadOnly(True)
self.power_meter_box.setPlaceholderText("Power meter data...")
layout.addWidget(self.system_msg_box, stretch=1)
layout.addWidget(self.power_meter_box, stretch=1)
return layout
def _create_control_section(self):
layout = QHBoxLayout()
layout.setSpacing(10)
serial_ctrl_layout = self._create_serial_controls()
layout.addLayout(serial_ctrl_layout)
load_ctrl_layout = self._create_load_control_buttons()
layout.addLayout(load_ctrl_layout)
layout.addStretch()
return layout
def _create_serial_controls(self):
layout = QHBoxLayout()
self.serial_combos = {typ: QComboBox() for typ in ['LMCI', 'LOAD', 'POWER_METER']}
self._populate_serial_combos()
for typ in ['LMCI', 'LOAD', 'POWER_METER']:
layout.addWidget(QLabel(f"{typ}:"))
layout.addWidget(self.serial_combos)
return layout
def _create_load_control_buttons(self):
layout = QHBoxLayout()
self.load_on_btn = QPushButton("Load ON")
self.load_off_btn = QPushButton("Load OFF")
self.load_on_btn.setStyleSheet(self.get_button_style("#4CAF50"))
self.load_off_btn.setStyleSheet(self.get_button_style("#f44336"))
layout.addWidget(self.load_on_btn)
layout.addWidget(self.load_off_btn)
return layout
def _create_status_bar(self):
self.status_label = QLabel()
self.update_datetime_label()
return self.status_label
def _populate_serial_combos(self):
ports =
for typ, combo in self.serial_combos.items():
combo.clear()
combo.addItems(ports)
if self.port_states['open']:
combo.setCurrentText(self.port_states['port'])
def _setup_timers(self):
self.datetime_timer = QTimer(self)
self.datetime_timer.timeout.connect(self.update_datetime_label)
self.datetime_timer.start(1000)
self.serial_refresh_timer = QTimer(self)
self.serial_refresh_timer.timeout.connect(self.refresh_serial_ports)
self.serial_refresh_timer.start(self.SERIAL_REFRESH_INTERVAL)
def _connect_signals(self):
for btn in self.standard_voltage_buttons:
btn.toggled.connect(self._on_voltage_selection_changed)
self.custom_voltage_input.textChanged.connect(self._on_custom_voltage_changed)
for btn in self.standard_load_buttons:
btn.toggled.connect(self._on_load_selection_changed)
self.custom_load_input.textChanged.connect(self._on_custom_load_changed)
for typ, combo in self.serial_combos.items():
combo.currentTextChanged.connect(partial(self._handle_serial_selection, port_type=typ))
self.load_on_btn.clicked.connect(self._activate_load)
self.load_off_btn.clicked.connect(self._deactivate_load)
def _handle_serial_selection(self, port_name, port_type):
current_port = self.port_states['port']
if port_name and port_name != current_port:
self._close_serial_port(port_type)
self._open_serial_port(port_name, port_type)
def _open_serial_port(self, port_name, port_type):
try:
port = self.serial_ports
port.port = port_name
port.open()
time.sleep(0.5)
if port.is_open:
self.port_states.update({'open': True, 'port': port_name})
logger.info(f"{port_type} connected: {port_name}")
self.serial_port_opened.emit(port_type, True)
else:
logger.warning(f"Failed to open {port_type} port")
except Exception as e:
logger.error(f"{port_type} connection error: {str(e)}")
def _close_serial_port(self, port_type):
if self.port_states['open']:
try:
self.serial_ports.close()
self.port_states.update({'open': False, 'port': None})
logger.info(f"{port_type} port closed")
except Exception as e:
logger.error(f"Error closing {port_type} port: {str(e)}")
def refresh_serial_ports(self):
ports =
for typ, combo in self.serial_combos.items():
current = combo.currentText()
combo.blockSignals(True)
combo.clear()
combo.addItems(ports)
if self.port_states['open']:
combo.setCurrentText(self.port_states['port'])
else:
combo.setCurrentIndex(-1)
combo.blockSignals(False)
def _update_power_meter_display_from_thread(self, data):
self.power_meter_data.update(data)
self._update_power_meter_display()
def _process_power_meter_response(self, key, response):
try:
decoded = response.decode().strip()
value = float(re.search(r"[-+]?\d*\.?\d+(?:[-+]?\d+)?", decoded).group())
self.power_meter_data = value
except (UnicodeDecodeError, AttributeError, ValueError) as e:
logger.warning(f"Invalid {key} data: {str(e)}")
self.power_meter_data = None
def _clear_serial_buffer(self, port_type):
port = self.serial_ports
while port.in_waiting > 0:
port.read(port.in_waiting)
def _update_power_meter_display(self):
text = (
"Power Meter:\n"
f"Urms: {self._format_measurement(self.power_meter_data['Urms'], 'V')}\n"
f"Irms: {self._format_measurement(self.power_meter_data['Irms'], 'A')}\n"
f"Power: {self._format_measurement(self.power_meter_data['P'], 'W')}"
)
self.power_meter_box.setText(text)
def update_datetime_label(self):
self.status_label.setText(f"Last update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
def _on_voltage_selection_changed(self, checked):
sender = self.sender()
if checked:
for btn in self.standard_voltage_buttons:
if btn != sender:
btn.setChecked(False)
self.custom_voltage_input.clear()
self.custom_voltage_input.setEnabled(not any(
btn.isChecked() for btn in self.standard_voltage_buttons))
def _on_custom_voltage_changed(self, text):
has_input = bool(text.strip())
for btn in self.standard_voltage_buttons:
btn.setEnabled(not has_input)
def _on_load_selection_changed(self, checked):
self.custom_load_input.setEnabled(not any(
btn.isChecked() for btn in self.standard_load_buttons))
def _on_custom_load_changed(self, text):
has_input = bool(text.strip())
for btn in self.standard_load_buttons:
btn.setEnabled(not has_input)
def _select_all_loads(self):
for btn in self.standard_load_buttons:
btn.setChecked(True)
self.custom_load_input.clear()
def _clear_all_loads(self):
for btn in self.standard_load_buttons:
btn.setChecked(False)
self.custom_load_input.clear()
def _send_voltage(self):
selected =
custom = self.custom_voltage_input.text().strip()
if custom:
try:
value = float(custom)
unit = self.voltage_unit_combo.currentText()
self._send_voltage_command(value, unit)
logger.info(f"Voltage set: {value} {unit}")
except ValueError:
logger.warning("Invalid voltage value!")
elif selected:
voltage = selected.split()
self._send_voltage_command(float(voltage), 'V')
logger.info(f"Voltage set: {selected}")
else:
logger.warning("No voltage selected!")
def _send_load(self):
selected =
custom = self.custom_load_input.text().strip()
if custom:
try:
value = float(custom)
self._send_load_command(value)
logger.info(f"Load set: {value} A")
except ValueError:
logger.warning("Invalid load value!")
elif selected:
loads = for btn in self.standard_load_buttons if btn.isChecked()]
logger.info(f"Load set: {', '.join(loads)} A")
else:
logger.warning("No load selected!")
def _send_load_command(self, value):
if not self.port_states['LOAD']['open']:
logger.warning("LOAD port not connected!")
return
try:
command = f"CURR {value}\n".encode()
self.serial_ports['LOAD'].write(command)
logger.info(f"Sent load command: I = {value} A")
self.serial_ports['LOAD'].write(b"CURR?\n")
response = self.serial_ports['LOAD'].readline().decode().strip()
if abs(float(response) - value) < 0.01:
logger.info("Load set successfully!")
self.load_active = True
self._update_load_buttons()
else:
logger.warning("Load setting verification failed!")
except serial.SerialException as e:
logger.error(f"Load communication error: {str(e)}")
except ValueError:
logger.warning("Invalid load response format!")
def _activate_load(self):
if self.port_states['LOAD']['open']:
try:
self.serial_ports['LOAD'].write(b"LOAD:ON\n")
self.load_active = True
self._update_load_buttons()
logger.info("Load activated")
except serial.SerialException as e:
logger.error(f"Load activation failed: {str(e)}")
else:
logger.warning("LOAD port not connected!")
def _deactivate_load(self):
if self.port_states['LOAD']['open']:
try:
self.serial_ports['LOAD'].write(b"LOAD:OFF\n")
self.load_active = False
self._update_load_buttons()
logger.info("Load deactivated")
except serial.SerialException as e:
logger.error(f"Load deactivation failed: {str(e)}")
else:
logger.warning("LOAD port not connected!")
def _update_load_buttons(self):
self.load_on_btn.setEnabled(not self.load_active)
self.load_off_btn.setEnabled(self.load_active)
def closeEvent(self, event):
self.datetime_timer.stop()
self.serial_refresh_timer.stop()
self.power_meter_thread.stop()
for port_type in ['LMCI', 'LOAD', 'POWER_METER']:
self._close_serial_port(port_type)
event.accept()
# ====================
# 单元测试部分
# ====================
class TestAutoTestApp(unittest.TestCase):
def setUp(self):
self.logic = PowerTesterLogic()
def test_format_measurement(self):
self.assertEqual(app._format_measurement(220.345, 'V'), "220.345 V")
self.assertEqual(app._format_measurement(None, 'A'), "N/A")
class TestPowerTesterLogic(unittest.TestCase):
def setUp(self):
self.logic = PowerTesterLogic()
def test_format_measurement(self):
self.assertEqual(self.logic.format_measurement(220.345, 'V'), "220.345 V")
self.assertEqual(self.logic.format_measurement(None, 'A'), "N/A")
if __name__ == '__main__':
import sys
unittest.main(exit=False)
app = QApplication(sys.argv)
ex = AutoTestApp()
ex.show()
sys.exit(app.exec_()) 针对UI美化问题,我建议进行以下优化(主要修改布局、颜色和控件样式):
主要修改点:
1. 使用网格布局替代表单布局,提高控件对齐精度
2. 统一按钮样式并优化悬停效果
3. 增加分组框标题样式
4. 优化文本显示区域样式
5. 添加状态指示灯
6. 调整整体配色方案
def initUI(self):
self.setWindowTitle('Auto Test System')
self.setStyleSheet("""
QWidget {
font-family: 'Segoe UI', Arial;
font-size: 10pt;
}
QGroupBox {
font-weight: bold;
border: 1px solid #ccc;
border-radius: 5px;
margin-top: 1ex;
padding-top: 12px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
}
QTextEdit, QLineEdit {
background-color: white;
border: 1px solid #ddd;
border-radius: 3px;
padding: 5px;
}
QPushButton {
min-height: 28px;
padding: 5px 12px;
border-radius: 4px;
}
""")
# 创建主布局
main_layout = QVBoxLayout()
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 15, 20, 15)
# 顶部控制区
top_layout = QHBoxLayout()
top_layout.addWidget(self._create_voltage_group(), 1)
top_layout.addWidget(self._create_load_group(), 1)
# 中间显示区
mid_layout = QHBoxLayout()
mid_layout.addWidget(self._create_feedback_group("System Messages", self.system_msg_box), 1)
mid_layout.addWidget(self._create_feedback_group("Power Meter", self.power_meter_box), 1)
# 底部控制区
bottom_layout = QHBoxLayout()
bottom_layout.addLayout(self._create_serial_controls())
bottom_layout.addLayout(self._create_load_control_buttons())
bottom_layout.addStretch()
# 组装主界面
main_layout.addLayout(top_layout)
main_layout.addLayout(mid_layout)
main_layout.addLayout(bottom_layout)
main_layout.addWidget(self._create_status_bar())
self.setLayout(main_layout)
def _create_feedback_group(self, title, widget):
group = QGroupBox(title)
layout = QVBoxLayout()
widget.setMinimumHeight(150)
widget.setStyleSheet("font-family: 'Consolas', 'Courier New';")
layout.addWidget(widget)
group.setLayout(layout)
return group
def _create_serial_controls(self):
layout = QGridLayout()
layout.setHorizontalSpacing(10)
layout.setVerticalSpacing(5)
row = 0
for i, typ in enumerate(['LMCI', 'LOAD', 'POWER_METER']):
layout.addWidget(QLabel(f"{typ} Port:"), row, 0)
layout.addWidget(self.serial_combos, row, 1)
# 添加状态指示灯
status_light = QLabel()
status_light.setFixedSize(16, 16)
status_light.setStyleSheet("background-color: #ccc; border-radius: 8px;")
self.status_lights = status_light
layout.addWidget(status_light, row, 2)
row += 1
return layout
def get_button_style(self, color):
return f"""
QPushButton {{
background-color: {color};
color: white;
border: none;
}}
QPushButton:hover {{
background-color: #2E7D32;
}}
QPushButton:disabled {{
background-color: #BDBDBD;
}}
"""
关键优化说明:
1. 使用CSS样式统一控件外观:
- 分组框添加圆角和标题居中
- 按钮统一高度和圆角
- 文本区域使用等宽字体
2. 布局优化:
- 电压/负载设置并排显示
- 消息框使用独立分组框
- 串口控制使用网格布局精确对齐
3. 状态可视化:
- 添加圆形状态指示灯(需在类中初始化 self.status_lights = {})
- 在串口状态变化时更新指示灯颜色:
def _update_status_light(self, port_type, connected):
color = "#4CAF50" if connected else "#f44336"
self.status_lights.setStyleSheet(
f"background-color: {color}; border-radius: 8px;")
4. 配色方案:
- 主色调:科技蓝 (#2196F3)
- 成功状态:绿色 (#4CAF50)
- 错误状态:红色 (#f44336)
- 禁用状态:灰色 (#BDBDBD)
建议在代码中添加以下初始化:
# 在 __init__ 中添加
self.status_lights = {}# 串口状态指示灯
# 在 _open_serial_port 成功时调用
self._update_status_light(port_type, True)
# 在 _close_serial_port 时调用
self._update_status_light(port_type, False)
这些修改将使界面更加现代化,同时保持功能完整性。重点优化了视觉层次和操作反馈,使状态信息更直观可见。
以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。 使用ttkbootstrap
页:
[1]