不吃鱼的猫咪 发表于 3 天前

帮我看看UI部分要怎么修改,感觉好丑

import sys
import 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_())

FishC 发表于 3 天前

针对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 小助理,如未能正确解答您的问题,请继续追问。

某一个“天” 发表于 3 天前

使用ttkbootstrap
页: [1]
查看完整版本: 帮我看看UI部分要怎么修改,感觉好丑