From 8a08b57c45598c90ef9f8f859da027866a066bd9 Mon Sep 17 00:00:00 2001 From: yaj <1229314433@qq.com> Date: Wed, 8 Apr 2026 18:33:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=B3=BB=E7=BB=9F=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=BC=B9=E7=AA=97(=E5=8C=85=E6=8B=AC=E5=A4=9A?= =?UTF-8?q?=E6=AE=B5=E6=8C=AF=E6=8D=A3=E8=AE=BE=E7=BD=AE=E3=80=81IP?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/feed_params_config.ini | 38 + config/other_ip_config.ini | 18 + view/widgets/arrow_combo_box.py | 176 +++ view/widgets/system_settings_dialog.py | 1434 ++++++++++++++++++++++++ 4 files changed, 1666 insertions(+) create mode 100644 config/feed_params_config.ini create mode 100644 config/other_ip_config.ini create mode 100644 view/widgets/arrow_combo_box.py create mode 100644 view/widgets/system_settings_dialog.py diff --git a/config/feed_params_config.ini b/config/feed_params_config.ini new file mode 100644 index 0000000..19184fe --- /dev/null +++ b/config/feed_params_config.ini @@ -0,0 +1,38 @@ +[general] +multi_stage_vibration = true + +[stages] +count = 4 + +[frequency_range] +min = 200 +max = 230 + +[value_range] +ratio_min = 0 +ratio_max = 100 +ratio_step = 20 +time_min = 0 +time_max = 300 +time_step = 30 + +[stage_1] +mode = ratio +value = 20 +frequency = 220 + +[stage_2] +mode = ratio +value = 60 +frequency = 220 + +[stage_3] +mode = time +value = 120 +frequency = 220 + +[stage_4] +mode = ratio +value = 80 +frequency = 220 + diff --git a/config/other_ip_config.ini b/config/other_ip_config.ini new file mode 100644 index 0000000..d34811e --- /dev/null +++ b/config/other_ip_config.ini @@ -0,0 +1,18 @@ +[count] +total = 3 + +[item_1] +name = OPC服务器 +ip = 192.168.1.100 +port = 4840 + +[item_2] +name = LED屏幕 +ip = 192.168.1.200 +port = 5000 + +[item_3] +name = 搅拌楼 +ip = 10.6.242.111 +port = 5001 + diff --git a/view/widgets/arrow_combo_box.py b/view/widgets/arrow_combo_box.py new file mode 100644 index 0000000..d7f03bd --- /dev/null +++ b/view/widgets/arrow_combo_box.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +""" +ArrowComboBox - 带动态箭头的下拉框组件 +展开时箭头向上,收起时箭头向下 +""" +import os +import re +import tempfile + +from PySide6.QtWidgets import QComboBox +from PySide6.QtGui import QPixmap, QPainter, QTransform, QPolygonF +from PySide6.QtCore import Qt, Signal, QTimer, QPointF + +from utils.image_paths import ImagePaths + + +class ArrowComboBox(QComboBox): + """带动态箭头的下拉框,展开时箭头向上,收起时箭头向下 + + Signals: + popupShown: 下拉列表展开时发出 + popupHidden: 下拉列表收起时发出 + arrowDirectionChanged: 箭头方向改变时发出 (is_up: bool) + """ + + # 信号定义 + popupShown = Signal() # 下拉列表展开 + popupHidden = Signal() # 下拉列表收起 + arrowDirectionChanged = Signal(bool) # 箭头方向改变 (True=向上, False=向下) + + # 类缓存 + _arrow_down_path = None # 向下箭头图片路径 + _arrow_up_path = None # 向上箭头图片路径 + _initialized = False # 是否已初始化 + + @classmethod + def init_arrow_resources(cls, arrow_image_path: str = None, arrow_size: tuple = (14, 8)): + """初始化箭头资源(类方法,只需调用一次) + + Args: + arrow_image_path: 自定义箭头图片路径,默认使用 ImagePaths.SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW + arrow_size: 箭头尺寸 (width, height),默认 (14, 8) + """ + if cls._initialized: + return + + arrow_width, arrow_height = arrow_size + + # 加载原始箭头图片 + if arrow_image_path is None: + arrow_image_path = ImagePaths.SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW + + original = QPixmap(arrow_image_path) + if original.isNull(): + # 如果加载失败,创建默认箭头 + original = QPixmap(arrow_width, arrow_height) + original.fill(Qt.transparent) + painter = QPainter(original) + painter.setRenderHint(QPainter.Antialiasing) + painter.setPen(Qt.NoPen) + painter.setBrush(Qt.white) + points = [ + QPointF(1, 1), + QPointF(arrow_width - 1, 1), + QPointF(arrow_width / 2, arrow_height - 1) + ] + painter.drawPolygon(QPolygonF(points)) + painter.end() + + down_pixmap = original.scaled(arrow_width, arrow_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 垂直翻转得到向上箭头 + transform = QTransform() + transform.scale(1, -1) + up_pixmap = down_pixmap.transformed(transform) + + # 保存到临时文件 + temp_dir = tempfile.gettempdir() + cls._arrow_down_path = os.path.join(temp_dir, "arrow_down.png").replace("\\", "/") + cls._arrow_up_path = os.path.join(temp_dir, "arrow_up.png").replace("\\", "/") + down_pixmap.save(cls._arrow_down_path, "PNG") + up_pixmap.save(cls._arrow_up_path, "PNG") + cls._initialized = True + + @classmethod + def get_arrow_paths(cls): + """获取箭头图片路径 + + Returns: + tuple: (向下箭头路径, 向上箭头路径) + """ + return cls._arrow_down_path, cls._arrow_up_path + + def __init__(self, parent=None): + super().__init__(parent) + ArrowComboBox.init_arrow_resources() + self._is_open = False + self._updating_style = False + self._arrow_size = (14, 8) # 默认箭头尺寸 + + # 延迟初始化箭头(等待样式设置后) + QTimer.singleShot(0, self._update_arrow_style) + + @property + def isPopupOpen(self) -> bool: + """返回下拉列表是否打开""" + return self._is_open + + @property + def arrowDirection(self) -> str: + """返回当前箭头方向 ('up' 或 'down')""" + return 'up' if self._is_open else 'down' + + def setArrowSize(self, width: int, height: int): + """设置箭头尺寸 + + Args: + width: 箭头宽度 + height: 箭头高度 + """ + self._arrow_size = (width, height) + self._update_arrow_style() + + def setArrowDirection(self, is_up: bool): + """手动设置箭头方向 + + Args: + is_up: True 为向上,False 为向下 + """ + if self._is_open != is_up: + self._is_open = is_up + self._update_arrow_style() + self.arrowDirectionChanged.emit(is_up) + + def showPopup(self): + """展开下拉列表时切换为向上箭头""" + super().showPopup() + self._is_open = True + self._update_arrow_style() + self.popupShown.emit() + self.arrowDirectionChanged.emit(True) + + def hidePopup(self): + """收起下拉列表时切换为向下箭头""" + super().hidePopup() + self._is_open = False + self._update_arrow_style() + self.popupHidden.emit() + self.arrowDirectionChanged.emit(False) + + def togglePopup(self): + """切换下拉列表的展开/收起状态""" + if self._is_open: + self.hidePopup() + else: + self.showPopup() + + def refreshArrowStyle(self): + """刷新箭头样式(外部调用)""" + self._update_arrow_style() + + def _update_arrow_style(self): + """更新箭头样式(内部方法)""" + if self._updating_style: + return + self._updating_style = True + try: + arrow_path = self._arrow_up_path if self._is_open else self._arrow_down_path + arrow_width, arrow_height = self._arrow_size + style = self.styleSheet() + # 移除旧的箭头样式 + style = re.sub(r'QComboBox::down-arrow\s*\{[^}]*\}', '', style) + style += f"""QComboBox::down-arrow {{ image: url({arrow_path}); width: {arrow_width}px; height: {arrow_height}px; }}""" + super().setStyleSheet(style) + finally: + self._updating_style = False diff --git a/view/widgets/system_settings_dialog.py b/view/widgets/system_settings_dialog.py new file mode 100644 index 0000000..7e6659f --- /dev/null +++ b/view/widgets/system_settings_dialog.py @@ -0,0 +1,1434 @@ +import configparser +import os +import sys + +# 添加项目根目录到Python路径,确保可以直接运行此文件 +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QWidget, + QPushButton, + QCheckBox, + QLineEdit, + QComboBox, + QScrollArea, + QStackedWidget, + QSpacerItem, + QSizePolicy, + QMessageBox, + QFrame, +) +from PySide6.QtGui import QPixmap, QFont, QPainter, QIcon, QIntValidator +from PySide6.QtCore import Qt, Signal + +from utils.image_paths import ImagePaths +from view.widgets.arrow_combo_box import ArrowComboBox + +""" + 系统设置弹窗: 包含投料参数设置、IP配置、AI算法参数设置等多个Tab页 +""" + +# 配置文件路径 +FEED_PARAMS_CONFIG = os.path.join(BASE_DIR, "config", "feed_params_config.ini") +CAMERA_CONFIG = os.path.join(BASE_DIR, "config", "camera_config.ini") +OTHER_IP_CONFIG = os.path.join(BASE_DIR, "config", "other_ip_config.ini") + + +# ==================== 通用样式 ==================== +COMMON_LABEL_STYLE = "color: #9fbfd4; font-size: 14px; background: transparent;" +COMMON_EDIT_STYLE = """ + QLineEdit { + background-color: #0a1a3a; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + padding: 4px 8px; + font-size: 14px; + } + QLineEdit:focus { + border: 1px solid #13fffc; + } +""" +COMMON_BTN_STYLE = """ + QPushButton { + background-color: #001c82; + color: #9fbfd4; + border: 1px solid #017cbc; + font-size: 14px; + font-weight: Bold; + border-radius: 3px; + padding: 6px 20px; + } + QPushButton:hover { + color: #2dcedb; + border: 1px solid #13fffc; + } +""" +TAB_BTN_NORMAL_STYLE = """ + QPushButton { + background-color: #0a1a3a; + color: #9fbfd4; + border: none; + border-bottom: 2px solid transparent; + font-size: 15px; + font-weight: Bold; + text-align: left; + padding: 10px 16px; + } + QPushButton:hover { + color: #13fffc; + background-color: #0d2255; + } +""" +TAB_BTN_SELECTED_STYLE = """ + QPushButton { + background-color: #0d2255; + color: #13fffc; + border: none; + border-left: 3px solid #13fffc; + font-size: 15px; + font-weight: Bold; + text-align: left; + padding: 10px 13px; + } +""" +CHECKBOX_STYLE = """ + QCheckBox { + color: #9fbfd4; + font-size: 14px; + spacing: 8px; + background: transparent; + } + QCheckBox::indicator { + width: 18px; + height: 18px; + border: 1px solid #017cbc; + border-radius: 3px; + background-color: #0a1a3a; + } + QCheckBox::indicator:checked { + background-color: #13fffc; + border: 1px solid #13fffc; + } +""" +COMBO_STYLE = """ + QComboBox { + background-color: #0a1a3a; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + padding: 4px 20px 4px 8px; + font-size: 14px; + min-width: 55px; + } + QComboBox:focus { + border: 1px solid #13fffc; + } + QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: right center; + width: 20px; + border: none; + border-left: 1px solid #017cbc; + } + QComboBox::down-arrow { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #13fffc; + } + QComboBox QAbstractItemView { + background-color: #0a1a3a; + color: #13fffc; + border: 1px solid #017cbc; + selection-background-color: #0d2255; + outline: none; + } + QComboBox QAbstractItemView::item { + height: 25px; + padding: 2px 6px; + } +""" +# 频率可编辑下拉框专用样式 +FREQ_COMBO_STYLE = """ + QComboBox { + background-color: #0a1a3a; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + padding: 4px 25px 4px 8px; + font-size: 14px; + font-weight: Bold; + min-width: 70px; + } + QComboBox:focus { + border: 1px solid #13fffc; + } + QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: right center; + width: 22px; + border: none; + border-left: 1px solid #017cbc; + } + QComboBox::down-arrow { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #13fffc; + } + QComboBox QAbstractItemView { + background-color: #0a1a3a; + color: #13fffc; + border: 1px solid #017cbc; + selection-background-color: #0d2255; + font-size: 14px; + font-weight: Bold; + outline: none; + } + QComboBox QAbstractItemView::item { + height: 28px; + padding: 4px 8px; + } + QComboBox QAbstractItemView::item:hover { + background-color: #0d2255; + } +""" +SECTION_TITLE_STYLE = "color: #13fffc; font-size: 16px; font-weight: Bold; background: transparent; padding: 4px 0px;" +SEPARATOR_STYLE = "background-color: #017cbc; max-height: 1px; border: none;" + + +class SystemSettingsDialog(QDialog): + """系统设置对话框""" + + # 投料参数立即生效信号: (multi_stage_enabled, stages_data) + # stages_data: list of dict, 每个dict包含 mode, value, frequency + feed_params_apply = Signal(bool, list) + + # IP配置立即生效信号 + ip_config_apply = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setWindowFlags(Qt.FramelessWindowHint) + + # 投料参数的阶段条目列表 + self.stage_rows = [] + + # 相机IP编辑控件 + self.camera_edits = {} + + # 其他IP条目列表 + self.other_ip_rows = [] + + # 频率范围 + self.freq_min = 200 + self.freq_max = 230 + + # 值范围配置 + self.ratio_min = 0 + self.ratio_max = 100 + self.ratio_step = 20 + self.time_min = 0 + self.time_max = 300 + self.time_step = 30 + + # 左侧Tab按钮列表 + self.tab_buttons = [] + + self._load_frequency_range() + self._load_value_range() + self._init_ui() + self._load_feed_params() + self._load_camera_config() + self._load_other_ip_config() + + def _load_frequency_range(self): + """从配置文件读取频率范围""" + config = configparser.ConfigParser() + if os.path.exists(FEED_PARAMS_CONFIG): + config.read(FEED_PARAMS_CONFIG, encoding="utf-8") + if config.has_section("frequency_range"): + self.freq_min = config.getint("frequency_range", "min", fallback=200) + self.freq_max = config.getint("frequency_range", "max", fallback=230) + + def _load_value_range(self): + """从配置文件读取值范围配置""" + config = configparser.ConfigParser() + if os.path.exists(FEED_PARAMS_CONFIG): + config.read(FEED_PARAMS_CONFIG, encoding="utf-8") + if config.has_section("value_range"): + self.ratio_min = config.getint("value_range", "ratio_min", fallback=0) + self.ratio_max = config.getint("value_range", "ratio_max", fallback=100) + self.ratio_step = config.getint("value_range", "ratio_step", fallback=20) + self.time_min = config.getint("value_range", "time_min", fallback=0) + self.time_max = config.getint("value_range", "time_max", fallback=300) + self.time_step = config.getint("value_range", "time_step", fallback=30) + + def _init_ui(self): + self._load_background() + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(32, 20, 32, 30) + main_layout.setSpacing(0) + + # 1. 顶部区域(标题 + 关闭按钮) + self._add_top_area(main_layout) + + # 2. 内容区域(左侧Tab按钮 + 右侧内容) + content_layout = QHBoxLayout() + content_layout.setSpacing(12) + + # 先创建右侧内容区域(使用QStackedWidget切换) + self.stacked_widget = QStackedWidget() + self.stacked_widget.setStyleSheet("background: transparent;") + + # 创建各Tab页 + self.stacked_widget.addWidget(self._create_feed_params_page()) # 0: 投料参数设置 + self.stacked_widget.addWidget(self._create_ip_config_page()) # 1: IP配置 + self.stacked_widget.addWidget(self._create_ai_params_page()) # 2: AI算法参数设置 + # 3-6: 预留空白页 + for _ in range(4): + placeholder = QWidget() + placeholder.setStyleSheet("background: transparent;") + placeholder_layout = QVBoxLayout(placeholder) + placeholder_label = QLabel("功能开发中...") + placeholder_label.setStyleSheet("color: #9fbfd4; font-size: 18px; background: transparent;") + placeholder_label.setAlignment(Qt.AlignCenter) + placeholder_layout.addWidget(placeholder_label) + self.stacked_widget.addWidget(placeholder) + + # 再创建左侧Tab按钮区域(会调用 _on_tab_clicked,需要 stacked_widget 已存在) + self._add_left_tabs(content_layout) + + content_layout.addWidget(self.stacked_widget) + main_layout.addLayout(content_layout) + + def _load_background(self): + self.bg_pixmap = QPixmap(ImagePaths.DESPATCH_DETAILS_POPUP_BG) + if self.bg_pixmap.isNull(): + self.setFixedSize(1000, 700) + else: + self.setFixedSize(self.bg_pixmap.size()) + + def _add_top_area(self, parent_layout): + top_layout = QHBoxLayout() + top_layout.setContentsMargins(0, 0, 0, 16) + + top_layout.addStretch() + + title_label = QLabel("系统设置") + font = QFont() + font.setPixelSize(24) + font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + font.setBold(True) + title_label.setFont(font) + title_label.setStyleSheet("color: #13fffc; font-weight: Bold; background: transparent;") + title_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(title_label) + + self._create_close_button(top_layout) + + parent_layout.addLayout(top_layout) + + def _create_close_button(self, parent_layout): + self.close_btn = QPushButton() + self.close_btn.setFixedSize(36, 36) + + close_icon = QPixmap(ImagePaths.DESPATCH_DETAILS_CLOSE_ICON) + if not close_icon.isNull(): + self.close_btn.setIcon(QIcon(close_icon)) + + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + padding: 0px; + } + QPushButton:hover { + background-color: red; + border-radius: 2px; + } + """) + self.close_btn.clicked.connect(self.close) + + parent_layout.addStretch() + parent_layout.addWidget(self.close_btn) + + def _add_left_tabs(self, parent_layout): + tab_widget = QWidget() + tab_widget.setFixedWidth(150) + tab_widget.setStyleSheet("background: transparent;") + tab_layout = QVBoxLayout(tab_widget) + tab_layout.setContentsMargins(0, 0, 0, 0) + tab_layout.setSpacing(19) # 左侧按钮之间的距离 + + tab_names = [ + "投料参数设置", + "IP配置", + "AI算法参数设置", + "设置四", + "设置五", + "设置六", + "设置七", + ] + + for i, name in enumerate(tab_names): + btn = QPushButton(name) + btn.setFixedHeight(42) + btn.setCursor(Qt.PointingHandCursor) + btn.setStyleSheet(TAB_BTN_NORMAL_STYLE) + btn.clicked.connect(lambda checked, idx=i: self._on_tab_clicked(idx)) + tab_layout.addWidget(btn) + self.tab_buttons.append(btn) + + tab_layout.addStretch() + parent_layout.addWidget(tab_widget) + + # 默认选中第一个 + self._on_tab_clicked(0) + + def _on_tab_clicked(self, index): + """切换Tab页""" + for i, btn in enumerate(self.tab_buttons): + if i == index: + btn.setStyleSheet(TAB_BTN_SELECTED_STYLE) + else: + btn.setStyleSheet(TAB_BTN_NORMAL_STYLE) + self.stacked_widget.setCurrentIndex(index) + + # ==================== 投料参数设置页 ==================== + def _create_feed_params_page(self): + page = QWidget() + page.setStyleSheet("background: transparent;") + layout = QVBoxLayout(page) + layout.setContentsMargins(12, 8, 12, 8) + layout.setSpacing(8) + + # 复选框: 开启多段振捣 + self.multi_stage_checkbox = QCheckBox("开启多段振捣") + self.multi_stage_checkbox.setStyleSheet(CHECKBOX_STYLE) + font = QFont() + font.setPixelSize(15) + font.setBold(True) + self.multi_stage_checkbox.setFont(font) + layout.addWidget(self.multi_stage_checkbox) + + # 分隔线 + sep = QFrame() + sep.setStyleSheet(SEPARATOR_STYLE) + sep.setFixedHeight(1) + layout.addWidget(sep) + + # 阶段列表区域(可滚动) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setStyleSheet(""" + QScrollArea { background: transparent; border: none; } + QScrollArea > QWidget > QWidget { background: transparent; } + /* 滚动条整体 */ + QScrollBar:vertical { + background: #0a1a3a; width: 8px; border: none; + } + /* 滚动条滑块 */ + QScrollBar::handle:vertical { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #039ec3, stop:1 #03f5ff); + border-radius: 4px; + min-height: 20px; + } + /* 滑块悬停效果 */ + QScrollBar::handle:vertical:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #16ffff, stop:1 #00347e); + } + /* 隐藏上下箭头按钮 */ + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0; + } + /* 滑块上下的空白区域(透明) */ + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: transparent; + } + """) + + self.stages_container = QWidget() + self.stages_container.setStyleSheet("background: transparent;") + self.stages_layout = QVBoxLayout(self.stages_container) + self.stages_layout.setContentsMargins(0, 4, 0, 4) + self.stages_layout.setSpacing(12) # 阶段条目之间的间距 + self.stages_layout.addStretch() + + scroll.setWidget(self.stages_container) + layout.addWidget(scroll) + + # 添加阶段按钮 + add_stage_btn = QPushButton("+ 添加阶段") + add_stage_btn.setStyleSheet(COMMON_BTN_STYLE) + add_stage_btn.setFixedSize(120, 32) + add_stage_btn.setCursor(Qt.PointingHandCursor) + add_stage_btn.clicked.connect(lambda: self._add_stage_row()) + layout.addWidget(add_stage_btn, alignment=Qt.AlignLeft) + + # 底部按钮区域 + btn_layout = QHBoxLayout() + btn_layout.addStretch() + apply_btn = QPushButton("立即生效") + apply_btn.setStyleSheet(COMMON_BTN_STYLE) + apply_btn.setFixedSize(100, 34) + apply_btn.setCursor(Qt.PointingHandCursor) + apply_btn.clicked.connect(self._on_feed_params_apply) + btn_layout.addWidget(apply_btn) + + save_btn = QPushButton("保存配置") + save_btn.setStyleSheet(COMMON_BTN_STYLE) + save_btn.setFixedSize(100, 34) + save_btn.setCursor(Qt.PointingHandCursor) + save_btn.clicked.connect(self._on_feed_params_save) + btn_layout.addWidget(save_btn) + btn_layout.addStretch() + layout.addLayout(btn_layout) + + return page + + def _add_stage_row(self, mode="ratio", value=None, frequency=220): + """添加一个阶段条目 + 如果value为None,则根据mode自动计算:取前面同单位阶段的最大值+1 + """ + # 自动计算默认值 + if value is None: + max_val = 0 + for row in self.stage_rows: + row_mode = row.mode_combo.currentIndex() # 0=%, 1=秒 + target_mode = 0 if mode == "ratio" else 1 + if row_mode == target_mode: + try: + val = int(row.value_combo.currentText()) + max_val = max(max_val, val) + except ValueError: + pass + value = max_val + 1 + else: + value = int(value) + + stage_index = len(self.stage_rows) + 1 + row_widget = QWidget() + row_widget.setStyleSheet("background: transparent;") + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + row_layout.setSpacing(6) + + # 阶段标签 + stage_label = QLabel(f"第{self._num_to_chinese(stage_index)}阶段") + stage_label.setStyleSheet(COMMON_LABEL_STYLE) + stage_label.setFixedWidth(80) + font = QFont() + font.setPixelSize(14) + font.setBold(True) + stage_label.setFont(font) + row_layout.addWidget(stage_label) + + # 减按钮 + minus_btn = QPushButton("-") + minus_btn.setFixedSize(28, 28) + minus_btn.setCursor(Qt.PointingHandCursor) + minus_btn.setStyleSheet(""" + QPushButton { + background-color: #001c82; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + font-size: 16px; + font-weight: Bold; + } + QPushButton:hover { + background-color: #0d2255; + border: 1px solid #13fffc; + } + QPushButton:pressed { + background-color: #017cbc; + } + """) + row_layout.addWidget(minus_btn) + + # 值下拉框(根据单位显示不同选项) + value_combo = ArrowComboBox() + value_combo.setStyleSheet(COMBO_STYLE) + value_combo.setEditable(True) + value_combo.setFixedWidth(60) + self._update_value_combo_items(value_combo, mode) + # 设置当前值 + value_combo.setCurrentText(str(value)) + row_layout.addWidget(value_combo) + + # 加按钮 + plus_btn = QPushButton("+") + plus_btn.setFixedSize(28, 28) + plus_btn.setCursor(Qt.PointingHandCursor) + plus_btn.setStyleSheet(""" + QPushButton { + background-color: #001c82; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + font-size: 16px; + font-weight: Bold; + } + QPushButton:hover { + background-color: #0d2255; + border: 1px solid #13fffc; + } + QPushButton:pressed { + background-color: #017cbc; + } + """) + row_layout.addWidget(plus_btn) + + # 单位选择(秒 或 %) + mode_combo = ArrowComboBox() + mode_combo.addItems(["%", "秒"]) + mode_combo.setStyleSheet(COMBO_STYLE) + mode_combo.setFixedWidth(55) + if mode == "ratio": + mode_combo.setCurrentIndex(0) + else: + mode_combo.setCurrentIndex(1) + row_layout.addWidget(mode_combo) + + # 单位切换时更新值下拉框选项 + mode_combo.currentIndexChanged.connect(lambda idx: self._on_mode_changed(row_widget, idx)) + + # 频率标签 + freq_label = QLabel("频率:") + freq_label.setStyleSheet(COMMON_LABEL_STYLE) + freq_label.setFixedWidth(40) + row_layout.addWidget(freq_label) + + # 频率减按钮 + freq_minus_btn = QPushButton("-") + freq_minus_btn.setFixedSize(28, 28) + freq_minus_btn.setCursor(Qt.PointingHandCursor) + freq_minus_btn.setStyleSheet(""" + QPushButton { + background-color: #001c82; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + font-size: 16px; + font-weight: Bold; + } + QPushButton:hover { + background-color: #0d2255; + border: 1px solid #13fffc; + } + QPushButton:pressed { + background-color: #017cbc; + } + """) + row_layout.addWidget(freq_minus_btn) + + # 频率可编辑下拉框(既可下拉选择预设值,也可直接输入) + freq_combo = ArrowComboBox() + freq_items = [str(f) for f in range(self.freq_min, self.freq_max + 1, 10)] + freq_combo.addItems(freq_items) + freq_combo.setEditable(True) # 设置为可编辑 + freq_combo.setInsertPolicy(QComboBox.NoInsert) # 不自动插入新条目 + freq_combo.setStyleSheet(FREQ_COMBO_STYLE) + freq_combo.setFixedWidth(80) + freq_combo.setCurrentText(str(frequency)) # 设置当前值 + freq_combo.lineEdit().setAlignment(Qt.AlignCenter) # 文字居中 + row_layout.addWidget(freq_combo) + + # 频率加按钮 + freq_plus_btn = QPushButton("+") + freq_plus_btn.setFixedSize(28, 28) + freq_plus_btn.setCursor(Qt.PointingHandCursor) + freq_plus_btn.setStyleSheet(""" + QPushButton { + background-color: #001c82; + color: #13fffc; + border: 1px solid #017cbc; + border-radius: 3px; + font-size: 16px; + font-weight: Bold; + } + QPushButton:hover { + background-color: #0d2255; + border: 1px solid #13fffc; + } + QPushButton:pressed { + background-color: #017cbc; + } + """) + row_layout.addWidget(freq_plus_btn) + + # Hz标签 + hz_label = QLabel("Hz") + hz_label.setStyleSheet(COMMON_LABEL_STYLE) + hz_label.setFixedWidth(25) + row_layout.addWidget(hz_label) + + # 删除按钮 + del_btn = QPushButton("×") + del_btn.setFixedSize(28, 28) + del_btn.setCursor(Qt.PointingHandCursor) + del_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #ff4444; + border: 1px solid #ff4444; + border-radius: 14px; + font-size: 16px; + font-weight: Bold; + } + QPushButton:hover { + background-color: #ff4444; + color: white; + } + """) + del_btn.clicked.connect(lambda: self._remove_stage_row(row_widget)) + row_layout.addWidget(del_btn) + + row_layout.addStretch() + + # 存储引用 + row_widget.stage_label = stage_label + row_widget.value_combo = value_combo + row_widget.mode_combo = mode_combo + row_widget.freq_combo = freq_combo + + # 绑定加减按钮事件 + minus_btn.clicked.connect(lambda: self._adjust_value(row_widget, -1)) # 值的减按钮 + plus_btn.clicked.connect(lambda: self._adjust_value(row_widget, 1)) # 值的加按钮 + freq_minus_btn.clicked.connect(lambda: self._adjust_freq(freq_combo, -1)) + freq_plus_btn.clicked.connect(lambda: self._adjust_freq(freq_combo, 1)) + + self.stage_rows.append(row_widget) + # 插入到stretch之前 + self.stages_layout.insertWidget(self.stages_layout.count() - 1, row_widget) + + def _get_same_mode_constraint(self, row_widget): + """获取当前阶段同单位的约束值,返回 (min_value, max_value) + min_value: 前面所有同单位阶段的最大值(新值必须大于此值),None表示前面没有同单位阶段 + max_value: 后面所有同单位阶段的最小值(新值必须小于此值),None表示后面没有同单位阶段 + """ + try: + current_idx = self.stage_rows.index(row_widget) + except ValueError: + return None, None + + current_mode = row_widget.mode_combo.currentIndex() # 0=%, 1=秒 + min_value = None # None 表示前面没有同单位阶段 + max_value = None + + # 遍历前面的阶段,找同单位的最大值 + for i in range(current_idx): + prev_row = self.stage_rows[i] + if prev_row.mode_combo.currentIndex() == current_mode: + try: + val = int(prev_row.value_combo.currentText()) + if min_value is None or val > min_value: + min_value = val + except ValueError: + pass + + # 遍历后面的阶段,找同单位的最小值 + for i in range(current_idx + 1, len(self.stage_rows)): + next_row = self.stage_rows[i] + if next_row.mode_combo.currentIndex() == current_mode: + try: + val = int(next_row.value_combo.currentText()) + if max_value is None or val < max_value: + max_value = val + except ValueError: + pass + + return min_value, max_value + + def _update_value_combo_items(self, value_combo, mode): + """根据单位更新值下拉框的选项 + + Args: + value_combo: 值下拉框控件 + mode: "ratio" 表示百分比,"time" 表示秒数 + """ + value_combo.clear() + if mode == "ratio": + # 百分比模式 + items = [str(v) for v in range(self.ratio_min, self.ratio_max + 1, self.ratio_step)] + else: + # 秒数模式 + items = [str(v) for v in range(self.time_min, self.time_max + 1, self.time_step)] + value_combo.addItems(items) + + def _on_mode_changed(self, row_widget, mode_index): + """单位切换时的处理 + + Args: + row_widget: 阶段行控件 + mode_index: 0=百分比, 1=秒数 + """ + mode = "ratio" if mode_index == 0 else "time" + value_combo = row_widget.value_combo + + # 保存当前值 + try: + current_value = int(value_combo.currentText()) + except ValueError: + current_value = 0 + + # 更新选项 + self._update_value_combo_items(value_combo, mode) + + # 尝试找到最接近的值 + if mode == "ratio": + # 在百分比范围内找最接近的值 + new_value = min(max(current_value, self.ratio_min), self.ratio_max) + # 找到最近的步进值 + step = self.ratio_step + new_value = round(new_value / step) * step + new_value = min(max(new_value, self.ratio_min), self.ratio_max) + else: + # 在秒数范围内找最接近的值 + new_value = min(max(current_value, self.time_min), self.time_max) + # 找到最近的步进值 + step = self.time_step + new_value = round(new_value / step) * step + new_value = min(max(new_value, self.time_min), self.time_max) + + value_combo.setCurrentText(str(new_value)) + + def _adjust_value(self, row_widget, delta): + """调整值(秒或百分比),加减1并验证约束""" + value_combo = row_widget.value_combo + mode = row_widget.mode_combo.currentIndex() # 0=%, 1=秒 + + # 根据单位确定范围 + if mode == 0: # 百分比 + min_limit = self.ratio_min + max_limit = self.ratio_max + else: # 秒数 + min_limit = self.time_min + max_limit = self.time_max + + try: + current = int(value_combo.currentText()) + except ValueError: + current = 0 + + new_value = current + delta + + # 获取同单位约束 + min_val, max_val = self._get_same_mode_constraint(row_widget) + + # 确保大于前面同单位阶段的最大值(如果有的话) + if min_val is not None and new_value <= min_val: + new_value = min_val + 1 + + # 确保小于后面同单位阶段的最小值(如果有的话) + if max_val is not None and new_value >= max_val: + new_value = max_val - 1 + + # 确保在配置范围内 + new_value = max(min_limit, min(max_limit, new_value)) + + value_combo.setCurrentText(str(new_value)) + + def _validate_stages(self): + """验证阶段数据是否有效,返回 (is_valid, error_message) + 验证规则:相同单位的阶段,数值必须递增 + """ + # 分别收集两种单位的所有阶段 + percent_stages = [] # (index, value) + time_stages = [] + + for i, row in enumerate(self.stage_rows): + mode = row.mode_combo.currentIndex() # 0=%, 1=秒 + try: + val = int(row.value_combo.currentText()) + except ValueError: + return False, f"第{self._num_to_chinese(i + 1)}阶段数值格式错误" + + if mode == 0: # % + percent_stages.append((i, val)) + else: # 秒 + time_stages.append((i, val)) + + # 验证百分比阶段递增 + for j in range(1, len(percent_stages)): + prev_idx, prev_val = percent_stages[j - 1] + curr_idx, curr_val = percent_stages[j] + if curr_val <= prev_val: + return False, f"第{self._num_to_chinese(curr_idx + 1)}阶段的百分数必须大于第{self._num_to_chinese(prev_idx + 1)}阶段的百分数" + + # 验证秒数阶段递增 + for j in range(1, len(time_stages)): + prev_idx, prev_val = time_stages[j - 1] + curr_idx, curr_val = time_stages[j] + if curr_val <= prev_val: + return False, f"第{self._num_to_chinese(curr_idx + 1)}阶段的秒数必须大于第{self._num_to_chinese(prev_idx + 1)}阶段的秒数" + + return True, "" + + def _adjust_freq(self, freq_combo, delta): + """调整频率(直接对数值加减1)""" + try: + current_value = int(freq_combo.currentText()) + except ValueError: + current_value = self.freq_min + new_value = current_value + delta + # 确保在范围内 + new_value = max(self.freq_min, min(self.freq_max, new_value)) + freq_combo.setCurrentText(str(new_value)) + + def _remove_stage_row(self, row_widget): + """删除一个阶段条目""" + if row_widget in self.stage_rows: + self.stage_rows.remove(row_widget) + self.stages_layout.removeWidget(row_widget) + row_widget.deleteLater() + self._update_stage_labels() + + def _update_stage_labels(self): + """更新所有阶段的编号标签""" + for i, row in enumerate(self.stage_rows): + row.stage_label.setText(f"第{self._num_to_chinese(i + 1)}阶段") + + @staticmethod + def _num_to_chinese(num): + """数字转中文""" + chinese = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", + "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十"] + if 1 <= num <= 20: + return chinese[num - 1] + return str(num) + + def _get_feed_params_data(self): + """获取投料参数数据""" + multi_stage = self.multi_stage_checkbox.isChecked() + stages = [] + for row in self.stage_rows: + mode = "ratio" if row.mode_combo.currentIndex() == 0 else "time" # 0=%=ratio, 1=秒=time + value = row.value_combo.currentText().strip() + frequency = row.freq_combo.currentText().strip() + stages.append({ + "mode": mode, + "value": value, + "frequency": frequency, + }) + return multi_stage, stages + + def _on_feed_params_apply(self): + """投料参数 立即生效""" + # 先验证 + is_valid, error_msg = self._validate_stages() + if not is_valid: + QMessageBox.warning(self, "验证失败", error_msg) + return + + multi_stage, stages = self._get_feed_params_data() + self.feed_params_apply.emit(multi_stage, stages) + + def _on_feed_params_save(self): + """投料参数 保存到配置文件""" + # 先验证 + is_valid, error_msg = self._validate_stages() + if not is_valid: + QMessageBox.warning(self, "验证失败", error_msg) + return + + multi_stage, stages = self._get_feed_params_data() + + config = configparser.ConfigParser() + config.read(FEED_PARAMS_CONFIG, encoding="utf-8") + + # 写入general + if not config.has_section("general"): + config.add_section("general") + config.set("general", "multi_stage_vibration", "true" if multi_stage else "false") + + # 写入stages count + if not config.has_section("stages"): + config.add_section("stages") + config.set("stages", "count", str(len(stages))) + + # 清除旧的stage_N段 + for sec in list(config.sections()): + if sec.startswith("stage_"): + config.remove_section(sec) + + # 写入每个阶段 + for i, stage in enumerate(stages): + sec = f"stage_{i + 1}" + config.add_section(sec) + config.set(sec, "mode", stage["mode"]) + config.set(sec, "value", stage["value"]) + config.set(sec, "frequency", stage["frequency"]) + + # 保留频率范围 + if not config.has_section("frequency_range"): + config.add_section("frequency_range") + config.set("frequency_range", "min", str(self.freq_min)) + config.set("frequency_range", "max", str(self.freq_max)) + + with open(FEED_PARAMS_CONFIG, "w", encoding="utf-8") as f: + config.write(f) + + def _load_feed_params(self): + """从配置文件加载投料参数""" + config = configparser.ConfigParser() + if not os.path.exists(FEED_PARAMS_CONFIG): + return + config.read(FEED_PARAMS_CONFIG, encoding="utf-8") + + # 加载多段振捣开关 + if config.has_section("general"): + enabled = config.get("general", "multi_stage_vibration", fallback="false") + self.multi_stage_checkbox.setChecked(enabled.lower() == "true") + + # 加载阶段 + count = 0 + if config.has_section("stages"): + count = config.getint("stages", "count", fallback=0) + + for i in range(1, count + 1): + sec = f"stage_{i}" + if config.has_section(sec): + mode = config.get(sec, "mode", fallback="ratio") + value = config.get(sec, "value", fallback="10") + frequency = config.getint(sec, "frequency", fallback=210) + self._add_stage_row(mode, value, frequency) + + # ==================== IP配置页 ==================== + def _create_ip_config_page(self): + page = QWidget() + page.setStyleSheet("background: transparent;") + layout = QVBoxLayout(page) + layout.setContentsMargins(12, 8, 12, 8) + layout.setSpacing(8) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + # IP配置页滚动条样式 + scroll.setStyleSheet(""" + QScrollArea { background: transparent; border: none; } + QScrollArea > QWidget > QWidget { background: transparent; } + /* 滚动条整体 */ + QScrollBar:vertical { + background: #0a1a3a; width: 8px; border: none; + } + /* 滚动条滑块 */ + QScrollBar::handle:vertical { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #039ec3, stop:1 #03f5ff); + border-radius: 4px; + min-height: 20px; + } + /* 滑块悬停效果 */ + QScrollBar::handle:vertical:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #16ffff, stop:1 #00347e); + } + /* 隐藏上下箭头按钮 */ + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0; + } + /* 滑块上下的空白区域(透明) */ + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: transparent; + } + """) + + scroll_content = QWidget() + scroll_content.setStyleSheet("background: transparent;") + scroll_layout = QVBoxLayout(scroll_content) + scroll_layout.setContentsMargins(0, 0, 0, 0) + scroll_layout.setSpacing(10) + + # --- 相机IP配置标题 --- + cam_title = QLabel("相机IP配置") + cam_title.setStyleSheet(SECTION_TITLE_STYLE) + scroll_layout.addWidget(cam_title) + + sep1 = QFrame() + sep1.setStyleSheet(SEPARATOR_STYLE) + sep1.setFixedHeight(1) + scroll_layout.addWidget(sep1) + + # 上位料斗相机 + self.camera_edits["上位料斗"] = self._add_camera_ip_group(scroll_layout, "上位料斗相机IP") + # 下位料斗相机 + self.camera_edits["下位料斗"] = self._add_camera_ip_group(scroll_layout, "下位料斗相机IP") + # 模具车相机 + self.camera_edits["模具车"] = self._add_camera_ip_group(scroll_layout, "模具车相机IP") + + # --- 其他IP配置标题 --- + other_title = QLabel("其他IP配置") + other_title.setStyleSheet(SECTION_TITLE_STYLE) + scroll_layout.addWidget(other_title) + + sep2 = QFrame() + sep2.setStyleSheet(SEPARATOR_STYLE) + sep2.setFixedHeight(1) + scroll_layout.addWidget(sep2) + + # 其他IP列表容器 + self.other_ip_container = QWidget() + self.other_ip_container.setStyleSheet("background: transparent;") + self.other_ip_layout = QVBoxLayout(self.other_ip_container) + self.other_ip_layout.setContentsMargins(0, 0, 0, 0) + self.other_ip_layout.setSpacing(6) + self.other_ip_layout.addStretch() + scroll_layout.addWidget(self.other_ip_container) + + # 添加条目按钮 + add_ip_btn = QPushButton("+ 添加IP配置") + add_ip_btn.setStyleSheet(COMMON_BTN_STYLE) + add_ip_btn.setFixedSize(130, 32) + add_ip_btn.setCursor(Qt.PointingHandCursor) + add_ip_btn.clicked.connect(lambda: self._add_other_ip_row()) + scroll_layout.addWidget(add_ip_btn, alignment=Qt.AlignLeft) + + scroll_layout.addStretch() + scroll.setWidget(scroll_content) + layout.addWidget(scroll) + + # 底部按钮 + btn_layout = QHBoxLayout() + btn_layout.addStretch() + apply_btn = QPushButton("立即生效") + apply_btn.setStyleSheet(COMMON_BTN_STYLE) + apply_btn.setFixedSize(100, 34) + apply_btn.setCursor(Qt.PointingHandCursor) + apply_btn.clicked.connect(self._on_ip_config_apply) + btn_layout.addWidget(apply_btn) + + save_btn = QPushButton("保存配置") + save_btn.setStyleSheet(COMMON_BTN_STYLE) + save_btn.setFixedSize(100, 34) + save_btn.setCursor(Qt.PointingHandCursor) + save_btn.clicked.connect(self._on_ip_config_save) + btn_layout.addWidget(save_btn) + btn_layout.addStretch() + layout.addLayout(btn_layout) + + return page + + def _add_camera_ip_group(self, parent_layout, title): + """添加一组相机IP配置(IP、端口、账号、密码)""" + group_widget = QWidget() + group_widget.setStyleSheet("background: transparent;") + group_layout = QVBoxLayout(group_widget) + group_layout.setContentsMargins(8, 4, 0, 4) + group_layout.setSpacing(4) + + # 标题 + title_label = QLabel(title) + title_label.setStyleSheet("color: #13fffc; font-size: 14px; font-weight: Bold; background: transparent;") + group_layout.addWidget(title_label) + + # IP和端口行 + row1 = QHBoxLayout() + row1.setSpacing(8) + + ip_label = QLabel("IP地址:") + ip_label.setStyleSheet(COMMON_LABEL_STYLE) + ip_label.setFixedWidth(55) + row1.addWidget(ip_label) + + ip_edit = QLineEdit() + ip_edit.setStyleSheet(COMMON_EDIT_STYLE) + ip_edit.setFixedWidth(150) + ip_edit.setPlaceholderText("192.168.x.x") + row1.addWidget(ip_edit) + + port_label = QLabel("端口:") + port_label.setStyleSheet(COMMON_LABEL_STYLE) + port_label.setFixedWidth(40) + row1.addWidget(port_label) + + port_edit = QLineEdit() + port_edit.setStyleSheet(COMMON_EDIT_STYLE) + port_edit.setFixedWidth(70) + port_edit.setValidator(QIntValidator(1, 65535)) + row1.addWidget(port_edit) + + row1.addStretch() + group_layout.addLayout(row1) + + # 账号和密码行 + row2 = QHBoxLayout() + row2.setSpacing(8) + + user_label = QLabel("账号:") + user_label.setStyleSheet(COMMON_LABEL_STYLE) + user_label.setFixedWidth(55) + row2.addWidget(user_label) + + user_edit = QLineEdit() + user_edit.setStyleSheet(COMMON_EDIT_STYLE) + user_edit.setFixedWidth(150) + row2.addWidget(user_edit) + + pwd_label = QLabel("密码:") + pwd_label.setStyleSheet(COMMON_LABEL_STYLE) + pwd_label.setFixedWidth(40) + row2.addWidget(pwd_label) + + pwd_edit = QLineEdit() + pwd_edit.setStyleSheet(COMMON_EDIT_STYLE) + pwd_edit.setFixedWidth(150) + pwd_edit.setEchoMode(QLineEdit.Password) + row2.addWidget(pwd_edit) + + row2.addStretch() + group_layout.addLayout(row2) + + parent_layout.addWidget(group_widget) + + return { + "ip": ip_edit, + "port": port_edit, + "username": user_edit, + "password": pwd_edit, + } + + def _add_other_ip_row(self, name="", ip="", port=""): + """添加一个其他IP配置条目""" + row_widget = QWidget() + row_widget.setStyleSheet("background: transparent;") + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(8, 2, 0, 2) + row_layout.setSpacing(8) + + name_label = QLabel("名称:") + name_label.setStyleSheet(COMMON_LABEL_STYLE) + name_label.setFixedWidth(40) + row_layout.addWidget(name_label) + + name_edit = QLineEdit(name) + name_edit.setStyleSheet(COMMON_EDIT_STYLE) + name_edit.setFixedWidth(100) + row_layout.addWidget(name_edit) + + ip_label = QLabel("IP:") + ip_label.setStyleSheet(COMMON_LABEL_STYLE) + ip_label.setFixedWidth(25) + row_layout.addWidget(ip_label) + + ip_edit = QLineEdit(ip) + ip_edit.setStyleSheet(COMMON_EDIT_STYLE) + ip_edit.setFixedWidth(140) + ip_edit.setPlaceholderText("192.168.x.x") + row_layout.addWidget(ip_edit) + + port_label = QLabel("端口:") + port_label.setStyleSheet(COMMON_LABEL_STYLE) + port_label.setFixedWidth(40) + row_layout.addWidget(port_label) + + port_edit = QLineEdit(port) + port_edit.setStyleSheet(COMMON_EDIT_STYLE) + port_edit.setFixedWidth(70) + port_edit.setValidator(QIntValidator(1, 65535)) + row_layout.addWidget(port_edit) + + # 删除按钮 + del_btn = QPushButton("×") + del_btn.setFixedSize(28, 28) + del_btn.setCursor(Qt.PointingHandCursor) + del_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #ff4444; + border: 1px solid #ff4444; + border-radius: 14px; + font-size: 16px; + font-weight: Bold; + } + QPushButton:hover { + background-color: #ff4444; + color: white; + } + """) + del_btn.clicked.connect(lambda: self._remove_other_ip_row(row_widget)) + row_layout.addWidget(del_btn) + + row_layout.addStretch() + + row_widget.name_edit = name_edit + row_widget.ip_edit = ip_edit + row_widget.port_edit = port_edit + + self.other_ip_rows.append(row_widget) + self.other_ip_layout.insertWidget(self.other_ip_layout.count() - 1, row_widget) + + def _remove_other_ip_row(self, row_widget): + """删除一个其他IP条目""" + if row_widget in self.other_ip_rows: + self.other_ip_rows.remove(row_widget) + self.other_ip_layout.removeWidget(row_widget) + row_widget.deleteLater() + + def _on_ip_config_apply(self): + """IP配置 立即生效""" + self.ip_config_apply.emit() + + def _on_ip_config_save(self): + """IP配置 保存到配置文件""" + self._save_camera_config() + self._save_other_ip_config() + + def _save_camera_config(self): + """保存相机IP到 camera_config.ini""" + config = configparser.ConfigParser() + config.read(CAMERA_CONFIG, encoding="utf-8") + + section_map = { + "上位料斗": "上位料斗", + "下位料斗": "下位料斗", + "模具车": "模具车", + } + + for key, section in section_map.items(): + if key in self.camera_edits: + edits = self.camera_edits[key] + if not config.has_section(section): + config.add_section(section) + config.set(section, "ip", edits["ip"].text().strip()) + config.set(section, "port", edits["port"].text().strip()) + config.set(section, "username", edits["username"].text().strip()) + config.set(section, "password", edits["password"].text().strip()) + + with open(CAMERA_CONFIG, "w", encoding="utf-8") as f: + config.write(f) + + def _save_other_ip_config(self): + """保存其他IP到 other_ip_config.ini""" + config = configparser.ConfigParser() + + config.add_section("count") + config.set("count", "total", str(len(self.other_ip_rows))) + + for i, row in enumerate(self.other_ip_rows): + sec = f"item_{i + 1}" + config.add_section(sec) + config.set(sec, "name", row.name_edit.text().strip()) + config.set(sec, "ip", row.ip_edit.text().strip()) + config.set(sec, "port", row.port_edit.text().strip()) + + with open(OTHER_IP_CONFIG, "w", encoding="utf-8") as f: + config.write(f) + + def _load_camera_config(self): + """从 camera_config.ini 加载相机IP""" + config = configparser.ConfigParser() + if not os.path.exists(CAMERA_CONFIG): + return + config.read(CAMERA_CONFIG, encoding="utf-8") + + section_map = { + "上位料斗": "上位料斗", + "下位料斗": "下位料斗", + "模具车": "模具车", + } + + for key, section in section_map.items(): + if config.has_section(section) and key in self.camera_edits: + edits = self.camera_edits[key] + edits["ip"].setText(config.get(section, "ip", fallback="")) + edits["port"].setText(config.get(section, "port", fallback="")) + edits["username"].setText(config.get(section, "username", fallback="")) + edits["password"].setText(config.get(section, "password", fallback="")) + + def _load_other_ip_config(self): + """从 other_ip_config.ini 加载其他IP""" + config = configparser.ConfigParser() + if not os.path.exists(OTHER_IP_CONFIG): + return + config.read(OTHER_IP_CONFIG, encoding="utf-8") + + count = 0 + if config.has_section("count"): + count = config.getint("count", "total", fallback=0) + + for i in range(1, count + 1): + sec = f"item_{i}" + if config.has_section(sec): + name = config.get(sec, "name", fallback="") + ip = config.get(sec, "ip", fallback="") + port = config.get(sec, "port", fallback="") + self._add_other_ip_row(name, ip, port) + + # ==================== AI算法参数设置页 ==================== + def _create_ai_params_page(self): + page = QWidget() + page.setStyleSheet("background: transparent;") + layout = QVBoxLayout(page) + layout.setContentsMargins(12, 8, 12, 8) + layout.setSpacing(8) + + title = QLabel("AI算法参数设置") + title.setStyleSheet(SECTION_TITLE_STYLE) + layout.addWidget(title) + + sep = QFrame() + sep.setStyleSheet(SEPARATOR_STYLE) + sep.setFixedHeight(1) + layout.addWidget(sep) + + placeholder = QLabel("AI算法参数设置功能开发中...") + placeholder.setStyleSheet("color: #9fbfd4; font-size: 16px; background: transparent;") + placeholder.setAlignment(Qt.AlignCenter) + layout.addWidget(placeholder) + layout.addStretch() + + return page + + # ==================== 对外接口 ==================== + def get_feed_params(self): + """获取当前投料参数设置(供外部调用立即生效)""" + return self._get_feed_params_data() + + def get_camera_ip_config(self): + """获取当前相机IP配置""" + result = {} + for key, edits in self.camera_edits.items(): + result[key] = { + "ip": edits["ip"].text().strip(), + "port": edits["port"].text().strip(), + "username": edits["username"].text().strip(), + "password": edits["password"].text().strip(), + } + return result + + def get_other_ip_config(self): + """获取当前其他IP配置""" + result = [] + for row in self.other_ip_rows: + result.append({ + "name": row.name_edit.text().strip(), + "ip": row.ip_edit.text().strip(), + "port": row.port_edit.text().strip(), + }) + return result + + def paintEvent(self, event): + if not self.bg_pixmap.isNull(): + painter = QPainter(self) + painter.drawPixmap(self.rect(), self.bg_pixmap) + super().paintEvent(event) + + +# 测试代码 +if __name__ == "__main__": + app = QApplication(sys.argv) + dialog = SystemSettingsDialog() + dialog.show() + sys.exit(app.exec())